<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Rustploy</title>
<meta name="description" content="Infrastructure management for professional teams" />
<meta name="theme-color" content="#0A0E14" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/favicon.png" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<link rel="stylesheet" href="/app.css" />
<script type="module" src="/app.js"></script>
</head>
<body>
<div class="screen" x-data x-cloak>
<column class="login_wrap" x-show="$store.app.screen === 'login'" x-data="login">
<column class="login_card">
<row class="login_head">
<svg width="34" height="34" viewBox="0 0 64 64" aria-hidden="true">
<rect width="64" height="64" rx="12" fill="#1b1d22"/>
<path d="M32 8 C32 8 44 18 44 34 L32 44 L20 34 C20 18 32 8 32 8Z" fill="#4ccfe6"/>
<circle cx="32" cy="28" r="5" fill="#1b1d22"/>
<circle cx="32" cy="28" r="3" fill="#4ccfe6" opacity="0.5"/>
<path d="M20 34 L14 44 L22 40Z" fill="#3ab8cf"/>
<path d="M44 34 L50 44 L42 40Z" fill="#3ab8cf"/>
<ellipse cx="32" cy="45" rx="5" ry="3" fill="#e6a040" opacity="0.9"/>
<ellipse cx="32" cy="49" rx="3" ry="4" fill="#e67840" opacity="0.6"/>
</svg>
<column class="login_brand">
<text class="login_title">Rustploy</text>
<text class="subtitle">Infrastructure management for professional teams</text>
</column>
</row>
<form @submit.prevent="submit()">
<column class="field_group">
<row class="field_row">
<text class="field_label" style="flex:1">ACCESS TOKEN</text>
<text class="field_label">SECURE</text>
</row>
<input class="field_input" type="password" placeholder="token de acesso"
x-model="$store.app.token" />
</column>
<column class="login_checks" style="margin-top:16px">
<label class="check_row">
<input type="checkbox" x-model="$store.app.rememberToken" @change="$store.app.persistPrefs()" />
<span>Remember token</span>
</label>
</column>
<button type="submit" class="connect_btn" style="margin-top:16px"
x-text="$store.app.statusLine === 'conectando…' ? 'Conectando…' : 'Connect'"
:disabled="$store.app.statusLine === 'conectando…'"></button>
</form>
<text class="err" x-show="$store.app.error" x-text="'⚠ ' + $store.app.error"></text>
<hr class="rule" style="margin:4px 0" />
<row class="status_row">
<text class="ok" style="font-size:10px">●</text>
<text class="muted" x-text="$store.app.statusLine"></text>
</row>
</column>
</column>
<row class="screen" x-show="$store.app.screen === 'shell'">
<column class="sidebar">
<column class="brand_box">
<text class="brand">Rustploy</text>
<text class="brand_sub">PRODUCTION CLUSTER</text>
</column>
<text class="nav_section">DEPLOYMENT OPS</text>
<button type="button" @click="$store.app.nav('deployments')"
:class="$store.app.view === 'deployments' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'deployments' ? 'nav_icon_on' : 'nav_icon_off'">▶</text>
<text :class="$store.app.view === 'deployments' ? 'nav_label_on' : 'nav_label_off'">Deployments</text>
</button>
<button type="button" @click="$store.app.nav('deploy_engine')"
:class="$store.app.view === 'deploy_engine' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'deploy_engine' ? 'nav_icon_on' : 'nav_icon_off'">⚡</text>
<text :class="$store.app.view === 'deploy_engine' ? 'nav_label_on' : 'nav_label_off'">Deploy Engine</text>
</button>
<button type="button" @click="$store.app.nav('monitoring')"
:class="$store.app.view === 'monitoring' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'monitoring' ? 'nav_icon_on' : 'nav_icon_off'">◔</text>
<text :class="$store.app.view === 'monitoring' ? 'nav_label_on' : 'nav_label_off'">Monitoring</text>
</button>
<button type="button" @click="$store.app.nav('schedules')"
:class="$store.app.view === 'schedules' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'schedules' ? 'nav_icon_on' : 'nav_icon_off'">◫</text>
<text :class="$store.app.view === 'schedules' ? 'nav_label_on' : 'nav_label_off'">Schedules</text>
</button>
<button type="button" @click="$store.app.nav('ingress')"
:class="$store.app.view === 'ingress' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'ingress' ? 'nav_icon_on' : 'nav_icon_off'">↦</text>
<text :class="$store.app.view === 'ingress' ? 'nav_label_on' : 'nav_label_off'">Ingress</text>
</button>
<button type="button" @click="$store.app.nav('docker')"
:class="$store.app.view === 'docker' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'docker' ? 'nav_icon_on' : 'nav_icon_off'">◉</text>
<text :class="$store.app.view === 'docker' ? 'nav_label_on' : 'nav_label_off'">Docker</text>
</button>
<text class="nav_section">MANAGEMENT</text>
<button type="button" @click="$store.app.nav('projects')"
:class="['projects','project','new_service','service'].includes($store.app.view) ? 'nav_row_on' : 'nav_row_off'">
<text :class="['projects','project','new_service','service'].includes($store.app.view) ? 'nav_icon_on' : 'nav_icon_off'">◧</text>
<text :class="['projects','project','new_service','service'].includes($store.app.view) ? 'nav_label_on' : 'nav_label_off'">Projects</text>
</button>
<column class="nav_spacer"></column>
<hr class="rule" />
<button type="button" @click="$store.app.nav('settings')"
:class="$store.app.view === 'settings' ? 'nav_row_on' : 'nav_row_off'">
<text :class="$store.app.view === 'settings' ? 'nav_icon_on' : 'nav_icon_off'">⌥</text>
<text :class="$store.app.view === 'settings' ? 'nav_label_on' : 'nav_label_off'">Settings</text>
</button>
</column>
<column class="main_col">
<row class="topbar">
<input class="search" type="text" placeholder="Search deployments, nodes…"
x-model="$store.app.search" @input="$store.app.searchChanged($event.target.value)" />
<text class="muted topbar_status" x-text="'● daemon v' + $store.app.daemonVersion"></text>
<button class="btn_danger" type="button" @click="$store.app.stopAll()">Stop All</button>
<button class="btn_ghost" type="button" @click="$store.app.disconnect()">Disconnect</button>
</row>
<hr class="rule" />
<column class="content" x-data="dashboard" x-show="$store.app.view === 'deployments'">
<row class="header_row">
<column class="header_titles">
<text class="title">Deployments</text>
<text class="subtitle">Real-time status of services in the cluster.</text>
</column>
<row class="stats_row stats_hide_lg">
<column class="stat_card">
<text class="label_cap">STATUS</text>
<text class="stat_num_g" x-text="store.servicesLabel"></text>
</column>
<column class="stat_card">
<text class="label_cap">UPTIME</text>
<text class="stat_num" x-text="store.daemonUptime"></text>
</column>
</row>
<button class="btn_danger" type="button" @click="store.clearFinished()">Limpar parados/falhos</button>
</row>
<text class="muted" x-show="store.deploymentsMsg" x-text="store.deploymentsMsg"></text>
<column class="panel_fill">
<row class="thead">
<text class="th col_svc">SERVIÇO</text>
<text class="th col_proj">PROJETO</text>
<text class="th col_state">ESTADO</text>
<text class="th col_dur">DURAÇÃO</text>
<text class="th col_start">INÍCIO</text>
</row>
<column class="content_center" x-show="store.dataLoading" style="padding:24px">
<text class="muted">carregando…</text>
</column>
<column class="scroll_fill" x-show="!store.dataLoading">
<column class="table_body">
<template x-for="d in rows" :key="d.service + d.start">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="d.service"></text>
<text class="td col_proj" x-text="d.project"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + d.stateKind">●</text>
<text class="state_lbl" :class="'state_' + d.stateKind" x-text="d.stateLabel"></text>
</row>
<text class="td col_dur" x-text="d.duration"></text>
<text class="muted col_start" x-text="d.start"></text>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="rows" style="padding:16px">
Nenhum deployment ainda.
</text>
</column>
</column>
</column>
</column>
<column class="content" x-data="deployEngine" x-show="$store.app.view === 'deploy_engine'">
<row class="header_row">
<column class="header_titles">
<text class="title">Deploy Engine</text>
<text class="subtitle">Pipeline de deploy: em andamento e histórico recente.</text>
</column>
<row class="stats_row stats_hide_lg">
<column class="stat_card">
<text class="label_cap">ATIVOS</text>
<text class="stat_num_g" x-text="active.length"></text>
</column>
<column class="stat_card">
<text class="label_cap">SUCESSO 24H</text>
<text class="stat_num" x-text="successCount"></text>
</column>
<column class="stat_card">
<text class="label_cap">FALHAS 24H</text>
<text class="stat_num" x-text="failedCount"></text>
</column>
<column class="stat_card">
<text class="label_cap">TOTAL 24H</text>
<text class="stat_num" x-text="totalCount"></text>
</column>
<column class="stat_card">
<text class="label_cap">UPTIME</text>
<text class="stat_num" x-text="uptime"></text>
</column>
</row>
</row>
<column class="section">
<row class="section_head">
<text class="section_title" x-text="'NA FILA (' + queued.length + ')'"></text>
<button class="copy_btn" type="button" @click="store.queueTogglePause()"
x-text="paused ? 'Retomar fila' : 'Pausar fila'"></button>
</row>
<column class="section_body">
<text class="banner_warn" x-show="paused">Fila pausada — deploys esperam até você retomar.</text>
<text class="empty_state" x-fallback="queued">Nenhum deploy na fila.</text>
<column class="table_body">
<template x-for="q in queued" :key="q.deploymentId">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="q.pos + '. ' + q.service"></text>
<text class="td col_start" x-text="q.project"></text>
<button class="copy_btn" type="button" @click="store.queuePromote(q.deploymentId)">↑ topo</button>
<button class="env_del" type="button" @click="store.queueCancel(q.deploymentId)">✕</button>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="section">
<row class="section_head">
<text class="section_title" x-text="'EXECUTANDO AGORA (' + active.length + ')'"></text>
</row>
<column class="section_body">
<row class="thead" x-show="active.length > 0">
<text class="th col_svc">SERVIÇO</text>
<text class="th col_proj">PROJETO</text>
<text class="th col_state">ESTADO</text>
<text class="th col_dur">TOTAL</text>
<text class="th col_start">FASE</text>
<text class="th col_act"></text>
</row>
<column class="table_body">
<text class="empty_state" x-fallback="active">Nenhum deploy em andamento.</text>
<template x-for="a in active" :key="a.serviceId">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="a.service"></text>
<text class="td col_proj" x-text="a.project"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + a.stateKind">●</text>
<text class="state_lbl" :class="'state_' + a.stateKind" x-text="a.stateLabel"></text>
</row>
<text class="muted col_dur" x-text="a.total"></text>
<text class="muted col_start" x-text="a.phase"></text>
<row class="col_act">
<button class="btn_sm_ghost" type="button" @click="openDetail(a.serviceId)">Detalhes</button>
</row>
</row>
<row class="stepper">
<template x-for="(st, i) in a.steps" :key="st.label">
<row class="step_item">
<text class="step_sep" x-show="i > 0">›</text>
<text class="step" :class="'step_' + st.status"
x-text="(st.status === 'done' ? '✓ ' : st.status === 'failed' ? '✕ ' : st.status === 'current' ? '● ' : '○ ') + st.label"></text>
</row>
</template>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="section_fill">
<row class="section_head">
<text class="section_title" x-text="'HISTÓRICO 24H (' + recent.length + ')'"></text>
</row>
<column class="section_body">
<row class="thead" x-show="recent.length > 0">
<text class="th col_icon"></text>
<text class="th col_svc">SERVIÇO</text>
<text class="th col_proj">PROJETO</text>
<text class="th col_state">ESTADO</text>
<text class="th col_dur">DURAÇÃO</text>
<text class="th col_start">INÍCIO</text>
</row>
</column>
<row class="empty_wrap" x-fallback="recent">
<text class="empty_state">Nenhum deploy concluído nas últimas 24h.</text>
</row>
<column class="section_scroll" x-show="recent.length > 0">
<column class="table_body">
<template x-for="(r, i) in recent" :key="r.service + r.start + i">
<column class="row_wrap">
<row class="trow">
<text class="td col_icon" x-text="r.icon"></text>
<text class="td_key col_svc" x-text="r.service"></text>
<text class="td col_proj" x-text="r.project"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + r.stateKind">●</text>
<text class="state_lbl" :class="'state_' + r.stateKind" x-text="r.stateLabel"></text>
</row>
<text class="muted col_dur" x-text="r.duration"></text>
<text class="muted col_start" x-text="r.start"></text>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="modal_backdrop" x-show="detail" @click.self="closeDetail()">
<column class="modal_box">
<row class="modal_head">
<text class="label_cap" x-text="'DEPLOY · ' + (detail ? detail.service : '')"></text>
<button class="btn_ghost" type="button" @click="closeDetail()">Fechar</button>
</row>
<column class="modal_body" x-show="detail">
<row class="stepper">
<template x-for="(st, i) in (detail ? detail.steps : [])" :key="st.label">
<row class="step_item">
<text class="step_sep" x-show="i > 0">›</text>
<text class="step" :class="'step_' + st.status"
x-text="(st.status === 'done' ? '✓ ' : st.status === 'failed' ? '✕ ' : st.status === 'current' ? '● ' : '○ ') + st.label"></text>
</row>
</template>
</row>
<row class="thead">
<text class="th col_state">ESTADO</text>
<text class="th col_dur">DURAÇÃO</text>
<text class="th col_start">MENSAGEM</text>
</row>
<template x-for="(d, i) in (detail ? detail.detail : [])" :key="i">
<column class="row_wrap">
<row class="trow">
<text class="td col_state" :class="'state_' + d.kind" x-text="d.label"></text>
<text class="muted col_dur" x-text="d.dur"></text>
<text class="muted col_start step_msg" x-text="d.msg"></text>
</row>
<hr class="rule" />
</column>
</template>
<text class="empty_state" x-show="detail && detail.detail.length === 0">Sem histórico de transições.</text>
</column>
</column>
</column>
</column>
<column class="content" x-data="monitoring" x-show="$store.app.view === 'monitoring'">
<row class="header_row">
<column class="header_titles">
<text class="title">Monitoring</text>
<text class="subtitle">Host and per-container resource usage.</text>
</column>
<row class="stats_row stats_hide_md">
<column class="stat_card">
<text class="label_cap">CPU</text>
<text class="stat_num_g" x-text="store.sysCpu"></text>
</column>
<column class="stat_card">
<text class="label_cap">MEMORY</text>
<text class="stat_num" x-text="store.sysMem"></text>
</column>
<column class="stat_card">
<text class="label_cap">DISK</text>
<text class="stat_num" x-text="store.sysDisk"></text>
</column>
<column class="stat_card">
<text class="label_cap">LOAD</text>
<text class="stat_num" x-text="store.sysLoad"></text>
</column>
</row>
</row>
<column class="panel_fill">
<row class="thead">
<text class="th col_svc">CONTAINER</text>
<text class="th col_proj">PROJETO</text>
<text class="th col_dur">CPU</text>
<text class="th col_dur">MEM</text>
<text class="th col_dur">RX</text>
<text class="th col_start">TX</text>
</row>
<column class="scroll_fill">
<column class="table_body">
<template x-for="m in rows" :key="m.name + m.project">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="m.name"></text>
<text class="td col_proj" x-text="m.project"></text>
<text class="td col_dur" x-text="m.cpu"></text>
<text class="td col_dur" x-text="m.mem"></text>
<text class="muted col_dur" x-text="m.rx"></text>
<text class="muted col_start" x-text="m.tx"></text>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="rows" style="padding:16px">
Nenhuma métrica ainda — aguardando o primeiro container em execução.
</text>
</column>
</column>
</column>
</column>
<column class="content" x-data="ingress" x-show="$store.app.view === 'ingress'">
<row class="header_row">
<column class="header_titles">
<text class="title">Ingress</text>
<text class="subtitle">Active routes in the reverse proxy.</text>
</column>
<row class="stats_row">
<column class="stat_card">
<text class="label_cap">ROUTES</text>
<text class="stat_num_g" x-text="routes.length"></text>
</column>
<column class="stat_card">
<text class="label_cap">PORTAS DE HOST</text>
<text class="stat_num" x-text="hostPorts.length"></text>
</column>
</row>
</row>
<column class="panel_fill">
<row class="thead">
<text class="th col_svc">DOMÍNIO</text>
<text class="th col_proj">SERVIÇO</text>
<text class="th col_dur">UPSTREAM</text>
<text class="th col_start">TLS</text>
</row>
<column class="scroll_fill">
<column class="table_body">
<template x-for="i in routes" :key="i.domain">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="i.domain"></text>
<text class="td col_proj" x-text="i.service"></text>
<text class="td col_dur" x-text="i.upstream"></text>
<text class="muted col_start" x-text="i.tls"></text>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
<text class="label_cap" x-text="'PORTAS TCP DE HOST (' + hostPorts.length + ')'"></text>
<row class="thead">
<text class="th col_svc">SERVIÇO</text>
<text class="th col_proj">PROJETO</text>
<text class="th col_dur">PORTA HOST</text>
<text class="th col_start">PORTA CONTAINER</text>
</row>
<text class="muted" x-fallback="hostPorts" style="padding:12px">
Nenhum serviço com porta de host configurada.
</text>
<column class="table_body">
<template x-for="hp in hostPorts" :key="hp.service">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="hp.service"></text>
<text class="td col_proj" x-text="hp.project"></text>
<text class="td col_dur" x-text="hp.hostPort"></text>
<text class="muted col_start" x-text="hp.containerPort"></text>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="content" x-data="docker" x-show="$store.app.view === 'docker'">
<row class="header_row">
<column class="header_titles">
<text class="title">Docker</text>
<text class="subtitle">Containers, imagens, volumes e networks do host.</text>
</column>
<row class="stats_row stats_hide_md">
<column class="stat_card">
<text class="label_cap">CONTAINERS</text>
<text class="stat_num_g" x-text="store.snap?.docker_containers?.length || 0"></text>
</column>
<column class="stat_card">
<text class="label_cap">IMAGENS</text>
<text class="stat_num" x-text="store.snap?.docker_images?.length || 0"></text>
</column>
<column class="stat_card">
<text class="label_cap">VOLUMES</text>
<text class="stat_num" x-text="store.snap?.docker_volumes?.length || 0"></text>
</column>
<column class="stat_card">
<text class="label_cap">NETWORKS</text>
<text class="stat_num_g" x-text="store.snap?.docker_networks?.length || 0"></text>
</column>
</row>
</row>
<row class="tabs">
<button type="button" :class="store.dockerTab === 'containers' ? 'tab_on' : 'tab_off'"
@click="store.dockerSetTab('containers')">Containers</button>
<button type="button" :class="store.dockerTab === 'images' ? 'tab_on' : 'tab_off'"
@click="store.dockerSetTab('images')">Images</button>
<button type="button" :class="store.dockerTab === 'volumes' ? 'tab_on' : 'tab_off'"
@click="store.dockerSetTab('volumes')">Volumes</button>
<button type="button" :class="store.dockerTab === 'networks' ? 'tab_on' : 'tab_off'"
@click="store.dockerSetTab('networks')">Networks</button>
<button type="button" :class="store.dockerTab === 'registry' ? 'tab_on' : 'tab_off'"
@click="store.dockerSetTab('registry')">Registry</button>
</row>
<hr class="rule" />
<column class="panel_fill" x-show="store.dockerTab === 'containers'">
<row class="env_head">
<text class="muted_fill" x-text="containers.length + ' container(s)'"></text>
<button class="btn_danger" type="button" @click="store.dockerPruneContainers()">Limpar parados</button>
</row>
<row class="thead">
<text class="th col_svc">NOME</text>
<text class="th col_img">IMAGEM</text>
<text class="th col_state">DONO</text>
<text class="th col_dur">ESTADO</text>
<text class="th col_act">AÇÃO</text>
</row>
<column class="scroll_fill">
<column class="table_body">
<template x-for="c in containers" :key="c.idFull">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="c.name"></text>
<text class="td col_img" x-text="c.image"></text>
<text class="muted col_state" x-text="c.owner"></text>
<row class="state_cell col_dur">
<text class="state_dot" :class="'state_' + c.stateKind">●</text>
<text class="state_lbl" :class="'state_' + c.stateKind" x-text="c.stateLabel"></text>
</row>
<row class="col_act">
<button class="dk_rm" type="button" x-show="c.canRemove"
@click="store.dockerRemoveContainer(c.idFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="containers" style="padding:16px">Nenhum container.</text>
</column>
</column>
</column>
<column class="panel_fill" x-show="store.dockerTab === 'images'">
<row class="env_head">
<text class="muted_fill" x-text="images.length + ' imagem(ns)'"></text>
<label class="check_row">
<input type="checkbox" x-model="store.onlyUsedImages" /><span>Somente em uso</span>
</label>
<label class="check_row">
<input type="checkbox" x-model="store.pruneAllImages" /><span>-a (limpar tudo, não só dangling)</span>
</label>
<button class="btn_danger" type="button" @click="store.dockerPruneImages()">Limpar sem uso</button>
</row>
<row class="thead">
<text class="th col_svc">TAGS</text>
<text class="th col_proj">PROJETO / SERVIÇO</text>
<text class="th col_dur">TAMANHO</text>
<text class="th col_dur">CRIADA</text>
<text class="th col_state">USO</text>
<text class="th col_act">AÇÃO</text>
</row>
<column class="scroll_fill">
<column class="table_body">
<template x-for="i in images" :key="i.idFull">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="i.tags"></text>
<text class="td col_proj" x-text="i.owner"></text>
<text class="muted col_dur" x-text="i.size"></text>
<text class="muted col_dur" x-text="i.created"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + i.inUseKind">●</text>
<text class="state_lbl" :class="'state_' + i.inUseKind" x-text="i.inUseLabel"></text>
</row>
<row class="col_act">
<button class="dk_rm" type="button" x-show="!i.inUse"
@click="store.dockerRemoveImage(i.idFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="images" style="padding:16px">Nenhuma imagem.</text>
</column>
</column>
</column>
<column class="panel_fill" x-show="store.dockerTab === 'volumes'">
<row class="env_head">
<text class="muted_fill" x-text="volumes.length + ' volume(s)'"></text>
<label class="check_row">
<input type="checkbox" x-model="store.onlyUsedVolumes" /><span>Somente em uso</span>
</label>
<label class="check_row">
<input type="checkbox" x-model="store.pruneAllVolumes" /><span>-a (limpar tudo, não só anônimos)</span>
</label>
<button class="btn_danger" type="button" @click="store.dockerPruneVolumes()">Limpar sem uso</button>
</row>
<row class="thead">
<text class="th col_svc">NOME</text>
<text class="th col_proj">DRIVER</text>
<text class="th col_dur">TAMANHO</text>
<text class="th col_state">USO</text>
<text class="th col_act">AÇÃO</text>
</row>
<column class="scroll_fill">
<column class="table_body">
<template x-for="v in volumes" :key="v.nameFull">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="v.name"></text>
<text class="td col_proj" x-text="v.driver"></text>
<text class="muted col_dur" x-text="v.size"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + v.inUseKind">●</text>
<text class="state_lbl" :class="'state_' + v.inUseKind" x-text="v.inUseLabel"></text>
</row>
<row class="col_act">
<button class="dk_rm" type="button" x-show="!v.inUse"
@click="store.dockerRemoveVolume(v.nameFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="volumes" style="padding:16px">Nenhum volume.</text>
</column>
</column>
</column>
<column class="panel_fill" x-show="store.dockerTab === 'networks'">
<row class="env_head">
<text class="muted_fill" x-text="networks.length + ' network(s)'"></text>
<label class="check_row">
<input type="checkbox" x-model="store.onlyUsedNetworks" /><span>Somente em uso</span>
</label>
<button class="btn_danger" type="button" @click="store.dockerPruneNetworks()">Limpar sem uso</button>
</row>
<row class="thead">
<text class="th col_svc">NOME</text>
<text class="th col_proj">PROJETO</text>
<text class="th col_dur">DRIVER</text>
<text class="th col_dur">CONTAINERS</text>
<text class="th col_state">USO</text>
<text class="th col_act">AÇÃO</text>
</row>
<column class="scroll_fill">
<column class="table_body">
<template x-for="n in networks" :key="n.idFull">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="n.name"></text>
<text class="td col_proj" x-text="n.project"></text>
<text class="muted col_dur" x-text="n.driver"></text>
<text class="muted col_dur" x-text="n.containers"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + n.inUseKind">●</text>
<text class="state_lbl" :class="'state_' + n.inUseKind" x-text="n.inUseLabel"></text>
</row>
<row class="col_act">
<button class="dk_rm" type="button" x-show="!n.inUse"
@click="store.dockerRemoveNetwork(n.idFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="networks" style="padding:16px">Nenhuma network.</text>
</column>
</column>
</column>
<column class="panel_fill" x-show="store.dockerTab === 'registry'">
<row class="env_head">
<text class="muted_fill"
x-text="'Registry ' + registryStatusLabel + ' · ' + repos.length + ' repositório(s)'"></text>
<button class="btn_danger" type="button" x-show="store.snap?.registry_status?.enabled"
@click="store.registryGc()">Executar GC</button>
</row>
<column x-show="!store.registrySelectedRepo">
<row class="thead">
<text class="th col_svc">REPOSITÓRIO</text>
<text class="th col_proj">TAGS</text>
<text class="th col_dur">TAMANHO</text>
<text class="th col_dur">CRIADO</text>
<text class="th col_act_2">AÇÃO</text>
</row>
<text class="muted" x-fallback="store.snap?.registry_repos" style="padding:16px">
Nenhum repositório ainda. Faça `docker push` para o registry embutido.
</text>
<template x-if="(store.snap?.registry_repos?.length || 0) > 0">
<text class="muted" x-fallback="repos" style="padding:16px">
Nenhum repositório corresponde à busca.
</text>
</template>
<column class="scroll_fill">
<column class="table_body">
<template x-for="r in repos" :key="r.name">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="r.name"></text>
<text class="td col_proj" x-text="r.tagCount"></text>
<text class="muted col_dur" x-text="r.size"></text>
<text class="muted col_dur" x-text="r.created"></text>
<row class="col_act_2">
<button class="copy_btn" type="button" title="Ver tags"
@click="store.registryOpenRepo(r.name)">≡</button>
<button class="env_del" type="button" title="Remover"
@click="store.registryRmRepo(r.name)">✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
<text class="label_cap" x-text="'TOKENS DE ACESSO (' + tokens.length + ')'"></text>
<row class="env_head">
<text class="muted_fill">Basic auth (docker login) — o segredo só aparece uma vez, na criação.</text>
<button class="btn_primary" type="button" @click="openTokenModal()">+ Novo token</button>
</row>
<text class="muted" x-fallback="tokens" style="padding:12px">
Nenhum token ainda. Sem token nenhum push/pull é autenticado — crie um antes de usar o registry.
</text>
<row class="thead" x-show="tokens.length > 0">
<text class="th col_svc">NOME</text>
<text class="th col_proj">ESCOPO</text>
<text class="th col_dur">CRIADO</text>
<text class="th col_dur">ÚLTIMO USO</text>
<text class="th col_act">AÇÃO</text>
</row>
<column class="table_body">
<template x-for="tk in tokens" :key="tk.name">
<row class="trow">
<text class="td_key col_svc" x-text="tk.name"></text>
<text class="td col_proj" x-text="tk.scope"></text>
<text class="muted col_dur" x-text="tk.created"></text>
<text class="muted col_dur" x-text="tk.lastUsed"></text>
<row class="col_act">
<button class="env_del" type="button" title="Revogar"
@click="store.registryRmToken(tk.name)">✕</button>
</row>
</row>
<hr class="rule" />
</template>
</column>
</column>
<column x-show="store.registrySelectedRepo">
<row class="env_head">
<text class="muted_fill" x-text="store.registrySelectedRepo + ' — ' + tags.length + ' tag(s)'"></text>
<button class="btn_ghost" type="button" @click="store.registryCloseRepo()">← Voltar</button>
</row>
<row class="thead">
<text class="th col_svc">TAG</text>
<text class="th col_proj">DIGEST</text>
<text class="th col_dur">TAMANHO</text>
<text class="th col_dur">ATUALIZADA</text>
<text class="th col_act">AÇÃO</text>
</row>
<column class="content_center" x-show="store.registryTagsLoading" style="padding:24px">
<text class="muted">carregando…</text>
</column>
<column class="scroll_fill" x-show="!store.registryTagsLoading">
<column class="table_body">
<template x-for="t in tags" :key="t.tag">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="t.tag"></text>
<text class="muted col_proj" x-text="t.digest"></text>
<text class="muted col_dur" x-text="t.size"></text>
<text class="muted col_dur" x-text="t.updated"></text>
<row class="col_act">
<button class="dk_rm" type="button" @click="store.registryRmTag(t.tag)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
</column>
<column class="modal_backdrop" x-show="showTokenModal" @click.self="closeTokenModal()">
<column class="modal_box">
<row class="modal_head">
<text class="label_cap">NOVO TOKEN DE REGISTRY</text>
<button class="btn_ghost" type="button" @click="closeTokenModal()">Fechar</button>
</row>
<column class="modal_body">
<template x-if="ntokStep === 'form'">
<column class="form_panel">
<text class="label_cap">NOME</text>
<input class="field_input" type="text" x-model="ntokName" placeholder="ci-deploy" />
<text class="label_cap" style="margin-top:14px">ESCOPO</text>
<row class="tabs">
<button type="button" :class="ntokScope === 'pull' ? 'tab_on' : 'tab_off'"
@click="ntokScope = 'pull'">pull</button>
<button type="button" :class="ntokScope === 'push' ? 'tab_on' : 'tab_off'"
@click="ntokScope = 'push'">push</button>
</row>
<text class="err" x-show="ntokErr" x-text="ntokErr" style="margin-top:8px"></text>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="ntokCreate()">Criar token</button>
</row>
</column>
</template>
<template x-if="ntokStep === 'reveal'">
<column class="form_panel">
<text class="muted_fill">
Guarde este comando agora — o segredo não é mostrado de novo.
</text>
<pre class="log_pre" x-text="ntokLoginCmd" style="margin-top:10px"></pre>
<row class="env_add" style="margin-top:10px">
<button class="copy_btn" type="button" @click="navigator.clipboard.writeText(ntokLoginCmd)">Copiar</button>
<button class="btn_primary" type="button" @click="closeTokenModal()">Concluir</button>
</row>
</column>
</template>
</column>
</column>
</column>
</column>
<column class="content" x-data="schedules" x-show="$store.app.view === 'schedules'">
<row class="header_row">
<column class="header_titles">
<text class="title">Schedules</text>
<text class="subtitle">Tarefas one-shot (docker-compose) agendadas ou manuais, de todos os projetos.</text>
</column>
<row class="stats_row stats_hide_md">
<column class="stat_card">
<text class="label_cap">JOBS</text>
<text class="stat_num_g" x-text="rows.length"></text>
</column>
</row>
<button class="btn_primary" type="button" @click="store.openNewJob()">+ Novo job</button>
</row>
<column class="panel_fill">
<row class="thead">
<text class="th col_svc">NOME</text>
<text class="th col_proj">PROJETO / GATILHO</text>
<text class="th col_state">ÚLTIMA EXECUÇÃO</text>
<text class="th col_dur">RECORRÊNCIA</text>
<text class="th col_dur">PRÓXIMA EXECUÇÃO</text>
<text class="th col_act_5">AÇÕES</text>
</row>
<text class="muted" x-fallback="rows" style="padding:16px">Nenhum job cadastrado ainda.</text>
<column class="scroll_fill">
<column class="table_body">
<template x-for="j in rows" :key="j.id">
<column class="row_wrap">
<row class="trow">
<text class="td_key col_svc" x-text="j.name"></text>
<text class="td col_proj" x-text="j.owner"></text>
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + j.lastRunKind">●</text>
<text class="state_lbl" :class="'state_' + j.lastRunKind" x-text="j.lastRunLabel"></text>
</row>
<text class="muted col_dur" x-text="j.recurrence"></text>
<text class="muted col_dur" x-text="j.nextRunAt"></text>
<row class="col_act_5">
<button class="copy_btn" type="button" title="Rodar agora" x-show="!j.running"
@click="store.jobRunNow(j.id)">▶</button>
<button class="env_del" type="button" title="Cancelar" x-show="j.running"
@click="store.jobRunCancel(j.lastRunId)">■</button>
<button class="copy_btn" type="button" title="Ver logs" x-show="j.lastRunId"
@click="store.openJobLogs(j.lastRunId)">≡</button>
<button class="copy_btn" type="button" :title="j.enabledLabel" @click="store.jobToggle(j.id)"
x-text="j.enabled ? '‖' : '▶'"></button>
<button class="copy_btn" type="button" title="Editar" @click="store.openEditJob(j.id)">✎</button>
<button class="env_del" type="button" title="Remover" @click="store.jobDelete(j.id)">✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
</column>
<column class="modal_backdrop" x-show="$store.app.showNewJob" @click.self="$store.app.closeNewJob()">
<column class="modal_box">
<row class="modal_head">
<text class="label_cap" x-text="$store.app.njobEditId ? 'EDITAR JOB' : 'NOVO JOB'"></text>
<row style="gap:8px">
<button class="btn_ghost" type="button"
x-show="$store.app.njobStep !== 'pick_project' && !$store.app.njobEditId"
@click="$store.app.njobBack()">‹ Voltar</button>
<button class="btn_ghost" type="button" @click="$store.app.closeNewJob()">Fechar</button>
</row>
</row>
<column class="modal_body">
<column class="grid" x-show="$store.app.njobStep === 'pick_project'">
<template x-for="p in $store.app.njobProjects" :key="p.id">
<column class="card">
<text class="card_name" x-text="p.name"></text>
<button class="card_open" type="button" @click="$store.app.njobPickProject(p.id, p.name)">Selecionar ›</button>
</column>
</template>
<text class="muted" x-fallback="$store.app.njobProjects" style="padding:16px">Nenhum projeto ainda.</text>
</column>
<column class="grid" x-show="$store.app.njobStep === 'pick_service'">
<column class="card">
<text class="card_name">Nenhum (autônomo)</text>
<text class="muted">Job sem serviço gatilho — só variáveis do projeto.</text>
<button class="card_open" type="button" @click="$store.app.njobPickNoService()">Selecionar ›</button>
</column>
<template x-for="e in $store.app.njobServicesFiltered" :key="e.service.id">
<column class="card">
<text class="card_name" x-text="e.service.spec.name"></text>
<button class="card_open" type="button"
@click="$store.app.njobPickService(e.service.id, e.service.spec.name)">Selecionar ›</button>
</column>
</template>
</column>
<column class="form_panel" x-show="$store.app.njobStep === 'form'" style="max-width:100%">
<text class="muted_fill" x-text="$store.app.njobProjectName + ' / ' + ($store.app.njobServiceName || 'nenhum (autônomo)')"></text>
<text class="label_cap" style="margin-top:10px">NOME</text>
<input class="field_input" type="text" x-model="$store.app.njobName" placeholder="backup-diario" />
<text class="label_cap" style="margin-top:14px">FONTE DO COMPOSE</text>
<row class="tabs">
<button type="button" :class="$store.app.njobSourceTab === 'compose' ? 'tab_on' : 'tab_off'"
@click="$store.app.njobSetSourceTab('compose')">Compose</button>
<button type="button" :class="$store.app.njobSourceTab === 'git' ? 'tab_on' : 'tab_off'"
@click="$store.app.njobSetSourceTab('git')">Git</button>
</row>
<template x-if="$store.app.njobSourceTab === 'compose'">
<column>
<text class="label_cap" style="margin-top:14px">DOCKER COMPOSE (YAML)</text>
<textarea class="env_editor" x-model="$store.app.njobCompose"></textarea>
</column>
</template>
<template x-if="$store.app.njobSourceTab === 'git'">
<column style="margin-top:10px; gap:10px">
<text class="muted">
Selecione a conta conectada, o repositório e a branch. O
repo precisa já ter um docker-compose.yml no caminho
informado.
</text>
<text class="muted" x-show="$store.app.njobGitMsg" x-text="$store.app.njobGitMsg"></text>
<text class="label_cap">CONTA GIT</text>
<select class="field_input" x-model="$store.app.njobGitProviderId"
@change="$store.app.njobGitProviderPick($event.target.value)">
<option value="">selecione a conta</option>
<template x-for="p in $store.app.njobGitProviders" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
<text class="muted">Conecte contas em Settings → Git.</text>
<text class="label_cap">REPOSITÓRIO</text>
<select class="field_input" x-model="$store.app.njobGitRepoFullName"
@change="$store.app.njobGitRepoPick($event.target.value)">
<option value="">selecione o repositório</option>
<template x-for="r in $store.app.njobGitRepos" :key="r.full_name">
<option :value="r.full_name" x-text="r.full_name"></option>
</template>
</select>
<text class="label_cap">BRANCH</text>
<select class="field_input" x-model="$store.app.njobGitBranch">
<option value="">selecione a branch</option>
<template x-for="b in $store.app.njobGitBranches" :key="b.name">
<option :value="b.name" x-text="b.name"></option>
</template>
</select>
<text class="label_cap">CAMINHO DO COMPOSE NO REPO</text>
<input class="field_input" type="text" x-model="$store.app.njobComposePath" placeholder="docker-compose.yml" />
</column>
</template>
<text class="label_cap" style="margin-top:14px">MAIN SERVICE</text>
<input class="field_input" type="text" x-model="$store.app.njobMainService" placeholder="job" />
<text class="label_cap" style="margin-top:14px">VARIÁVEIS DE AMBIENTE (opcional)</text>
<text class="muted">
Sobrescrevem as do projeto/serviço gatilho por cima. Formato
KEY=VALUE, uma por linha (# comentário).
</text>
<textarea class="env_editor" x-model="$store.app.njobEnvText" placeholder="FLOW_MYENV=test"></textarea>
<text class="label_cap" style="margin-top:14px">RECORRÊNCIA</text>
<row class="tabs">
<button type="button" :class="$store.app.njobKind === 'manual' ? 'tab_on' : 'tab_off'" @click="$store.app.njobKind = 'manual'">Manual</button>
<button type="button" :class="$store.app.njobKind === 'interval' ? 'tab_on' : 'tab_off'" @click="$store.app.njobKind = 'interval'">Intervalo</button>
<button type="button" :class="$store.app.njobKind === 'daily' ? 'tab_on' : 'tab_off'" @click="$store.app.njobKind = 'daily'">Diário</button>
<button type="button" :class="$store.app.njobKind === 'weekly' ? 'tab_on' : 'tab_off'" @click="$store.app.njobKind = 'weekly'">Semanal</button>
</row>
<row class="hc_grid" style="margin-top:8px" x-show="$store.app.njobKind === 'interval'">
<column class="hc_field">
<text class="label_cap">A CADA (HORAS)</text>
<input class="field_input" type="number" min="1" max="168" x-model="$store.app.njobHours" />
</column>
</row>
<row class="hc_grid" style="margin-top:8px" x-show="$store.app.njobKind === 'daily' || $store.app.njobKind === 'weekly'">
<column class="hc_field" x-show="$store.app.njobKind === 'weekly'">
<text class="label_cap">DIA DA SEMANA</text>
<select class="field_input" x-model="$store.app.njobWeekday">
<option value="0">Seg</option>
<option value="1">Ter</option>
<option value="2">Qua</option>
<option value="3">Qui</option>
<option value="4">Sex</option>
<option value="5">Sáb</option>
<option value="6">Dom</option>
</select>
</column>
<column class="hc_field">
<text class="label_cap">HORÁRIO</text>
<input class="field_input" type="time" x-model="$store.app.njobTime" />
</column>
</row>
<text class="err" x-show="$store.app.njobErr" x-text="$store.app.njobErr" style="margin-top:8px"></text>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="$store.app.njobCreate()" :disabled="$store.app.njobSubmitting"
x-text="$store.app.njobSubmitting ? ($store.app.njobEditId ? 'Salvando…' : 'Criando…') : ($store.app.njobEditId ? 'Salvar' : 'Criar job')"></button>
</row>
</column>
</column>
</column>
</column>
<column class="modal_backdrop" x-show="$store.app.jobLogsFor" @click.self="$store.app.closeJobLogs()">
<column class="modal_box">
<row class="modal_head">
<text class="label_cap">LOGS DO JOB</text>
<button class="btn_ghost" type="button" @click="$store.app.closeJobLogs()">Fechar</button>
</row>
<column class="modal_body log_panel">
<column class="scroll_fill">
<column class="log_body">
<template x-for="(l, i) in $store.app.jobLogLines" :key="i">
<row class="log_line">
<text class="log_time" x-text="$store.app.timeHms(l.timestamp)"></text>
<text class="log_text" :style="l.stream === 'Stderr' ? 'color:#F85149' : ''" x-text="l.line"></text>
</row>
</template>
<text class="muted" x-fallback="$store.app.jobLogLines" style="padding:12px">
Aguardando linhas de log…
</text>
</column>
</column>
</column>
</column>
</column>
<column class="content" x-data="settings" x-show="$store.app.view === 'settings'">
<row class="header_row">
<column class="header_titles">
<text class="title">Settings</text>
<text class="subtitle">Daemon web server, HTTPS e contas Git.</text>
</column>
</row>
<row class="tabs">
<button type="button" :class="store.settingsTab === 'web' ? 'tab_on' : 'tab_off'"
@click="store.settingsTab = 'web'">Web Server</button>
<button type="button" :class="store.settingsTab === 'git' ? 'tab_on' : 'tab_off'"
@click="store.settingsTab = 'git'">Git</button>
<button type="button" :class="store.settingsTab === 'iac' ? 'tab_on' : 'tab_off'"
@click="store.settingsTab = 'iac'">Infra as Code</button>
<button type="button" :class="store.settingsTab === 'maintenance' ? 'tab_on' : 'tab_off'"
@click="store.settingsTab = 'maintenance'">Manutenção</button>
</row>
<hr class="rule" />
<column class="scroll_fill" x-show="store.settingsTab === 'web'">
<column class="form_panel">
<text class="label_cap">URL PÚBLICA DA API</text>
<row class="kv_row">
<text class="kv_val_fill" x-text="store.ssPublicBase || '—'"></text>
<button class="copy_btn" type="button" @click="navigator.clipboard.writeText(store.ssPublicBase)">Copiar</button>
</row>
<text class="muted">
Derivada de [api] domain/port no daemon — é a base das URLs de webhook dos serviços e
do callback OAuth (mesma porta da API). Para mudá-la, configure api.domain e reinicie o daemon.
</text>
<text class="label_cap" style="margin-top:14px">E-MAIL LET'S ENCRYPT (HTTPS AUTOMÁTICO)</text>
<input class="field_input" type="text" x-model="store.ssEmail" placeholder="voce@exemplo.com" />
<text class="muted">Certificados TLS emitidos automaticamente. Requer restart do daemon.</text>
<text class="label_cap" style="margin-top:14px">DOMÍNIO DO REGISTRY (EXPOSIÇÃO PÚBLICA)</text>
<input class="field_input" type="text" x-model="store.ssRegistryDomain" placeholder="registry.meusite.com" />
<text class="muted">
Expõe o registry Docker embutido via ingress + certificado automático. Requer [registry]
enabled = true na config. Vazio = só acessível em 127.0.0.1.
</text>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="store.settingsSave()">Save</button>
<text class="muted" x-text="store.settingsMsg"></text>
</row>
</column>
</column>
<column class="scroll_fill" x-show="store.settingsTab === 'git'">
<column class="form_panel">
<text class="label_cap" x-text="'CONTAS CONECTADAS (' + gitProviderRows.length + ')'"></text>
<text class="muted" x-fallback="gitProviderRows">Nenhuma conta Git conectada ainda.</text>
<column class="kv_list">
<template x-for="p in gitProviderRows" :key="p.id">
<row class="kv_row">
<column class="header_titles">
<text class="env_key" x-text="p.name"></text>
<text class="muted" x-text="p.kind + ' · ' + p.baseUrl + ' · ' + p.authMode + ' · ' + p.account"></text>
</column>
<button class="btn_danger" type="button" @click="store.gpDelete(p.id)">Remover</button>
</row>
</template>
</column>
<hr class="rule" />
<text class="label_cap">CONECTAR CONTA GIT</text>
<row class="tabs">
<button type="button" :class="store.gpKind === 'gitea' ? 'tab_on' : 'tab_off'"
@click="store.gpKind = 'gitea'">Gitea</button>
<button type="button" :class="store.gpKind === 'github' ? 'tab_on' : 'tab_off'"
@click="store.gpKind = 'github'">GitHub</button>
</row>
<text class="muted" x-show="store.gpMsg" x-text="store.gpMsg"></text>
<button class="btn_primary" type="button" x-show="store.gpOauthUrl"
@click="window.open(store.gpOauthUrl, '_blank')">Abrir autorização no navegador</button>
<row class="hc_grid" style="margin-top:8px">
<column class="hc_field">
<text class="label_cap">NOME</text>
<input class="field_input" type="text" x-model="store.gpName" placeholder="Meu Gitea" />
</column>
<column class="hc_field">
<text class="label_cap">BASE URL</text>
<input class="field_input" type="text" x-model="store.gpBaseUrl" placeholder="https://gitea.exemplo.com" />
<text class="muted" x-show="store.gpKind === 'github'">
Deixe vazio para github.com. Só preencha para GitHub Enterprise Server.
</text>
</column>
</row>
<text class="label_cap" style="margin-top:14px">MÉTODO</text>
<row class="tabs">
<button type="button" :class="store.gpMode === 'oauth' ? 'tab_on' : 'tab_off'"
@click="store.gpMode = 'oauth'">OAuth2</button>
<button type="button" :class="store.gpMode === 'pat' ? 'tab_on' : 'tab_off'"
@click="store.gpMode = 'pat'">Token (PAT)</button>
</row>
<template x-if="store.gpMode !== 'pat'">
<column style="gap:8px; margin-top:8px">
<text class="label_cap">CLIENT ID</text>
<input class="field_input" type="text" x-model="store.gpClientId" />
<text class="label_cap">CLIENT SECRET</text>
<input class="field_input" type="password" x-model="store.gpClientSecret" />
<text class="label_cap">REDIRECT URI (cole no app OAuth do provedor)</text>
<row class="kv_row">
<text class="kv_val_fill" x-text="store.gpRedirect"></text>
<button class="copy_btn" type="button" @click="navigator.clipboard.writeText(store.gpRedirect)">Copiar</button>
</row>
<text class="muted" x-show="store.gpKind !== 'github'">
No Gitea: Settings → Applications → crie um OAuth2 app e cole a Redirect URI acima.
</text>
<text class="muted" x-show="store.gpKind === 'github'">
No GitHub: Settings → Developer settings → OAuth Apps → New OAuth App e cole a Redirect
URI acima em "Authorization callback URL".
</text>
</column>
</template>
<template x-if="store.gpMode === 'pat'">
<column style="gap:8px; margin-top:8px">
<text class="label_cap">PERSONAL ACCESS TOKEN</text>
<input class="field_input" type="password" x-model="store.gpPat" />
<text class="muted" x-show="store.gpKind !== 'github'">
No Gitea: Settings → Applications → Generate New Token (escopo: repo).
</text>
<text class="muted" x-show="store.gpKind === 'github'">
No GitHub: Settings → Developer settings → Personal access tokens → gere um token
com escopo "repo".
</text>
</column>
</template>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="store.gpConnect()">Conectar conta</button>
<button class="btn_ghost" type="button" @click="store.gpRefresh()">Atualizar lista</button>
</row>
</column>
</column>
<column class="scroll_fill" x-show="store.settingsTab === 'iac'">
<column class="form_panel">
<text class="label_cap">EXPORTAR</text>
<text class="muted">
Gera um YAML com todos os projetos e serviços — valores de env var nunca aparecem no
YAML, só o nome entre cifrão-chaves. Os valores reais das vars (exceto secrets, que
nunca saem do daemon) vão no TOML ao lado, aninhado por projeto → serviço.
</text>
<row class="env_add">
<button class="btn_primary" type="button" @click="store.iacExport()">Exportar tudo</button>
<text class="muted" x-text="store.iacExportMsg"></text>
</row>
<template x-if="store.iacHasExport">
<column>
<row class="env_head">
<text class="label_cap">rustploy.yml</text>
<button class="copy_btn" type="button" @click="navigator.clipboard.writeText(store.iacYaml)">Copiar</button>
</row>
<textarea class="env_editor" readonly x-model="store.iacYaml"></textarea>
<row class="env_head">
<text class="label_cap">variáveis.toml</text>
<button class="copy_btn" type="button" @click="navigator.clipboard.writeText(store.iacDotenv)">Copiar</button>
</row>
<textarea class="env_editor" readonly x-model="store.iacDotenv"></textarea>
</column>
</template>
<hr class="rule" />
<text class="label_cap">IMPORTAR</text>
<text class="muted">
Cole o YAML e o TOML de variáveis correspondente. Falta o valor de alguma variável usada
no YAML? O import é cancelado antes de aplicar qualquer coisa.
</text>
<text class="label_cap">rustploy.yml</text>
<textarea class="env_editor" x-model="store.iacImportYaml"
placeholder="Cole aqui o YAML exportado (ou escrito à mão)"></textarea>
<text class="label_cap">variáveis.toml</text>
<textarea class="env_editor" x-model="store.iacImportDotenv"
placeholder='Cole aqui o TOML de variáveis ([project."App".env] com DB_PASS = "s3cr3t")'></textarea>
<label class="check_row">
<input type="checkbox" x-model="store.iacPrune" /><span>Remover serviços ausentes do manifesto (prune)</span>
</label>
<label class="check_row">
<input type="checkbox" x-model="store.iacDeploy" /><span>Disparar deploy dos serviços criados/alterados</span>
</label>
<row class="env_add" style="margin-top:8px">
<button class="btn_primary" type="button" @click="store.iacImport()">Importar</button>
<text class="muted" x-text="store.iacImportMsg"></text>
</row>
<text class="err" x-show="store.iacHasMissing" x-text="'Variáveis sem valor no TOML: ' + store.iacMissingVars"></text>
<template x-if="store.iacHasReport">
<column>
<text class="label_cap">RESULTADO</text>
<column class="kv_list">
<template x-for="(l, i) in store.iacReportLines" :key="i">
<row class="kv_row"><text class="muted" x-text="l"></text></row>
</template>
</column>
</column>
</template>
</column>
</column>
<column class="scroll_fill" x-show="store.settingsTab === 'maintenance'">
<column class="form_panel">
<text class="label_cap">LIMPEZA AUTOMÁTICA</text>
<label class="check_row">
<input type="checkbox" x-model="store.dcEnabled" /><span>Ativar limpeza automática</span>
</label>
<text class="muted">
Roda sozinha, no horário abaixo, os recursos marcados nesta tela.
Desligada por padrão — nada é removido até você marcar pelo menos
um recurso e ativar.
</text>
<text class="label_cap" style="margin-top:14px">FREQUÊNCIA</text>
<row class="tabs">
<button type="button" :class="store.dcKind === 'interval' ? 'tab_on' : 'tab_off'"
@click="store.dcKind = 'interval'">A cada N horas</button>
<button type="button" :class="store.dcKind === 'daily' ? 'tab_on' : 'tab_off'"
@click="store.dcKind = 'daily'">Todo dia</button>
<button type="button" :class="store.dcKind === 'weekly' ? 'tab_on' : 'tab_off'"
@click="store.dcKind = 'weekly'">Toda semana</button>
</row>
<row class="hc_grid" x-show="store.dcKind === 'interval'" style="margin-top:8px">
<column class="hc_field">
<text class="label_cap">A CADA (HORAS)</text>
<input class="field_input" type="number" min="1" max="168" x-model="store.dcHours" placeholder="6" />
</column>
</row>
<row class="tabs" x-show="store.dcKind === 'weekly'" style="margin-top:8px">
<button type="button" :class="store.dcWeekday === '0' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '0'">Seg</button>
<button type="button" :class="store.dcWeekday === '1' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '1'">Ter</button>
<button type="button" :class="store.dcWeekday === '2' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '2'">Qua</button>
<button type="button" :class="store.dcWeekday === '3' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '3'">Qui</button>
<button type="button" :class="store.dcWeekday === '4' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '4'">Sex</button>
<button type="button" :class="store.dcWeekday === '5' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '5'">Sáb</button>
<button type="button" :class="store.dcWeekday === '6' ? 'tab_on' : 'tab_off'" @click="store.dcWeekday = '6'">Dom</button>
</row>
<row class="hc_grid" x-show="store.dcKind !== 'interval'" style="margin-top:8px">
<column class="hc_field">
<text class="label_cap">HORÁRIO</text>
<input class="field_input" type="time" x-model="store.dcTime" />
</column>
</row>
<text class="muted" style="margin-top:8px" x-text="'Próxima execução agendada: ' + store.dcNextRunLabel"></text>
<hr class="rule" />
<text class="label_cap">O QUE LIMPAR</text>
<label class="check_row">
<input type="checkbox" x-model="store.dcContainers" /><span>Containers parados</span>
</label>
<label class="check_row">
<input type="checkbox" x-model="store.dcImages" /><span>Imagens sem uso</span>
</label>
<label class="check_row" x-show="store.dcImages" style="margin-left:20px">
<input type="checkbox" x-model="store.dcImagesAll" /><span>incluir todas as não usadas (não só as sem tag)</span>
</label>
<label class="check_row">
<input type="checkbox" x-model="store.dcNetworks" /><span>Redes sem uso</span>
</label>
<label class="check_row">
<input type="checkbox" x-model="store.dcBuildCache" /><span>Cache de build</span>
</label>
<hr class="rule" />
<text class="label_cap err">VOLUMES — CUIDADO</text>
<text class="muted">
Volumes sem uso criados por fora do rustploy (não pelos seus
serviços, que só usam bind mount) podem conter dado que ninguém
recriaria sozinho. Confirme que sabe o que existe no host antes
de ativar.
</text>
<label class="check_row">
<input type="checkbox" x-model="store.dcVolumes" /><span>Volumes sem uso</span>
</label>
<label class="check_row" x-show="store.dcVolumes" style="margin-left:20px">
<input type="checkbox" x-model="store.dcVolumesAll" /><span>incluir todos os sem uso (não só os anônimos)</span>
</label>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="store.dcSave()">Salvar</button>
<button class="tab_off" type="button" x-show="!store.dcRunning" @click="store.dcRunNow()">Executar agora</button>
<button class="tab_off" type="button" x-show="store.dcRunning" disabled>Executando…</button>
<text class="muted" x-text="store.dcMsg"></text>
</row>
<hr class="rule" />
<text class="label_cap">ÚLTIMA EXECUÇÃO</text>
<text class="muted" x-text="store.dcLastRunText"></text>
</column>
</column>
</column>
<column class="content" x-data="projects" x-show="$store.app.view === 'projects'">
<row class="header_row">
<column class="header_titles">
<text class="title">Projects</text>
<text class="subtitle">Projetos do cluster e a contagem de serviços de cada um.</text>
</column>
</row>
<row class="env_add">
<button class="btn_primary" type="button" @click="showNewForm = !showNewForm"
x-text="showNewForm ? 'Cancelar' : '+ Novo projeto'"></button>
</row>
<column class="form_panel" x-show="showNewForm" style="max-width:480px">
<input class="field_input" type="text" placeholder="Nome do projeto" x-model="newName" />
<input class="field_input" type="text" placeholder="Descrição (opcional)" x-model="newDesc"
style="margin-top:8px" />
<text class="err" x-show="newError" x-text="newError" style="margin-top:6px"></text>
<row class="env_add" style="margin-top:10px">
<button class="btn_primary" type="button" @click="submitNew()">Criar</button>
</row>
</column>
<column class="scroll_fill">
<column class="grid">
<template x-for="row in rows" :key="row.id">
<column class="card">
<row class="card_head">
<text class="card_name" x-text="row.name"></text>
</row>
<text class="muted" x-text="row.serviceCount + ' serviço(s) · ' + row.runningCount + ' rodando'"></text>
<text class="muted" x-text="row.description || '—'"></text>
<hr class="rule" />
<template x-if="editingId === row.id">
<column style="gap:8px; margin-top:8px">
<input class="field_input" type="text" x-model="editName" />
<input class="field_input" type="text" x-model="editDesc" />
<row class="env_add">
<button class="btn_primary" type="button" @click="saveEdit()">Salvar</button>
<button class="btn_ghost" type="button" @click="cancelEdit()">Cancelar</button>
</row>
</column>
</template>
<template x-if="editingId !== row.id">
<column style="gap:6px; margin-top:8px">
<row style="gap:8px; align-items:center">
<button class="card_open" type="button" @click="store.openProject(row.id)">Abrir ›</button>
<button class="card_edit" type="button" @click="startEdit(row)">Editar</button>
<button class="card_remove" type="button" x-show="row.canDelete"
@click="store.deleteProject(row.id)">Remover</button>
</row>
<text class="muted" x-show="!row.canDelete">Não removível: tem serviços</text>
</column>
</template>
</column>
</template>
<text class="muted" x-fallback="rows" style="padding:16px">Nenhum projeto ainda.</text>
</column>
</column>
</column>
<column class="content" x-data="projectDetail" x-show="$store.app.view === 'project'">
<row class="proj_topbar">
<button class="btn_ghost" type="button" @click="store.nav('projects')">‹ Projetos</button>
<column class="hspacer"></column>
<row class="proj_actions" x-show="!editing">
<button class="btn_sm_primary" type="button" @click="store.openNewService()">+ Novo serviço</button>
<button class="btn_sm_ghost" type="button" @click="startEdit()">Editar projeto</button>
<button class="btn_sm_danger" type="button" x-show="canDelete"
@click="store.deleteProject(store.selectedProjectId)">Remover projeto</button>
</row>
</row>
<template x-if="editing">
<column class="field_group" style="max-width:480px">
<input class="field_input" type="text" x-model="editName" />
<input class="field_input" type="text" x-model="editDesc" style="margin-top:8px" />
<row class="env_add" style="margin-top:8px">
<button class="btn_primary" type="button" @click="saveEdit()">Salvar</button>
<button class="btn_ghost" type="button" @click="cancelEdit()">Cancelar</button>
</row>
</column>
</template>
<template x-if="!editing">
<row class="header_row">
<column class="header_titles">
<text class="title" x-text="project ? project.name : ''"></text>
<text class="subtitle" x-text="project ? (project.description || '') : ''"></text>
</column>
</row>
</template>
<row class="tabs">
<button type="button" :class="projTab === 'services' ? 'tab_on' : 'tab_off'"
@click="projTab = 'services'">Serviços</button>
<button type="button" :class="projTab === 'env' ? 'tab_on' : 'tab_off'"
@click="projTab = 'env'" x-text="'Variáveis (' + (project ? project.env_vars.length : 0) + ')'"></button>
<button type="button" :class="projTab === 'secrets' ? 'tab_on' : 'tab_off'"
@click="projTab = 'secrets'" x-text="'Secrets (' + secrets.length + ')'"></button>
<button type="button" :class="projTab === 'jobs' ? 'tab_on' : 'tab_off'"
@click="projTab = 'jobs'" x-text="'Jobs (' + jobs.length + ')'"></button>
</row>
<hr class="rule" />
<column class="scroll_fill" x-show="projTab === 'services'">
<column class="grid">
<template x-for="s in services" :key="s.id">
<column class="card">
<row class="card_head">
<row style="align-items:center; gap:6px">
<text class="c_name" x-text="s.name"></text>
<text class="c_port" x-text="':' + s.port"></text>
</row>
<row class="badge">
<text class="state_dot" :class="'state_' + s.statusKind">●</text>
<text class="badge_lbl" :class="'state_' + s.statusKind" x-text="s.statusLabel"></text>
</row>
</row>
<row class="card_ctr">
<text class="muted" x-text="'name: ' + s.containerName"></text>
</row>
<row style="gap:6px">
<text class="muted" x-text="'id: ' + s.containerId"></text>
<text class="muted" x-show="s.containerExtra" x-text="'extra: ' + s.containerExtra"></text>
</row>
<hr class="rule" />
<row class="card_stats">
<column class="card_stat">
<text class="label_cap">CPU</text>
<text class="card_stat_num" x-text="s.cpu"></text>
</column>
<column class="card_stat">
<text class="label_cap">MEM</text>
<text class="card_stat_num" x-text="s.mem"></text>
</column>
</row>
<button class="card_open" type="button" style="width:100%"
@click="store.openService(s.id)">Abrir ›</button>
<row class="card_actions">
<button class="card_stop" type="button" @click="store.stopService(s.id)">Parar</button>
<button class="card_remove" type="button" @click="store.stopAndDeleteService(s.id)">Remover</button>
</row>
</column>
</template>
<text class="muted" x-fallback="services" style="padding:16px">
Nenhum serviço neste projeto ainda.
</text>
</column>
</column>
<column class="env_wrap" x-show="projTab === 'env'">
<row class="env_head">
<text class="muted_fill">Herdadas por todos os serviços deste projeto no deploy.</text>
<button class="btn_export" type="button" @click="openEnvText()" x-show="!envTextOpen">.env</button>
<button class="btn_export_on" type="button" @click="closeEnvText()" x-show="envTextOpen">Fechar</button>
</row>
<template x-if="!envTextOpen">
<column style="gap:10px">
<row class="env_add">
<input class="env_in_key" type="text" placeholder="KEY" x-model="newEnvKey" />
<input class="env_in_val" type="text" placeholder="value" x-model="newEnvValue" />
<button class="btn_primary" type="button" @click="addEnvVar()">Adicionar</button>
</row>
<column class="kv_list">
<template x-for="e in envVars" :key="e.key">
<column style="width:100%">
<row class="kv_row" x-show="e.isComment">
<text class="muted" x-text="e.text"></text>
</row>
<row class="kv_row" x-show="!e.isComment">
<button class="env_del" type="button" @click="delEnvVar(e.key)">✕</button>
<text class="env_sep"></text>
<text class="env_key" x-text="e.key"></text>
<text class="kv_val_fill" x-text="e.isSecret ? '(secret)' : e.value"></text>
</row>
</column>
</template>
<text class="muted" x-fallback="envVars" style="padding:12px">Nenhuma variável.</text>
</column>
</column>
</template>
<template x-if="envTextOpen">
<column style="gap:8px">
<text class="muted">Cole/edite no formato KEY=VALUE (# comentário).</text>
<textarea class="env_editor" x-model="envText" placeholder="KEY=VALUE (# comentário)"></textarea>
<row class="env_add">
<button class="btn_primary" type="button" @click="saveEnvText()">Salvar</button>
<button class="btn_ghost" type="button" @click="closeEnvText()">Cancelar</button>
</row>
</column>
</template>
</column>
<column class="env_wrap" x-show="projTab === 'secrets'">
<text class="muted_fill">
Valores sensíveis (tokens, senhas, chaves de API) cifrados com a chave mestre do
servidor. Depois de salvo, o valor nunca mais é exibido — para trocá-lo, salve um novo
com o mesmo nome. Para usar num serviço, escreva <secret:NOME> como valor de uma
variável de ambiente.
</text>
<row class="env_add">
<input class="env_in_key" type="text" placeholder="NOME" x-model="newSecretName" />
<input class="env_in_val" type="password" placeholder="valor (cifrado ao salvar)" x-model="newSecretValue" />
<button class="btn_primary" type="button" @click="submitSecret()">Salvar secret</button>
</row>
<column class="kv_list">
<template x-for="s in secrets" :key="s.name">
<row class="kv_row">
<button class="env_del" type="button" @click="store.deleteSecret(s.name)">✕</button>
<text class="env_sep"></text>
<text class="env_key" x-text="s.name"></text>
<text class="kv_val_fill">cifrado</text>
</row>
</template>
<text class="muted" x-fallback="secrets" style="padding:12px">
Nenhum secret neste projeto ainda.
</text>
</column>
</column>
<column class="env_wrap" x-show="projTab === 'jobs'">
<row class="env_head">
<text class="muted_fill" x-text="jobs.length + ' job(s)'"></text>
<button class="btn_sm_primary" type="button" @click="store.openNewJob()">+ Novo job</button>
</row>
<text class="muted" x-fallback="jobs" style="padding:12px">
Nenhum job cadastrado ainda.
</text>
<column class="kv_list">
<template x-for="j in jobs" :key="j.id">
<row class="kv_row">
<text class="env_key" x-text="j.name"></text>
<text class="muted col_dur" x-text="j.recurrence"></text>
<row class="state_cell">
<text class="state_dot" :class="'state_' + j.lastRunKind">●</text>
<text class="state_lbl" :class="'state_' + j.lastRunKind" x-text="j.lastRunLabel"></text>
</row>
<text class="muted kv_val_fill" x-text="'→ ' + j.nextRunAt"></text>
<button class="copy_btn" type="button" x-show="!j.running" @click="store.jobRunNow(j.id)">Rodar agora</button>
<button class="env_del" type="button" x-show="j.running" @click="store.jobRunCancel(j.lastRunId)">Cancelar</button>
<button class="copy_btn" type="button" x-show="j.lastRunId" @click="store.openJobLogs(j.lastRunId)">Ver logs</button>
<button class="copy_btn" type="button" @click="store.jobToggle(j.id)" x-text="j.enabledLabel"></button>
<button class="copy_btn" type="button" @click="store.openEditJob(j.id)">Editar</button>
<button class="env_del" type="button" @click="store.jobDelete(j.id)">✕</button>
</row>
</template>
</column>
</column>
</column>
<column class="content" x-data="newService" x-show="$store.app.view === 'new_service'">
<row class="header_row">
<column class="header_titles">
<text class="title">Novo serviço</text>
<text class="subtitle" x-show="step === 'pick_type'">Escolha o tipo de serviço.</text>
</column>
<row class="proj_actions">
<button class="btn_sm_ghost" type="button" @click="gotoType()" x-show="step !== 'pick_type'">‹ Tipo</button>
<button class="btn_sm_ghost" type="button" @click="cancel()">Cancelar</button>
</row>
</row>
<text class="err" x-show="error" x-text="error"></text>
<column class="grid" x-show="step === 'pick_type'">
<column class="card">
<text class="card_name">Application</text>
<text class="muted">Repositório Git (build) ou imagem pronta de registry.</text>
<button class="card_open" type="button" @click="gotoApp()">Selecionar ›</button>
</column>
<column class="card">
<text class="card_name">Compose Stack</text>
<text class="muted">Cole um docker-compose.yml próprio.</text>
<button class="card_open" type="button" @click="gotoCompose()">Selecionar ›</button>
</column>
<column class="card">
<text class="card_name">Database</text>
<text class="muted">PostgreSQL, MySQL, MariaDB, MongoDB ou Redis já configurados.</text>
<button class="card_open" type="button" @click="gotoDb()">Selecionar ›</button>
</column>
<column class="card">
<text class="card_name">Broker</text>
<text class="muted">Kafka, RabbitMQ ou NATS já configurados.</text>
<button class="card_open" type="button" @click="gotoBroker()">Selecionar ›</button>
</column>
<column class="card">
<text class="card_name">Template</text>
<text class="muted">Blueprints prontos do catálogo (stacks completas).</text>
<button class="card_open" type="button" @click="gotoTemplate()">Selecionar ›</button>
</column>
</column>
<column class="form_panel" x-show="step === 'app_form'">
<text class="label_cap">NOME</text>
<input class="field_input" type="text" x-model="appName" placeholder="meu-servico" />
<text class="label_cap" style="margin-top:14px">ORIGEM</text>
<row class="tabs">
<button type="button" :class="sourceKind === 'git' ? 'tab_on' : 'tab_off'" @click="sourceKind = 'git'">Git</button>
<button type="button" :class="sourceKind === 'registry' ? 'tab_on' : 'tab_off'" @click="sourceKind = 'registry'">Registry</button>
</row>
<template x-if="sourceKind === 'git'">
<column style="gap:8px; margin-top:8px">
<input class="field_input" type="text" x-model="gitUrl" placeholder="https://github.com/usuario/repo.git" />
<input class="field_input" type="text" x-model="gitBranch" placeholder="main" />
</column>
</template>
<template x-if="sourceKind === 'registry'">
<column style="margin-top:8px">
<input class="field_input" type="text" x-model="registryImage" placeholder="nginx:latest" />
</column>
</template>
<text class="label_cap" style="margin-top:14px">PORTA</text>
<input class="field_input" type="number" x-model="appPort" />
<text class="label_cap" style="margin-top:14px">DOMÍNIO (opcional)</text>
<input class="field_input" type="text" x-model="appDomain" placeholder="meuservico.dominio.com" />
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="submitApp()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar Application'"></button>
</row>
</column>
<column class="form_panel" x-show="step === 'compose_form'">
<text class="label_cap">NOME</text>
<input class="field_input" type="text" x-model="composeName" placeholder="meu-stack" />
<text class="label_cap" style="margin-top:14px">PORTA</text>
<input class="field_input" type="number" x-model="composePort" />
<text class="label_cap" style="margin-top:14px">DOMÍNIO (opcional)</text>
<input class="field_input" type="text" x-model="composeDomain" placeholder="meuservico.dominio.com" />
<text class="label_cap" style="margin-top:14px">DOCKER COMPOSE (YAML)</text>
<textarea class="env_editor" x-model="composeText"></textarea>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="submitCompose()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar Compose Stack'"></button>
</row>
</column>
<column class="grid" x-show="step === 'pick_db'">
<template x-for="db in catalog.dbs" :key="db.id">
<column class="card">
<text class="card_name" x-text="db.label"></text>
<text class="muted" x-text="db.image"></text>
<button class="card_open" type="button" @click="pickDb(db)">Selecionar ›</button>
</column>
</template>
</column>
<column class="form_panel" x-show="step === 'db_form'">
<text class="label_cap" x-text="selectedDb ? selectedDb.label : ''"></text>
<input class="field_input" type="text" x-model="dbServiceName" placeholder="nome do serviço (opcional)" style="margin-top:8px" />
<template x-if="selectedDb && selectedDb.has_db_name">
<input class="field_input" type="text" x-model="dbName" placeholder="nome do banco" style="margin-top:8px" />
</template>
<template x-if="selectedDb && selectedDb.has_user">
<input class="field_input" type="text" x-model="dbUser" placeholder="usuário" style="margin-top:8px" />
</template>
<input class="field_input" type="password" x-model="dbPassword" placeholder="senha" style="margin-top:8px" />
<template x-if="selectedDb && selectedDb.has_rootpw">
<input class="field_input" type="password" x-model="dbRootPassword" placeholder="senha root" style="margin-top:8px" />
</template>
<template x-if="selectedDb && selectedDb.has_replica">
<label class="check_row" style="margin-top:8px">
<input type="checkbox" x-model="dbUseReplica" /><span>Use Replica Sets</span>
</label>
</template>
<input class="field_input" type="text" x-model="dbImage" placeholder="imagem" style="margin-top:8px" />
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="submitDb()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar'"></button>
</row>
</column>
<column class="grid" x-show="step === 'pick_broker'">
<template x-for="b in catalog.brokers" :key="b.id">
<column class="card">
<text class="card_name" x-text="b.label"></text>
<text class="muted" x-text="b.image"></text>
<button class="card_open" type="button" @click="pickBroker(b)">Selecionar ›</button>
</column>
</template>
</column>
<column class="form_panel" x-show="step === 'broker_form'">
<text class="label_cap" x-text="selectedBroker ? selectedBroker.label : ''"></text>
<input class="field_input" type="text" x-model="brokerServiceName" placeholder="nome do serviço (opcional)" style="margin-top:8px" />
<template x-if="selectedBroker && selectedBroker.has_user">
<column style="gap:8px; margin-top:8px">
<input class="field_input" type="text" x-model="brokerUser" placeholder="usuário" />
<input class="field_input" type="password" x-model="brokerPassword" placeholder="senha" />
</column>
</template>
<input class="field_input" type="text" x-model="brokerImage" placeholder="imagem" style="margin-top:8px" />
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="submitBroker()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar'"></button>
</row>
</column>
<column x-show="step === 'pick_template'">
<input class="field_input" type="text" x-model="templateSearch" placeholder="Buscar template…" style="max-width:400px" />
<column class="grid" style="margin-top:14px">
<template x-for="t in filteredTemplates" :key="t.id">
<column class="card">
<text class="card_name" x-text="t.name"></text>
<text class="muted" x-text="t.description"></text>
<button class="card_open" type="button" @click="pickTemplate(t)">Selecionar ›</button>
</column>
</template>
<text class="muted" x-fallback="filteredTemplates" style="padding:16px">
Nenhum template encontrado.
</text>
</column>
</column>
<column class="form_panel" x-show="step === 'template_form'">
<text class="label_cap" x-text="selectedTemplate ? selectedTemplate.name : ''"></text>
<input class="field_input" type="text" x-model="templateServiceName" placeholder="nome do serviço (opcional)" style="margin-top:8px" />
<template x-for="(v, i) in (selectedTemplate ? selectedTemplate.vars : [])" :key="v.idx">
<column style="margin-top:8px">
<text class="label_cap" x-text="v.label"></text>
<input class="field_input" type="text" x-model="templateValues[i]" :placeholder="v.placeholder" />
</column>
</template>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="submitTemplate()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar'"></button>
</row>
</column>
</column>
<column class="content" x-data="serviceDetail" x-show="$store.app.view === 'service'">
<row class="svc_head">
<column class="header_titles">
<row class="svc_title_row">
<button class="btn_ghost" type="button" @click="store.nav('project')">‹ Voltar</button>
<text class="title svc_title" x-text="svc ? svc.spec.name : ''"></text>
</row>
<row class="svc_substatus">
<row class="badge">
<text class="state_dot" :class="'state_' + statusKind">●</text>
<text class="badge_lbl" :class="'state_' + statusKind" x-text="statusLabel"></text>
</row>
</row>
<text class="muted" x-show="store.serviceMsg" x-text="store.serviceMsg"></text>
</column>
<row class="svc_actions" x-show="svc">
<button class="btn_primary" type="button" @click="store.deployStart()">Deploy</button>
<button class="btn_blue" type="button" @click="store.serviceReload()">Reload</button>
<button class="btn_blue" type="button" @click="store.deployRollback()">Rollback</button>
<button class="btn_danger" type="button" @click="store.serviceStop()">Stop</button>
</row>
</row>
<column class="content_center" x-show="store.serviceLoading" style="padding:24px">
<text class="muted">carregando…</text>
</column>
<template x-if="!store.serviceLoading && svc">
<column class="svc_tabs_wrap">
<row class="tabs">
<button type="button" :class="store.serviceTab === 'general' ? 'tab_on' : 'tab_off'"
@click="initGeneralForm(); store.setServiceTab('general')">General</button>
<button type="button" :class="store.serviceTab === 'connection' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('connection')">Connection</button>
<button type="button" :class="store.serviceTab === 'environment' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('environment')">Environment</button>
<button type="button" :class="store.serviceTab === 'domains' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('domains')">Domains</button>
<button type="button" :class="store.serviceTab === 'deployments' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('deployments')">Deployments</button>
<button type="button" x-show="canMigrate" :class="store.serviceTab === 'migrar' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('migrar')">Migrar</button>
<button type="button" x-show="isShared" :class="store.serviceTab === 'databases' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('databases')">Databases</button>
<button type="button" :class="store.serviceTab === 'healthcheck' ? 'tab_on' : 'tab_off'"
@click="initHcForm(); store.setServiceTab('healthcheck')">Healthcheck</button>
<button type="button" :class="store.serviceTab === 'logs' ? 'tab_on' : 'tab_off'"
@click="store.setServiceTab('logs')">Logs</button>
<button type="button" :class="store.serviceTab === 'advanced' ? 'tab_on' : 'tab_off'"
@click="initAdvForm(); store.setServiceTab('advanced')">Advanced</button>
</row>
<hr class="rule" />
<column class="panel_pad" x-show="store.serviceTab === 'general'">
<column class="form_panel" style="margin-bottom:14px">
<text class="label_cap">NOME DO SERVIÇO</text>
<row class="env_add">
<input class="field_input" type="text" x-model="editName" placeholder="Nome do serviço"
@keydown.enter.prevent="renameService()" />
<button class="btn_primary" type="button" @click="renameService()">Renomear</button>
</row>
<text class="muted" x-text="renameNote"></text>
</column>
<row class="kv_row"><text class="kv_key">Origem</text><text class="kv_val" x-text="sourceText"></text></row>
<row class="kv_row"><text class="kv_key">Porta</text><text class="kv_val" x-text="svc.spec.port"></text></row>
<row class="kv_row"><text class="kv_key">Réplicas</text><text class="kv_val" x-text="svc.spec.replicas"></text></row>
<row class="kv_row" x-show="svc.live_container_id">
<text class="kv_key">Container ao vivo</text>
<text class="kv_val" x-text="svc.live_container_id"></text>
</row>
<template x-if="isCompose">
<column class="form_panel" style="margin-top:14px">
<text class="label_cap">DOCKER COMPOSE (YAML)</text>
<textarea class="env_editor" x-model="composeText"></textarea>
<row class="env_add">
<button class="btn_primary" type="button" @click="saveCompose()">Salvar compose</button>
<button class="btn_ghost" type="button" @click="cancelCompose()">Cancelar</button>
<text class="muted" x-text="store.serviceMsg"></text>
</row>
</column>
</template>
<template x-if="!isCompose">
<column class="form_panel" style="margin-top:14px">
<text class="label_cap">PROVIDER</text>
<row class="tabs">
<button type="button" :class="provTab === 'git' ? 'tab_on' : 'tab_off'"
@click="setProvTab('git')">Git</button>
<button type="button" :class="provTab === 'gitea' ? 'tab_on' : 'tab_off'"
@click="setProvTab('gitea')">Gitea/GitHub</button>
<button type="button" :class="provTab === 'zip' ? 'tab_on' : 'tab_off'"
@click="setProvTab('zip')">Zip</button>
</row>
<hr class="rule" />
<template x-if="provTab === 'git'">
<column style="gap:8px">
<text class="label_cap">SOURCE — REPOSITORY URL / IMAGE</text>
<input class="field_input" type="text" x-model="fRepoUrl" placeholder="github.com/user/repo ou nginx:latest" />
<text class="muted">URL de repositório vira Git; caso contrário é imagem de registry.</text>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">BRANCH</text>
<input class="field_input" type="text" x-model="fBranch" placeholder="main" />
</column>
<column class="hc_field">
<text class="label_cap">PORT</text>
<input class="field_input" type="text" x-model="fGenPort" placeholder="80" />
</column>
</row>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">USERNAME</text>
<input class="field_input" type="text" x-model="fUsername" placeholder="(opcional)" />
</column>
<column class="hc_field">
<text class="label_cap">CREDENTIALS (secret)</text>
<input class="field_input" type="text" x-model="fCredentials" placeholder="(opcional)" />
<text class="muted">
Nome de um secret do projeto (aba Secrets), puro — sem <secret:>. Usado
para clonar repositório Git privado via HTTPS; não tem efeito com URL SSH.
</text>
</column>
</row>
<text class="label_cap">BUILD PATH (root)</text>
<input class="field_input" type="text" x-model="fBuildPath" placeholder="." />
<text class="label_cap">WATCH PATHS (separados por vírgula)</text>
<input class="field_input" type="text" x-model="fWatchPaths" placeholder="src, Cargo.toml" />
<label class="check_row"><input type="checkbox" x-model="fSubmodules" /><span>Enable submodules</span></label>
<text class="label_cap">BUILD ENGINE: DOCKERFILE</text>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">DOCKERFILE</text>
<input class="field_input" type="text" x-model="fDockerfile" placeholder="Dockerfile" />
</column>
<column class="hc_field">
<text class="label_cap">CONTEXT PATH</text>
<input class="field_input" type="text" x-model="fContextPath" placeholder="." />
</column>
</row>
<text class="label_cap">BUILD STAGE (opcional)</text>
<input class="field_input" type="text" x-model="fBuildStage" placeholder="(multi-stage target)" />
<row class="env_add">
<button class="btn_primary" type="button" @click="saveSource()">Save</button>
<text class="muted" x-text="store.serviceMsg"></text>
</row>
</column>
</template>
<template x-if="provTab === 'zip'">
<column style="gap:8px">
<text class="label_cap">ZIP (Dockerfile na raiz)</text>
<input class="field_input" type="file" accept=".zip" @change="onArchiveFileChange($event)" />
<text class="muted">
O zip deve conter um Dockerfile na raiz. Depois do envio, o deploy normal usa os
arquivos descompactados pelo daemon.
</text>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">PORT</text>
<input class="field_input" type="text" x-model="fArchivePort" placeholder="80" />
</column>
<column class="hc_field">
<text class="label_cap">STATUS</text>
<text class="muted" x-text="archiveMsg"></text>
</column>
</row>
<row class="env_add">
<button class="btn_primary" type="button" @click="uploadArchive()">Enviar zip</button>
</row>
</column>
</template>
<template x-if="provTab === 'gitea'">
<column style="gap:8px">
<text class="muted">Selecione a conta conectada, o repositório e a branch a implantar.</text>
<text class="muted" x-show="giteaMsg" x-text="giteaMsg"></text>
<text class="label_cap">CONTA CONECTADA</text>
<select class="field_input" x-model="giteaProviderId" @change="giteaProviderPick($event.target.value)">
<option value="">selecione a conta</option>
<template x-for="p in giteaProviders" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
<text class="muted">Conecte contas em Settings → Git.</text>
<text class="label_cap">REPOSITÓRIO</text>
<select class="field_input" x-model="giteaRepoFullName" @change="giteaRepoPick($event.target.value)">
<option value="">selecione o repositório</option>
<template x-for="r in giteaRepos" :key="r.full_name">
<option :value="r.full_name" x-text="r.full_name"></option>
</template>
</select>
<text class="label_cap">BRANCH</text>
<select class="field_input" x-model="fBranch">
<option value="">selecione a branch</option>
<template x-for="b in giteaBranches" :key="b.name">
<option :value="b.name" x-text="b.name"></option>
</template>
</select>
<text class="label_cap">SELEÇÃO ATUAL</text>
<row class="kv_row">
<text class="kv_key">Repo (clone URL)</text>
<text class="kv_val_fill" x-text="fRepoUrl"></text>
</row>
<text class="label_cap">BUILD</text>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">BUILD PATH (root)</text>
<input class="field_input" type="text" x-model="fBuildPath" placeholder="." />
</column>
<column class="hc_field">
<text class="label_cap">PORT</text>
<input class="field_input" type="text" x-model="fGenPort" placeholder="80" />
</column>
</row>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">DOCKERFILE</text>
<input class="field_input" type="text" x-model="fDockerfile" placeholder="Dockerfile" />
</column>
<column class="hc_field">
<text class="label_cap">CONTEXT PATH</text>
<input class="field_input" type="text" x-model="fContextPath" placeholder="." />
</column>
</row>
<text class="label_cap">WATCH PATHS (separados por vírgula)</text>
<input class="field_input" type="text" x-model="fWatchPaths" placeholder="src, Cargo.toml" />
<label class="check_row"><input type="checkbox" x-model="fSubmodules" /><span>Enable submodules</span></label>
<row class="env_add">
<button class="btn_primary" type="button" @click="saveSource()">Save</button>
<text class="muted" x-text="store.serviceMsg"></text>
</row>
</column>
</template>
</column>
</template>
<button class="btn_danger" type="button" style="margin-top:14px; align-self:flex-start"
@click="store.deleteService(svc.id)">Remover serviço</button>
</column>
<column class="kv_list" x-show="store.serviceTab === 'connection'">
<row class="kv_row">
<text class="kv_key">Container port</text>
<text class="kv_val_fill" x-text="connectionInfo.port"></text>
<button class="copy_btn" type="button" @click="copyToClipboard(String(connectionInfo.port))">Copiar</button>
</row>
<row class="kv_row">
<text class="kv_key">Host port</text>
<text class="kv_val_fill" x-text="connectionInfo.hostPort"></text>
<button class="copy_btn" type="button" @click="copyToClipboard(String(connectionInfo.hostPort))">Copiar</button>
</row>
<row class="kv_row">
<text class="kv_key">Domain</text>
<text class="kv_val_fill" x-text="connectionInfo.domain"></text>
<button class="copy_btn" type="button" @click="copyToClipboard(connectionInfo.domain)">Copiar</button>
</row>
<row class="kv_row">
<text class="kv_key">TLS</text>
<text class="kv_val_fill" x-text="connectionInfo.tls"></text>
</row>
<row class="kv_row">
<text class="kv_key">External URL</text>
<text class="kv_val_fill" x-text="connectionInfo.externalUrl"></text>
<button class="copy_btn" type="button" @click="copyToClipboard(connectionInfo.externalUrl)">Copiar</button>
</row>
<row class="kv_row">
<text class="kv_key">Internal URL</text>
<text class="kv_val_fill" x-text="connectionInfo.internalUrl"></text>
<button class="copy_btn" type="button" @click="copyToClipboard(connectionInfo.internalUrl)">Copiar</button>
</row>
<row class="kv_row" x-show="canShare">
<text class="kv_key">Compartilhado</text>
<text class="kv_val_fill" x-text="isShared ? 'true' : 'false'"></text>
<button class="copy_btn" type="button" x-show="!isShared" @click="setShared(true)">Compartilhar</button>
<button class="copy_btn" type="button" x-show="isShared" @click="setShared(false)">Desligar</button>
</row>
<row class="kv_row" x-show="connectionInfo.dbKind">
<text class="kv_key">Tipo</text>
<text class="kv_val_fill" x-text="connectionInfo.dbKind"></text>
</row>
<text class="muted" x-show="copyMsg" x-text="copyMsg"></text>
<hr class="rule" />
<text class="label_cap" x-text="'CONTAINERS (' + liveContainers.length + ')'"></text>
<text class="muted" x-fallback="liveContainers">Nenhum container em execução.</text>
<row class="kv_row" x-show="liveContainers.length > 0" style="flex-wrap:wrap">
<template x-for="c in liveContainers" :key="c.id">
<row class="badge" style="margin-right:18px">
<text class="state_dot" :class="'state_' + c.kind">●</text>
<text class="kv_val_fill" x-text="c.name + ' ' + c.idShort"></text>
</row>
</template>
</row>
</column>
<column class="env_wrap" x-show="store.serviceTab === 'environment'">
<row class="env_head">
<text class="muted_fill" x-text="(svc ? svc.spec.env_vars.length : 0) + ' variável(eis)'"></text>
<button class="btn_export" type="button" @click="openEnvText()" x-show="!envTextOpen">.env</button>
<button class="btn_export_on" type="button" @click="closeEnvText()" x-show="envTextOpen">Fechar</button>
</row>
<template x-if="!envTextOpen">
<column style="gap:10px">
<row class="env_add">
<input class="env_in_key" type="text" placeholder="KEY" x-model="newEnvKey" />
<input class="env_in_val" type="text" placeholder="value" x-model="newEnvValue" />
<button class="btn_primary" type="button" @click="addEnvVar()">Adicionar</button>
</row>
<text class="muted_fill">
Para um valor sensível, use um secret do projeto: escreva <secret:NOME> ou
secret:NOME como valor. O secret é criado na aba Secrets do projeto.
</text>
<column class="kv_list">
<template x-for="e in envVars" :key="e.key">
<column style="width:100%">
<row class="kv_row" x-show="e.isComment">
<text class="muted" x-text="e.text"></text>
</row>
<row class="kv_row" x-show="!e.isComment">
<button class="env_del" type="button" @click="delEnvVar(e.key)">✕</button>
<text class="env_sep"></text>
<text class="env_key" x-text="e.key"></text>
<text class="kv_val_fill" x-text="e.isSecret ? '(secret)' : e.value"></text>
</row>
</column>
</template>
<text class="muted" x-fallback="envVars" style="padding:12px">Nenhuma variável.</text>
</column>
</column>
</template>
<template x-if="envTextOpen">
<column style="gap:8px">
<text class="muted">Cole/edite no formato KEY=VALUE (# comentário).</text>
<textarea class="env_editor" x-model="envText" placeholder="KEY=VALUE (# comentário)"></textarea>
<row class="env_add">
<button class="btn_primary" type="button" @click="saveEnvText()">Salvar</button>
<button class="btn_ghost" type="button" @click="closeEnvText()">Cancelar</button>
</row>
</column>
</template>
</column>
<column class="env_wrap" x-show="store.serviceTab === 'domains'">
<row class="env_add">
<input class="env_in_key" type="text" placeholder="dominio.com" x-model="newDomain" />
<input class="env_in_val" type="text" placeholder="porta (opcional)" x-model="newDomainPort" />
<label class="check_row"><input type="checkbox" x-model="newDomainTls" /><span>TLS</span></label>
<button class="btn_primary" type="button" @click="addDomain()">Adicionar</button>
</row>
<column class="kv_list">
<template x-for="d in domains" :key="d.domain">
<row class="kv_row">
<button class="env_del" type="button" @click="delDomain(d.domain)">✕</button>
<text class="env_sep"></text>
<text class="env_key" x-text="d.domain"></text>
<text class="kv_val_fill" x-text="(d.tls ? 'https' : 'http') + ' → :' + d.port"></text>
</row>
</template>
<text class="muted" x-fallback="domains" style="padding:12px">
Nenhum domínio configurado.
</text>
</column>
</column>
<column class="form_panel" x-show="store.serviceTab === 'migrar'">
<text class="label_cap">MIGRAR PARA BANCO COMPARTILHADO</text>
<text class="muted">Crie antes o database de destino no servidor compartilhado (aba Databases de lá, marcando "Não mexer em env var"). Mesmo motor e mesmo projeto. A migração é com parada: os serviços que usam este banco ficam fora do ar durante a cópia.</text>
<text class="label_cap">DATABASE DE ORIGEM (dentro deste banco)</text>
<input class="field_input" x-model="fMig.db" placeholder="nome do database" />
<text class="label_cap">DESTINO</text>
<select class="select" x-model="fMig.dest">
<option value="">database de destino...</option>
<template x-for="d in migDests" :key="d.id"><option :value="d.id" x-text="d.name"></option></template>
</select>
<text class="label_cap">ENV VAR A TROCAR</text>
<input class="field_input" x-model="fMig.env" placeholder="padrão DATABASE_URL / MONGODB_URI" />
<row class="env_add">
<button class="btn_primary" type="button" @click="startMigration()">Migrar</button>
<button class="btn_ghost" type="button" @click="loadMigration()">Atualizar</button>
</row>
<template x-if="mig">
<column>
<text class="label_cap" x-text="'ANDAMENTO — ' + mig.status"></text>
<column class="kv_list">
<template x-for="s in migSteps" :key="s.name">
<row class="kv_row">
<text class="env_key" x-text="s.icon"></text>
<text class="kv_val_fill" x-text="s.name"></text>
<text class="muted" x-text="s.detail"></text>
</row>
</template>
</column>
<template x-if="mig.status === 'Completed'">
<column>
<text class="muted">Migração concluída — o banco antigo foi mantido (parado) para rollback.</text>
<row class="env_add">
<button class="btn_ghost" type="button" @click="rollbackMigration()">Reverter</button>
<button class="btn_ghost" type="button" @click="discardOldDb()">Descartar banco antigo</button>
</row>
</column>
</template>
</column>
</template>
</column>
<column class="form_panel" x-show="store.serviceTab === 'databases'">
<text class="label_cap" x-text="'DATABASES (' + mdbs.length + ')'"></text>
<text class="muted">Cada database tem usuário próprio, sem acesso aos demais. O projeto consumidor recebe a connection string numa env var.</text>
<column class="kv_list">
<template x-for="d in mdbs" :key="d.id">
<row class="kv_row">
<button class="env_del" type="button" @click="deleteMdb(d)">✕</button>
<button class="tab_off" type="button" @click="showMdb(d)" x-text="d.name"></button>
<text class="kv_val_fill" x-text="'→ ' + d.project"></text>
<text class="muted" x-text="d.envVar"></text>
</row>
</template>
</column>
<row class="kv_row" x-show="mdbUrl">
<text class="kv_key">URL</text>
<text class="kv_val_fill" x-text="mdbLabel"></text>
<button class="copy_btn" type="button" @click="copyToClipboard(mdbUrl)">Copiar</button>
</row>
<text class="label_cap">NOVO DATABASE</text>
<input class="field_input" x-model="fMdb.name" placeholder="nome (a-z, 0-9, _)" />
<select class="select" x-model="fMdb.project">
<option value="">projeto consumidor...</option>
<template x-for="p in mdbProjects" :key="p.id"><option :value="p.id" x-text="p.name"></option></template>
</select>
<input class="field_input" x-model="fMdb.env" placeholder="env var (padrão DATABASE_URL / MONGODB_URI)" />
<input class="field_input" x-model="fMdb.limit" placeholder="limite de conexões (opcional; não vale no Mongo)" />
<input class="field_input" x-model="fMdb.timeout" placeholder="timeout de query em ms (opcional; só Postgres)" />
<label class="check_row"><input type="checkbox" x-model="fMdb.overwrite" /> Sobrescrever a env var se o projeto já a tiver</label>
<label class="check_row"><input type="checkbox" x-model="fMdb.skipEnv" /> Não mexer em env var (vou migrar os dados antes)</label>
<row class="env_add">
<button class="btn_primary" type="button" @click="createMdb()">Criar database</button>
</row>
</column>
<column class="panel_fill" x-show="store.serviceTab === 'deployments'">
<row class="thead">
<text class="th col_state">ESTADO</text>
<text class="th col_dur">DURAÇÃO</text>
<text class="th col_start">INÍCIO</text>
<text class="th col_act_2">AÇÃO</text>
</row>
<column class="table_body">
<template x-for="d in deployments" :key="d.id">
<column class="row_wrap">
<row class="trow">
<row class="state_cell col_state">
<text class="state_dot" :class="'state_' + d.stateKind">●</text>
<text class="state_lbl" :class="'state_' + d.stateKind" x-text="d.stateLabel"></text>
</row>
<text class="td col_dur" x-text="d.duration"></text>
<text class="muted col_start" x-text="d.start"></text>
<row class="dep_actions col_act_2">
<button class="dep_logs_btn" type="button" @click="viewBuildLog(d.id)" title="Ver build log">↗</button>
<button class="dep_logs_btn" type="button" x-show="!d.terminal" @click="abortDeployment(d.id)" title="Cancelar">⏹</button>
<button class="dep_del_btn" type="button" x-show="d.terminal" @click="removeDeployment(d.id)" title="Remover">✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="muted" x-fallback="deployments" style="padding:16px">Nenhum deploy ainda.</text>
</column>
</column>
<column class="form_panel" x-show="store.serviceTab === 'healthcheck'">
<text class="label_cap">KIND</text>
<row class="tabs">
<button type="button" :class="hcKind === 'none' ? 'tab_on' : 'tab_off'" @click="hcKind = 'none'">None</button>
<button type="button" :class="hcKind === 'tcp' ? 'tab_on' : 'tab_off'" @click="hcKind = 'tcp'">Tcp</button>
<button type="button" :class="hcKind === 'http' ? 'tab_on' : 'tab_off'" @click="hcKind = 'http'">Http</button>
<button type="button" :class="hcKind === 'docker' ? 'tab_on' : 'tab_off'" @click="hcKind = 'docker'">Docker</button>
</row>
<template x-if="hcKind === 'http'">
<column style="gap:8px; margin-top:8px">
<text class="label_cap">HTTP PATH</text>
<input class="field_input" type="text" x-model="hcPath" placeholder="/health" />
<text class="label_cap">EXPECTED STATUS</text>
<input class="field_input" type="number" min="100" max="599" x-model="hcStatus" placeholder="200" />
</column>
</template>
<row class="hc_grid" style="margin-top:14px">
<column class="hc_field">
<text class="label_cap">INTERVAL (s)</text>
<input class="field_input" type="number" min="1" max="3600" x-model="hcInterval" />
</column>
<column class="hc_field">
<text class="label_cap">TIMEOUT (s)</text>
<input class="field_input" type="number" min="1" max="3600" x-model="hcTimeout" />
</column>
</row>
<row class="hc_grid">
<column class="hc_field">
<text class="label_cap">RETRIES</text>
<input class="field_input" type="number" min="1" max="10" x-model="hcRetries" />
</column>
<column class="hc_field">
<text class="label_cap">START PERIOD (s)</text>
<input class="field_input" type="number" min="0" max="3600" x-model="hcStart" />
</column>
</row>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="saveHealthcheck()">Save</button>
<text class="muted" x-show="store.serviceMsg" x-text="store.serviceMsg"></text>
</row>
</column>
<column class="log_panel" x-show="store.serviceTab === 'logs'">
<column class="scroll_fill">
<column class="log_body">
<template x-for="(l, i) in store.serviceLogLines" :key="i">
<row class="log_line">
<text class="log_time" x-text="timeHms(l.timestamp)"></text>
<text class="log_text" :style="l.stream === 'Stderr' ? 'color:#F85149' : ''" x-text="l.line"></text>
</row>
</template>
<text class="muted" x-fallback="store.serviceLogLines" style="padding:12px">
Aguardando linhas de log…
</text>
</column>
</column>
</column>
<column class="form_panel" x-show="store.serviceTab === 'advanced'">
<text class="label_cap">REPLICAS</text>
<input class="field_input" type="text" x-model="advReplicas" placeholder="1" />
<text class="label_cap" style="margin-top:14px">RUN COMMAND</text>
<input class="field_input" type="text" x-model="advRunCommand" placeholder="(padrão da imagem)" />
<text class="muted" style="margin-top:8px" x-text="'Run args atuais: ' + runArgsText"></text>
<row class="env_add" style="margin-top:14px">
<button class="btn_primary" type="button" @click="saveAdvanced()">Save</button>
<text class="muted" x-show="store.serviceMsg" x-text="store.serviceMsg"></text>
</row>
<text class="label_cap" style="margin-top:14px" x-text="'FILA DE PRÉ-DEPLOY (' + preDeployChecks.length + ')'"></text>
<text class="muted">
Roda esses Jobs em ORDEM antes do deploy (manual ou webhook),
um de cada vez. Se o exit code de algum for diferente de 0, a
fila para e o deploy não acontece — os checks seguintes não
rodam. Cadastre jobs em Schedules.
</text>
<column class="kv_list">
<template x-for="(c, i) in preDeployChecks" :key="c.id">
<row class="kv_row">
<button class="env_del" type="button" @click="pdcDel(c.id)">✕</button>
<text class="env_sep"></text>
<text class="env_key" x-text="c.index + '.'"></text>
<text class="kv_val_fill" x-text="c.name"></text>
<button class="btn_ghost" type="button" :disabled="i === 0" @click="pdcMove(c.id, -1)">↑</button>
<button class="btn_ghost" type="button" :disabled="i === preDeployChecks.length - 1" @click="pdcMove(c.id, 1)">↓</button>
</row>
</template>
<text class="muted" x-fallback="preDeployChecks" style="padding:12px">
Nenhum check configurado — deploy segue direto.
</text>
</column>
<text class="label_cap" style="margin-top:14px">ADICIONAR À FILA</text>
<row class="env_add">
<select class="field_input" x-model="advPdcAddJobId">
<option value="">escolher job...</option>
<template x-for="j in preDeployAvailableJobs" :key="j.id">
<option :value="j.id" x-text="j.name"></option>
</template>
</select>
<button class="btn_primary" type="button" @click="pdcAdd()">Adicionar à fila</button>
</row>
</column>
</column>
</template>
<column class="modal_backdrop" x-show="buildLogFor" @click.self="closeBuildLog()">
<column class="modal_box">
<row class="modal_head">
<text class="label_cap">BUILD LOG</text>
<button class="btn_ghost" type="button" @click="closeBuildLog()">Fechar</button>
</row>
<column class="modal_body">
<pre class="log_pre" x-text="buildLogText"></pre>
</column>
</column>
</column>
</column>
</column>
</row>
<column class="toast_wrap" aria-live="polite" aria-atomic="false">
<template x-for="t in $store.app.toasts" :key="t.id">
<row class="toast" :class="'toast_' + t.kind" role="status">
<text class="toast_icon" aria-hidden="true"
x-text="({ info: 'ℹ', success: '✓', warning: '⚠', error: '✕' })[t.kind] || 'ℹ'"></text>
<text class="toast_msg" x-text="t.message"></text>
<button class="toast_close" type="button" aria-label="Fechar"
@click="$store.app.dismissToast(t.id)">×</button>
</row>
</template>
</column>
</div>
</body>
</html>