Clarify supported templates and finish release documentation
All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 24s
All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 24s
This commit is contained in:
parent
1a8334f705
commit
e2c0e703ea
54
README.md
54
README.md
@ -1,51 +1,51 @@
|
|||||||
# TS6 Botflow
|
# TS6 Botflow
|
||||||
|
|
||||||
Painel local XanaX para gerir bots TeamSpeak, com login Discord e editor visual de flows. Mant?m os log?tipos originais e o tema flat roxo. As pastas dos projetos de origem permanecem intactas.
|
Painel local XanaX para gerir bots TeamSpeak, com login Discord e editor visual de flows. Mantém os logótipos originais e o tema flat roxo. As pastas dos projetos de origem permanecem intactas.
|
||||||
|
|
||||||
## Funcionalidades
|
## Funcionalidades
|
||||||
|
|
||||||
- Dashboard com bots, flows e execu??es guardados na base de dados.
|
- Dashboard com bots, flows e execuções guardados na base de dados.
|
||||||
- Editor com blocos arrast?veis, liga??es, condi??es, propriedades, zoom e grava??o.
|
- Editor com blocos arrastáveis, ligações, condições, propriedades, zoom e gravação.
|
||||||
- Blocos: temporizador, in?cio manual, informa??es do servidor, clientes, ordena??o, limite, prepara??o de texto, edi??o da descri??o de canal, mensagem, espera e registo.
|
- Blocos: temporizador, início manual, informações do servidor, clientes, ordenação, limite, preparação de texto, edição da descrição de canal, mensagem, espera e registo.
|
||||||
- Simula??o identificada com dados de exemplo, sem enviar pedidos ao TeamSpeak.
|
- Simulação identificada com dados de exemplo, sem enviar pedidos ao TeamSpeak.
|
||||||
- Execu??o manual ou por temporizador, com hist?rico e resultado de cada passo. Um bot n?o executa duas vezes em simult?neo.
|
- Execução manual ou por temporizador, com histórico e resultado de cada passo. Um bot não executa duas vezes em simultâneo.
|
||||||
- Liga??es WebQuery, teste de liga??o e API keys cifradas em repouso.
|
- Ligações WebQuery, teste de ligação e API keys cifradas em repouso.
|
||||||
- Vari?veis por bot, acess?veis como `{{variables.nome}}`. Outros valores: `{{server.virtualserver_clientsonline}}`, `{{count}}`, `{{top}}`, `{{text}}` e `{{time}}`.
|
- Variáveis por bot: `{{variables.nome}}`. Outros valores: `{{server.virtualserver_clientsonline}}`, `{{count}}`, `{{top}}`, `{{text}}` e `{{time}}`.
|
||||||
|
|
||||||
O primeiro arranque cria um rascunho **Estat?sticas**, desativado, para atualizar a descri??o do canal **21** a cada 30 segundos. O top inicial ordena as pessoas atualmente online pela data de entrada; n?o ? um ranking hist?rico de tempo acumulado.
|
O primeiro arranque cria um rascunho **Estatísticas**, desativado, para atualizar a descrição do canal **21** a cada 30 segundos. O top inicial ordena as pessoas atualmente online pela data de entrada; não é um ranking histórico de tempo acumulado.
|
||||||
|
|
||||||
Esta vers?o n?o inclui reprodu??o de m?sica, eventos SSH, webhooks, edi??o de permiss?es nem gest?o de perfis pelo painel. Cada bot tem um flow. O editor disponibiliza os blocos implementados acima.
|
Esta versão não inclui reprodução de música, eventos SSH, webhooks, edição de permissões nem gestão de perfis pelo painel. Cada bot tem um flow. O editor disponibiliza os blocos implementados acima.
|
||||||
|
|
||||||
## Docker / Portainer
|
## Docker / Portainer
|
||||||
|
|
||||||
Para Web editor com imagem constru?da no Gitea, consultar [deploy/README.md](deploy/README.md) e usar [deploy/bot-stack.yaml](deploy/bot-stack.yaml). Cada push em `main` executa os testes e publica a imagem. Depois do workflow verde, atualizar a stack no Portainer com download da imagem e recria??o do container. Restart sozinho n?o descarrega a nova imagem.
|
Para Web editor com imagem construída no Gitea, consultar [deploy/README.md](deploy/README.md) e usar [deploy/bot-stack.yaml](deploy/bot-stack.yaml). Cada push em `main` executa os testes e publica a imagem. Depois do workflow verde, atualizar a stack no Portainer com download da imagem e recriação do container. Restart sozinho não descarrega a nova imagem.
|
||||||
|
|
||||||
Para construir localmente com Docker Compose, usar `compose.yaml`. A aplica??o escuta na porta 3080 e preserva os dados num volume montado em `/app/data`.
|
Para construir localmente com Docker Compose, usar `compose.yaml`. A aplicação escuta na porta 3080 e preserva os dados num volume montado em `/app/data`.
|
||||||
|
|
||||||
Vari?veis de ambiente:
|
Variáveis de ambiente:
|
||||||
|
|
||||||
- `APP_URL`: endere?o do navegador, por exemplo `http://192.168.88.99:3080`.
|
- `APP_URL`: endereço do navegador, por exemplo `http://192.168.88.99:3080`.
|
||||||
- `DISCORD_CLIENT_ID`: ID da aplica??o Discord.
|
- `DISCORD_CLIENT_ID`: ID da aplicação Discord.
|
||||||
- `DISCORD_CLIENT_SECRET`: segredo OAuth da aplica??o.
|
- `DISCORD_CLIENT_SECRET`: segredo OAuth da aplicação.
|
||||||
- `OWNER_DISCORD_ID`: ID num?rico da conta do dono.
|
- `OWNER_DISCORD_ID`: ID numérico da conta do dono.
|
||||||
- `ALLOWED_DISCORD_IDS`: IDs de leitores separados por v?rgulas. Por defeito, `723613831196311603`; valor vazio remove todos os leitores.
|
- `ALLOWED_DISCORD_IDS`: IDs de leitores separados por vírgulas. Por defeito, `723613831196311603`; valor vazio remove todos os leitores.
|
||||||
|
|
||||||
No Discord Developer Portal, adicionar o redirect exato `http://192.168.88.99:3080/auth/discord/callback`, ajustando ao `APP_URL`. O navegador e o container precisam de Internet para o OAuth; o site pode continuar apenas na rede local.
|
No Discord Developer Portal, adicionar o redirect exato `http://192.168.88.99:3080/auth/discord/callback`, ajustando ao `APP_URL`. O navegador e o container precisam de Internet para o OAuth; o site pode continuar apenas na rede local.
|
||||||
|
|
||||||
O **Dono** pode configurar, guardar, simular e executar. O **Leitor** pode consultar. Outras contas s?o recusadas; as permiss?es s?o tamb?m verificadas no servidor.
|
O **Dono** pode configurar, guardar, simular e executar. O **Leitor** pode consultar. Outras contas são recusadas; as permissões são também verificadas no servidor.
|
||||||
|
|
||||||
## Ligar ao TeamSpeak
|
## Ligar ao TeamSpeak
|
||||||
|
|
||||||
Entrar com a conta do dono. Em **Liga??es**, configurar o endere?o WebQuery HTTP/HTTPS, o ID do servidor virtual e a API key TeamSpeak. Por exemplo, `http://teamspeak6:10080` funciona quando ambos os containers partilham uma rede Docker com esse nome resolv?vel. Testar a liga??o, escolher esse servidor na configura??o do bot, simular e s? depois ativar.
|
Entrar com a conta do dono. Em **Ligações**, configurar o endereço WebQuery HTTP/HTTPS, o ID do servidor virtual e a API key TeamSpeak. Por exemplo, `http://teamspeak6:10080` funciona quando ambos os containers partilham uma rede Docker com esse nome resolvível. Testar a ligação, escolher esse servidor na configuração do bot, simular e só depois ativar.
|
||||||
|
|
||||||
O temporizador aceita intervalos a partir de um segundo. A execu??o seguinte aguarda a conclus?o da anterior; pedidos de rede e blocos de espera podem prolongar o intervalo efetivo. A configura??o de permiss?es e prote??o contra flood do TeamSpeak deve permitir os pedidos do bot.
|
O temporizador aceita intervalos a partir de um segundo. A execução seguinte aguarda a conclusão da anterior; pedidos de rede e blocos de espera podem prolongar o intervalo efetivo. A configuração de permissões e proteção contra flood do TeamSpeak deve permitir os pedidos do bot.
|
||||||
|
|
||||||
Sess?es e flows usam SQLite no volume. A chave de cifra das API keys fica em `/app/data/credentials.key`; preservar o volume completo nas c?pias de seguran?a. O hist?rico ret?m as ?ltimas 500 execu??es. N?o adicionar `.env` nem dados ao Git.
|
Sessões e flows usam SQLite no volume. A chave de cifra das API keys fica em `/app/data/credentials.key`; preservar o volume completo nas cópias de segurança. O histórico retém as últimas 500 execuções. Não adicionar `.env` nem dados ao Git.
|
||||||
|
|
||||||
## Desenvolvimento e valida??o
|
## Desenvolvimento e validação
|
||||||
|
|
||||||
Node 22.15+, mesmas vari?veis de ambiente e `npm start`. Executar `npm test` para verificar autentica??o, restri??es de acesso, persist?ncia, simula??o e execu??o WebQuery com transporte de teste. O build Docker executa os testes antes da publica??o.
|
Node 22.15+, mesmas variáveis de ambiente e `npm start`. Executar `npm test` para verificar autenticação, restrições de acesso, persistência, simulação e execução WebQuery com transporte de teste. O build Docker executa os testes antes da publicação.
|
||||||
|
|
||||||
O editor foi verificado em navegador desktop e m?vel. A execu??o no TeamSpeak real depende da liga??o e API key configuradas na instala??o; os testes locais n?o alteram o servidor real.
|
O editor foi verificado em navegador desktop e móvel. A execução no TeamSpeak real depende da ligação e API key configuradas na instalação; os testes locais não alteram o servidor real.
|
||||||
|
|
||||||
A fonte Public Sans ? distribu?da em `public/fonts` com a licen?a OFL. Os log?tipos foram copiados sem altera??es do projeto existente.
|
A fonte Public Sans é distribuída em `public/fonts` com a licença OFL. Os logótipos foram copiados sem alterações do projeto existente.
|
||||||
|
|||||||
@ -21,7 +21,7 @@ function navigate(name){if(dirty&&!confirm('Sair sem guardar as alterações do
|
|||||||
function botCard(bot){const runs=data.executions.filter(e=>e.botId===bot.id);return `<article class="card bot-card"><div class="bot-card-top"><span class="icon-tile ${esc(bot.color)}">${icon(bot.color==='red'?'shield':bot.color==='orange'?'music':bot.color==='cyan'?'users':'bot')}</span><div class="bot-card-title"><h2>${esc(bot.name)}</h2><span class="badge ${bot.enabled?'green':'gray'}">${bot.enabled?'Ativo':'Rascunho'}</span></div><button class="switch ${bot.enabled?'on':''}" data-toggle="${bot.id}" aria-label="Ativar ou desativar ${esc(bot.name)}" aria-pressed="${bot.enabled}" ${dis()}></button></div><p class="bot-description">${esc(bot.description||'Personaliza o flow deste bot.')}</p><div class="bot-meta"><span>${icon('flow')}1 flow</span><span>${icon('play')}${runs.length} execuções</span><span>${icon('clock')}${ago(runs[0]?.startedAt)}</span></div><div class="bot-actions"><button class="button secondary" data-open="${bot.id}">${icon('flow')}Abrir flow</button><button class="icon-button bordered" data-config="${bot.id}" aria-label="Configurar bot">${icon('settings')}</button></div></article>`;}
|
function botCard(bot){const runs=data.executions.filter(e=>e.botId===bot.id);return `<article class="card bot-card"><div class="bot-card-top"><span class="icon-tile ${esc(bot.color)}">${icon(bot.color==='red'?'shield':bot.color==='orange'?'music':bot.color==='cyan'?'users':'bot')}</span><div class="bot-card-title"><h2>${esc(bot.name)}</h2><span class="badge ${bot.enabled?'green':'gray'}">${bot.enabled?'Ativo':'Rascunho'}</span></div><button class="switch ${bot.enabled?'on':''}" data-toggle="${bot.id}" aria-label="Ativar ou desativar ${esc(bot.name)}" aria-pressed="${bot.enabled}" ${dis()}></button></div><p class="bot-description">${esc(bot.description||'Personaliza o flow deste bot.')}</p><div class="bot-meta"><span>${icon('flow')}1 flow</span><span>${icon('play')}${runs.length} execuções</span><span>${icon('clock')}${ago(runs[0]?.startedAt)}</span></div><div class="bot-actions"><button class="button secondary" data-open="${bot.id}">${icon('flow')}Abrir flow</button><button class="icon-button bordered" data-config="${bot.id}" aria-label="Configurar bot">${icon('settings')}</button></div></article>`;}
|
||||||
function wireBots(){document.querySelectorAll('[data-open]').forEach(b=>b.onclick=()=>{botId=b.dataset.open;navigate('editor');});document.querySelectorAll('[data-config]').forEach(b=>b.onclick=()=>configureBot(b.dataset.config));const create=document.querySelector('#create-bot');if(create)create.onclick=()=>newBot();document.querySelectorAll('[data-toggle]').forEach(b=>b.onclick=async()=>{try{const bot=data.bots.find(bot=>bot.id===b.dataset.toggle);await api(`/api/bots/${bot.id}`,{method:'PUT',body:JSON.stringify({enabled:!bot.enabled})});await refresh();navigate(page);toast('Estado atualizado.');}catch(e){toast(e.message,true);}});}
|
function wireBots(){document.querySelectorAll('[data-open]').forEach(b=>b.onclick=()=>{botId=b.dataset.open;navigate('editor');});document.querySelectorAll('[data-config]').forEach(b=>b.onclick=()=>configureBot(b.dataset.config));const create=document.querySelector('#create-bot');if(create)create.onclick=()=>newBot();document.querySelectorAll('[data-toggle]').forEach(b=>b.onclick=async()=>{try{const bot=data.bots.find(bot=>bot.id===b.dataset.toggle);await api(`/api/bots/${bot.id}`,{method:'PUT',body:JSON.stringify({enabled:!bot.enabled})});await refresh();navigate(page);toast('Estado atualizado.');}catch(e){toast(e.message,true);}});}
|
||||||
function dashboardPage(){const today=new Date().toLocaleDateString('pt-PT',{timeZone:'Europe/Lisbon'});document.querySelector('#content').innerHTML=heading('Dashboard','Uma visão geral dos teus bots e automações.',button('create-bot','Criar bot'))+`<div class="stats">${[['Bots ativos',data.bots.filter(b=>b.enabled).length,'bot','green'],['Execuções hoje',data.executions.filter(e=>new Date(e.startedAt).toLocaleDateString('pt-PT',{timeZone:'Europe/Lisbon'})===today).length,'play','cyan'],['Flows guardados',data.bots.length,'flow','purple'],['Servidores configurados',data.connections.length,'link','orange']].map(([label,n,i,c])=>`<div class="card stat"><span class="icon-tile ${c}">${icon(i)}</span><div><span>${label}</span><strong>${n}</strong></div></div>`).join('')}</div><div class="section-heading"><h2>Os teus bots</h2><button class="text-button" id="all-bots">Ver todos →</button></div><div class="bot-grid">${data.bots.map(botCard).join('')}</div><div class="dashboard-bottom"><section class="card"><div class="section-heading"><h2>Atividade recente</h2><button class="text-button" id="all-runs">Ver execuções →</button></div>${data.executions.slice(0,4).map(runRow).join('')||empty('clock','Ainda sem execuções','Simula um flow para acompanhar aqui cada passo.')}</section><section class="card quick-start"><span class="icon-tile purple">${icon('flow')}</span><h2>O teu primeiro flow</h2><p>O rascunho Estatísticas prepara a descrição do canal 21. Escolhe a ligação e simula antes de ativar.</p><button class="button secondary" id="first-flow">${icon('flow')}Abrir editor</button></section></div>`;wireBots();document.querySelector('#all-bots').onclick=()=>navigate('bots');document.querySelector('#all-runs').onclick=()=>navigate('executions');document.querySelector('#first-flow').onclick=()=>navigate('editor');}
|
function dashboardPage(){const today=new Date().toLocaleDateString('pt-PT',{timeZone:'Europe/Lisbon'});document.querySelector('#content').innerHTML=heading('Dashboard','Uma visão geral dos teus bots e automações.',button('create-bot','Criar bot'))+`<div class="stats">${[['Bots ativos',data.bots.filter(b=>b.enabled).length,'bot','green'],['Execuções hoje',data.executions.filter(e=>new Date(e.startedAt).toLocaleDateString('pt-PT',{timeZone:'Europe/Lisbon'})===today).length,'play','cyan'],['Flows guardados',data.bots.length,'flow','purple'],['Servidores configurados',data.connections.length,'link','orange']].map(([label,n,i,c])=>`<div class="card stat"><span class="icon-tile ${c}">${icon(i)}</span><div><span>${label}</span><strong>${n}</strong></div></div>`).join('')}</div><div class="section-heading"><h2>Os teus bots</h2><button class="text-button" id="all-bots">Ver todos →</button></div><div class="bot-grid">${data.bots.map(botCard).join('')}</div><div class="dashboard-bottom"><section class="card"><div class="section-heading"><h2>Atividade recente</h2><button class="text-button" id="all-runs">Ver execuções →</button></div>${data.executions.slice(0,4).map(runRow).join('')||empty('clock','Ainda sem execuções','Simula um flow para acompanhar aqui cada passo.')}</section><section class="card quick-start"><span class="icon-tile purple">${icon('flow')}</span><h2>O teu primeiro flow</h2><p>O rascunho Estatísticas prepara a descrição do canal 21. Escolhe a ligação e simula antes de ativar.</p><button class="button secondary" id="first-flow">${icon('flow')}Abrir editor</button></section></div>`;wireBots();document.querySelector('#all-bots').onclick=()=>navigate('bots');document.querySelector('#all-runs').onclick=()=>navigate('executions');document.querySelector('#first-flow').onclick=()=>navigate('editor');}
|
||||||
function botsPage(){document.querySelector('#content').innerHTML=heading('Bots','Gere os teus bots do TeamSpeak num só lugar.',button('create-bot','Criar bot'))+`<div class="filterbar"><input id="search-bots" placeholder="Pesquisar bots…" aria-label="Pesquisar bots"><span class="muted">${data.bots.length} bots · ${data.bots.filter(b=>b.enabled).length} ativos</span></div><div class="bot-grid" id="bot-list">${data.bots.map(botCard).join('')}</div><div class="section-heading templates-heading"><h2>Começar um novo bot</h2><span class="muted">Rascunhos para personalizar no editor</span></div><div class="template-grid">${[['Estatísticas','green','bot'],['Moderação','red','shield'],['Boas-vindas','cyan','users'],['Música','orange','music']].map(([name,c,i])=>`<button class="template-card" data-template="${name}" ${dis()}><span class="icon-tile ${c}">${icon(i)}</span><strong>${name}</strong><p>Cria um rascunho e configura os seus blocos.</p><span>Criar bot →</span></button>`).join('')}</div>`;wireBots();document.querySelector('#search-bots').oninput=e=>{document.querySelector('#bot-list').innerHTML=data.bots.filter(b=>b.name.toLowerCase().includes(e.target.value.toLowerCase())).map(botCard).join('');wireBots();};document.querySelectorAll('[data-template]').forEach(b=>b.onclick=()=>newBot(b.dataset.template));}
|
function botsPage(){document.querySelector('#content').innerHTML=heading('Bots','Gere os teus bots do TeamSpeak num só lugar.',button('create-bot','Criar bot'))+`<div class="filterbar"><input id="search-bots" placeholder="Pesquisar bots…" aria-label="Pesquisar bots"><span class="muted">${data.bots.length} bots · ${data.bots.filter(b=>b.enabled).length} ativos</span></div><div class="bot-grid" id="bot-list">${data.bots.map(botCard).join('')}</div><div class="section-heading templates-heading"><h2>Começar um novo bot</h2><span class="muted">Rascunhos para personalizar no editor</span></div><div class="template-grid">${[['Estatísticas','green','bot'],['Personalizado','purple','flow']].map(([name,c,i])=>`<button class="template-card" data-template="${name}" ${dis()}><span class="icon-tile ${c}">${icon(i)}</span><strong>${name}</strong><p>Cria um rascunho e configura os seus blocos.</p><span>Criar bot →</span></button>`).join('')}</div>`;wireBots();document.querySelector('#search-bots').oninput=e=>{document.querySelector('#bot-list').innerHTML=data.bots.filter(b=>b.name.toLowerCase().includes(e.target.value.toLowerCase())).map(botCard).join('');wireBots();};document.querySelectorAll('[data-template]').forEach(b=>b.onclick=()=>newBot(b.dataset.template));}
|
||||||
function modal(title,body,submit){const dialog=document.querySelector('#dialog');dialog.innerHTML=`<form id="modal-form"><div class="modal-heading"><h2>${title}</h2><button class="icon-button" type="button" id="close-dialog" aria-label="Fechar">${icon('close')}</button></div>${body}<div class="modal-actions"><button type="button" class="button secondary" id="cancel">Cancelar</button><button class="button primary" ${dis()}>${icon('save')}Guardar</button></div></form>`;dialog.showModal();document.querySelector('#close-dialog').onclick=document.querySelector('#cancel').onclick=()=>dialog.close();document.querySelector('#modal-form').onsubmit=async e=>{e.preventDefault();try{await submit(new FormData(e.target));dialog.close();await refresh();dirty=false;navigate(page);toast('Guardado.');}catch(error){toast(error.message,true);}};}
|
function modal(title,body,submit){const dialog=document.querySelector('#dialog');dialog.innerHTML=`<form id="modal-form"><div class="modal-heading"><h2>${title}</h2><button class="icon-button" type="button" id="close-dialog" aria-label="Fechar">${icon('close')}</button></div>${body}<div class="modal-actions"><button type="button" class="button secondary" id="cancel">Cancelar</button><button class="button primary" ${dis()}>${icon('save')}Guardar</button></div></form>`;dialog.showModal();document.querySelector('#close-dialog').onclick=document.querySelector('#cancel').onclick=()=>dialog.close();document.querySelector('#modal-form').onsubmit=async e=>{e.preventDefault();try{await submit(new FormData(e.target));dialog.close();await refresh();dirty=false;navigate(page);toast('Guardado.');}catch(error){toast(error.message,true);}};}
|
||||||
function newBot(name='Novo bot'){modal('Criar bot',`<label>Nome<input name="name" value="${esc(name)}" required maxlength="80"></label><label>Descrição<textarea name="description"></textarea></label><label>Cor<select name="color">${['purple','green','red','cyan','orange'].map(c=>`<option>${c}</option>`).join('')}</select></label><p class="form-help">O bot começa desativado. Usa o flow de estatísticas como base ou substitui os blocos.</p>`,async form=>{const bot=await api('/api/bots',{method:'POST',body:JSON.stringify(Object.fromEntries(form))});botId=bot.id;page='editor';});}
|
function newBot(name='Novo bot'){modal('Criar bot',`<label>Nome<input name="name" value="${esc(name)}" required maxlength="80"></label><label>Descrição<textarea name="description"></textarea></label><label>Cor<select name="color">${['purple','green','red','cyan','orange'].map(c=>`<option>${c}</option>`).join('')}</select></label><p class="form-help">O bot começa desativado. Usa o flow de estatísticas como base ou substitui os blocos.</p>`,async form=>{const bot=await api('/api/bots',{method:'POST',body:JSON.stringify(Object.fromEntries(form))});botId=bot.id;page='editor';});}
|
||||||
function configureBot(id){botId=id;const bot=selectedBot();modal('Configuração do bot',`<div class="config-tabs"><span class="active">Geral</span><button type="button" id="config-vars">Variáveis</button></div><label>Nome<input name="name" value="${esc(bot.name)}" required></label><label>Descrição<textarea name="description">${esc(bot.description)}</textarea></label><label>Servidor<select name="connectionId"><option value="">Escolher ligação</option>${data.connections.map(c=>`<option value="${c.id}" ${bot.connectionId===c.id?'selected':''}>${esc(c.name)}</option>`).join('')}</select></label><p class="form-help">Escolhe uma ligação para executar o flow no TeamSpeak.</p>`,f=>api(`/api/bots/${bot.id}`,{method:'PUT',body:JSON.stringify(Object.fromEntries(f))}));document.querySelector('#config-vars').onclick=()=>{document.querySelector('#dialog').close();navigate('variables');};}
|
function configureBot(id){botId=id;const bot=selectedBot();modal('Configuração do bot',`<div class="config-tabs"><span class="active">Geral</span><button type="button" id="config-vars">Variáveis</button></div><label>Nome<input name="name" value="${esc(bot.name)}" required></label><label>Descrição<textarea name="description">${esc(bot.description)}</textarea></label><label>Servidor<select name="connectionId"><option value="">Escolher ligação</option>${data.connections.map(c=>`<option value="${c.id}" ${bot.connectionId===c.id?'selected':''}>${esc(c.name)}</option>`).join('')}</select></label><p class="form-help">Escolhe uma ligação para executar o flow no TeamSpeak.</p>`,f=>api(`/api/bots/${bot.id}`,{method:'PUT',body:JSON.stringify(Object.fromEntries(f))}));document.querySelector('#config-vars').onclick=()=>{document.querySelector('#dialog').close();navigate('variables');};}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ with others.
|
|||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
redistributed and/or sold with any software provided that any reserved
|
redistributed and/or sold with any software provided that any reserved
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
however, cannot be released under any other type of license. The
|
however, cannot be released under any other type of license. The
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user