<!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" 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="page-head__subtitle">Infrastructure management for professional teams</text>
</column>
</row>
<form @submit.prevent="submit()">
<column class="field">
<row class="field__row">
<text class="field__label" style="flex:1">CONNECTION TOKEN</text>
<text class="field__label">SECURE</text>
</row>
<input class="field__input" type="password" placeholder="cole o token de conexão"
x-model="$store.app.accessKey" />
<text class="u-muted" x-show="otherServerWarning" x-text="otherServerWarning"></text>
</column>
<column class="login__checks" style="margin-top:16px">
<label class="check-row">
<input type="checkbox" x-model="$store.app.rememberAccessKey" @change="$store.app.persistPrefs()" />
<span>Remember access key</span>
</label>
</column>
<button type="submit" class="login__connect" style="margin-top:16px"
x-text="$store.app.statusLine === 'conectando…' ? 'Conectando…' : 'Connect'"
:disabled="$store.app.statusLine === 'conectando…'"></button>
</form>
<text class="u-error" x-show="$store.app.error" x-text="'⚠ ' + $store.app.error"></text>
<hr class="rule" style="margin:4px 0" />
<row class="login__status">
<text class="u-ok" style="font-size:10px">●</text>
<text class="u-muted" x-text="$store.app.statusLine"></text>
</row>
</column>
</column>
<row class="screen" x-show="$store.app.screen === 'shell'">
<column class="sidebar" x-show="$store.app.menuOpen" x-data="{
groups: [
{ title: 'DEPLOYMENT OPS', items: [
{ v: 'deployments', label: 'Deployments', icon: '▶' },
{ v: 'deploy_engine', label: 'Deploy Engine', icon: '⚡' },
{ v: 'monitoring', label: 'Monitoring', icon: '◔' },
{ v: 'schedules', label: 'Schedules', icon: '◫' },
{ v: 'ingress', label: 'Ingress', icon: '↦' },
{ v: 'docker', label: 'Runtime', icon: '◉' } ] },
{ title: 'MANAGEMENT', items: [
{ v: 'projects', label: 'Projects', icon: '◧', also: ['project', 'service'] } ] } ],
on(it) { const v = $store.app.view; return v === it.v || (it.also || []).includes(v); }
}">
<row class="brand__row">
<column class="brand">
<text class="brand__name">Rustploy</text>
<text class="brand__sub">PRODUCTION CLUSTER</text>
</column>
</row>
<template x-for="group in groups" :key="group.title">
<column class="sidebar__group">
<text class="sidebar__section" x-text="group.title"></text>
<template x-for="navItem in group.items" :key="navItem.v">
<button type="button" :class="on(navItem) ? 'nav-item nav-item--active' : 'nav-item'" :title="navItem.label"
@click="$store.app.nav(navItem.v)">
<span class="nav-item__icon" x-text="navItem.icon"></span>
<text class="nav-item__label" x-text="navItem.label"></text>
</button>
</template>
</column>
</template>
<column class="sidebar__spacer"></column>
<hr class="rule" />
<button type="button" @click="$store.app.nav('settings')" title="Settings"
:class="$store.app.view === 'settings' ? 'nav-item nav-item--active' : 'nav-item'">
<span class="nav-item__icon" x-text="'⌥'"></span>
<text class="nav-item__label">Settings</text>
</button>
<button type="button" @click="$store.app.nav('support')" title="Support"
:class="$store.app.view === 'support' ? 'nav-item nav-item--active' : 'nav-item'">
<span class="nav-item__icon" x-text="'◊'"></span>
<text class="nav-item__label">Support</text>
</button>
</column>
<column class="shell__main">
<row class="topbar">
<button class="btn btn--ghost topbar__menu" type="button" title="Mostrar/ocultar o menu"
@click="$store.app.menuOpen = !$store.app.menuOpen">☰</button>
<row class="topbar__search-wrap">
<input class="topbar__search" type="text" placeholder="Search deployments, nodes…"
x-model="$store.app.search" @input="$store.app.searchChanged($event.target.value)" />
</row>
<text class="u-muted topbar__status" :title="location.origin" x-text="'● ' + location.host + ' · daemon v' + $store.app.daemonVersion"></text>
<button class="btn btn--danger btn-icon" type="button" @click="$store.app.stopAll()" title="Para todos os containers gerenciados">
<text>Stop All</text>
</button>
<button class="btn btn--ghost" type="button" @click="$store.app.disconnect()">Disconnect</button>
</row>
<hr class="rule" />
<column class="content" x-show="$store.app.view === 'support'">
<text class="page-head__title">Support</text>
<text class="page-head__subtitle">github.com/antoniofernandodj/rustploy</text>
</column>
<column class="content" x-data="dashboard" x-show="$store.app.view === 'deployments'">
<row class="page-head">
<column class="page-head__titles">
<text class="page-head__title">Deployments</text>
<text class="page-head__subtitle">Real-time status of services in the cluster.</text>
</column>
<row class="page-head__stats page-head__stats--hide-lg">
<column class="stat-card">
<text class="u-label-cap">STATUS</text>
<text class="stat-card__num stat-card__num--ok" x-text="store.servicesLabel"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">UPTIME</text>
<text class="stat-card__num" x-text="store.daemonUptime"></text>
</column>
</row>
<button class="btn btn--danger" type="button" @click="store.clearFinished()">Limpar parados/falhos</button>
</row>
<text class="u-muted" x-show="store.deploymentsMessage" x-text="store.deploymentsMessage"></text>
<column class="panel panel--fill">
<row class="table__head">
<text class="table__head-cell table__col-svc">SERVIÇO</text>
<text class="table__head-cell table__col-proj">PROJETO</text>
<text class="table__head-cell table__col-state">ESTADO</text>
<text class="table__head-cell table__col-dur">DURAÇÃO</text>
<text class="table__head-cell table__col-start">INÍCIO</text>
</row>
<column class="u-center-fill" x-show="store.dataLoading" style="padding:24px">
<text class="u-muted">carregando…</text>
</column>
<column class="u-fill-all" x-show="!store.dataLoading">
<column class="table__body">
<template x-for="row in rows" :key="row.service + row.start">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="row.service"></text>
<text class="table__cell table__col-proj" x-text="row.project"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + row.stateKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="row.stateLabel"></text>
</row>
<text class="table__cell table__col-dur" x-text="row.duration"></text>
<text class="u-muted table__col-start" x-text="row.start"></text>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-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="page-head">
<column class="page-head__titles">
<text class="page-head__title">Deploy Engine</text>
<text class="page-head__subtitle">Pipeline de deploy: em andamento e histórico recente.</text>
</column>
<row class="page-head__stats page-head__stats--hide-lg">
<column class="stat-card">
<text class="u-label-cap">ATIVOS</text>
<text class="stat-card__num stat-card__num--ok" x-text="active.length"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">SUCESSO 24H</text>
<text class="stat-card__num" x-text="successCount"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">FALHAS 24H</text>
<text class="stat-card__num" x-text="failedCount"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">TOTAL 24H</text>
<text class="stat-card__num" x-text="totalCount"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">UPTIME</text>
<text class="stat-card__num" x-text="uptime"></text>
</column>
</row>
</row>
<row class="tabs">
<button type="button" :class="tab === 'fila' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="tab = 'fila'"
x-text="'Fila (' + queued.length + ')'"></button>
<button type="button" :class="tab === 'executando' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="tab = 'executando'"
x-text="'Executando (' + active.length + ')'"></button>
<button type="button" :class="tab === 'historico' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="tab = 'historico'"
x-text="'Histórico 24h (' + recent.length + ')'"></button>
</row>
<column class="section" x-show="tab === 'fila'">
<row class="section__head">
<text class="section__title" x-text="'NA FILA (' + queued.length + ')'"></text>
<button class="btn btn--copy" type="button" @click="store.queueTogglePause()"
x-text="paused ? 'Retomar fila' : 'Pausar fila'"></button>
</row>
<column class="section__body">
<text class="banner 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="queuedDeployment in queued" :key="queuedDeployment.deploymentId">
<column class="list-row" draggable="true" :class="{ drop_over: overId === queuedDeployment.deploymentId && dragId !== queuedDeployment.deploymentId, dragging: dragId === queuedDeployment.deploymentId }"
@dragstart="dragStart($event, queuedDeployment.deploymentId)" @dragover.prevent="overId = queuedDeployment.deploymentId"
@dragleave="overId === queuedDeployment.deploymentId && (overId = null)" @drop.prevent="dropOn(queuedDeployment.deploymentId)" @dragend="dragEnd()">
<row class="table__row">
<span class="drag-grip" title="Arraste para reordenar">⠿</span>
<text class="table__cell table__cell--key table__col-svc" x-text="queuedDeployment.pos + '. ' + queuedDeployment.service"></text>
<text class="table__cell table__col-start" x-text="queuedDeployment.project"></text>
<button class="btn btn--copy" type="button" @click="store.queuePromote(queuedDeployment.deploymentId)">↑ topo</button>
<button class="env__del" type="button" @click="store.queueCancel(queuedDeployment.deploymentId)">✕</button>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="section" x-show="tab === 'executando'">
<row class="section__head">
<text class="section__title" x-text="'EXECUTANDO AGORA (' + active.length + ')'"></text>
</row>
<column class="section__body">
<row class="table__head" x-show="active.length > 0">
<text class="table__head-cell table__col-svc">SERVIÇO</text>
<text class="table__head-cell table__col-proj">PROJETO</text>
<text class="table__head-cell table__col-state">ESTADO</text>
<text class="table__head-cell table__col-dur">TOTAL</text>
<text class="table__head-cell table__col-start">FASE</text>
<text class="table__head-cell table__col-act"></text>
</row>
<column class="table__body">
<text class="empty-state" x-fallback="active">Nenhum deploy em andamento.</text>
<template x-for="activeDeployment in active" :key="activeDeployment.serviceId">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="activeDeployment.service"></text>
<text class="table__cell table__col-proj" x-text="activeDeployment.project"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + activeDeployment.stateKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="activeDeployment.stateLabel"></text>
</row>
<text class="u-muted table__col-dur" x-text="activeDeployment.total"></text>
<text class="u-muted table__col-start" x-text="activeDeployment.phase"></text>
<row class="table__col-act">
<button class="btn btn--sm-ghost" type="button" @click="openDetail(activeDeployment.serviceId)">Detalhes</button>
<button class="env__del" type="button" title="Abortar deploy"
@click="store.queueCancel(activeDeployment.deploymentId)">■</button>
</row>
</row>
<row class="stepper">
<template x-for="(activeStep, index) in activeDeployment.steps" :key="activeStep.label">
<row class="stepper__item">
<text class="stepper__sep" x-show="index > 0">›</text>
<text class="stepper__step" :class="'stepper__step--' + activeStep.status"
x-text="(activeStep.status === 'done' ? '✓ ' : activeStep.status === 'failed' ? '✕ ' : activeStep.status === 'current' ? '● ' : '○ ') + activeStep.label"></text>
</row>
</template>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="section section--fill" x-show="tab === 'historico'">
<row class="section__head">
<text class="section__title" x-text="'HISTÓRICO 24H (' + recent.length + ')'"></text>
</row>
<column class="section__body">
<row class="table__head" x-show="recent.length > 0">
<text class="table__head-cell table__col-icon"></text>
<text class="table__head-cell table__col-svc">SERVIÇO</text>
<text class="table__head-cell table__col-proj">PROJETO</text>
<text class="table__head-cell table__col-state">ESTADO</text>
<text class="table__head-cell table__col-dur">DURAÇÃO</text>
<text class="table__head-cell table__col-start">INÍCIO</text>
</row>
</column>
<row class="empty-state__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="(recentDeployment, index) in recent" :key="recentDeployment.service + recentDeployment.start + index">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__col-icon" x-text="recentDeployment.icon"></text>
<text class="table__cell table__cell--key table__col-svc" x-text="recentDeployment.service"></text>
<text class="table__cell table__col-proj" x-text="recentDeployment.project"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + recentDeployment.stateKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="recentDeployment.stateLabel"></text>
</row>
<text class="u-muted table__col-dur" x-text="recentDeployment.duration"></text>
<text class="u-muted table__col-start" x-text="recentDeployment.start"></text>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
<column class="modal__backdrop" x-show="detail">
<column class="modal__box" x-win="'deploy_detail'" data-w="860" data-h="600">
<row class="modal__head">
<text class="u-label-cap" x-text="'DEPLOY · ' + (detail ? detail.service : '')"></text>
<button class="btn btn--ghost" type="button" @click="closeDetail()">Fechar</button>
</row>
<column class="modal__body" x-show="detail">
<row class="stepper">
<template x-for="(step, index) in (detail ? detail.steps : [])" :key="step.label">
<row class="stepper__item">
<text class="stepper__sep" x-show="index > 0">›</text>
<text class="stepper__step" :class="'stepper__step--' + step.status"
x-text="(step.status === 'done' ? '✓ ' : step.status === 'failed' ? '✕ ' : step.status === 'current' ? '● ' : '○ ') + step.label"></text>
</row>
</template>
</row>
<row class="table__head">
<text class="table__head-cell table__col-state">ESTADO</text>
<text class="table__head-cell table__col-dur">DURAÇÃO</text>
<text class="table__head-cell table__col-start">MENSAGEM</text>
</row>
<template x-for="(detailRow, index) in (detail ? detail.detail : [])" :key="index">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__col-state" :class="'state-cell--' + detailRow.kind" x-text="detailRow.label"></text>
<text class="u-muted table__col-dur" x-text="detailRow.dur"></text>
<text class="u-muted table__col-start stepper__msg" x-text="detailRow.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="page-head">
<column class="page-head__titles">
<text class="page-head__title">Monitoring</text>
<text class="page-head__subtitle">Host and per-container resource usage.</text>
</column>
<row class="page-head__stats page-head__stats--hide-md">
<column class="stat-card">
<text class="u-label-cap">CPU</text>
<text class="stat-card__num stat-card__num--ok" x-text="store.hostCpu"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">MEMORY</text>
<text class="stat-card__num" x-text="store.hostMemory"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">DISK</text>
<text class="stat-card__num" x-text="store.hostDisk"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">LOAD</text>
<text class="stat-card__num" x-text="store.hostLoad"></text>
</column>
</row>
</row>
<column class="panel panel--fill">
<row class="table__head">
<text class="table__head-cell table__col-svc">CONTAINER</text>
<text class="table__head-cell table__col-proj">PROJETO</text>
<text class="table__head-cell table__col-dur">CPU</text>
<text class="table__head-cell table__col-dur">MEM</text>
<text class="table__head-cell table__col-dur">RX</text>
<text class="table__head-cell table__col-start">TX</text>
</row>
<column class="u-fill-all">
<column class="table__body">
<template x-for="row in rows" :key="row.name + row.project">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="row.name"></text>
<text class="table__cell table__col-proj" x-text="row.project"></text>
<text class="table__cell table__col-dur" x-text="row.cpu"></text>
<text class="table__cell table__col-dur" x-text="row.mem"></text>
<text class="u-muted table__col-dur" x-text="row.rx"></text>
<text class="u-muted table__col-start" x-text="row.tx"></text>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-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="page-head">
<column class="page-head__titles">
<text class="page-head__title">Ingress</text>
<text class="page-head__subtitle">Active routes in the reverse proxy.</text>
</column>
<row class="page-head__stats">
<column class="stat-card">
<text class="u-label-cap">ROUTES</text>
<text class="stat-card__num stat-card__num--ok" x-text="routes.length"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">PORTAS DE HOST</text>
<text class="stat-card__num" x-text="hostPorts.length"></text>
</column>
</row>
</row>
<column class="panel panel--fill">
<row class="table__head">
<text class="table__head-cell table__col-svc">DOMÍNIO</text>
<text class="table__head-cell table__col-proj">SERVIÇO</text>
<text class="table__head-cell table__col-dur">UPSTREAM</text>
<text class="table__head-cell table__col-start">TLS</text>
</row>
<column class="u-fill-all">
<column class="table__body">
<template x-for="route in routes" :key="route.domain">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="route.domain"></text>
<text class="table__cell table__col-proj" x-text="route.service"></text>
<text class="table__cell table__col-dur" x-text="route.upstream"></text>
<text class="u-muted table__col-start" x-text="route.tls"></text>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
<text class="u-label-cap" x-text="'PORTAS TCP DE HOST (' + hostPorts.length + ')'"></text>
<row class="table__head">
<text class="table__head-cell table__col-svc">SERVIÇO</text>
<text class="table__head-cell table__col-proj">PROJETO</text>
<text class="table__head-cell table__col-dur">PORTA HOST</text>
<text class="table__head-cell table__col-start">PORTA CONTAINER</text>
</row>
<text class="u-muted" x-fallback="hostPorts" style="padding:12px">
Nenhum serviço com porta de host configurada.
</text>
<column class="table__body">
<template x-for="hostPortEntry in hostPorts" :key="hostPortEntry.service">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="hostPortEntry.service"></text>
<text class="table__cell table__col-proj" x-text="hostPortEntry.project"></text>
<text class="table__cell table__col-dur" x-text="hostPortEntry.hostPort"></text>
<text class="u-muted table__col-start" x-text="hostPortEntry.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="page-head">
<column class="page-head__titles">
<text class="page-head__title">Runtime</text>
<text class="page-head__subtitle">Containers, imagens, volumes e networks do host.</text>
</column>
<row class="page-head__stats page-head__stats--hide-md">
<column class="stat-card">
<text class="u-label-cap">CONTAINERS</text>
<text class="stat-card__num stat-card__num--ok" x-text="store.snap?.docker_containers?.length || 0"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">IMAGENS</text>
<text class="stat-card__num" x-text="store.snap?.docker_images?.length || 0"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">VOLUMES</text>
<text class="stat-card__num" x-text="store.snap?.docker_volumes?.length || 0"></text>
</column>
<column class="stat-card">
<text class="u-label-cap">NETWORKS</text>
<text class="stat-card__num stat-card__num--ok" x-text="store.snap?.docker_networks?.length || 0"></text>
</column>
</row>
</row>
<row class="tabs">
<button type="button" :class="store.dockerTab === 'containers' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerSetTab('containers')">Containers</button>
<button type="button" :class="store.dockerTab === 'images' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerSetTab('images')">Images</button>
<button type="button" :class="store.dockerTab === 'volumes' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerSetTab('volumes')">Volumes</button>
<button type="button" :class="store.dockerTab === 'networks' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerSetTab('networks')">Networks</button>
<button type="button" :class="store.dockerTab === 'registry' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerSetTab('registry')">Registry</button>
</row>
<hr class="rule" />
<column class="panel panel--fill" x-show="store.dockerTab === 'containers'">
<row class="env__head">
<text class="u-muted-grow" x-text="containers.length + ' container(s)'"></text>
<button class="btn btn--danger" type="button" @click="store.dockerPruneContainers()">Limpar parados</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc">NOME</text>
<text class="table__head-cell table__col-img">IMAGEM</text>
<text class="table__head-cell table__col-state">DONO</text>
<text class="table__head-cell table__col-dur">ESTADO</text>
<text class="table__head-cell table__col-act">AÇÃO</text>
</row>
<column class="u-fill-all">
<column class="table__body">
<template x-for="container in containers" :key="container.idFull">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="container.name"></text>
<text class="table__cell table__col-img" x-text="container.image"></text>
<text class="u-muted table__col-state" x-text="container.owner"></text>
<row class="state-cell table__col-dur" :class="'state-cell--' + container.stateKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="container.stateLabel"></text>
</row>
<row class="table__col-act">
<button class="btn btn--row-remove" type="button" x-show="container.canRemove"
@click="store.dockerRemoveContainer(container.idFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="containers" style="padding:16px">Nenhum container.</text>
</column>
</column>
</column>
<column class="panel panel--fill" x-show="store.dockerTab === 'images'">
<row class="env__head">
<text class="u-muted-grow" 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 btn--danger" type="button" @click="store.dockerPruneImages()">Limpar sem uso</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc">TAGS</text>
<text class="table__head-cell table__col-proj">PROJETO / SERVIÇO</text>
<text class="table__head-cell table__col-dur">TAMANHO</text>
<text class="table__head-cell table__col-dur">CRIADA</text>
<text class="table__head-cell table__col-state">USO</text>
<text class="table__head-cell table__col-act">AÇÃO</text>
</row>
<column class="u-fill-all">
<column class="table__body">
<template x-for="image in images" :key="image.idFull">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="image.tags"></text>
<text class="table__cell table__col-proj" x-text="image.owner"></text>
<text class="u-muted table__col-dur" x-text="image.size"></text>
<text class="u-muted table__col-dur" x-text="image.created"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + image.inUseKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="image.inUseLabel"></text>
</row>
<row class="table__col-act">
<button class="btn btn--row-remove" type="button" x-show="!image.inUse"
@click="store.dockerRemoveImage(image.idFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="images" style="padding:16px">Nenhuma imagem.</text>
</column>
</column>
</column>
<column class="panel panel--fill" x-show="store.dockerTab === 'volumes'">
<row class="env__head">
<text class="u-muted-grow" 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 btn--danger" type="button" @click="store.dockerPruneVolumes()">Limpar sem uso</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc">NOME</text>
<text class="table__head-cell table__col-proj">DRIVER</text>
<text class="table__head-cell table__col-dur">TAMANHO</text>
<text class="table__head-cell table__col-state">USO</text>
<text class="table__head-cell table__col-act">AÇÃO</text>
</row>
<column class="u-fill-all">
<column class="table__body">
<template x-for="volume in volumes" :key="volume.nameFull">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="volume.name"></text>
<text class="table__cell table__col-proj" x-text="volume.driver"></text>
<text class="u-muted table__col-dur" x-text="volume.size"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + volume.inUseKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="volume.inUseLabel"></text>
</row>
<row class="table__col-act">
<button class="btn btn--row-remove" type="button" x-show="!volume.inUse"
@click="store.dockerRemoveVolume(volume.nameFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="volumes" style="padding:16px">Nenhum volume.</text>
</column>
</column>
</column>
<column class="panel panel--fill" x-show="store.dockerTab === 'networks'">
<row class="env__head">
<text class="u-muted-grow" 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 btn--danger" type="button" @click="store.dockerPruneNetworks()">Limpar sem uso</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc">NOME</text>
<text class="table__head-cell table__col-proj">PROJETO</text>
<text class="table__head-cell table__col-dur">DRIVER</text>
<text class="table__head-cell table__col-dur">CONTAINERS</text>
<text class="table__head-cell table__col-state">USO</text>
<text class="table__head-cell table__col-act">AÇÃO</text>
</row>
<column class="u-fill-all">
<column class="table__body">
<template x-for="network in networks" :key="network.idFull">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="network.name"></text>
<text class="table__cell table__col-proj" x-text="network.project"></text>
<text class="u-muted table__col-dur" x-text="network.driver"></text>
<text class="u-muted table__col-dur" x-text="network.containers"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + network.inUseKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="network.inUseLabel"></text>
</row>
<row class="table__col-act">
<button class="btn btn--row-remove" type="button" x-show="!network.inUse"
@click="store.dockerRemoveNetwork(network.idFull)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="networks" style="padding:16px">Nenhuma network.</text>
</column>
</column>
</column>
<column class="panel panel--fill" x-show="store.dockerTab === 'registry'">
<row class="env__head">
<text class="u-muted-grow"
x-text="'Registry ' + registryStatusLabel + ' · ' + repos.length + ' repositório(s)'"></text>
<button class="btn 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="table__head">
<text class="table__head-cell table__col-svc">REPOSITÓRIO</text>
<text class="table__head-cell table__col-proj">TAGS</text>
<text class="table__head-cell table__col-dur">TAMANHO</text>
<text class="table__head-cell table__col-dur">CRIADO</text>
<text class="table__head-cell table__col-act-2">AÇÃO</text>
</row>
<text class="u-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="u-muted" x-fallback="repos" style="padding:16px">
Nenhum repositório corresponde à busca.
</text>
</template>
<column class="u-fill-all">
<column class="table__body">
<template x-for="repo in repos" :key="repo.name">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="repo.name"></text>
<text class="table__cell table__col-proj" x-text="repo.tagCount"></text>
<text class="u-muted table__col-dur" x-text="repo.size"></text>
<text class="u-muted table__col-dur" x-text="repo.created"></text>
<row class="table__col-act-2">
<button class="btn btn--copy" type="button" title="Ver tags"
@click="store.registryOpenRepo(repo.name)">≡</button>
<button class="env__del" type="button" title="Remover"
@click="store.registryRemoveRepo(repo.name)">✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
<text class="u-label-cap" x-text="'TOKENS DE ACESSO (' + tokens.length + ')'"></text>
<row class="env__head">
<text class="u-muted-grow">Basic auth (docker login) — o segredo só aparece uma vez, na criação.</text>
<button class="btn btn--primary" type="button" @click="openTokenModal()">+ Novo token</button>
</row>
<text class="u-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="table__head" x-show="tokens.length > 0">
<text class="table__head-cell table__col-svc">NOME</text>
<text class="table__head-cell table__col-proj">ESCOPO</text>
<text class="table__head-cell table__col-dur">CRIADO</text>
<text class="table__head-cell table__col-dur">ÚLTIMO USO</text>
<text class="table__head-cell table__col-act">AÇÃO</text>
</row>
<column class="table__body">
<template x-for="token in tokens" :key="token.name">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="token.name"></text>
<text class="table__cell table__col-proj" x-text="token.scope"></text>
<text class="u-muted table__col-dur" x-text="token.created"></text>
<text class="u-muted table__col-dur" x-text="token.lastUsed"></text>
<row class="table__col-act">
<button class="env__del" type="button" title="Revogar"
@click="store.registryRemoveToken(token.name)">✕</button>
</row>
</row>
<hr class="rule" />
</template>
</column>
</column>
<column x-show="store.registrySelectedRepo">
<row class="env__head">
<text class="u-muted-grow" x-text="store.registrySelectedRepo + ' — ' + tags.length + ' tag(s)'"></text>
<button class="btn btn--ghost" type="button" @click="store.registryCloseRepo()">← Voltar</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc">TAG</text>
<text class="table__head-cell table__col-proj">DIGEST</text>
<text class="table__head-cell table__col-dur">TAMANHO</text>
<text class="table__head-cell table__col-dur">ATUALIZADA</text>
<text class="table__head-cell table__col-act">AÇÃO</text>
</row>
<column class="u-center-fill" x-show="store.registryTagsLoading" style="padding:24px">
<text class="u-muted">carregando…</text>
</column>
<column class="u-fill-all" x-show="!store.registryTagsLoading">
<column class="table__body">
<template x-for="tagItem in tags" :key="tagItem.tag">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="tagItem.tag"></text>
<text class="u-muted table__col-proj" x-text="tagItem.digest"></text>
<text class="u-muted table__col-dur" x-text="tagItem.size"></text>
<text class="u-muted table__col-dur" x-text="tagItem.updated"></text>
<row class="table__col-act">
<button class="btn btn--row-remove" type="button" @click="store.registryRemoveTag(tagItem.tag)">Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
</column>
<column class="modal__backdrop" x-show="showTokenModal">
<column class="modal__box" x-win="'registry_token'" data-w="520" data-h="420">
<row class="modal__head">
<text class="u-label-cap">NOVO TOKEN DE REGISTRY</text>
<button class="btn btn--ghost" type="button" @click="closeTokenModal()">Fechar</button>
</row>
<column class="modal__body">
<template x-if="newRegistryTokenStep === 'form'">
<column class="form-panel">
<text class="u-label-cap">NOME</text>
<input class="field__input" type="text" x-model="newRegistryTokenName" placeholder="ci-deploy" />
<text class="u-label-cap" style="margin-top:14px">ESCOPO</text>
<row class="tabs">
<button type="button" :class="newRegistryTokenScope === 'pull' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="newRegistryTokenScope = 'pull'">pull</button>
<button type="button" :class="newRegistryTokenScope === 'push' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="newRegistryTokenScope = 'push'">push</button>
</row>
<text class="u-error" x-show="newRegistryTokenError" x-text="newRegistryTokenError" style="margin-top:8px"></text>
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="newRegistryTokenCreate()">Criar token</button>
</row>
</column>
</template>
<template x-if="newRegistryTokenStep === 'reveal'">
<column class="form-panel">
<text class="u-muted-grow">
Guarde este comando agora — o segredo não é mostrado de novo.
</text>
<pre class="log-panel__pre" x-text="newRegistryTokenLoginCommand" style="margin-top:10px"></pre>
<row class="env__add" style="margin-top:10px">
<button class="btn btn--copy" type="button" @click="navigator.clipboard.writeText(newRegistryTokenLoginCommand)">Copiar</button>
<button class="btn 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="page-head">
<column class="page-head__titles">
<text class="page-head__title">Schedules</text>
<text class="page-head__subtitle">Tarefas one-shot (docker-compose) agendadas ou manuais, de todos os projetos.</text>
</column>
<row class="page-head__stats page-head__stats--hide-md">
<column class="stat-card">
<text class="u-label-cap">JOBS</text>
<text class="stat-card__num stat-card__num--ok" x-text="rows.length"></text>
</column>
</row>
<button class="btn btn--primary" type="button" @click="store.openNewJob()">+ Novo job</button>
</row>
<column class="panel panel--fill">
<row class="table__head">
<text class="table__head-cell table__col-svc">NOME</text>
<text class="table__head-cell table__col-proj">PROJETO / GATILHO</text>
<text class="table__head-cell table__col-state">ÚLTIMA EXECUÇÃO</text>
<text class="table__head-cell table__col-dur">RECORRÊNCIA</text>
<text class="table__head-cell table__col-dur">PRÓXIMA EXECUÇÃO</text>
<text class="table__head-cell table__col-warm">AQUECIMENTO</text>
<text class="table__head-cell table__col-act-2">AÇÕES</text>
</row>
<text class="u-muted" x-fallback="rows" style="padding:16px">Nenhum job cadastrado ainda.</text>
<column class="u-fill-all">
<column class="table__body">
<template x-for="row in rows" :key="row.id">
<column class="list-row">
<row class="table__row">
<text class="table__cell table__cell--key table__col-svc" x-text="row.name"></text>
<text class="table__cell table__col-proj" x-text="row.owner"></text>
<row class="state-cell table__col-state" :class="'state-cell--' + row.lastRunKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="row.lastRunLabel"></text>
</row>
<text class="u-muted table__col-dur" x-text="row.recurrence"></text>
<text class="u-muted table__col-dur" x-text="row.nextRunAt"></text>
<row class="state-cell table__col-warm" :class="'state-cell--' + row.warmKind" :title="row.warmReason">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="row.warmLabel"></text>
</row>
<row class="table__col-act-2">
<button class="btn btn--copy" type="button" title="Ações do job"
@click="store.openJobActions(row.id)">⋯</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
</column>
</column>
<column class="modal__backdrop" x-show="$store.app.jobActionsJobId !== ''">
<column class="modal__box" x-win="'job_actions'" data-w="380" data-h="460">
<row class="modal__head">
<text class="u-label-cap" x-text="$store.app.jobActionsRow ? $store.app.jobActionsRow.name : 'JOB'"></text>
<button class="btn btn--ghost" type="button" @click="$store.app.closeJobActions()">Fechar</button>
</row>
<column class="modal__body job-actions" x-show="$store.app.jobActionsRow">
<column class="job-actions__status">
<row class="state-cell" :class="'state-cell--' + $store.app.jobActionsRow?.warmKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="$store.app.jobActionsRow?.warmLabel"></text>
</row>
<text class="u-muted" x-show="$store.app.jobActionsRow?.warmReason"
x-text="$store.app.jobActionsRow?.warmReason"></text>
<text class="u-muted" x-show="$store.app.jobActionsRow?.isWarm"
x-text="'aquecido em ' + $store.app.jobActionsRow?.warmedAtLabel"></text>
</column>
<button class="btn btn--ghost job-actions__item" type="button"
x-show="!$store.app.jobActionsRow?.running"
@click="$store.app.jobRunNow($store.app.jobActionsRow.id); $store.app.closeJobActions()">▶ Rodar agora</button>
<button class="btn btn--ghost job-actions__item job-actions__item--danger" type="button"
x-show="$store.app.jobActionsRow?.running"
@click="$store.app.jobRunCancel($store.app.jobActionsRow.lastRunId); $store.app.closeJobActions()">■ Cancelar execução</button>
<button class="btn btn--ghost job-actions__item" type="button"
:disabled="$store.app.jobActionsRow?.isWarming"
@click="$store.app.jobWarm($store.app.jobActionsRow.id)">◐ Aquecer</button>
<button class="btn btn--ghost job-actions__item" type="button"
x-show="$store.app.jobActionsRow?.isWarm"
:disabled="$store.app.jobActionsRow?.isWarming"
title="Refaz o build do zero e rebaixa as imagens base"
@click="$store.app.jobRebuild($store.app.jobActionsRow.id)">↻ Reconstruir (sem cache)</button>
<button class="btn btn--ghost job-actions__item" type="button"
x-show="$store.app.jobActionsRow?.lastRunId"
@click="$store.app.openJobLogs($store.app.jobActionsRow.lastRunId); $store.app.closeJobActions()">≡ Ver logs</button>
<button class="btn btn--ghost job-actions__item" type="button"
@click="$store.app.jobToggle($store.app.jobActionsRow.id); $store.app.closeJobActions()"
x-text="$store.app.jobActionsRow?.enabled ? '‖ Pausar' : '▶ Ativar'"></button>
<button class="btn btn--ghost job-actions__item" type="button"
@click="$store.app.openEditJob($store.app.jobActionsRow.id); $store.app.closeJobActions()">✎ Editar</button>
<button class="btn btn--ghost job-actions__item job-actions__item--danger" type="button"
@click="$store.app.jobDelete($store.app.jobActionsRow.id); $store.app.closeJobActions()">✕ Remover</button>
</column>
</column>
</column>
<column class="modal__backdrop" x-show="$store.app.showNewJob">
<column class="modal__box" x-win="'new_job'" data-w="760" data-h="640">
<row class="modal__head">
<text class="u-label-cap" x-text="$store.app.newJobEditId ? 'EDITAR JOB' : 'NOVO JOB'"></text>
<row style="gap:8px">
<button class="btn btn--ghost" type="button"
x-show="$store.app.newJobStep !== 'pick_project' && !$store.app.newJobEditId"
@click="$store.app.newJobBack()">‹ Voltar</button>
<button class="btn btn--ghost" type="button" @click="$store.app.closeNewJob()">Fechar</button>
</row>
</row>
<column class="modal__body">
<column class="grid" x-show="$store.app.newJobStep === 'pick_project'">
<template x-for="newJobProject in $store.app.newJobProjects" :key="newJobProject.id">
<column class="card">
<text class="card__name" x-text="newJobProject.name"></text>
<button class="card__action card__action--open" type="button" @click="$store.app.newJobPickProject(newJobProject.id, newJobProject.name)">Selecionar ›</button>
</column>
</template>
<text class="u-muted" x-fallback="$store.app.newJobProjects" style="padding:16px">Nenhum projeto ainda.</text>
</column>
<column class="grid" x-show="$store.app.newJobStep === 'pick_service'">
<column class="card">
<text class="card__name">Nenhum (autônomo)</text>
<text class="u-muted">Job sem serviço gatilho — só variáveis do projeto.</text>
<button class="card__action card__action--open" type="button" @click="$store.app.newJobPickNoService()">Selecionar ›</button>
</column>
<template x-for="filteredService in $store.app.newJobServicesFiltered" :key="filteredService.service.id">
<column class="card">
<text class="card__name" x-text="filteredService.service.spec.name"></text>
<button class="card__action card__action--open" type="button"
@click="$store.app.newJobPickService(filteredService.service.id, filteredService.service.spec.name)">Selecionar ›</button>
</column>
</template>
</column>
<column class="form-panel" x-show="$store.app.newJobStep === 'form'" style="max-width:100%">
<text class="u-muted-grow" x-text="$store.app.newJobProjectName + ' / ' + ($store.app.newJobServiceName || 'nenhum (autônomo)')"></text>
<text class="u-label-cap" style="margin-top:10px">NOME</text>
<input class="field__input" type="text" x-model="$store.app.newJobName" placeholder="backup-diario" />
<text class="u-label-cap" style="margin-top:14px">FONTE DO COMPOSE</text>
<row class="tabs">
<button type="button" :class="$store.app.newJobSourceTab === 'compose' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="$store.app.newJobSetSourceTab('compose')">Compose</button>
<button type="button" :class="$store.app.newJobSourceTab === 'git' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="$store.app.newJobSetSourceTab('git')">Git</button>
</row>
<template x-if="$store.app.newJobSourceTab === 'compose'">
<column>
<text class="u-label-cap" style="margin-top:14px">DOCKER COMPOSE (YAML)</text>
<textarea class="env__editor" x-model="$store.app.newJobCompose"></textarea>
</column>
</template>
<template x-if="$store.app.newJobSourceTab === 'git'">
<column style="margin-top:10px; gap:10px">
<text class="u-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="u-muted" x-show="$store.app.newJobGitMessage" x-text="$store.app.newJobGitMessage"></text>
<text class="u-label-cap">CONTA GIT</text>
<select class="field__input" x-model="$store.app.newJobGitProviderId"
@change="$store.app.newJobGitProviderPick($event.target.value)">
<option value="">selecione a conta</option>
<template x-for="newJobGitProvider in $store.app.newJobGitProviders" :key="newJobGitProvider.id">
<option :value="newJobGitProvider.id" x-text="newJobGitProvider.name"></option>
</template>
</select>
<text class="u-muted">Conecte contas em Settings → Git.</text>
<text class="u-label-cap">REPOSITÓRIO</text>
<select class="field__input" x-model="$store.app.newJobGitRepoFullName"
@change="$store.app.newJobGitRepoPick($event.target.value)">
<option value="">selecione o repositório</option>
<template x-for="newJobGitRepo in $store.app.newJobGitRepos" :key="newJobGitRepo.full_name">
<option :value="newJobGitRepo.full_name" x-text="newJobGitRepo.full_name"></option>
</template>
</select>
<text class="u-label-cap">BRANCH</text>
<select class="field__input" x-model="$store.app.newJobGitBranch">
<option value="">selecione a branch</option>
<template x-for="branch in $store.app.newJobGitBranches" :key="branch.name">
<option :value="branch.name" x-text="branch.name"></option>
</template>
</select>
<text class="u-label-cap">CAMINHO DO COMPOSE NO REPO</text>
<input class="field__input" type="text" x-model="$store.app.newJobComposePath" placeholder="docker-compose.yml" />
</column>
</template>
<text class="u-label-cap" style="margin-top:14px">MAIN SERVICE</text>
<input class="field__input" type="text" x-model="$store.app.newJobMainService" placeholder="job" />
<text class="u-label-cap" style="margin-top:14px">VARIÁVEIS DE AMBIENTE (opcional)</text>
<text class="u-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.newJobEnvText" placeholder="FLOW_MYENV=test"></textarea>
<text class="u-label-cap" style="margin-top:14px">RECORRÊNCIA</text>
<row class="tabs">
<button type="button" :class="$store.app.newJobKind === 'manual' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="$store.app.newJobKind = 'manual'">Manual</button>
<button type="button" :class="$store.app.newJobKind === 'interval' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="$store.app.newJobKind = 'interval'">Intervalo</button>
<button type="button" :class="$store.app.newJobKind === 'daily' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="$store.app.newJobKind = 'daily'">Diário</button>
<button type="button" :class="$store.app.newJobKind === 'weekly' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="$store.app.newJobKind = 'weekly'">Semanal</button>
</row>
<row class="healthcheck__grid" style="margin-top:8px" x-show="$store.app.newJobKind === 'interval'">
<column class="healthcheck__field">
<text class="u-label-cap">A CADA (HORAS)</text>
<input class="field__input" type="number" min="1" max="168" x-model="$store.app.newJobHours" />
</column>
</row>
<row class="healthcheck__grid" style="margin-top:8px" x-show="$store.app.newJobKind === 'daily' || $store.app.newJobKind === 'weekly'">
<column class="healthcheck__field" x-show="$store.app.newJobKind === 'weekly'">
<text class="u-label-cap">DIA DA SEMANA</text>
<select class="field__input" x-model="$store.app.newJobWeekday">
<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="healthcheck__field">
<text class="u-label-cap">HORÁRIO</text>
<input class="field__input" type="time" x-model="$store.app.newJobTime" />
</column>
</row>
<text class="u-error" x-show="$store.app.newJobError" x-text="$store.app.newJobError" style="margin-top:8px"></text>
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="$store.app.newJobCreate()" :disabled="$store.app.newJobSubmitting"
x-text="$store.app.newJobSubmitting ? ($store.app.newJobEditId ? 'Salvando…' : 'Criando…') : ($store.app.newJobEditId ? 'Salvar' : 'Criar job')"></button>
</row>
</column>
</column>
</column>
</column>
<column class="modal__backdrop" x-show="$store.app.jobLogsFor">
<column class="modal__box" x-win="'job_logs'" data-w="860" data-h="560">
<row class="modal__head">
<text class="u-label-cap">LOGS DO JOB</text>
<button class="btn btn--ghost" type="button" @click="$store.app.closeJobLogs()">Fechar</button>
</row>
<column class="modal__body log-panel">
<column class="u-fill-all" x-follow>
<column class="log-panel__body">
<template x-for="(jobLogLine, index) in $store.app.jobLogLines" :key="index">
<row class="log-panel__line">
<text class="log-panel__time" x-text="$store.app.timeHms(jobLogLine.timestamp)"></text>
<text class="log-panel__text" :style="jobLogLine.stream === 'Stderr' ? 'color:#F85149' : ''" x-text="jobLogLine.line"></text>
</row>
</template>
<text class="u-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="page-head">
<column class="page-head__titles">
<text class="page-head__title">Settings</text>
<text class="page-head__subtitle">Daemon web server, HTTPS e contas Git.</text>
</column>
</row>
<row class="tabs">
<button type="button" :class="store.settingsTab === 'web' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.settingsTab = 'web'">Web Server</button>
<button type="button" :class="store.settingsTab === 'git' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.settingsTab = 'git'">Git</button>
<button type="button" :class="store.settingsTab === 'iac' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.settingsTab = 'iac'">Infra as Code</button>
<button type="button" :class="store.settingsTab === 'maintenance' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.settingsTab = 'maintenance'">Manutenção</button>
</row>
<hr class="rule" />
<column class="u-fill-all" x-show="store.settingsTab === 'web'">
<column class="form-panel">
<text class="u-label-cap">URL PÚBLICA DA API</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact" x-text="store.serverSettingsPublicBase || '—'"></text>
<button class="btn btn--copy" type="button" @click="navigator.clipboard.writeText(store.serverSettingsPublicBase)">Copiar</button>
</row>
<text class="u-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="u-label-cap" style="margin-top:14px">TOKEN DE CONEXÃO</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact" x-text="store.serverAccessKey ? store.serverAccessKey.slice(0, 12) + '…' + store.serverAccessKey.slice(-6) : '—'"></text>
<button class="btn btn--copy" type="button" :disabled="!store.serverAccessKey"
@click="navigator.clipboard.writeText(store.serverAccessKey)">Copiar</button>
</row>
<text class="u-muted">
base64 de host:porta::token — endereço do servidor + token numa string só. É o que se cola
no login da GUI e da webui, e o que um agente de IA usa em POST /api/login. O daemon também
o deixa no config.toml, em [api].connection_token. Vale tanto quanto o token: não compartilhe à toa.
</text>
<text class="u-label-cap" style="margin-top:14px">CONECTAR UM AGENTE (CLAUDE CODE)</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact">claude mcp add … rustploy …/mcp</text>
<button class="btn btn--copy" type="button" @click="navigator.clipboard.writeText(store.serverMcpCommand)">Copiar</button>
</row>
<text class="u-muted">
Copia o comando que registra o MCP deste servidor no Claude Code, já com o token. Cole num
terminal e abra uma sessão nova do Claude Code; confira com /mcp. Contém o token: não cole em lugar público.
</text>
<text class="u-label-cap" style="margin-top:14px">E-MAIL LET'S ENCRYPT (HTTPS AUTOMÁTICO)</text>
<input class="field__input" type="text" x-model="store.serverSettingsEmail" placeholder="voce@exemplo.com" />
<text class="u-muted">Certificados TLS emitidos automaticamente. Requer restart do daemon.</text>
<text class="u-label-cap" style="margin-top:14px">DOMÍNIO DO REGISTRY (EXPOSIÇÃO PÚBLICA)</text>
<input class="field__input" type="text" x-model="store.serverSettingsRegistryDomain" placeholder="registry.meusite.com" />
<text class="u-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 btn--primary" type="button" @click="store.settingsSave()">Save</button>
<text class="u-muted" x-text="store.settingsMessage"></text>
</row>
</column>
</column>
<column class="u-fill-all" x-show="store.settingsTab === 'git'">
<column class="form-panel">
<text class="u-label-cap" x-text="'CONTAS CONECTADAS (' + gitProviderRows.length + ')'"></text>
<text class="u-muted" x-fallback="gitProviderRows">Nenhuma conta Git conectada ainda.</text>
<column class="kv-list">
<template x-for="gitProviderRow in gitProviderRows" :key="gitProviderRow.id">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-text="gitProviderRow.name"></text>
<text class="u-muted" x-text="gitProviderRow.kind + ' · ' + gitProviderRow.baseUrl + ' · ' + gitProviderRow.authMode + ' · ' + gitProviderRow.account"></text>
</column>
<button class="btn btn--danger" type="button" @click="store.gitProviderDelete(gitProviderRow.id)">Remover</button>
</row>
</template>
</column>
<hr class="rule" />
<text class="u-label-cap">CONECTAR CONTA GIT</text>
<row class="tabs">
<button type="button" :class="store.gitProviderKind === 'gitea' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.gitProviderKind = 'gitea'">Gitea</button>
<button type="button" :class="store.gitProviderKind === 'github' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.gitProviderKind = 'github'">GitHub</button>
</row>
<text class="u-muted" x-show="store.gitProviderMessage" x-text="store.gitProviderMessage"></text>
<button class="btn btn--primary" type="button" x-show="store.gitProviderOauthUrl"
@click="window.open(store.gitProviderOauthUrl, '_blank')">Abrir autorização no navegador</button>
<row class="healthcheck__grid" style="margin-top:8px">
<column class="healthcheck__field">
<text class="u-label-cap">NOME</text>
<input class="field__input" type="text" x-model="store.gitProviderName" placeholder="Meu Gitea" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">BASE URL</text>
<input class="field__input" type="text" x-model="store.gitProviderBaseUrl" placeholder="https://gitea.exemplo.com" />
<text class="u-muted" x-show="store.gitProviderKind === 'github'">
Deixe vazio para github.com. Só preencha para GitHub Enterprise Server.
</text>
</column>
</row>
<text class="u-label-cap" style="margin-top:14px">MÉTODO</text>
<row class="tabs">
<button type="button" :class="store.gitProviderMode === 'oauth' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.gitProviderMode = 'oauth'">OAuth2</button>
<button type="button" :class="store.gitProviderMode === 'pat' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.gitProviderMode = 'pat'">Token (PAT)</button>
</row>
<template x-if="store.gitProviderMode !== 'pat'">
<column style="gap:8px; margin-top:8px">
<text class="u-label-cap">CLIENT ID</text>
<input class="field__input" type="text" x-model="store.gitProviderClientId" />
<text class="u-label-cap">CLIENT SECRET</text>
<input class="field__input" type="password" x-model="store.gitProviderClientSecret" />
<text class="u-label-cap">REDIRECT URI (cole no app OAuth do provedor)</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact" x-text="store.gitProviderRedirect"></text>
<button class="btn btn--copy" type="button" @click="navigator.clipboard.writeText(store.gitProviderRedirect)">Copiar</button>
</row>
<text class="u-muted" x-show="store.gitProviderKind !== 'github'">
No Gitea: Settings → Applications → crie um OAuth2 app e cole a Redirect URI acima.
</text>
<text class="u-muted" x-show="store.gitProviderKind === '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.gitProviderMode === 'pat'">
<column style="gap:8px; margin-top:8px">
<text class="u-label-cap">PERSONAL ACCESS TOKEN</text>
<input class="field__input" type="password" x-model="store.gitProviderPersonalAccessToken" />
<text class="u-muted" x-show="store.gitProviderKind !== 'github'">
No Gitea: Settings → Applications → Generate New Token (escopo: repo).
</text>
<text class="u-muted" x-show="store.gitProviderKind === '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 btn--primary" type="button" @click="store.gitProviderConnect()">Conectar conta</button>
<button class="btn btn--ghost" type="button" @click="store.gitProviderRefresh()">Atualizar lista</button>
</row>
</column>
</column>
<column class="u-fill-all" x-show="store.settingsTab === 'iac'">
<column class="form-panel">
<text class="u-label-cap">EXPORTAR</text>
<text class="u-muted">
Gera um `.zip` com rustploy.yml + rustploy.vars.toml e o baixa pelo navegador. Os
valores das env vars ficam só no TOML; secrets nunca saem do daemon.
</text>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="store.manifestExport()">Exportar para um .zip…</button>
<text class="u-muted" x-text="store.manifestExportMessage"></text>
</row>
<template x-if="store.manifestHasExport">
<column>
<row class="env__head">
<text class="u-label-cap">rustploy.yml</text>
<button class="btn btn--copy" type="button" @click="navigator.clipboard.writeText(store.manifestYaml)">Copiar</button>
</row>
<textarea class="env__editor" readonly x-model="store.manifestYaml"></textarea>
<row class="env__head">
<text class="u-label-cap">rustploy.vars.toml</text>
<button class="btn btn--copy" type="button" @click="navigator.clipboard.writeText(store.manifestDotenv)">Copiar</button>
</row>
<textarea class="env__editor" readonly x-model="store.manifestDotenv"></textarea>
</column>
</template>
<hr class="rule" />
<text class="u-label-cap">IMPORTAR</text>
<text class="u-muted">
Escolha o `.zip` do manifesto (o mesmo que o Exportar gera). Ele precisa ter exatamente
um .yml e um .toml na raiz — nada mais, ou o import é recusado. Falta o valor de alguma
variável usada no YAML? Também é cancelado antes de aplicar qualquer coisa (a faltante
vem rotulada com o escopo, ex. "Chiquitos/api: APP_PORT").
</text>
<label class="check-row">
<input type="checkbox" x-model="store.manifestPrune" /><span>Remover serviços ausentes do manifesto (prune)</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.manifestDeploy" /><span>Disparar deploy dos serviços criados/alterados</span>
</label>
<row class="env__add" style="margin-top:8px">
<input class="field__input" type="file" accept=".zip,application/zip" @change="store.manifestPickZip($event)" />
</row>
<text class="u-muted" x-show="store.manifestZipName" x-text="'Zip carregado: ' + store.manifestZipName"></text>
<text class="u-label-cap">…OU COLE O TEXTO</text>
<text class="u-label-cap">rustploy.yml</text>
<textarea class="env__editor" x-model="store.manifestImportYaml"
@input="store.manifestZipName = ''"
placeholder="Cole aqui o YAML exportado (ou escrito à mão)"></textarea>
<text class="u-label-cap">rustploy.vars.toml</text>
<textarea class="env__editor" x-model="store.manifestImportDotenv"
@input="store.manifestZipName = ''"
placeholder='Cole aqui o TOML de variáveis ([project."App".env] com DB_PASS = "s3cr3t")'></textarea>
<row class="env__add" style="margin-top:8px">
<button class="btn btn--primary" type="button" @click="store.manifestImport()">Importar</button>
<text class="u-muted" x-text="store.manifestImportMessage"></text>
</row>
<text class="u-error" x-show="store.manifestHasMissing" x-text="'Variáveis sem valor no TOML: ' + store.manifestMissingVars"></text>
<template x-if="store.manifestHasReport">
<column>
<text class="u-label-cap">RESULTADO</text>
<column class="kv-list">
<template x-for="(manifestReportLine, index) in store.manifestReportLines" :key="index">
<row class="kv-list__row"><text class="u-muted" x-text="manifestReportLine"></text></row>
</template>
</column>
</column>
</template>
</column>
</column>
<column class="u-fill-all" x-show="store.settingsTab === 'maintenance'">
<column class="form-panel">
<text class="u-label-cap">LIMPEZA AUTOMÁTICA</text>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupEnabled" /><span>Ativar limpeza automática</span>
</label>
<text class="u-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="u-label-cap" style="margin-top:14px">FREQUÊNCIA</text>
<row class="tabs">
<button type="button" :class="store.dockerCleanupKind === 'interval' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerCleanupKind = 'interval'">A cada N horas</button>
<button type="button" :class="store.dockerCleanupKind === 'daily' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerCleanupKind = 'daily'">Todo dia</button>
<button type="button" :class="store.dockerCleanupKind === 'weekly' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerCleanupKind = 'weekly'">Toda semana</button>
</row>
<row class="healthcheck__grid" x-show="store.dockerCleanupKind === 'interval'" style="margin-top:8px">
<column class="healthcheck__field">
<text class="u-label-cap">A CADA (HORAS)</text>
<input class="field__input" type="number" min="1" max="168" x-model="store.dockerCleanupHours" placeholder="6" />
</column>
</row>
<row class="tabs" x-show="store.dockerCleanupKind === 'weekly'" style="margin-top:8px">
<button type="button" :class="store.dockerCleanupWeekday === '0' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '0'">Seg</button>
<button type="button" :class="store.dockerCleanupWeekday === '1' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '1'">Ter</button>
<button type="button" :class="store.dockerCleanupWeekday === '2' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '2'">Qua</button>
<button type="button" :class="store.dockerCleanupWeekday === '3' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '3'">Qui</button>
<button type="button" :class="store.dockerCleanupWeekday === '4' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '4'">Sex</button>
<button type="button" :class="store.dockerCleanupWeekday === '5' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '5'">Sáb</button>
<button type="button" :class="store.dockerCleanupWeekday === '6' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '6'">Dom</button>
</row>
<row class="healthcheck__grid" x-show="store.dockerCleanupKind !== 'interval'" style="margin-top:8px">
<column class="healthcheck__field">
<text class="u-label-cap">HORÁRIO</text>
<input class="field__input" type="time" x-model="store.dockerCleanupTime" />
</column>
</row>
<text class="u-muted" style="margin-top:8px" x-text="'Próxima execução agendada: ' + store.dockerCleanupNextRunLabel"></text>
<hr class="rule" />
<text class="u-label-cap">O QUE LIMPAR</text>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupContainers" /><span>Containers parados</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupImages" /><span>Imagens sem uso</span>
</label>
<label class="check-row" x-show="store.dockerCleanupImages" style="margin-left:20px">
<input type="checkbox" x-model="store.dockerCleanupImagesAll" /><span>incluir todas as não usadas (não só as sem tag)</span>
</label>
<row class="healthcheck__grid" x-show="store.dockerCleanupImages" style="margin-left:20px">
<column class="healthcheck__field">
<text class="u-label-cap">SÓ IMAGENS CRIADAS HÁ MAIS DE (HORAS)</text>
<input class="field__input" type="number" min="0" max="8760" x-model="store.dockerCleanupImagesOlderThanHours" placeholder="168" />
<text class="u-muted">0 não filtra por idade. O runtime olha a data de criação da imagem, não a do último uso: as imagens base antigas continuam elegíveis.</text>
</column>
</row>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupNetworks" /><span>Redes sem uso</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupBuildCache" /><span>Cache de build</span>
</label>
<row class="healthcheck__grid" x-show="store.dockerCleanupBuildCache" style="margin-left:20px">
<column class="healthcheck__field">
<text class="u-label-cap">MANTER O CACHE USADO NAS ÚLTIMAS (HORAS)</text>
<input class="field__input" type="number" min="0" max="8760" x-model="store.dockerCleanupBuildCacheOlderThanHours" placeholder="168" />
<text class="u-muted">0 remove todo o cache sem uso. Com um prazo, o cache de um job ou deploy recente sobrevive e o próximo build não recomeça do zero.</text>
</column>
</row>
<hr class="rule" />
<text class="u-label-cap u-error">VOLUMES — CUIDADO</text>
<text class="u-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.dockerCleanupVolumes" /><span>Volumes sem uso</span>
</label>
<label class="check-row" x-show="store.dockerCleanupVolumes" style="margin-left:20px">
<input type="checkbox" x-model="store.dockerCleanupVolumesAll" /><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 btn--primary" type="button" @click="store.dockerCleanupSave()">Salvar</button>
<button class="tabs__tab" type="button" x-show="!store.dockerCleanupRunning" @click="store.dockerCleanupRunNow()">Executar agora</button>
<button class="tabs__tab" type="button" x-show="store.dockerCleanupRunning" disabled>Executando…</button>
<text class="u-muted" x-text="store.dockerCleanupMessage"></text>
</row>
<hr class="rule" />
<text class="u-label-cap">ÚLTIMA EXECUÇÃO</text>
<text class="u-muted" x-text="store.dockerCleanupLastRunText"></text>
</column>
</column>
</column>
<column class="content" x-data="projects" x-show="$store.app.view === 'projects'">
<row class="page-head">
<column class="page-head__titles">
<text class="page-head__title">Projects</text>
<text class="page-head__subtitle">Projetos do cluster e a contagem de serviços de cada um.</text>
</column>
<row class="page-head__stats">
<column class="stat-card"><text class="u-label-cap">SERVICES</text><text class="stat-card__num" x-text="servicesCount"></text></column>
<column class="stat-card"><text class="u-label-cap">PROJECTS</text><text class="stat-card__num stat-card__num--ok" x-text="rows.length"></text></column>
</row>
</row>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="store.openProjectWin()">+ Novo projeto</button>
</row>
<column class="u-fill-all">
<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="u-muted" x-text="row.serviceCount + ' serviço(s) · ' + row.runningCount + ' rodando'"></text>
<text class="u-muted" x-text="row.description || '—'"></text>
<hr class="rule" />
<button class="card__action card__action--open" type="button" @click="store.openProject(row.id)">Abrir ›</button>
<button class="card__action card__action--remove" type="button" x-show="row.canDelete"
@click="store.deleteProject(row.id)">Remover</button>
<text class="u-muted" x-show="!row.canDelete">Não removível: tem serviços</text>
</column>
</template>
<text class="u-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-bar">
<button class="btn btn--ghost" type="button" @click="store.nav('projects')">‹ Projetos</button>
<column class="u-grow"></column>
<row class="proj-bar__actions" x-show="!editing">
<button class="btn btn--sm-primary" type="button" @click="store.openNewService()">+ Novo serviço</button>
<button class="btn btn--sm-ghost" type="button" @click="store.openProjectWin(project)">Editar projeto</button>
<button class="btn btn--sm-danger" type="button" x-show="canDelete"
@click="store.deleteProject(store.selectedProjectId)">Remover projeto</button>
</row>
</row>
<template x-if="true">
<row class="page-head">
<column class="page-head__titles">
<text class="page-head__title" x-text="project ? project.name : ''"></text>
<text class="page-head__subtitle" x-text="project ? (project.description || '') : ''"></text>
</column>
</row>
</template>
<row class="tabs">
<button type="button" :class="projectTab === 'services' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="projectTab = 'services'">Serviços</button>
<button type="button" :class="projectTab === 'env' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="projectTab = 'env'" x-text="'Variáveis (' + (project ? project.env_vars.length : 0) + ')'"></button>
<button type="button" :class="projectTab === 'secrets' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="projectTab = 'secrets'" x-text="'Secrets (' + secrets.length + ')'"></button>
<button type="button" :class="projectTab === 'jobs' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="projectTab = 'jobs'" x-text="'Jobs (' + jobs.length + ')'"></button>
</row>
<hr class="rule" />
<column class="u-fill-all" x-show="projectTab === 'services'">
<column class="grid">
<template x-for="service in services" :key="service.id">
<column class="card">
<row class="card__head">
<row style="align-items:center; gap:6px">
<text class="card__title" x-text="service.name"></text>
<text class="card__port" x-text="':' + service.port"></text>
</row>
<row class="badge">
<text class="status-dot" :class="'state-cell--' + service.statusKind">●</text>
<text class="badge__label" :class="'state-cell--' + service.statusKind" x-text="service.statusLabel"></text>
</row>
</row>
<row class="card__container" style="gap:6px;align-items:center">
<button class="btn--copy-mini" type="button" title="Copiar nome" x-show="service.containerNameFull"
@click="$store.app.copyText(service.containerNameFull, 'nome')">⧉</button>
<text class="u-muted" x-text="'name: ' + service.containerName"></text>
</row>
<row style="gap:6px;align-items:center">
<button class="btn--copy-mini" type="button" title="Copiar id" x-show="service.containerIdFull"
@click="$store.app.copyText(service.containerIdFull, 'id')">⧉</button>
<text class="u-muted" x-text="'id: ' + service.containerId"></text>
<text class="u-muted" x-show="service.containerExtra" x-text="'extra: ' + service.containerExtra"></text>
</row>
<hr class="rule" />
<row class="card__stats">
<column class="card__stat">
<text class="u-label-cap">CPU</text>
<text class="card__stat-num" x-text="service.cpu"></text>
</column>
<column class="card__stat">
<text class="u-label-cap">MEM</text>
<text class="card__stat-num" x-text="service.mem"></text>
</column>
</row>
<button class="card__action card__action--open" type="button" style="width:100%"
@click="store.openService(service.id)">Abrir ›</button>
<row class="card__actions">
<button class="card__action card__action--icon" type="button" title="Parar" aria-label="Parar serviço"
@click="store.stopService(service.id)">■</button>
<button class="card__action card__action--icon card__action--icon-danger" type="button" title="Remover" aria-label="Remover serviço"
@click="store.stopAndDeleteService(service.id)">✕</button>
<button class="card__action card__action--icon" type="button" title="Exportar serviço (arquivo para recriá-lo em outro servidor)"
aria-label="Exportar serviço" @click="store.openExportWin(service.id)">⇪</button>
</row>
</column>
</template>
<text class="u-muted" x-fallback="services" style="padding:16px">
Nenhum serviço neste projeto ainda.
</text>
</column>
</column>
<column class="env" x-show="projectTab === 'env'">
<row class="env__head">
<text class="u-muted-grow">Herdadas por todos os serviços deste projeto no deploy.</text>
<button class="btn btn--export" type="button" @click="openEnvText()" x-show="!envTextOpen">.env</button>
<button class="btn 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__input" type="text" placeholder="KEY" x-model="newEnvKey" />
<input class="env__input" type="text" placeholder="value" x-model="newEnvValue" />
<button class="btn btn--primary" type="button" @click="addEnvVar()">Adicionar</button>
</row>
<column class="kv-list">
<template x-for="envVar in envVars" :key="envVar.key">
<column style="width:100%">
<row class="kv-list__row" x-show="envVar.isComment">
<text class="u-muted" x-text="envVar.text"></text>
</row>
<row class="kv-list__row" x-show="!envVar.isComment">
<button class="env__del" type="button" @click="deleteEnvVar(envVar.key)">✕</button>
<text class="env__sep"></text>
<text class="env__key" x-text="envVar.key"></text>
<text class="kv-list__val kv-list__val--compact" x-text="envVar.isSecret ? '(secret)' : envVar.value"></text>
</row>
</column>
</template>
<text class="u-muted" x-fallback="envVars" style="padding:12px">Nenhuma variável.</text>
</column>
</column>
</template>
<template x-if="envTextOpen">
<column style="gap:8px">
<text class="u-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 btn--primary" type="button" @click="saveEnvText()">Salvar</button>
<button class="btn btn--ghost" type="button" @click="closeEnvText()">Cancelar</button>
</row>
</column>
</template>
</column>
<column class="env" x-show="projectTab === 'secrets'">
<text class="u-muted-grow">
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__input" type="text" placeholder="NOME" x-model="newSecretName" />
<input class="env__input" type="password" placeholder="valor (cifrado ao salvar)" x-model="newSecretValue" />
<button class="btn btn--primary" type="button" @click="submitSecret()">Salvar secret</button>
</row>
<column class="kv-list">
<template x-for="secret in secrets" :key="secret.name">
<row class="kv-list__row">
<button class="env__del" type="button" @click="store.deleteSecret(secret.name)">✕</button>
<text class="env__sep"></text>
<text class="env__key" x-text="secret.name"></text>
<text class="kv-list__val kv-list__val--compact">cifrado</text>
</row>
</template>
<text class="u-muted" x-fallback="secrets" style="padding:12px">
Nenhum secret neste projeto ainda.
</text>
</column>
</column>
<column class="env" x-show="projectTab === 'jobs'">
<row class="env__head">
<text class="u-muted-grow" x-text="jobs.length + ' job(s)'"></text>
<button class="btn btn--sm-primary" type="button" @click="store.openNewJob()">+ Novo job</button>
</row>
<text class="u-muted" x-fallback="jobs" style="padding:12px">
Nenhum job cadastrado ainda.
</text>
<column class="kv-list">
<template x-for="job in jobs" :key="job.id">
<row class="kv-list__row">
<text class="env__key" x-text="job.name"></text>
<text class="u-muted table__col-dur" x-text="job.recurrence"></text>
<row class="state-cell" :class="'state-cell--' + job.lastRunKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="job.lastRunLabel"></text>
</row>
<text class="u-muted kv-list__val kv-list__val--compact" x-text="'→ ' + job.nextRunAt"></text>
<row class="state-cell" :class="'state-cell--' + job.warmKind" :title="job.warmReason">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="job.warmLabel"></text>
</row>
<button class="btn btn--copy" type="button" title="Ações do job" @click="store.openJobActions(job.id)">⋯</button>
</row>
</template>
</column>
</column>
</column>
<column class="modal__backdrop" x-data="newService" x-show="$store.app.showNewService"
@newservice-reset.window="reset($event)">
<column class="modal__box" x-win="'new_service'" data-w="760" data-h="640">
<row class="modal__head">
<text class="u-label-cap">NOVO SERVIÇO</text>
<button class="btn btn--ghost" type="button" @click="cancel()">Fechar</button>
</row>
<column class="modal__body" style="gap:14px">
<row class="proj-bar__actions" x-show="step !== 'pick_type'" style="flex-shrink:0">
<button class="btn btn--sm-ghost" type="button" @click="gotoType()">‹ Tipo</button>
</row>
<text class="page-head__subtitle" x-show="step === 'pick_type'" style="flex-shrink:0">Escolha o tipo e configure o serviço antes de criar.</text>
<text class="u-error" x-show="error" x-text="error"></text>
<column class="form-panel" x-show="step === 'pick_type'">
<text class="u-label-cap">ESCOLHA O TIPO DE SERVIÇO</text>
<column class="kv-list">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Application</text>
<text class="u-muted">Web app via Git ou imagem</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoApp()">Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Database</text>
<text class="u-muted">Banco de dados gerenciado</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoDatabase()">Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Broker</text>
<text class="u-muted">Mensageria: Kafka, RabbitMQ, NATS</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoBroker()">Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Compose</text>
<text class="u-muted">Stack Docker Compose</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoCompose()">Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Template</text>
<text class="u-muted">A partir de um preset</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoTemplate()">Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Importar</text>
<text class="u-muted">Recriar um serviço exportado de outro servidor</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoImport()">Selecionar ›</button>
</row>
</column>
</column>
<template x-if="step === 'import'">
<column class="form-panel" x-data="serviceImport" style="gap:12px">
<text class="u-muted">
O serviço será criado no projeto aberto, sem deploy — revise e clique em Deploy depois.
Só a configuração vem no arquivo; os dados (volumes, bancos) não.
</text>
<column style="gap:12px" x-show="!report">
<column class="field">
<text class="field__label">ARQUIVO DO PACOTE (.yml)</text>
<input class="field__input" type="file" accept=".yml,.yaml,text/yaml" @change="pickFile($event)" />
</column>
<column class="field">
<text class="field__label">…OU COLE O TEXTO</text>
<textarea class="env__editor" x-model="text" placeholder="apiVersion: rustploy/v1 kind: Service …"
style="min-height:110px"></textarea>
<row class="env__add">
<button class="btn btn--blue" type="button" @click="analyze(true)" :disabled="analyzing"
x-text="analyzing ? 'Analisando…' : 'Analisar'"></button>
</row>
</column>
</column>
<row class="env__add" x-show="report">
<text class="u-muted" x-text="'Pacote: ' + (fileName || 'texto colado')"></text>
<button class="btn btn--sm-ghost" type="button" @click="clear()">Trocar pacote</button>
</row>
<text class="u-error" x-show="error" x-text="error"></text>
<template x-if="report">
<column style="gap:14px">
<column class="field">
<text class="field__label">NOME DO SERVIÇO NOVO</text>
<input class="field__input" type="text" x-model="name" @change="analyze()" />
<text class="u-error" x-show="report.name_conflict">
Já existe um serviço com esse nome neste projeto — escolha outro.
</text>
</column>
<column style="gap:6px" x-show="report.warnings.length">
<text class="field__label">ATENÇÃO</text>
<template x-for="warning in report.warnings" :key="warning.code">
<text class="u-muted" x-text="'• ' + warning.message"></text>
</template>
</column>
<column style="gap:6px" x-show="report.missing_service_vars.length">
<text class="field__label">VARIÁVEIS DO SERVIÇO SEM VALOR (obrigatório)</text>
<template x-for="missingServiceVar in report.missing_service_vars" :key="missingServiceVar">
<row class="var-row">
<text class="var-row__key" x-text="missingServiceVar"></text>
<input class="field__input" type="text" x-model="vars[missingServiceVar]" @change="analyze()" placeholder="valor" />
</row>
</template>
</column>
<column style="gap:6px" x-show="report.missing_project_vars.length">
<text class="field__label">VARIÁVEIS DO PROJETO SEM VALOR (obrigatório)</text>
<template x-for="missingProjectVar in report.missing_project_vars" :key="missingProjectVar">
<row class="var-row">
<text class="var-row__key" x-text="missingProjectVar"></text>
<input class="field__input" type="text" x-model="projectVars[missingProjectVar]" @change="analyze()" placeholder="valor" />
</row>
</template>
</column>
<column style="gap:6px" x-show="report.missing_secrets.length">
<text class="field__label">SECRETS QUE ESTE PROJETO NÃO TEM</text>
<text class="u-muted">Informe o valor para criá-los, ou deixe vazio para seguir sem (o serviço fica com a referência quebrada até você criar o secret).</text>
<template x-for="missingSecret in report.missing_secrets" :key="missingSecret">
<row class="var-row">
<text class="var-row__key" x-text="missingSecret"></text>
<input class="field__input" type="password" x-model="secrets[missingSecret]" @change="analyze()" placeholder="valor do secret" autocomplete="off" />
</row>
</template>
</column>
<column class="field" x-show="report.missing_git_provider">
<text class="field__label" x-text="'GIT PROVIDER \'' + report.missing_git_provider + '\' NÃO EXISTE AQUI'"></text>
<select class="field__input" x-model="gitProviderId" @change="analyze()">
<option value="">seguir sem provider</option>
<template x-for="gitProvider in gitProviders" :key="gitProvider.id">
<option :value="gitProvider.id" x-text="gitProvider.name"></option>
</template>
</select>
</column>
<column style="gap:6px" x-show="report.project_env.length">
<text class="field__label">VARIÁVEIS DO PROJETO TRAZIDAS</text>
<text class="u-muted">Nada do projeto é sobrescrito sem você escolher.</text>
<template x-for="statusEntry in report.project_env" :key="statusEntry.key">
<row class="var-row">
<text class="var-row__key" x-text="statusEntry.key"></text>
<text class="u-muted" x-text="(statusEntry.is_secret ? 'secret · ' : '') + stateLabel(statusEntry.state)"></text>
<select class="field__input" x-show="statusEntry.state !== 'Same'" :value="projectEnv[statusEntry.key] || statusEntry.choice"
@change="chooseEnv(statusEntry.key, $event.target.value)">
<template x-for="choice in choicesFor(statusEntry)" :key="choice">
<option :value="choice" :selected="(projectEnv[statusEntry.key] || statusEntry.choice) === choice" x-text="choiceLabel(statusEntry, choice)"></option>
</template>
</select>
<text class="u-muted" x-show="statusEntry.state === 'Same'">já existe igual — nada a fazer</text>
</row>
</template>
</column>
<label class="check-row">
<input type="checkbox" x-model="dropDomains" @change="analyze()" />
<span>Não trazer os domínios</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="deploy" />
<span>Fazer deploy logo depois de criar</span>
</label>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="create()" :disabled="!canCreate || creating"
x-text="creating ? 'Criando…' : 'Criar serviço'"></button>
<button class="btn btn--ghost" type="button" @click="$store.app.closeNewService()">Cancelar</button>
</row>
<text class="u-muted" x-show="hasMissing">Preencha os valores obrigatórios acima para criar.</text>
</column>
</template>
</column>
</template>
<column class="form-panel" x-show="step === 'app_form'">
<text class="u-label-cap">NOVA APPLICATION</text>
<text class="u-label-cap">NOME</text>
<input class="field__input" type="text" x-model="appName" placeholder="meu-servico" />
<text class="u-label-cap" style="margin-top:14px">ORIGEM</text>
<row class="tabs">
<button type="button" :class="sourceKind === 'git' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="sourceKind = 'git'">Git</button>
<button type="button" :class="sourceKind === 'registry' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @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="u-label-cap" style="margin-top:14px">PORTA</text>
<input class="field__input" type="number" x-model="appPort" />
<text class="u-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 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="u-label-cap">NOVA COMPOSE STACK</text>
<text class="u-label-cap">NOME</text>
<input class="field__input" type="text" x-model="composeName" placeholder="meu-stack" />
<text class="u-label-cap" style="margin-top:14px">PORTA</text>
<input class="field__input" type="number" x-model="composePort" />
<text class="u-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="u-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 btn--primary" type="button" @click="submitCompose()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar Compose Stack'"></button>
</row>
</column>
<column class="form-panel" x-show="step === 'pick_db'">
<text class="u-label-cap">ESCOLHA O BANCO DE DADOS</text>
<column class="kv-list">
<template x-for="database in catalog.dbs" :key="database.id">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-text="database.label"></text>
<text class="u-muted" x-text="database.image"></text>
</column>
<button class="btn btn--primary" type="button" @click="pickDatabase(database)">Selecionar ›</button>
</row>
</template>
</column>
</column>
<column class="form-panel" x-show="step === 'db_form'">
<text class="u-label-cap" x-text="'NOVA ' + (selectedDatabase ? selectedDatabase.label : '')"></text>
<text class="u-label-cap" style="margin-top:8px">NOME</text>
<input class="field__input" type="text" x-model="databaseServiceName" placeholder="meu-banco" />
<template x-if="selectedDatabase && selectedDatabase.has_db_name">
<column style="gap:6px; margin-top:8px"><text class="u-label-cap">DATABASE NAME</text>
<input class="field__input" type="text" x-model="databaseName" placeholder="app" /></column>
</template>
<template x-if="selectedDatabase && selectedDatabase.has_user">
<column style="gap:6px; margin-top:8px"><text class="u-label-cap">USER</text>
<input class="field__input" type="text" x-model="databaseUser" placeholder="user" /></column>
</template>
<text class="u-label-cap" style="margin-top:8px">PASSWORD</text>
<input class="field__input" type="password" x-model="databasePassword" placeholder="(vazio = sem senha, só no Redis)" />
<template x-if="selectedDatabase && selectedDatabase.has_rootpw">
<column style="gap:6px; margin-top:8px"><text class="u-label-cap">ROOT PASSWORD</text>
<input class="field__input" type="password" x-model="databaseRootPassword" /></column>
</template>
<template x-if="selectedDatabase && selectedDatabase.has_replica">
<label class="check-row" style="margin-top:8px">
<input type="checkbox" x-model="databaseUseReplica" /><span>Use Replica Sets</span>
</label>
</template>
<text class="u-label-cap" style="margin-top:8px">DOCKER IMAGE</text>
<input class="field__input" type="text" x-model="databaseImage" placeholder="imagem" />
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="submitDatabase()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar'"></button>
</row>
</column>
<column class="form-panel" x-show="step === 'pick_broker'">
<text class="u-label-cap">ESCOLHA O BROKER</text>
<column class="kv-list">
<template x-for="broker in catalog.brokers" :key="broker.id">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-text="broker.label"></text>
<text class="u-muted" x-text="broker.image"></text>
</column>
<button class="btn btn--primary" type="button" @click="pickBroker(broker)">Selecionar ›</button>
</row>
</template>
</column>
</column>
<column class="form-panel" x-show="step === 'broker_form'">
<text class="u-label-cap" x-text="'NOVO ' + (selectedBroker ? selectedBroker.label : '')"></text>
<text class="u-label-cap" style="margin-top:8px">NOME</text>
<input class="field__input" type="text" x-model="brokerServiceName" placeholder="meu-broker" />
<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>
<text class="u-label-cap" style="margin-top:8px">DOCKER IMAGE</text>
<input class="field__input" type="text" x-model="brokerImage" placeholder="imagem" />
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="submitBroker()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar'"></button>
</row>
</column>
<column x-show="step === 'pick_template'">
<text class="u-label-cap">BUSCAR</text>
<input class="field__input" type="text" x-model="templateSearch" placeholder="buscar templates…" />
<column class="kv-list" style="margin-top:14px">
<template x-for="filteredTemplate in filteredTemplates" :key="filteredTemplate.id">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-text="filteredTemplate.name"></text>
<text class="u-muted" x-show="filteredTemplate.description" x-text="filteredTemplate.description"></text>
</column>
<button class="btn btn--primary" type="button" @click="pickTemplate(filteredTemplate)">Escolher ›</button>
</row>
</template>
<text class="u-muted" x-fallback="filteredTemplates" style="padding:16px">
Nenhum template encontrado.
</text>
</column>
</column>
<column class="form-panel" x-show="step === 'template_form'">
<text class="u-label-cap" x-text="(selectedTemplate ? selectedTemplate.name : '') + ' — CONFIGURAR'"></text>
<text class="u-label-cap" style="margin-top:8px">NOME DO SERVIÇO</text>
<input class="field__input" type="text" x-model="templateServiceName" :placeholder="selectedTemplate ? selectedTemplate.name : ''" />
<template x-for="(variable, index) in (selectedTemplate ? selectedTemplate.vars : [])" :key="variable.idx">
<column style="margin-top:8px">
<text class="u-label-cap" x-text="variable.label"></text>
<input class="field__input" type="text" x-model="templateValues[index]" :placeholder="variable.placeholder" />
</column>
</template>
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="submitTemplate()" :disabled="submitting"
x-text="submitting ? 'Criando…' : 'Criar'"></button>
</row>
</column>
</column>
</column>
</column>
<column class="modal__backdrop" x-data="projectWin" x-show="$store.app.projectWin">
<column class="modal__box" x-win="'project_form'" data-w="480" data-h="340">
<row class="modal__head">
<text class="u-label-cap" x-text="editing ? 'EDITAR PROJETO' : 'NOVO PROJETO'"></text>
<button class="btn btn--ghost" type="button" @click="store.closeProjectWin()">Fechar</button>
</row>
<column class="modal__body" style="gap:12px">
<form style="display:flex; flex-direction:column; gap:12px; flex-shrink:0" @submit.prevent="submit()">
<column class="field">
<text class="field__label">NOME</text>
<input class="field__input" type="text" placeholder="meu-projeto" x-model="name" />
</column>
<column class="field">
<text class="field__label">DESCRIÇÃO (opcional)</text>
<input class="field__input" type="text" placeholder="Para que serve este projeto" x-model="desc" />
</column>
<text class="u-error" x-show="error" x-text="error"></text>
<row class="env__add">
<button class="btn btn--primary" type="submit" :disabled="busy"
x-text="busy ? 'Salvando…' : (editing ? 'Salvar' : 'Criar projeto')"></button>
<button class="btn btn--ghost" type="button" @click="store.closeProjectWin()">Cancelar</button>
</row>
</form>
</column>
</column>
</column>
<column class="modal__backdrop" x-data="serviceExport" x-show="$store.app.exportWin">
<column class="modal__box" x-win="'service_export'" data-w="640" data-h="620">
<row class="modal__head">
<text class="u-label-cap" x-text="'EXPORTAR · ' + (plan ? plan.service_name : '')"></text>
<button class="btn btn--ghost" type="button" @click="close()">Fechar</button>
</row>
<column class="modal__body" style="gap:14px">
<text class="u-muted" x-show="loading">carregando…</text>
<text class="u-error" x-show="error" x-text="error"></text>
<text class="u-error" x-show="plan && plan.blocked" x-text="plan && plan.blocked"></text>
<template x-if="plan && !plan.blocked">
<column style="gap:14px">
<text class="u-muted">
Gera um arquivo para recriar este serviço em outro servidor: abra um projeto de lá,
Novo serviço → Importar. Só a configuração vai; os dados (volumes, bancos) não.
</text>
<column style="gap:6px">
<text class="field__label" x-text="'VARIÁVEIS DO SERVIÇO (' + plan.service_env.length + ') — todas vão'"></text>
<text class="u-muted" x-show="!plan.service_env.length">Este serviço não tem variáveis próprias.</text>
<row class="chip-row">
<template x-for="envVar in plan.service_env" :key="envVar.key">
<text class="chip" x-text="envVar.key + (envVar.is_secret ? ' 🔒' : '')"></text>
</template>
</row>
</column>
<column style="gap:6px">
<text class="field__label" x-text="'VARIÁVEIS DO PROJETO “' + plan.project_name + '” — marque as que o app precisa'"></text>
<text class="u-muted">
O container recebe TODAS as variáveis do projeto, e o rustploy não sabe quais o código lê.
As marcadas por padrão são as que o serviço cita como ${VAR}; o resto é com você.
</text>
<row class="env__add" x-show="plan.project_env.length">
<input class="field__input" type="text" placeholder="filtrar…" x-model="filter" />
<button class="btn btn--sm-ghost" type="button" @click="pickSuggested()">Sugeridas</button>
<button class="btn btn--sm-ghost" type="button" @click="pickAll()">Todas</button>
<button class="btn btn--sm-ghost" type="button" @click="pickNone()">Nenhuma</button>
</row>
<text class="u-muted" x-show="!plan.project_env.length">O projeto não tem variáveis.</text>
<template x-for="projectVar in projectVars" :key="projectVar.key">
<label class="check-row">
<input type="checkbox" x-model="picked[projectVar.key]" />
<span x-text="projectVar.key"></span>
<span class="u-muted" x-show="projectVar.is_secret">· secret</span>
<span class="u-muted" x-show="projectVar.suggested">· citada pelo serviço</span>
</label>
</template>
</column>
<label class="check-row">
<input type="checkbox" x-model="includeValues" />
<span>Incluir os valores das variáveis</span>
</label>
<text class="u-error" x-show="includeValues">
O arquivo vai conter segredos em texto. Trate-o como uma senha.
(Secrets do rustploy, “secret:NOME”, nunca são decifrados: só a referência vai.)
</text>
<text class="u-muted" x-show="!includeValues">
Sem valores: o arquivo leva só os nomes, e quem importar preenche os valores.
</text>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="download()">Baixar .yml</button>
<button class="btn btn--blue" type="button" @click="copy()">Copiar texto</button>
<button class="btn btn--ghost" type="button" @click="close()">Fechar</button>
</row>
</column>
</template>
</column>
</column>
</column>
<column class="modal__backdrop" x-show="$store.app.logWin">
<column class="modal__box" x-win="'service_logs'" data-w="900" data-h="560">
<row class="modal__head">
<text class="u-label-cap" x-text="'LOGS · ' + ($store.app.serviceNameById($store.app.selectedServiceId) || '')"></text>
<button class="btn btn--ghost" type="button" @click="$store.app.logWin = false">Fechar</button>
</row>
<column class="modal__body log-panel" style="border:none; border-radius:0">
<column class="u-fill-all" x-follow>
<column class="log-panel__body">
<template x-for="(serviceLogLine, index) in $store.app.serviceLogLines" :key="index">
<row class="log-panel__line">
<text class="log-panel__time" x-text="$store.app.timeHms(serviceLogLine.timestamp)"></text>
<text class="log-panel__text" :style="serviceLogLine.stream === 'Stderr' ? 'color:#F85149' : ''" x-text="serviceLogLine.line"></text>
</row>
</template>
<text class="u-muted" x-fallback="$store.app.serviceLogLines" style="padding:12px">Aguardando linhas de log…</text>
</column>
</column>
</column>
</column>
</column>
<column class="content" x-data="serviceDetail" x-show="$store.app.view === 'service'">
<row class="service__head">
<column class="page-head__titles">
<row class="service__title-row">
<button class="btn btn--ghost" type="button" @click="store.nav('project')" x-text="'‹ ' + projectName"></button>
<text class="page-head__title service__title" x-text="svc ? svc.spec.name : ''"></text>
</row>
<row class="service__substatus">
<row class="badge">
<text class="status-dot" :class="'state-cell--' + statusKind">●</text>
<text class="badge__label" :class="'state-cell--' + statusKind" x-text="statusLabel"></text>
</row>
</row>
<text class="u-muted" x-show="store.serviceMessage" x-text="store.serviceMessage"></text>
<template x-if="svc"><row class="service__actions service__actions--compact">
<button class="btn btn--primary" type="button" title="Deploy" @click="store.deployStart()">▶</button>
<button class="btn btn--blue" type="button" title="Reload" @click="store.serviceReload()">⟳</button>
<button class="btn btn--blue" type="button" title="Rollback" @click="store.deployRollback()">↩</button>
<button class="btn btn--danger" type="button" title="Stop" @click="store.serviceStop()">■</button>
<button class="btn btn--ghost" type="button" title="Exportar serviço"
@click="store.openExportWin(store.selectedServiceId)">⇪</button>
</row></template>
</column>
<template x-if="svc"><row class="service__actions service__actions--full">
<button class="btn btn--primary" type="button" @click="store.deployStart()">Deploy</button>
<button class="btn btn--blue" type="button" @click="store.serviceReload()">Reload</button>
<button class="btn btn--blue" type="button" @click="store.deployRollback()">Rollback</button>
<button class="btn btn--danger" type="button" @click="store.serviceStop()">Stop</button>
<button class="btn btn--ghost" type="button" title="Gera um arquivo para recriar este serviço em outro servidor"
@click="store.openExportWin(store.selectedServiceId)">Exportar</button>
</row></template>
</row>
<column class="u-center-fill" x-show="store.serviceLoading" style="padding:24px">
<text class="u-muted">carregando…</text>
</column>
<template x-if="!store.serviceLoading && svc">
<column class="service__tabs-wrap">
<row class="tabs">
<button type="button" :class="store.serviceTab === 'general' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="initGeneralForm(); store.setServiceTab('general')">General</button>
<button type="button" :class="store.serviceTab === 'connection' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('connection')">Connection</button>
<button type="button" :class="store.serviceTab === 'environment' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('environment')">Environment</button>
<button type="button" :class="store.serviceTab === 'domains' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('domains')">Domains</button>
<button type="button" :class="store.serviceTab === 'deployments' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('deployments')">Deployments</button>
<button type="button" x-show="canMigrate" :class="store.serviceTab === 'migrar' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('migrar')">Migrar</button>
<button type="button" x-show="isShared" :class="store.serviceTab === 'databases' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('databases')">Databases</button>
<button type="button" :class="store.serviceTab === 'healthcheck' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="initHealthcheckForm(); store.setServiceTab('healthcheck')">Healthcheck</button>
<button type="button" :class="store.serviceTab === 'logs' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('logs')">Logs</button>
<button type="button" :class="store.serviceTab === 'advanced' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="initAdvancedForm(); 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="u-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 btn--primary" type="button" @click="renameService()">Renomear</button>
</row>
<text class="u-muted" x-text="renameNote"></text>
</column>
<row class="kv-list__row"><text class="kv-list__key">Origem</text><text class="kv-list__val" x-text="sourceText"></text></row>
<row class="kv-list__row"><text class="kv-list__key">Porta</text><text class="kv-list__val" x-text="svc.spec.port"></text></row>
<row class="kv-list__row"><text class="kv-list__key">Réplicas</text><text class="kv-list__val" x-text="svc.spec.replicas"></text></row>
<row class="kv-list__row" x-show="svc.live_container_id">
<text class="kv-list__key">Container ao vivo</text>
<text class="kv-list__val" x-text="svc.live_container_id"></text>
</row>
<template x-if="isCompose">
<column class="form-panel" style="margin-top:14px">
<text class="u-label-cap">DOCKER COMPOSE (YAML)</text>
<textarea class="env__editor" x-model="composeText"></textarea>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="saveCompose()">Salvar compose</button>
<button class="btn btn--ghost" type="button" @click="cancelCompose()">Cancelar</button>
<text class="u-muted" x-text="store.serviceMessage"></text>
</row>
</column>
</template>
<template x-if="!isCompose">
<column class="form-panel" style="margin-top:14px">
<text class="u-label-cap">PROVIDER</text>
<row class="tabs">
<button type="button" :class="providerTab === 'git' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="setProviderTab('git')">Git</button>
<button type="button" :class="providerTab === 'gitea' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="setProviderTab('gitea')">Gitea/GitHub</button>
<button type="button" :class="providerTab === 'zip' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="setProviderTab('zip')">Zip</button>
</row>
<hr class="rule" />
<template x-if="providerTab === 'git'">
<column style="gap:8px">
<text class="u-label-cap">SOURCE — REPOSITORY URL / IMAGE</text>
<input class="field__input" type="text" x-model="serviceFormRepoUrl" placeholder="github.com/user/repo ou nginx:latest" />
<text class="u-muted">URL de repositório vira Git; caso contrário é imagem de registry.</text>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">BRANCH</text>
<input class="field__input" type="text" x-model="serviceFormBranch" placeholder="main" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">PORT</text>
<input class="field__input" type="text" x-model="serviceFormGeneralPort" placeholder="80" />
</column>
</row>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">USERNAME</text>
<input class="field__input" type="text" x-model="serviceFormUsername" placeholder="(opcional)" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">CREDENTIALS (secret)</text>
<input class="field__input" type="text" x-model="serviceFormCredentials" placeholder="(opcional)" />
<text class="u-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="u-label-cap">BUILD PATH (root)</text>
<input class="field__input" type="text" x-model="serviceFormBuildPath" placeholder="." />
<text class="u-label-cap">WATCH PATHS (separados por vírgula)</text>
<input class="field__input" type="text" x-model="serviceFormWatchPaths" placeholder="src, Cargo.toml" />
<label class="check-row"><input type="checkbox" x-model="serviceFormSubmodules" /><span>Enable submodules</span></label>
<text class="u-label-cap">BUILD ENGINE: DOCKERFILE</text>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">DOCKERFILE</text>
<input class="field__input" type="text" x-model="serviceFormDockerfile" placeholder="Dockerfile" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">CONTEXT PATH</text>
<input class="field__input" type="text" x-model="serviceFormContextPath" placeholder="." />
</column>
</row>
<text class="u-label-cap">BUILD STAGE (opcional)</text>
<input class="field__input" type="text" x-model="serviceFormBuildStage" placeholder="(multi-stage target)" />
<row class="env__add">
<button class="btn btn--primary" type="button" @click="saveSource()">Save</button>
<text class="u-muted" x-text="store.serviceMessage"></text>
</row>
</column>
</template>
<template x-if="providerTab === 'zip'">
<column style="gap:8px">
<text class="u-label-cap">ZIP (Dockerfile na raiz)</text>
<input class="field__input" type="file" accept=".zip" @change="onArchiveFileChange($event)" />
<text class="u-muted">
O zip deve conter um Dockerfile na raiz. Depois do envio, o deploy normal usa os
arquivos descompactados pelo daemon.
</text>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">PORT</text>
<input class="field__input" type="text" x-model="serviceFormArchivePort" placeholder="80" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">STATUS</text>
<text class="u-muted" x-text="archiveMessage"></text>
</column>
</row>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="uploadArchive()">Enviar zip</button>
</row>
</column>
</template>
<template x-if="providerTab === 'gitea'">
<column style="gap:8px">
<text class="u-muted">Selecione a conta conectada, o repositório e a branch a implantar.</text>
<text class="u-muted" x-show="giteaMessage" x-text="giteaMessage"></text>
<text class="u-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="giteaProvider in giteaProviders" :key="giteaProvider.id">
<option :value="giteaProvider.id" x-text="giteaProvider.name"></option>
</template>
</select>
<text class="u-muted">Conecte contas em Settings → Git.</text>
<text class="u-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="giteaRepo in giteaRepos" :key="giteaRepo.full_name">
<option :value="giteaRepo.full_name" x-text="giteaRepo.full_name"></option>
</template>
</select>
<text class="u-label-cap">BRANCH</text>
<select class="field__input" x-model="serviceFormBranch">
<option value="">selecione a branch</option>
<template x-for="giteaBranch in giteaBranches" :key="giteaBranch.name">
<option :value="giteaBranch.name" x-text="giteaBranch.name"></option>
</template>
</select>
<text class="u-label-cap">SELEÇÃO ATUAL</text>
<row class="kv-list__row">
<text class="kv-list__key">Repo (clone URL)</text>
<text class="kv-list__val kv-list__val--compact" x-text="serviceFormRepoUrl"></text>
</row>
<text class="u-label-cap">BUILD</text>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">BUILD PATH (root)</text>
<input class="field__input" type="text" x-model="serviceFormBuildPath" placeholder="." />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">PORT</text>
<input class="field__input" type="text" x-model="serviceFormGeneralPort" placeholder="80" />
</column>
</row>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">DOCKERFILE</text>
<input class="field__input" type="text" x-model="serviceFormDockerfile" placeholder="Dockerfile" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">CONTEXT PATH</text>
<input class="field__input" type="text" x-model="serviceFormContextPath" placeholder="." />
</column>
</row>
<text class="u-label-cap">WATCH PATHS (separados por vírgula)</text>
<input class="field__input" type="text" x-model="serviceFormWatchPaths" placeholder="src, Cargo.toml" />
<label class="check-row"><input type="checkbox" x-model="serviceFormSubmodules" /><span>Enable submodules</span></label>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="saveSource()">Save</button>
<text class="u-muted" x-text="store.serviceMessage"></text>
</row>
</column>
</template>
</column>
</template>
<button class="btn 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-list__row">
<text class="kv-list__key">Container port</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.port"></text>
<button class="btn btn--copy" type="button" @click="copyToClipboard(String(connectionInfo.port))">Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key">Host port</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.hostPort"></text>
<button class="btn btn--copy" type="button" @click="copyToClipboard(String(connectionInfo.hostPort))">Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key">Domain</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.domain"></text>
<button class="btn btn--copy" type="button" @click="copyToClipboard(connectionInfo.domain)">Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key">TLS</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.tls"></text>
</row>
<row class="kv-list__row">
<text class="kv-list__key">External URL</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.externalUrl"></text>
<button class="btn btn--copy" type="button" @click="copyToClipboard(connectionInfo.externalUrl)">Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key">Internal URL</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.internalUrl"></text>
<button class="btn btn--copy" type="button" @click="copyToClipboard(connectionInfo.internalUrl)">Copiar</button>
</row>
<row class="kv-list__row" x-show="canShare">
<text class="kv-list__key">Compartilhado</text>
<text class="kv-list__val kv-list__val--compact" x-text="isShared ? 'true' : 'false'"></text>
<button class="btn btn--copy" type="button" x-show="!isShared" @click="setShared(true)">Compartilhar</button>
<button class="btn btn--copy" type="button" x-show="isShared" @click="setShared(false)">Desligar</button>
</row>
<row class="kv-list__row" x-show="connectionInfo.databaseKind">
<text class="kv-list__key">Tipo</text>
<text class="kv-list__val kv-list__val--compact" x-text="connectionInfo.databaseKind"></text>
</row>
<text class="u-muted" x-show="copyMessage" x-text="copyMessage"></text>
<hr class="rule" />
<text class="u-label-cap" x-text="'CONTAINERS (' + liveContainers.length + ')'"></text>
<text class="u-muted" x-fallback="liveContainers">Nenhum container em execução.</text>
<row class="kv-list__row" x-show="liveContainers.length > 0" style="flex-wrap:wrap">
<template x-for="liveContainer in liveContainers" :key="liveContainer.id">
<row class="badge" style="margin-right:18px">
<text class="status-dot" :class="'state-cell--' + liveContainer.kind">●</text>
<text class="kv-list__val kv-list__val--compact" x-text="liveContainer.name + ' ' + liveContainer.idShort"></text>
</row>
</template>
</row>
</column>
<column class="env" x-show="store.serviceTab === 'environment'">
<row class="env__head">
<text class="u-muted-grow" x-text="(svc ? svc.spec.env_vars.length : 0) + ' variável(eis)'"></text>
<button class="btn btn--export" type="button" @click="openEnvText()" x-show="!envTextOpen">.env</button>
<button class="btn 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__input" type="text" placeholder="KEY" x-model="newEnvKey" />
<input class="env__input" type="text" placeholder="value" x-model="newEnvValue" />
<button class="btn btn--primary" type="button" @click="addEnvVar()">Adicionar</button>
</row>
<text class="u-muted-grow">
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="envVar in envVars" :key="envVar.key">
<column style="width:100%">
<row class="kv-list__row" x-show="envVar.isComment">
<text class="u-muted" x-text="envVar.text"></text>
</row>
<row class="kv-list__row" x-show="!envVar.isComment">
<button class="env__del" type="button" @click="deleteEnvVar(envVar.key)">✕</button>
<text class="env__sep"></text>
<text class="env__key" x-text="envVar.key"></text>
<text class="kv-list__val kv-list__val--compact" x-text="envVar.isSecret ? '(secret)' : envVar.value"></text>
</row>
</column>
</template>
<text class="u-muted" x-fallback="envVars" style="padding:12px">Nenhuma variável.</text>
</column>
</column>
</template>
<template x-if="envTextOpen">
<column style="gap:8px">
<text class="u-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 btn--primary" type="button" @click="saveEnvText()">Salvar</button>
<button class="btn btn--ghost" type="button" @click="closeEnvText()">Cancelar</button>
</row>
</column>
</template>
</column>
<column class="env" x-show="store.serviceTab === 'domains'">
<row class="env__add">
<input class="env__input" type="text" placeholder="dominio.com" x-model="newDomain" />
<input class="env__input" 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 btn--primary" type="button" @click="addDomain()">Adicionar</button>
</row>
<column class="kv-list">
<template x-for="domain in domains" :key="domain.domain">
<row class="kv-list__row">
<button class="env__del" type="button" @click="deleteDomain(domain.domain)">✕</button>
<text class="env__sep"></text>
<text class="env__key" x-text="domain.domain"></text>
<text class="kv-list__val kv-list__val--compact" x-text="(domain.tls ? 'https' : 'http') + ' → :' + domain.port"></text>
</row>
</template>
<text class="u-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="u-label-cap">MIGRAR PARA BANCO COMPARTILHADO</text>
<text class="u-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="u-label-cap">DATABASE DE ORIGEM (dentro deste banco)</text>
<input class="field__input" x-model="serviceFormMigration.db" placeholder="nome do database" />
<text class="u-label-cap">DESTINO</text>
<select class="form-select" x-model="serviceFormMigration.dest">
<option value="">database de destino...</option>
<template x-for="migrationDestination in migrationDestinations" :key="migrationDestination.id"><option :value="migrationDestination.id" x-text="migrationDestination.name"></option></template>
</select>
<text class="u-label-cap">ENV VAR A TROCAR</text>
<input class="field__input" x-model="serviceFormMigration.env" placeholder="padrão DATABASE_URL / MONGODB_URI" />
<row class="env__add">
<button class="btn btn--primary" type="button" @click="startMigration()">Migrar</button>
<button class="btn btn--ghost" type="button" @click="loadMigration()">Atualizar</button>
</row>
<template x-if="mig">
<column>
<text class="u-label-cap" x-text="'ANDAMENTO — ' + mig.status"></text>
<column class="kv-list">
<template x-for="migrationStep in migrationSteps" :key="migrationStep.name">
<row class="kv-list__row">
<text class="env__key" x-text="migrationStep.icon"></text>
<text class="kv-list__val kv-list__val--compact" x-text="migrationStep.name"></text>
<text class="u-muted" x-text="migrationStep.detail"></text>
</row>
</template>
</column>
<template x-if="mig.status === 'Completed'">
<column>
<text class="u-muted">Migração concluída — o banco antigo foi mantido (parado) para rollback.</text>
<row class="env__add">
<button class="btn btn--ghost" type="button" @click="rollbackMigration()">Reverter</button>
<button class="btn btn--ghost" type="button" @click="discardOldDatabase()">Descartar banco antigo</button>
</row>
</column>
</template>
</column>
</template>
</column>
<column class="form-panel" x-show="store.serviceTab === 'databases'">
<text class="u-label-cap" x-text="'DATABASES (' + mdbs.length + ')'"></text>
<text class="u-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="sharedDatabase in mdbs" :key="sharedDatabase.id">
<row class="kv-list__row">
<button class="env__del" type="button" @click="deleteSharedDatabase(sharedDatabase)">✕</button>
<button class="tabs__tab" type="button" @click="showSharedDatabase(sharedDatabase)" x-text="sharedDatabase.name"></button>
<text class="kv-list__val kv-list__val--compact" x-text="'→ ' + sharedDatabase.project"></text>
<text class="u-muted" x-text="sharedDatabase.envVar"></text>
</row>
</template>
</column>
<row class="kv-list__row" x-show="sharedDatabaseUrl">
<text class="kv-list__key">URL</text>
<text class="kv-list__val kv-list__val--compact" x-text="sharedDatabaseLabel"></text>
<button class="btn btn--copy" type="button" @click="copyToClipboard(sharedDatabaseUrl)">Copiar</button>
</row>
<text class="u-label-cap">NOVO DATABASE</text>
<input class="field__input" x-model="serviceFormSharedDatabase.name" placeholder="nome (a-z, 0-9, _)" />
<select class="form-select" x-model="serviceFormSharedDatabase.project">
<option value="">projeto consumidor...</option>
<template x-for="sharedDatabaseProject in sharedDatabaseProjects" :key="sharedDatabaseProject.id"><option :value="sharedDatabaseProject.id" x-text="sharedDatabaseProject.name"></option></template>
</select>
<input class="field__input" x-model="serviceFormSharedDatabase.env" placeholder="env var (padrão DATABASE_URL / MONGODB_URI)" />
<input class="field__input" x-model="serviceFormSharedDatabase.limit" placeholder="limite de conexões (opcional; não vale no Mongo)" />
<input class="field__input" x-model="serviceFormSharedDatabase.timeout" placeholder="timeout de query em ms (opcional; só Postgres)" />
<label class="check-row"><input type="checkbox" x-model="serviceFormSharedDatabase.overwrite" /> Sobrescrever a env var se o projeto já a tiver</label>
<label class="check-row"><input type="checkbox" x-model="serviceFormSharedDatabase.skipEnv" /> Não mexer em env var (vou migrar os dados antes)</label>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="createSharedDatabase()">Criar database</button>
</row>
</column>
<column class="env" x-show="store.serviceTab === 'deployments'">
<column class="form-panel" x-show="webhookSupported" style="max-width:none">
<row class="env__head">
<text class="u-label-cap">WEBHOOK DE DEPLOY</text>
<text class="u-ok" x-show="copyMessage" x-text="copyMessage"></text>
</row>
<template x-if="webhookUrl">
<row class="webhook__row">
<input class="field__input webhook__url" type="text" readonly :value="webhookUrl" @focus="$event.target.select()" />
<button class="btn btn--copy btn-icon" type="button" @click="copyToClipboard(webhookUrl)">
<text>Copiar</text></button>
<button class="btn btn--dep-del btn-icon" type="button" :disabled="webhookBusy" @click="regenWebhook()">
<text>Regenerar</text></button>
</row>
</template>
<text class="u-muted" x-show="!webhookUrl">
O token nasce no primeiro deploy deste serviço — rode um deploy para a URL aparecer.
</text>
<text class="u-muted">
Um POST nesta URL dispara um deploy. O corpo é ignorado: o token na própria URL é a
autenticação. Regenerar invalida a URL anterior.
</text>
</column>
<column class="panel panel--fill">
<row class="table__head">
<text class="table__head-cell table__col-state">ESTADO</text>
<text class="table__head-cell table__col-dur">DURAÇÃO</text>
<text class="table__head-cell table__col-start">INÍCIO</text>
<text class="table__head-cell table__col-act-2">AÇÃO</text>
</row>
<column class="table__body">
<template x-for="deployment in deployments" :key="deployment.id">
<column class="list-row">
<row class="table__row">
<row class="state-cell table__col-state" :class="'state-cell--' + deployment.stateKind">
<text class="status-dot">●</text>
<text class="state-cell__label" x-text="deployment.stateLabel"></text>
</row>
<text class="table__cell table__col-dur" x-text="deployment.duration"></text>
<text class="u-muted table__col-start" x-text="deployment.start"></text>
<row class="deployments__actions table__col-act-2">
<button class="btn btn--dep-logs" type="button" @click="viewBuildLog(deployment.id)" title="Ver build log">↗</button>
<button class="btn btn--dep-logs" type="button" x-show="!deployment.terminal" @click="abortDeployment(deployment.id)" title="Cancelar">⏹</button>
<button class="btn btn--dep-del" type="button" x-show="deployment.terminal" @click="removeDeployment(deployment.id)" title="Remover">✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="deployments" style="padding:16px">Nenhum deploy ainda.</text>
</column>
</column>
</column>
<column class="form-panel" x-show="store.serviceTab === 'healthcheck'">
<text class="u-label-cap">KIND</text>
<row class="tabs">
<button type="button" :class="healthcheckKind === 'none' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="healthcheckKind = 'none'">None</button>
<button type="button" :class="healthcheckKind === 'tcp' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="healthcheckKind = 'tcp'">Tcp</button>
<button type="button" :class="healthcheckKind === 'http' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="healthcheckKind = 'http'">Http</button>
<button type="button" :class="healthcheckKind === 'docker' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="healthcheckKind = 'docker'">Docker</button>
</row>
<template x-if="healthcheckKind === 'http'">
<column style="gap:8px; margin-top:8px">
<text class="u-label-cap">HTTP PATH</text>
<input class="field__input" type="text" x-model="healthcheckPath" placeholder="/health" />
<text class="u-label-cap">EXPECTED STATUS</text>
<input class="field__input" type="number" min="100" max="599" x-model="healthcheckStatus" placeholder="200" />
</column>
</template>
<row class="healthcheck__grid" style="margin-top:14px">
<column class="healthcheck__field">
<text class="u-label-cap">INTERVAL (s)</text>
<input class="field__input" type="number" min="1" max="3600" x-model="healthcheckInterval" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">TIMEOUT (s)</text>
<input class="field__input" type="number" min="1" max="3600" x-model="healthcheckTimeout" />
</column>
</row>
<row class="healthcheck__grid">
<column class="healthcheck__field">
<text class="u-label-cap">RETRIES</text>
<input class="field__input" type="number" min="1" max="10" x-model="healthcheckRetries" />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">START PERIOD (s)</text>
<input class="field__input" type="number" min="0" max="3600" x-model="healthcheckStart" />
</column>
</row>
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="saveHealthcheck()">Save</button>
<text class="u-muted" x-show="store.serviceMessage" x-text="store.serviceMessage"></text>
</row>
</column>
<column class="log-panel" x-show="store.serviceTab === 'logs'">
<row class="env__head">
<text class="u-muted-grow" x-text="(store.serviceLogLines || []).length + ' linhas · ao vivo'"></text>
<button class="btn btn--sm-ghost" type="button" @click="store.logWin = true">Abrir em janela ⧉</button>
</row>
<column class="u-fill-all" x-follow>
<column class="log-panel__body">
<template x-for="(serviceLogLine, index) in store.serviceLogLines" :key="index">
<row class="log-panel__line">
<text class="log-panel__time" x-text="timeHms(serviceLogLine.timestamp)"></text>
<text class="log-panel__text" :style="serviceLogLine.stream === 'Stderr' ? 'color:#F85149' : ''" x-text="serviceLogLine.line"></text>
</row>
</template>
<text class="u-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="u-label-cap">REPLICAS</text>
<input class="field__input" type="text" x-model="advancedReplicas" placeholder="1" />
<text class="u-label-cap" style="margin-top:14px">RUN COMMAND</text>
<input class="field__input" type="text" x-model="advancedRunCommand" placeholder="(padrão da imagem)" />
<text class="u-muted" style="margin-top:8px" x-text="'Run args atuais: ' + runArgsText"></text>
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="saveAdvanced()">Save</button>
<text class="u-muted" x-show="store.serviceMessage" x-text="store.serviceMessage"></text>
</row>
<text class="u-label-cap" style="margin-top:14px" x-text="'FILA DE PRÉ-DEPLOY (' + preDeployChecks.length + ')'"></text>
<text class="u-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="(preDeployCheck, index) in preDeployChecks" :key="preDeployCheck.id">
<row class="kv-list__row">
<button class="env__del" type="button" @click="preDeployCheckDelete(preDeployCheck.id)">✕</button>
<text class="env__sep"></text>
<text class="env__key" x-text="preDeployCheck.index + '.'"></text>
<text class="kv-list__val kv-list__val--compact" x-text="preDeployCheck.name"></text>
<button class="btn btn--ghost" type="button" :disabled="index === 0" @click="preDeployCheckMove(preDeployCheck.id, -1)">↑</button>
<button class="btn btn--ghost" type="button" :disabled="index === preDeployChecks.length - 1" @click="preDeployCheckMove(preDeployCheck.id, 1)">↓</button>
</row>
</template>
<text class="u-muted" x-fallback="preDeployChecks" style="padding:12px">
Nenhum check configurado — deploy segue direto.
</text>
</column>
<text class="u-label-cap" style="margin-top:14px">ADICIONAR À FILA</text>
<row class="env__add">
<select class="field__input" x-model="advancedPreDeployCheckAddJobId">
<option value="">escolher job...</option>
<template x-for="preDeployAvailableJob in preDeployAvailableJobs" :key="preDeployAvailableJob.id">
<option :value="preDeployAvailableJob.id" x-text="preDeployAvailableJob.name"></option>
</template>
</select>
<button class="btn btn--primary" type="button" @click="preDeployCheckAdd()">Adicionar à fila</button>
</row>
</column>
</column>
</template>
<column class="modal__backdrop" x-show="buildLogFor">
<column class="modal__box" x-win="'build_log'" data-w="900" data-h="600">
<row class="modal__head">
<text class="u-label-cap">BUILD LOG</text>
<button class="btn btn--ghost" type="button" @click="closeBuildLog()">Fechar</button>
</row>
<column class="modal__body">
<pre class="log-panel__pre" x-text="buildLogText"></pre>
</column>
</column>
</column>
</column>
</column>
</row>
<column class="toast-wrap" aria-live="polite" aria-atomic="false">
<template x-for="toast in $store.app.toasts" :key="toast.id">
<row class="toast" :class="'toast--' + toast.kind" role="status" :style="'--ms:' + toast.ms + 'ms'">
<text class="toast__icon" aria-hidden="true"
x-text="({ info: 'ℹ', success: '✓', warning: '⚠', error: '✕' })[toast.kind] || 'ℹ'"></text>
<text class="toast__msg" x-text="toast.message"></text>
<button class="toast__close" type="button" aria-label="Fechar"
@click="$store.app.dismissToast(toast.id)">×</button>
<span class="toast__bar"></span>
</row>
</template>
</column>
</div>
</body>
</html>