<!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" x-t>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" x-t>CONNECTION TOKEN</text>
<text class="field__label" x-t>SECURE</text>
</row>
<input class="field__input" type="password" placeholder="cole o token de conexão" x-t:placeholder
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 x-t>Remember access key</span>
</label>
</column>
<row class="language-switch" style="margin-top:16px">
<button type="button" class="btn" :class="$store.i18n.locale === 'en' ? 'btn--primary' : 'btn--ghost'" @click="$store.i18n.set('en')">English</button>
<button type="button" class="btn" :class="$store.i18n.locale === 'pt-BR' ? 'btn--primary' : 'btn--ghost'" @click="$store.i18n.set('pt-BR')">Português</button>
</row>
<button type="submit" class="login__connect" style="margin-top:16px"
x-text="$store.app.statusLine === $t('conectando…') ? $t('Conectando…') : $t('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: 'running', label: 'Em execução', icon: '●' },
{ 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" x-t>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" x-t:title
: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" x-t>Settings</text>
<span class="nav-item__badge" x-show="$store.app.updateIsAvailable" title="Há uma versão nova do daemon" x-t:title x-text="'●'"></span>
</button>
<button type="button" @click="$store.app.nav('support')" title="Support" x-t:title
: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" x-t>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" x-t:title
@click="$store.app.menuOpen = !$store.app.menuOpen">☰</button>
<row class="topbar__search-wrap">
<input class="topbar__search" type="text" placeholder="Search deployments, nodes…" x-t:placeholder
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>
<row class="language-switch">
<button type="button" class="btn" :class="$store.i18n.locale === 'en' ? 'btn--primary' : 'btn--ghost'" @click="$store.i18n.set('en')">English</button>
<button type="button" class="btn" :class="$store.i18n.locale === 'pt-BR' ? 'btn--primary' : 'btn--ghost'" @click="$store.i18n.set('pt-BR')">Português</button>
</row>
<button class="btn btn--danger btn-icon" type="button" @click="$store.app.stopAll()" title="Para todos os containers gerenciados" x-t:title>
<text x-t>Stop All</text>
</button>
<button class="btn btn--ghost" type="button" @click="$store.app.disconnect()" x-t>Disconnect</button>
</row>
<hr class="rule" />
<row class="banner banner--warn" x-show="$store.app.stateGitAlertMessage" style="align-items:center; gap:12px">
<text class="u-muted-grow" style="color:var(--warn)" x-text="$store.app.stateGitAlertMessage"></text>
<button class="btn btn--copy" type="button" @click="$store.app.stateGitOpen()" x-t>Abrir</button>
</row>
<column class="content" x-show="$store.app.view === 'support'">
<text class="page-head__title" x-t>Support</text>
<text class="page-head__subtitle">github.com/antoniofernandodj/rustploy</text>
</column>
<column class="content" x-data="running" x-show="$store.app.view === 'running'">
<row class="page-head">
<column class="page-head__titles">
<text class="page-head__title" x-t>Em execução</text>
<text class="page-head__subtitle" x-t>Projetos e serviços rodando agora — projetos sem serviço no ar não aparecem.</text>
</column>
<row class="page-head__stats page-head__stats--hide-lg">
<column class="stat">
<text class="u-label-cap" x-t>SERVIÇOS NO AR</text>
<text class="stat__num" x-text="totalServices"></text>
</column>
</row>
</row>
<column class="u-fill-all" style="gap:44px;overflow-y:auto">
<template x-for="project in projects" :key="project.project_id">
<column style="gap:12px;flex-shrink:0">
<row class="card__head">
<row style="align-items:center;gap:8px">
<text class="card__title" x-text="project.project_name"></text>
<text class="u-muted" x-text="project.count + (project.count === 1 ? $t(' serviço') : $t(' serviços'))"></text>
</row>
<button class="card__action card__action--open" type="button" style="width:auto" @click="store.openProject(project.project_id)" x-t>Abrir projeto ›</button>
</row>
<column class="grid grid--two">
<template x-for="svc in project.services" :key="svc.service_id">
<column class="card">
<row class="card__head">
<row style="align-items:center;gap:6px">
<text class="card__title" x-text="svc.name"></text>
<text class="card__port" x-text="':' + svc.port"></text>
</row>
<row style="gap:6px;align-items:center">
<row class="badge" x-show="svc.hasWarm" :title="svc.warmReason">
<text class="status-dot" :class="'state-cell--' + svc.warmKind">◐</text>
<text class="badge__label" :class="'state-cell--' + svc.warmKind" x-text="svc.warmLabel"></text>
</row>
<row class="badge">
<text class="status-dot" :class="svc.isDegraded ? 'state-cell--warn' : 'state-cell--ok'">●</text>
<text class="badge__label" :class="svc.isDegraded ? 'state-cell--warn' : 'state-cell--ok'" x-text="svc.isDegraded ? $t('degradado') : $t('rodando')"></text>
</row>
</row>
</row>
<text class="u-label-cap" x-text="svc.kind_label"></text>
<row style="gap:6px;align-items:center" x-show="svc.kind === 'git'">
<text class="u-label-cap">BRANCH</text>
<text class="card__title" x-text="svc.branch"></text>
</row>
<text class="u-muted" x-show="svc.kind === 'git'" x-text="svc.repo_url"></text>
<text class="u-muted" x-show="svc.kind === 'registry'" x-text="$t('imagem: ') + svc.image"></text>
<text class="u-muted" x-show="svc.kind === 'database' || svc.kind === 'broker'"
x-text="$t('motor: ') + svc.engine + $t(' · porta ') + svc.port"></text>
<text class="u-muted" x-show="svc.domains.length > 0" x-text="$t('domínio: ') + svc.domainsText"></text>
<row class="card__stats">
<column class="card__stat">
<text class="u-label-cap">CPU</text>
<text class="card__stat-num" x-text="svc.cpu"></text>
</column>
<column class="card__stat">
<text class="u-label-cap">MEM</text>
<text class="card__stat-num" x-text="svc.mem"></text>
</column>
</row>
<hr class="rule" />
<text class="u-label-cap" x-t>PRÉ-DEPLOY CHECKS</text>
<text class="u-muted" x-show="!svc.hasChecks" x-t>nenhum</text>
<template x-for="(check, checkIndex) in svc.pre_deploy_checks" :key="check.job_id">
<text class="u-muted" x-text="(checkIndex + 1) + '. ' + check.name"></text>
</template>
<text class="u-label-cap" x-t>WEBHOOK DE DEPLOY</text>
<text class="u-muted" x-show="!svc.webhook" x-t>sem webhook</text>
<row class="webhook__row" x-show="svc.webhook">
<input class="field__input webhook__url" type="text" readonly :value="svc.webhookShown" />
<button class="btn btn--copy btn-icon" type="button" title="Copiar URL" x-t:title @click="store.copyText(svc.webhook, 'webhook')">⧉</button>
<button class="btn btn--ghost" type="button" @click="toggleReveal(svc.service_id)" x-text="svc.isRevealed ? $t('Ocultar') : $t('Mostrar')"></button>
</row>
<button class="card__action card__action--open" type="button" style="width:100%"
@click="store.openService(svc.service_id)" x-t>Abrir serviço ›</button>
<row class="card__actions">
<button class="card__action card__action--icon" type="button" title="Deploy" @click="store.deployServiceFromList(svc.service_id)">▶ Deploy</button>
<button class="card__action card__action--icon" type="button" title="Parar" x-t:title aria-label="Parar serviço" x-t:aria-label
@click="store.stopService(svc.service_id)">■</button>
</row>
</column>
</template>
</column>
</column>
</template>
<text class="u-muted" x-fallback="projects" style="padding:16px" x-t>
Nenhum serviço em execução agora.
</text>
</column>
</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" x-t>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()" x-t>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" x-t>SERVIÇO</text>
<text class="table__head-cell table__col-proj" x-t>PROJETO</text>
<text class="table__head-cell table__col-state" x-t>ESTADO</text>
<text class="table__head-cell table__col-dur" x-t>DURAÇÃO</text>
<text class="table__head-cell table__col-start" x-t>INÍCIO</text>
</row>
<column class="u-center-fill" x-show="store.dataLoading" style="padding:24px">
<text class="u-muted" x-t>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">
<button class="table-link table__col-svc" @click="$store.app.openService(row.serviceId)" x-text="row.service"></button>
<button class="table-link table__col-proj" @click="$store.app.openProject(row.projectId)" x-text="row.project"></button>
<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" x-t>
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" x-t>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" x-t>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" x-t>SUCESSO 24H</text>
<text class="stat-card__num" x-text="successCount"></text>
</column>
<column class="stat-card">
<text class="u-label-cap" x-t>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="$t('Fila (') + queued.length + ')'"></button>
<button type="button" :class="tab === 'executando' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="tab = 'executando'"
x-text="$t('Executando (') + active.length + ')'"></button>
<button type="button" :class="tab === 'historico' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="tab = 'historico'"
x-text="$t('Histórico 24h (') + recent.length + ')'"></button>
</row>
<column class="section" x-show="tab === 'fila'">
<row class="section__head">
<text class="section__title" x-text="$t('NA FILA (') + queued.length + ')'"></text>
<button class="btn btn--copy" type="button" @click="store.queueTogglePause()"
x-text="paused ? $t('Retomar fila') : $t('Pausar fila')"></button>
</row>
<column class="section__body">
<text class="banner banner--warn" x-show="paused" x-t>Fila pausada — deploys esperam até você retomar.</text>
<text class="empty-state" x-fallback="queued" x-t>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" x-t:title>⠿</span>
<row class="table__col-svc">
<text class="table__cell table__cell--key" x-text="queuedDeployment.pos + '.'" style="margin-right:6px"></text>
<button class="table-link" @click="$store.app.openService(queuedDeployment.serviceId)" x-text="queuedDeployment.service"></button>
</row>
<text class="table__cell table__col-start" x-text="queuedDeployment.project"></text>
<button class="btn btn--copy" type="button" @click="store.queuePromote(queuedDeployment.deploymentId)" x-t>↑ 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="$t('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" x-t>SERVIÇO</text>
<text class="table__head-cell table__col-proj" x-t>PROJETO</text>
<text class="table__head-cell table__col-state" x-t>ESTADO</text>
<text class="table__head-cell table__col-dur">TOTAL</text>
<text class="table__head-cell table__col-start" x-t>FASE</text>
<text class="table__head-cell table__col-act"></text>
</row>
<column class="table__body">
<text class="empty-state" x-fallback="active" x-t>Nenhum deploy em andamento.</text>
<template x-for="activeDeployment in active" :key="activeDeployment.serviceId">
<column class="list-row">
<row class="table__row">
<button class="table-link table__col-svc" @click="$store.app.openService(activeDeployment.serviceId)" x-text="activeDeployment.service"></button>
<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)" x-t>Detalhes</button>
<button class="env__del" type="button" title="Abortar deploy" x-t:title
@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="$t('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" x-t>SERVIÇO</text>
<text class="table__head-cell table__col-proj" x-t>PROJETO</text>
<text class="table__head-cell table__col-state" x-t>ESTADO</text>
<text class="table__head-cell table__col-dur" x-t>DURAÇÃO</text>
<text class="table__head-cell table__col-start" x-t>INÍCIO</text>
</row>
</column>
<row class="empty-state__wrap" x-fallback="recent">
<text class="empty-state" x-t>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>
<button class="table-link table__col-svc" @click="$store.app.openService(recentDeployment.serviceId)" x-text="recentDeployment.service"></button>
<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()" x-t>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" x-t>ESTADO</text>
<text class="table__head-cell table__col-dur" x-t>DURAÇÃO</text>
<text class="table__head-cell table__col-start" x-t>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" x-t>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" x-t>Monitoring</text>
<text class="page-head__subtitle" x-t>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" x-t>MEMORY</text>
<text class="stat-card__num" x-text="store.hostMemory"></text>
</column>
<column class="stat-card">
<text class="u-label-cap" x-t>DISK</text>
<text class="stat-card__num" x-text="store.hostDisk"></text>
</column>
<column class="stat-card">
<text class="u-label-cap" x-t>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" x-t>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">
<button class="table-link table__col-svc" @click="$store.app.openService(row.serviceId)" x-text="row.name"></button>
<button class="table-link table__col-proj" @click="$store.app.openProject(row.projectId)" x-text="row.project"></button>
<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" x-t>
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" x-t>Active routes in the reverse proxy.</text>
</column>
<row class="page-head__stats">
<column class="stat-card">
<text class="u-label-cap" x-t>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" x-t>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" x-t>DOMÍNIO</text>
<text class="table__head-cell table__col-proj" x-t>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>
<button class="table-link table__col-proj" @click="$store.app.openService(route.serviceId)" x-text="route.service"></button>
<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="$t('PORTAS TCP DE HOST (') + hostPorts.length + ')'"></text>
<row class="table__head">
<text class="table__head-cell table__col-svc" x-t>SERVIÇO</text>
<text class="table__head-cell table__col-proj" x-t>PROJETO</text>
<text class="table__head-cell table__col-dur" x-t>PORTA HOST</text>
<text class="table__head-cell table__col-start" x-t>PORTA CONTAINER</text>
</row>
<text class="u-muted" x-fallback="hostPorts" style="padding:12px" x-t>
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">
<button class="table-link table__col-svc" @click="$store.app.openService(hostPortEntry.serviceId)" x-text="hostPortEntry.service"></button>
<button class="table-link table__col-proj" @click="$store.app.openProject(hostPortEntry.projectId)" x-text="hostPortEntry.project"></button>
<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" x-t>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" x-t>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')" x-t>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()" x-t>Limpar parados</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc" x-t>NOME</text>
<text class="table__head-cell table__col-img" x-t>IMAGEM</text>
<text class="table__head-cell table__col-state" x-t>DONO</text>
<text class="table__head-cell table__col-dur" x-t>ESTADO</text>
<text class="table__head-cell table__col-act" x-t>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" :style="container.isOrphan ? 'color:var(--warn)' : ''" :title="container.isOrphan ? $t('O serviço sumiu do estado por um commit externo (git). Pare e remova este container.') : ''" 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.canStop"
@click="store.dockerStopContainer(container.idFull)" x-t>Parar</button>
<button class="btn btn--row-remove" type="button" x-show="container.canRemove"
@click="store.dockerRemoveContainer(container.idFull)" x-t>Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="containers" style="padding:16px" x-t>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 + $t(' imagem(ns)')"></text>
<label class="check-row">
<input type="checkbox" x-model="store.onlyUsedImages" /><span x-t>Somente em uso</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.pruneAllImages" /><span x-t>-a (limpar tudo, não só dangling)</span>
</label>
<button class="btn btn--danger" type="button" @click="store.dockerPruneImages()" x-t>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" x-t>PROJETO / SERVIÇO</text>
<text class="table__head-cell table__col-dur" x-t>TAMANHO</text>
<text class="table__head-cell table__col-dur" x-t>CRIADA</text>
<text class="table__head-cell table__col-state" x-t>USO</text>
<text class="table__head-cell table__col-act" x-t>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)" x-t>Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="images" style="padding:16px" x-t>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 x-t>Somente em uso</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.pruneAllVolumes" /><span x-t>-a (limpar tudo, não só anônimos)</span>
</label>
<button class="btn btn--danger" type="button" @click="store.dockerPruneVolumes()" x-t>Limpar sem uso</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc" x-t>NOME</text>
<text class="table__head-cell table__col-proj">DRIVER</text>
<text class="table__head-cell table__col-dur" x-t>TAMANHO</text>
<text class="table__head-cell table__col-state" x-t>USO</text>
<text class="table__head-cell table__col-act" x-t>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)" x-t>Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="volumes" style="padding:16px" x-t>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 x-t>Somente em uso</span>
</label>
<button class="btn btn--danger" type="button" @click="store.dockerPruneNetworks()" x-t>Limpar sem uso</button>
</row>
<row class="table__head">
<text class="table__head-cell table__col-svc" x-t>NOME</text>
<text class="table__head-cell table__col-proj" x-t>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" x-t>USO</text>
<text class="table__head-cell table__col-act" x-t>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)" x-t>Remover</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="networks" style="padding:16px" x-t>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()" x-t>Executar GC</button>
</row>
<column x-show="!store.registrySelectedRepo">
<row class="table__head">
<text class="table__head-cell table__col-svc" x-t>REPOSITÓRIO</text>
<text class="table__head-cell table__col-proj">TAGS</text>
<text class="table__head-cell table__col-dur" x-t>TAMANHO</text>
<text class="table__head-cell table__col-dur" x-t>CRIADO</text>
<text class="table__head-cell table__col-act-2" x-t>AÇÃO</text>
</row>
<text class="u-muted" x-fallback="store.snap?.registry_repos" style="padding:16px" x-t>
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" x-t>
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" x-t:title
@click="store.registryOpenRepo(repo.name)">≡</button>
<button class="env__del" type="button" title="Remover" x-t:title
@click="store.registryRemoveRepo(repo.name)">✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
</column>
</column>
<text class="u-label-cap" x-text="$t('TOKENS DE ACESSO (') + tokens.length + ')'"></text>
<row class="env__head">
<text class="u-muted-grow" x-t>Basic auth (docker login) — o segredo só aparece uma vez, na criação.</text>
<button class="btn btn--primary" type="button" @click="openTokenModal()" x-t>+ Novo token</button>
</row>
<text class="u-muted" x-fallback="tokens" style="padding:12px" x-t>
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" x-t>NOME</text>
<text class="table__head-cell table__col-proj" x-t>ESCOPO</text>
<text class="table__head-cell table__col-dur" x-t>CRIADO</text>
<text class="table__head-cell table__col-dur" x-t>ÚLTIMO USO</text>
<text class="table__head-cell table__col-act" x-t>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" x-t:title
@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()" x-t>← 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" x-t>TAMANHO</text>
<text class="table__head-cell table__col-dur" x-t>ATUALIZADA</text>
<text class="table__head-cell table__col-act" x-t>AÇÃO</text>
</row>
<column class="u-center-fill" x-show="store.registryTagsLoading" style="padding:24px">
<text class="u-muted" x-t>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)" x-t>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" x-t>NOVO TOKEN DE REGISTRY</text>
<button class="btn btn--ghost" type="button" @click="closeTokenModal()" x-t>Fechar</button>
</row>
<column class="modal__body">
<template x-if="newRegistryTokenStep === 'form'">
<column class="form-panel">
<text class="u-label-cap" x-t>NOME</text>
<input class="field__input" type="text" x-model="newRegistryTokenName" placeholder="ci-deploy" />
<text class="u-label-cap" style="margin-top:14px" x-t>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()" x-t>Criar token</button>
</row>
</column>
</template>
<template x-if="newRegistryTokenStep === 'reveal'">
<column class="form-panel">
<text class="u-muted-grow" x-t>
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="$store.app.copyText(newRegistryTokenLoginCommand, 'texto')" x-t>Copiar</button>
<button class="btn btn--primary" type="button" @click="closeTokenModal()" x-t>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" x-t>Schedules</text>
<text class="page-head__subtitle" x-t>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()" x-t>+ Novo job</button>
</row>
<column class="panel panel--fill">
<row class="table__head">
<text class="table__head-cell table__col-svc" x-t>NOME</text>
<text class="table__head-cell table__col-proj" x-t>PROJETO / GATILHO</text>
<text class="table__head-cell table__col-state" x-t>ÚLTIMA EXECUÇÃO</text>
<text class="table__head-cell table__col-dur" x-t>RECORRÊNCIA</text>
<text class="table__head-cell table__col-dur" x-t>PRÓXIMA EXECUÇÃO</text>
<text class="table__head-cell table__col-warm" x-t>AQUECIMENTO</text>
<text class="table__head-cell table__col-act-2" x-t>AÇÕES</text>
</row>
<text class="u-muted" x-fallback="rows" style="padding:16px" x-t>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" x-t:title
@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()" x-t>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="$t('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()" x-t>▶ 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()" x-t>■ 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)" x-t>◐ 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" x-t:title
@click="$store.app.jobRebuild($store.app.jobActionsRow.id)" x-t>↻ 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()" x-t>≡ 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 ? $t('‖ Pausar') : $t('▶ Ativar')"></button>
<button class="btn btn--ghost job-actions__item" type="button"
@click="$store.app.openEditJob($store.app.jobActionsRow.id); $store.app.closeJobActions()" x-t>✎ 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()" x-t>✕ 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 ? $t('EDITAR JOB') : $t('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()" x-t>‹ Voltar</button>
<button class="btn btn--ghost" type="button" @click="$store.app.closeNewJob()" x-t>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)" x-t>Selecionar ›</button>
</column>
</template>
<text class="u-muted" x-fallback="$store.app.newJobProjects" style="padding:16px" x-t>Nenhum projeto ainda.</text>
</column>
<column class="grid" x-show="$store.app.newJobStep === 'pick_service'">
<column class="card">
<text class="card__name" x-t>Nenhum (autônomo)</text>
<text class="u-muted" x-t>Job sem serviço gatilho — só variáveis do projeto.</text>
<button class="card__action card__action--open" type="button" @click="$store.app.newJobPickNoService()" x-t>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)" x-t>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" x-t>NOME</text>
<input class="field__input" type="text" x-model="$store.app.newJobName" placeholder="backup-diario" x-t:placeholder />
<text class="u-label-cap" style="margin-top:14px" x-t>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" x-t>
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" x-t>CONTA GIT</text>
<select class="field__input" x-model="$store.app.newJobGitProviderId"
@change="$store.app.newJobGitProviderPick($event.target.value)">
<option value="" x-t>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" x-t>Conecte contas em Settings → Git.</text>
<text class="u-label-cap" x-t>REPOSITÓRIO</text>
<select class="field__input" x-model="$store.app.newJobGitRepoFullName"
@change="$store.app.newJobGitRepoPick($event.target.value)">
<option value="" x-t>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="" x-t>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" x-t>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" x-t>VARIÁVEIS DE AMBIENTE (opcional)</text>
<text class="u-muted" x-t>
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" x-t>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'" x-t>Intervalo</button>
<button type="button" :class="$store.app.newJobKind === 'daily' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="$store.app.newJobKind = 'daily'" x-t>Diário</button>
<button type="button" :class="$store.app.newJobKind === 'weekly' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="$store.app.newJobKind = 'weekly'" x-t>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" x-t>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" x-t>DIA DA SEMANA</text>
<select class="field__input" x-model="$store.app.newJobWeekday">
<option value="0" x-t>Seg</option>
<option value="1" x-t>Ter</option>
<option value="2" x-t>Qua</option>
<option value="3" x-t>Qui</option>
<option value="4" x-t>Sex</option>
<option value="5" x-t>Sáb</option>
<option value="6" x-t>Dom</option>
</select>
</column>
<column class="healthcheck__field">
<text class="u-label-cap" x-t>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 ? $t('Salvando…') : $t('Criando…')) : ($store.app.newJobEditId ? $t('Salvar') : $t('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" x-t>LOGS DO JOB</text>
<button class="btn btn--ghost" type="button" @click="$store.app.closeJobLogs()" x-t>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" x-t>
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" x-t>Settings</text>
<text class="page-head__subtitle" x-t>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'" x-t>Manutenção</button>
<button type="button" :class="store.settingsTab === 'gitsync' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.settingsTab = 'gitsync'; store.stateGitFormLoad()"><span x-t>Sincronização</span><span class="nav-item__badge" x-show="store.stateGitAlertMessage" x-text="' ●'"></span></button>
<button type="button" :class="store.settingsTab === 'updates' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.settingsTab = 'updates'"><span x-t>Atualizações</span><span class="nav-item__badge" x-show="store.updateIsAvailable" x-text="' ●'"></span></button>
</row>
<hr class="rule" />
<column class="u-fill-all" x-show="store.settingsTab === 'web'">
<column class="form-panel">
<text class="u-label-cap" x-t>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="$store.app.copyText(store.serverSettingsPublicBase, 'texto')" x-t>Copiar</button>
</row>
<text class="u-muted" x-t>
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" x-t>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="$store.app.copyText(store.serverAccessKey, 'token')" x-t>Copiar</button>
</row>
<text class="u-muted" x-t>
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" x-t>CONECTAR UM AGENTE DE IA — 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="store.copyText(store.serverMcpCommand, 'comando')" x-t>Copiar</button>
</row>
<text class="u-label-cap" style="margin-top:14px">CODEX (~/.codex/config.toml)</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact">[mcp_servers.rustploy] url = …/mcp</text>
<button class="btn btn--copy" type="button" @click="store.copyText(store.serverMcpCommandCodex, 'comando')" x-t>Copiar</button>
</row>
<text class="u-label-cap" style="margin-top:14px">GEMINI CLI</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact">gemini mcp add … rustploy …/mcp</text>
<button class="btn btn--copy" type="button" @click="store.copyText(store.serverMcpCommandGemini, 'comando')" x-t>Copiar</button>
</row>
<text class="u-muted" x-t>
Cada botão copia o registro do MCP deste servidor para o agente correspondente, já com o
token. Claude Code e Gemini: cole num terminal; Codex: acrescente o bloco ao
~/.codex/config.toml. Abra uma sessão nova do agente e confira com /mcp. Contém o token:
não cole em lugar público.
</text>
<text class="u-label-cap" style="margin-top:14px" x-t>E-MAIL LET'S ENCRYPT (HTTPS AUTOMÁTICO)</text>
<input class="field__input" type="text" x-model="store.serverSettingsEmail" placeholder="voce@exemplo.com" x-t:placeholder />
<text class="u-muted" x-t>Certificados TLS emitidos automaticamente. Requer restart do daemon.</text>
<text class="u-label-cap" style="margin-top:14px" x-t>DOMÍNIO DO REGISTRY (EXPOSIÇÃO PÚBLICA)</text>
<input class="field__input" type="text" x-model="store.serverSettingsRegistryDomain" placeholder="registry.meusite.com" x-t:placeholder />
<text class="u-muted" x-t>
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()" x-t>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="$t('CONTAS CONECTADAS (') + gitProviderRows.length + ')'"></text>
<text class="u-muted" x-fallback="gitProviderRows" x-t>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)" x-t>Remover</button>
</row>
</template>
</column>
<hr class="rule" />
<text class="u-label-cap" x-t>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')" x-t>Abrir autorização no navegador</button>
<row class="healthcheck__grid" style="margin-top:8px">
<column class="healthcheck__field">
<text class="u-label-cap" x-t>NOME</text>
<input class="field__input" type="text" x-model="store.gitProviderName" placeholder="Meu Gitea" x-t:placeholder />
</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" x-t:placeholder />
<text class="u-muted" x-show="store.gitProviderKind === 'github'" x-t>
Deixe vazio para github.com. Só preencha para GitHub Enterprise Server.
</text>
</column>
</row>
<text class="u-label-cap" style="margin-top:14px" x-t>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" x-t>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="$store.app.copyText(store.gitProviderRedirect, 'texto')" x-t>Copiar</button>
</row>
<text class="u-muted" x-show="store.gitProviderKind !== 'github'" x-t>
No Gitea: Settings → Applications → crie um OAuth2 app e cole a Redirect URI acima.
</text>
<text class="u-muted" x-show="store.gitProviderKind === 'github'" x-t>
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'" x-t>
No Gitea: Settings → Applications → Generate New Token (escopo: repo).
</text>
<text class="u-muted" x-show="store.gitProviderKind === 'github'" x-t>
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()" x-t>Conectar conta</button>
<button class="btn btn--ghost" type="button" @click="store.gitProviderRefresh()" x-t>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" x-t>EXPORTAR</text>
<text class="u-muted" x-t>
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()" x-t>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="$store.app.copyText(store.manifestYaml, 'texto')" x-t>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="$store.app.copyText(store.manifestDotenv, 'texto')" x-t>Copiar</button>
</row>
<textarea class="env__editor" readonly x-model="store.manifestDotenv"></textarea>
</column>
</template>
<hr class="rule" />
<text class="u-label-cap" x-t>IMPORTAR</text>
<text class="u-muted" x-t>
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 x-t>Remover serviços ausentes do manifesto (prune)</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.manifestDeploy" /><span x-t>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="$t('Zip carregado: ') + store.manifestZipName"></text>
<text class="u-label-cap" x-t>…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)" x-t:placeholder></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()" x-t>Importar</button>
<text class="u-muted" x-text="store.manifestImportMessage"></text>
</row>
<text class="u-error" x-show="store.manifestHasMissing" x-text="$t('Variáveis sem valor no TOML: ') + store.manifestMissingVars"></text>
<template x-if="store.manifestHasReport">
<column>
<text class="u-label-cap" x-t>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 === 'gitsync'">
<column class="form-panel">
<text class="u-label-cap" x-t>SINCRONIZAÇÃO COM O GIT</text>
<text class="u-muted" x-t>
Versiona o estado (projetos, serviços, jobs e secrets cifrados) num repositório privado do
seu provedor Git. Desligada por padrão. Só liga se não houver nenhum valor em claro.
</text>
<text class="banner banner--warn" x-show="store.stateGitStatus && !store.stateGitStatus.git_available" x-t>
O programa git não está instalado neste servidor.
</text>
<text class="banner banner--warn" x-show="store.stateGitStatus && !store.stateGitStatus.recovery_configured" x-t>
Sem recovery_recipient no config.toml: perder o config.toml deste servidor significa perder todos os secrets, inclusive os do repositório.
</text>
<text class="u-label-cap" style="margin-top:10px" x-t>MODO</text>
<row class="tabs">
<button type="button" :class="store.stateGitMode === 'off' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.stateGitMode = 'off'" x-t>Desligado</button>
<button type="button" :class="store.stateGitMode === 'commit' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.stateGitMode = 'commit'" x-t>Só commit (local)</button>
<button type="button" :class="store.stateGitMode === 'sync' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.stateGitMode = 'sync'" x-t>Sincronizar</button>
</row>
<text class="u-muted" x-show="store.stateGitMode === 'commit'" x-t>Faz um commit local a cada mudança. Não usa a rede.</text>
<text class="u-muted" x-show="store.stateGitMode === 'sync'" x-t>Commita, envia ao repositório e busca o que foi mudado por fora (a cada 60 s, ou em "Sincronizar agora").</text>
<column x-show="store.stateGitMode === 'sync'" style="gap:10px">
<text class="u-label-cap" x-t>REPOSITÓRIO (https://…)</text>
<input class="field__input" type="text" x-model="store.stateGitRemote" placeholder="https://github.com/org/rustploy-estado.git" />
<text class="u-label-cap" x-t>CONTA GIT (credencial)</text>
<select class="field__input" x-model="store.stateGitProviderId">
<option value="" x-t>selecione a conta</option>
<template x-for="gitProviderRow in gitProviderRows" :key="gitProviderRow.id">
<option :value="gitProviderRow.id" x-text="gitProviderRow.name"></option>
</template>
</select>
<text class="u-muted" x-t>Conecte contas na aba Git. Endereços SSH (git@…) não são aceitos.</text>
</column>
<column x-show="store.stateGitMode !== 'off'" style="gap:10px">
<text class="u-label-cap" x-t>BRANCH</text>
<input class="field__input" type="text" x-model="store.stateGitBranch" placeholder="main" />
</column>
<row style="gap:8px; margin-top:6px">
<button class="btn btn--primary" type="button" @click="store.stateGitConfigure()" x-t>Salvar e aplicar</button>
<button class="btn btn--copy" type="button" x-show="store.stateGitStatus && store.stateGitStatus.mode === 'sync'"
@click="store.stateGitSyncNow()" x-t>Sincronizar agora</button>
</row>
</column>
<column class="form-panel" x-show="store.stateGitStatus && store.stateGitStatus.mode !== 'off'" style="margin-top:12px">
<text class="u-label-cap" x-t>SITUAÇÃO</text>
<column class="kv-list">
<row class="kv-list__row">
<text class="kv-list__key" x-t>Último commit</text>
<text class="kv-list__val kv-list__val--compact"
x-text="store.stateGitStatus?.last_commit ? (store.stateGitStatus.last_commit.sha.slice(0, 7) + ' · ' + store.stateGitStatus.last_commit.subject) : '—'"></text>
</row>
<row class="kv-list__row" x-show="store.stateGitStatus && store.stateGitStatus.mode === 'sync'">
<text class="kv-list__key" x-t>Último envio</text>
<text class="kv-list__val kv-list__val--compact"
x-text="store.stateGitStatus?.last_push_at ? ((store.stateGitStatus.last_push_ok ? $t('ok') : $t('falhou')) + ' · ' + store.stateGitTimeLabel(store.stateGitStatus.last_push_at)) : '—'"></text>
</row>
<row class="kv-list__row" x-show="store.stateGitStatus && store.stateGitStatus.mode === 'sync'">
<text class="kv-list__key" x-t>Última consulta ao repositório</text>
<text class="kv-list__val kv-list__val--compact" x-text="store.stateGitTimeLabel(store.stateGitStatus?.last_fetch_at)"></text>
</row>
<row class="kv-list__row" x-show="store.stateGitStatus && store.stateGitStatus.mode === 'sync'">
<text class="kv-list__key" x-t>À frente / atrás do repositório</text>
<text class="kv-list__val kv-list__val--compact" x-text="(store.stateGitStatus?.ahead || 0) + ' / ' + (store.stateGitStatus?.behind || 0)"></text>
</row>
</column>
<text class="banner banner--warn" x-show="store.stateGitStatus && store.stateGitStatus.last_error" x-text="store.stateGitStatus?.last_error"></text>
</column>
<column class="form-panel" x-show="store.stateGitStatus && store.stateGitStatus.paused_reason" style="margin-top:12px">
<text class="u-label-cap" x-t>HISTÓRICO DIVERGIDO — SINCRONIZAÇÃO PAUSADA</text>
<text class="u-muted" x-text="store.stateGitStatus?.paused_reason"></text>
<text class="u-muted" x-t>
O servidor não decide qual lado vence: escolher um descarta o outro (com cópia de segurança numa branch).
Enquanto isso, nada é enviado, buscado nem commitado; suas edições aqui continuam valendo.
</text>
<row style="gap:8px">
<button class="btn btn--danger" type="button" @click="store.stateGitResolve('UseRemote')" x-t>Usar o do repositório</button>
<button class="btn btn--danger" type="button" @click="store.stateGitResolve('UseLocal')" x-t>Usar o do servidor</button>
</row>
<text class="u-muted" x-t>
Para juntar os dois lados, resolva por SSH (veja a página "Sincronizar o estado com um repositório Git" na documentação) e clique em Retomar.
</text>
<row><button class="btn btn--copy" type="button" @click="store.stateGitResume()" x-t>Retomar</button></row>
</column>
<column class="form-panel" x-show="store.stateGitStatus && store.stateGitStatus.incoming_rejected" style="margin-top:12px">
<text class="u-label-cap" x-t>COMMIT DO REPOSITÓRIO RECUSADO</text>
<text class="u-muted" x-text="$t('Commit') + ' ' + (store.stateGitStatus?.incoming_rejected?.sha || '').slice(0, 12)"></text>
<text class="banner banner--warn" x-text="store.stateGitStatus?.incoming_rejected?.reason"></text>
<text class="u-muted" x-t>Nada foi aplicado. Corrija no repositório com um commit novo; o daemon tenta de novo sozinho.</text>
</column>
<column class="form-panel" x-show="store.stateGitStatus && store.stateGitStatus.removed_in_git && store.stateGitStatus.removed_in_git.length" style="margin-top:12px">
<text class="u-label-cap" x-t>REMOVIDOS NO GIT</text>
<text class="u-muted" x-t>
Estes itens sumiram do estado por um commit do repositório. O container, os volumes e os dados NÃO foram tocados.
O domínio sai do ar. Para limpar: Runtime → Containers → Parar → Remover (e Volumes, se houver dados).
Se foi engano, desfaça o commit no Git: o serviço volta e reencontra o container.
</text>
<column class="kv-list">
<template x-for="removal in (store.stateGitStatus?.removed_in_git || [])" :key="removal.id">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-text="removal.name"></text>
<text class="u-muted" x-text="removal.kind + (removal.project ? ' · ' + removal.project : '') + ' · ' + removal.sha.slice(0, 7) + ' · ' + store.stateGitTimeLabel(removal.at)"></text>
</column>
<button class="btn btn--copy" type="button" @click="store.stateGitDismissRemoval(removal.id)" x-t>Esquecer</button>
</row>
</template>
</column>
</column>
<column class="form-panel" style="margin-top:12px">
<text class="u-label-cap" x-text="$t('VALORES EM CLARO (') + store.stateGitAuditFindings.length + ')'"></text>
<text class="u-muted" x-t>
A sincronização só liga, e só commita, sem nenhum destes. Mova-os para secret (na tela de variáveis de ambiente).
Uma suspeita que na verdade não é segredo pode ser marcada como "não é segredo"; certezas não.
</text>
<text class="u-muted" x-fallback="store.stateGitAuditFindings" x-t>Nenhum valor em claro encontrado.</text>
<column class="kv-list" x-show="store.stateGitAuditFindings.length">
<template x-for="finding in store.stateGitAuditFindings" :key="finding.project + '|' + finding.scope + '|' + finding.what">
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-text="finding.what"></text>
<text class="u-muted" x-text="(finding.project ? finding.project + ' · ' : '') + finding.scope + ' · ' + (finding.definite ? $t('certeza') : $t('suspeita'))"></text>
</column>
<button class="btn btn--copy" type="button" x-show="!finding.definite && !finding.allowed"
@click="store.stateGitAllowFinding(finding, true)" x-t>Não é segredo</button>
<button class="btn btn--copy" type="button" x-show="!finding.definite && finding.allowed"
@click="store.stateGitAllowFinding(finding, false)" x-t>Voltar a travar</button>
</row>
</template>
</column>
</column>
</column>
<column class="u-fill-all" x-show="store.settingsTab === 'maintenance'">
<column class="form-panel">
<text class="u-label-cap" x-t>LIMPEZA AUTOMÁTICA</text>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupEnabled" /><span x-t>Ativar limpeza automática</span>
</label>
<text class="u-muted" x-t>
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" x-t>FREQUÊNCIA</text>
<row class="tabs">
<button type="button" :class="store.dockerCleanupKind === 'interval' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerCleanupKind = 'interval'" x-t>A cada N horas</button>
<button type="button" :class="store.dockerCleanupKind === 'daily' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerCleanupKind = 'daily'" x-t>Todo dia</button>
<button type="button" :class="store.dockerCleanupKind === 'weekly' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.dockerCleanupKind = 'weekly'" x-t>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" x-t>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'" x-t>Seg</button>
<button type="button" :class="store.dockerCleanupWeekday === '1' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '1'" x-t>Ter</button>
<button type="button" :class="store.dockerCleanupWeekday === '2' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '2'" x-t>Qua</button>
<button type="button" :class="store.dockerCleanupWeekday === '3' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '3'" x-t>Qui</button>
<button type="button" :class="store.dockerCleanupWeekday === '4' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '4'" x-t>Sex</button>
<button type="button" :class="store.dockerCleanupWeekday === '5' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '5'" x-t>Sáb</button>
<button type="button" :class="store.dockerCleanupWeekday === '6' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'" @click="store.dockerCleanupWeekday = '6'" x-t>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" x-t>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="$t('Próxima execução agendada: ') + store.dockerCleanupNextRunLabel"></text>
<hr class="rule" />
<text class="u-label-cap" x-t>O QUE LIMPAR</text>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupContainers" /><span x-t>Containers parados</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupImages" /><span x-t>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 x-t>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" x-t>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" x-t>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 x-t>Redes sem uso</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupBuildCache" /><span x-t>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" x-t>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" x-t>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>
<label class="check-row">
<input type="checkbox" x-model="store.dockerCleanupWarmAfter" /><span x-t>Aquecer os builds depois da limpeza</span>
</label>
<text class="u-muted" style="margin-left:20px" x-t>
Depois de limpar, reconstrói a imagem de cada serviço com repositório Git ou zip, um de cada vez, na
fila de deploy. Sem isso, o primeiro deploy depois da limpeza paga o build inteiro.
</text>
<hr class="rule" />
<text class="u-label-cap u-error" x-t>VOLUMES — CUIDADO</text>
<text class="u-muted" x-t>
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 x-t>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 x-t>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()" x-t>Salvar</button>
<button class="tabs__tab" type="button" x-show="!store.dockerCleanupRunning" @click="store.dockerCleanupRunNow()" x-t>Executar agora</button>
<button class="tabs__tab" type="button" x-show="store.dockerCleanupRunning" disabled x-t>Executando…</button>
<text class="u-muted" x-text="store.dockerCleanupMessage"></text>
</row>
<hr class="rule" />
<text class="u-label-cap" x-t>ÚLTIMA EXECUÇÃO</text>
<text class="u-muted" x-text="store.dockerCleanupLastRunText"></text>
</column>
</column>
<column class="u-fill-all" x-show="store.settingsTab === 'updates'">
<column class="form-panel">
<text class="u-label-cap">DAEMON</text>
<row class="kv-list__row">
<text class="kv-list__key" x-t>Versão instalada</text>
<text class="kv-list__val kv-list__val--compact" x-text="store.updateCurrentVersion || '—'"></text>
</row>
<row class="kv-list__row">
<text class="kv-list__key" x-t>Última versão</text>
<text class="kv-list__val kv-list__val--compact" x-text="store.updateLatestVersion || '—'"></text>
</row>
<row class="kv-list__row">
<text class="kv-list__key" x-t>Verificado em</text>
<text class="kv-list__val kv-list__val--compact" x-text="store.updateCheckedAtLabel || '—'"></text>
</row>
<row class="healthcheck__grid">
<button class="btn btn--ghost btn--sm" type="button" @click="$store.app.updateCheckNow()" x-t>Verificar atualizações</button>
</row>
<text class="u-muted" x-text="store.updateMessage"></text>
<column x-show="store.updateIsHelperAvailable">
<text class="u-label-cap" style="margin-top:14px" x-t>ATUALIZAR PELO APP</text>
<text class="u-muted" x-t>
O daemon baixa a versão nova, confere a assinatura e a instala por um helper root do próprio
pacote. O daemon reinicia (os containers dos serviços continuam rodando) e esta tela fica sem
resposta por alguns segundos. Antes, o daemon guarda um snapshot do banco.
</text>
<row class="healthcheck__grid">
<button class="btn btn--primary btn--sm" type="button" x-show="store.updateIsAvailable"
@click="$store.app.updateApply()" x-t>Atualizar daemon</button>
<button class="btn btn--ghost btn--sm" type="button"
@click="$store.app.updateRollback()" x-t>Reverter para a versão anterior</button>
</row>
</column>
<text class="u-muted" x-text="store.updateProgressMessage"></text>
<column x-show="store.updateIsAvailable">
<text class="u-label-cap" style="margin-top:14px" x-t>COMO ATUALIZAR À MÃO</text>
<text class="u-muted" x-t>
Há uma versão nova. Por enquanto a atualização é manual: rode o comando abaixo na VPS.
Seus dados, o config.toml e a master.key são preservados, e os containers dos serviços
continuam rodando.
</text>
<row class="kv-list__row">
<text class="kv-list__val kv-list__val--compact" x-text="store.updateInstallCommand"></text>
<button class="btn btn--copy" type="button" @click="$store.app.copyText(store.updateInstallCommand, 'comando')" x-t>Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key" x-t>Notas da release</text>
<text class="kv-list__val kv-list__val--compact" x-text="store.updateReleaseUrl"></text>
<button class="btn btn--copy" type="button" @click="$store.app.copyText(store.updateReleaseUrl, 'endereço')" x-t>Copiar</button>
</row>
</column>
</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" x-t>Projects</text>
<text class="page-head__subtitle" x-t>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" x-t>SERVICES</text><text class="stat-card__num" x-text="servicesCount"></text></column>
<column class="stat-card"><text class="u-label-cap" x-t>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()" x-t>+ 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 + $t(' serviço(s) · ') + row.runningCount + $t(' 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)" x-t>Abrir ›</button>
<button class="card__action card__action--remove" type="button" x-show="row.canDelete"
@click="store.deleteProject(row.id)" x-t>Remover</button>
<text class="u-muted" x-show="!row.canDelete" x-t>Não removível: tem serviços</text>
</column>
</template>
<text class="u-muted" x-fallback="rows" style="padding:16px" x-t>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')" x-t>‹ 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()" x-t>+ Novo serviço</button>
<button class="btn btn--sm-ghost" type="button" @click="store.openProjectWin(project)" x-t>Editar projeto</button>
<button class="btn btn--sm-danger" type="button" x-show="canDelete"
@click="store.deleteProject(store.selectedProjectId)" x-t>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'" x-t>Serviços</button>
<button type="button" :class="projectTab === 'env' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="projectTab = 'env'" x-text="$t('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 class="badge" x-show="$store.app.stateGitPendingDeploy(service.id)"
:title="$t('Um commit do repositório mudou este serviço. O que está rodando só acompanha no próximo deploy.')">
<text class="status-dot state-cell--warn">◆</text>
<text class="badge__label state-cell--warn" x-t>mudança pendente de deploy</text>
</row>
<row class="badge" x-show="service.hasWarm" :title="service.warmReason">
<text class="status-dot" :class="'state-cell--' + service.warmKind">◐</text>
<text class="badge__label" :class="'state-cell--' + service.warmKind" x-text="service.warmLabel"></text>
</row>
</row>
<row class="card__container" style="gap:6px;align-items:center">
<button class="btn--copy-mini" type="button" title="Copiar nome" x-t:title 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-t:title 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)" x-t>Abrir ›</button>
<row class="card__actions">
<button class="card__action card__action--icon" type="button" title="Parar" x-t:title aria-label="Parar serviço" x-t:aria-label
@click="store.stopService(service.id)">■</button>
<button class="card__action card__action--icon card__action--icon-danger" type="button" title="Remover" x-t:title aria-label="Remover serviço" x-t:aria-label
@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)" x-t:title
aria-label="Exportar serviço" x-t:aria-label @click="store.openExportWin(service.id)">⇪</button>
</row>
</column>
</template>
<text class="u-muted" x-fallback="services" style="padding:16px" x-t>
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" x-t>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" x-t>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()" x-t>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" x-t>Nenhuma variável.</text>
</column>
</column>
</template>
<template x-if="envTextOpen">
<column style="gap:8px">
<text class="u-muted" x-t>Cole/edite no formato KEY=VALUE (# comentário).</text>
<textarea class="env__editor" x-model="envText" placeholder="KEY=VALUE (# comentário)" x-t:placeholder></textarea>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="saveEnvText()" x-t>Salvar</button>
<button class="btn btn--ghost" type="button" @click="closeEnvText()" x-t>Cancelar</button>
</row>
</column>
</template>
</column>
<column class="env" x-show="projectTab === 'secrets'">
<text class="u-muted-grow">
<span x-t>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</span> <secret:NOME> <span x-t>como valor de uma
variável de ambiente.</span>
</text>
<row class="env__add">
<input class="env__input" type="text" placeholder="NOME" x-t:placeholder x-model="newSecretName" />
<input class="env__input" type="password" placeholder="valor (cifrado ao salvar)" x-t:placeholder x-model="newSecretValue" />
<button class="btn btn--primary" type="button" @click="submitSecret()" x-t>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" x-t>cifrado</text>
</row>
</template>
<text class="u-muted" x-fallback="secrets" style="padding:12px" x-t>
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()" x-t>+ Novo job</button>
</row>
<text class="u-muted" x-fallback="jobs" style="padding:12px" x-t>
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" x-t:title @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" x-t>NOVO SERVIÇO</text>
<button class="btn btn--ghost" type="button" @click="cancel()" x-t>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()" x-t>‹ Tipo</button>
</row>
<text class="page-head__subtitle" x-show="step === 'pick_type'" style="flex-shrink:0" x-t>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" x-t>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" x-t>Web app via Git ou imagem</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoApp()" x-t>Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Database</text>
<text class="u-muted" x-t>Banco de dados gerenciado</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoDatabase()" x-t>Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Broker</text>
<text class="u-muted" x-t>Mensageria: Kafka, RabbitMQ, NATS</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoBroker()" x-t>Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Compose</text>
<text class="u-muted" x-t>Stack Docker Compose</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoCompose()" x-t>Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key">Template</text>
<text class="u-muted" x-t>A partir de um preset</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoTemplate()" x-t>Selecionar ›</button>
</row>
<row class="kv-list__row">
<column class="page-head__titles">
<text class="env__key" x-t>Importar</text>
<text class="u-muted" x-t>Recriar um serviço exportado de outro servidor</text>
</column>
<button class="btn btn--primary" type="button" @click="gotoImport()" x-t>Selecionar ›</button>
</row>
</column>
</column>
<template x-if="step === 'import'">
<column class="form-panel" x-data="serviceImport" style="gap:12px">
<text class="u-muted" x-t>
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" x-t>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" x-t>…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 ? $t('Analisando…') : $t('Analisar')"></button>
</row>
</column>
</column>
<row class="env__add" x-show="report">
<text class="u-muted" x-text="$t('Pacote: ') + (fileName || $t('texto colado'))"></text>
<button class="btn btn--sm-ghost" type="button" @click="clear()" x-t>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" x-t>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" x-t>
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" x-t>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" x-t>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" x-t:placeholder />
</row>
</template>
</column>
<column style="gap:6px" x-show="report.missing_project_vars.length">
<text class="field__label" x-t>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" x-t:placeholder />
</row>
</template>
</column>
<column style="gap:6px" x-show="report.missing_secrets.length">
<text class="field__label" x-t>SECRETS QUE ESTE PROJETO NÃO TEM</text>
<text class="u-muted" x-t>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" x-t:placeholder 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 + '\$t(' NÃO EXISTE AQUI')"></text>
<select class="field__input" x-model="gitProviderId" @change="analyze()">
<option value="" x-t>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" x-t>VARIÁVEIS DO PROJETO TRAZIDAS</text>
<text class="u-muted" x-t>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 ? $t('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'" x-t>já existe igual — nada a fazer</text>
</row>
</template>
</column>
<label class="check-row">
<input type="checkbox" x-model="dropDomains" @change="analyze()" />
<span x-t>Não trazer os domínios</span>
</label>
<label class="check-row">
<input type="checkbox" x-model="deploy" />
<span x-t>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 ? $t('Criando…') : $t('Criar serviço')"></button>
<button class="btn btn--ghost" type="button" @click="$store.app.closeNewService()" x-t>Cancelar</button>
</row>
<text class="u-muted" x-show="hasMissing" x-t>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" x-t>NOVA APPLICATION</text>
<text class="u-label-cap" x-t>NOME</text>
<input class="field__input" type="text" x-model="appName" placeholder="meu-servico" x-t:placeholder />
<text class="u-label-cap" style="margin-top:14px" x-t>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" x-t:placeholder />
<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" x-t>PORTA</text>
<input class="field__input" type="number" x-model="appPort" />
<text class="u-label-cap" style="margin-top:14px" x-t>DOMÍNIO (opcional)</text>
<input class="field__input" type="text" x-model="appDomain" placeholder="meuservico.dominio.com" x-t:placeholder />
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="submitApp()" :disabled="submitting"
x-text="submitting ? $t('Criando…') : $t('Criar Application')"></button>
</row>
</column>
<column class="form-panel" x-show="step === 'compose_form'">
<text class="u-label-cap" x-t>NOVA COMPOSE STACK</text>
<text class="u-label-cap" x-t>NOME</text>
<input class="field__input" type="text" x-model="composeName" placeholder="meu-stack" x-t:placeholder />
<text class="u-label-cap" style="margin-top:14px" x-t>PORTA</text>
<input class="field__input" type="number" x-model="composePort" />
<text class="u-label-cap" style="margin-top:14px" x-t>DOMÍNIO (opcional)</text>
<input class="field__input" type="text" x-model="composeDomain" placeholder="meuservico.dominio.com" x-t:placeholder />
<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 ? $t('Criando…') : $t('Criar Compose Stack')"></button>
</row>
</column>
<column class="form-panel" x-show="step === 'pick_db'">
<text class="u-label-cap" x-t>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)" x-t>Selecionar ›</button>
</row>
</template>
</column>
</column>
<column class="form-panel" x-show="step === 'db_form'">
<text class="u-label-cap" x-text="$t('NOVA ') + (selectedDatabase ? selectedDatabase.label : '')"></text>
<text class="u-label-cap" style="margin-top:8px" x-t>NOME</text>
<input class="field__input" type="text" x-model="databaseServiceName" placeholder="meu-banco" x-t:placeholder />
<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)" x-t:placeholder />
<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" x-t:placeholder />
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="submitDatabase()" :disabled="submitting"
x-text="submitting ? $t('Criando…') : $t('Criar')"></button>
</row>
</column>
<column class="form-panel" x-show="step === 'pick_broker'">
<text class="u-label-cap" x-t>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)" x-t>Selecionar ›</button>
</row>
</template>
</column>
</column>
<column class="form-panel" x-show="step === 'broker_form'">
<text class="u-label-cap" x-text="$t('NOVO ') + (selectedBroker ? selectedBroker.label : '')"></text>
<text class="u-label-cap" style="margin-top:8px" x-t>NOME</text>
<input class="field__input" type="text" x-model="brokerServiceName" placeholder="meu-broker" x-t:placeholder />
<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" x-t:placeholder />
<input class="field__input" type="password" x-model="brokerPassword" placeholder="senha" x-t:placeholder />
</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" x-t:placeholder />
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="submitBroker()" :disabled="submitting"
x-text="submitting ? $t('Criando…') : $t('Criar')"></button>
</row>
</column>
<column x-show="step === 'pick_template'">
<text class="u-label-cap" x-t>BUSCAR</text>
<input class="field__input" type="text" x-model="templateSearch" placeholder="buscar templates…" x-t:placeholder />
<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)" x-t>Escolher ›</button>
</row>
</template>
<text class="u-muted" x-fallback="filteredTemplates" style="padding:16px" x-t>
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 : '') + $t(' — CONFIGURAR')"></text>
<text class="u-label-cap" style="margin-top:8px" x-t>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 ? $t('Criando…') : $t('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 ? $t('EDITAR PROJETO') : $t('NOVO PROJETO')"></text>
<button class="btn btn--ghost" type="button" @click="store.closeProjectWin()" x-t>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" x-t>NOME</text>
<input class="field__input" type="text" placeholder="meu-projeto" x-t:placeholder x-model="name" />
</column>
<column class="field">
<text class="field__label" x-t>DESCRIÇÃO (opcional)</text>
<input class="field__input" type="text" placeholder="Para que serve este projeto" x-t:placeholder 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 ? $t('Salvando…') : (editing ? $t('Salvar') : $t('Criar projeto'))"></button>
<button class="btn btn--ghost" type="button" @click="store.closeProjectWin()" x-t>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="$t('EXPORTAR · ') + (plan ? plan.service_name : '')"></text>
<button class="btn btn--ghost" type="button" @click="close()" x-t>Fechar</button>
</row>
<column class="modal__body" style="gap:14px">
<text class="u-muted" x-show="loading" x-t>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" x-t>
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="$t('VARIÁVEIS DO SERVIÇO (') + plan.service_env.length + $t(') — todas vão')"></text>
<text class="u-muted" x-show="!plan.service_env.length" x-t>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="$t('VARIÁVEIS DO PROJETO “') + plan.project_name + $t('” — 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-t:placeholder x-model="filter" />
<button class="btn btn--sm-ghost" type="button" @click="pickSuggested()" x-t>Sugeridas</button>
<button class="btn btn--sm-ghost" type="button" @click="pickAll()" x-t>Todas</button>
<button class="btn btn--sm-ghost" type="button" @click="pickNone()" x-t>Nenhuma</button>
</row>
<text class="u-muted" x-show="!plan.project_env.length" x-t>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" x-t>· citada pelo serviço</span>
</label>
</template>
</column>
<label class="check-row">
<input type="checkbox" x-model="includeValues" />
<span x-t>Incluir os valores das variáveis</span>
</label>
<text class="u-error" x-show="includeValues" x-t>
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" x-t>
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()" x-t>Baixar .yml</button>
<button class="btn btn--blue" type="button" @click="copy()" x-t>Copiar texto</button>
<button class="btn btn--ghost" type="button" @click="close()" x-t>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" x-t>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" x-t>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 class="badge" x-show="svc && store.stateGitPendingDeploy(svc.id)"
:title="$t('Um commit do repositório mudou este serviço. O que está rodando só acompanha no próximo deploy.')">
<text class="status-dot state-cell--warn">◆</text>
<text class="badge__label state-cell--warn" x-t>mudança pendente de deploy</text>
</row>
<row class="badge" x-show="warm" :title="warm?.reason || ''">
<text class="status-dot" :class="'state-cell--' + warmKind">◐</text>
<text class="badge__label" :class="'state-cell--' + warmKind" x-text="warmLabel"></text>
</row>
</row>
<text class="u-muted" x-show="warm?.reason && !warm?.warming" x-text="$t('frio: ') + (warm?.reason || '')"></text>
<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" x-t:title @click="store.serviceStop()">■</button>
<button class="btn btn--ghost" type="button" x-show="warm" :disabled="warm?.warming"
title="Aquecer: constrói a imagem para o próximo deploy achar o cache, sem mexer no container no ar" x-t:title
@click="store.serviceWarm(store.selectedServiceId)">◐</button>
<button class="btn btn--ghost" type="button" title="Exportar serviço" x-t:title
@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()" x-t>Stop</button>
<button class="btn btn--ghost" type="button" x-show="warm" :disabled="warm?.warming"
title="Constrói a imagem para o próximo deploy achar o cache, sem mexer no container no ar" x-t:title
@click="store.serviceWarm(store.selectedServiceId)" x-t>Aquecer</button>
<button class="btn btn--ghost" type="button" title="Gera um arquivo para recriar este serviço em outro servidor" x-t:title
@click="store.openExportWin(store.selectedServiceId)" x-t>Exportar</button>
</row></template>
</row>
<column class="u-center-fill" x-show="store.serviceLoading" style="padding:24px">
<text class="u-muted" x-t>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')" x-t>General</button>
<button type="button" :class="store.serviceTab === 'connection' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('connection')" x-t>Connection</button>
<button type="button" :class="store.serviceTab === 'environment' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('environment')" x-t>Environment</button>
<button type="button" :class="store.serviceTab === 'domains' ? 'tabs__tab tabs__tab--active' : 'tabs__tab'"
@click="store.setServiceTab('domains')" x-t>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')" x-t>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')" x-t>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" x-t>NOME DO SERVIÇO</text>
<row class="env__add">
<input class="field__input" type="text" x-model="editName" placeholder="Nome do serviço" x-t:placeholder
@keydown.enter.prevent="renameService()" />
<button class="btn btn--primary" type="button" @click="renameService()" x-t>Renomear</button>
</row>
<text class="u-muted" x-text="renameNote"></text>
</column>
<row class="kv-list__row"><text class="kv-list__key" x-t>Origem</text><text class="kv-list__val" x-text="sourceText"></text></row>
<row class="kv-list__row"><text class="kv-list__key" x-t>Porta</text><text class="kv-list__val" x-text="svc.spec.port"></text></row>
<row class="kv-list__row"><text class="kv-list__key" x-t>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" x-t>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()" x-t>Salvar compose</button>
<button class="btn btn--ghost" type="button" @click="cancelCompose()" x-t>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" x-t:placeholder />
<text class="u-muted" x-t>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)" x-t:placeholder />
</column>
<column class="healthcheck__field">
<text class="u-label-cap">CREDENTIALS (secret)</text>
<input class="field__input" type="text" x-model="serviceFormCredentials" placeholder="(opcional)" x-t:placeholder />
<text class="u-muted">
<span x-t>Nome de um secret do projeto (aba Secrets), puro — sem</span> <secret:><span x-t>. Usado
para clonar repositório Git privado via HTTPS; não tem efeito com URL SSH.</span>
</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" x-t>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" x-t>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()" x-t>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" x-t>ZIP (Dockerfile na raiz)</text>
<input class="field__input" type="file" accept=".zip" @change="onArchiveFileChange($event)" />
<text class="u-muted" x-t>
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()" x-t>Enviar zip</button>
</row>
</column>
</template>
<template x-if="providerTab === 'gitea'">
<column style="gap:8px">
<text class="u-muted" x-t>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" x-t>CONTA CONECTADA</text>
<select class="field__input" x-model="giteaProviderId" @change="giteaProviderPick($event.target.value)">
<option value="" x-t>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" x-t>Conecte contas em Settings → Git.</text>
<text class="u-label-cap" x-t>REPOSITÓRIO</text>
<select class="field__input" x-model="giteaRepoFullName" @change="giteaRepoPick($event.target.value)">
<option value="" x-t>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="" x-t>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" x-t>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" x-t>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()" x-t>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)" x-t>Remover serviço</button>
</column>
<column class="kv-list" x-show="store.serviceTab === 'connection'">
<row class="kv-list__row">
<text class="kv-list__key" x-t>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))" x-t>Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key" x-t>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))" x-t>Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key" x-t>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)" x-t>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" x-t>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)" x-t>Copiar</button>
</row>
<row class="kv-list__row">
<text class="kv-list__key" x-t>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)" x-t>Copiar</button>
</row>
<row class="kv-list__row" x-show="canShare">
<text class="kv-list__key" x-t>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)" x-t>Compartilhar</button>
<button class="btn btn--copy" type="button" x-show="isShared" @click="setShared(false)" x-t>Desligar</button>
</row>
<row class="kv-list__row" x-show="connectionInfo.databaseKind">
<text class="kv-list__key" x-t>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" x-t>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) + $t(' 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" x-t>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()" x-t>Adicionar</button>
</row>
<text class="u-muted-grow">
<span x-t>Para um valor sensível, use um secret do projeto: escreva</span> <secret:NOME> <span x-t>ou
secret:NOME como valor. O secret é criado na aba Secrets do projeto.</span>
</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" x-t>Nenhuma variável.</text>
</column>
</column>
</template>
<template x-if="envTextOpen">
<column style="gap:8px">
<text class="u-muted" x-t>Cole/edite no formato KEY=VALUE (# comentário).</text>
<textarea class="env__editor" x-model="envText" placeholder="KEY=VALUE (# comentário)" x-t:placeholder></textarea>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="saveEnvText()" x-t>Salvar</button>
<button class="btn btn--ghost" type="button" @click="closeEnvText()" x-t>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-t:placeholder x-model="newDomain" />
<input class="env__input" type="text" placeholder="porta (opcional)" x-t:placeholder 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()" x-t>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" x-t>
Nenhum domínio configurado.
</text>
</column>
</column>
<column class="form-panel" x-show="store.serviceTab === 'migrar'">
<text class="u-label-cap" x-t>MIGRAR PARA BANCO COMPARTILHADO</text>
<text class="u-muted" x-t>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" x-t>DATABASE DE ORIGEM (dentro deste banco)</text>
<input class="field__input" x-model="serviceFormMigration.db" placeholder="nome do database" x-t:placeholder />
<text class="u-label-cap" x-t>DESTINO</text>
<select class="form-select" x-model="serviceFormMigration.dest">
<option value="" x-t>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" x-t>ENV VAR A TROCAR</text>
<input class="field__input" x-model="serviceFormMigration.env" placeholder="padrão DATABASE_URL / MONGODB_URI" x-t:placeholder />
<row class="env__add">
<button class="btn btn--primary" type="button" @click="startMigration()" x-t>Migrar</button>
<button class="btn btn--ghost" type="button" @click="loadMigration()" x-t>Atualizar</button>
</row>
<template x-if="mig">
<column>
<text class="u-label-cap" x-text="$t('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" x-t>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()" x-t>Reverter</button>
<button class="btn btn--ghost" type="button" @click="discardOldDatabase()" x-t>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" x-t>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)" x-t>Copiar</button>
</row>
<text class="u-label-cap" x-t>NOVO DATABASE</text>
<input class="field__input" x-model="serviceFormSharedDatabase.name" placeholder="nome (a-z, 0-9, _)" x-t:placeholder />
<select class="form-select" x-model="serviceFormSharedDatabase.project">
<option value="" x-t>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)" x-t:placeholder />
<input class="field__input" x-model="serviceFormSharedDatabase.limit" placeholder="limite de conexões (opcional; não vale no Mongo)" x-t:placeholder />
<input class="field__input" x-model="serviceFormSharedDatabase.timeout" placeholder="timeout de query em ms (opcional; só Postgres)" x-t:placeholder />
<label class="check-row"><input type="checkbox" x-model="serviceFormSharedDatabase.overwrite" /> <span x-t>Sobrescrever a env var se o projeto já a tiver</span></label>
<label class="check-row"><input type="checkbox" x-model="serviceFormSharedDatabase.skipEnv" /> <span x-t>Não mexer em env var (vou migrar os dados antes)</span></label>
<row class="env__add">
<button class="btn btn--primary" type="button" @click="createSharedDatabase()" x-t>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" x-t>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 x-t>Copiar</text></button>
<button class="btn btn--dep-del btn-icon" type="button" :disabled="webhookBusy" @click="regenWebhook()">
<text x-t>Regenerar</text></button>
</row>
</template>
<text class="u-muted" x-show="!webhookUrl" x-t>
O token nasce no primeiro deploy deste serviço — rode um deploy para a URL aparecer.
</text>
<text class="u-muted" x-t>
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" x-t>ESTADO</text>
<text class="table__head-cell table__col-dur" x-t>DURAÇÃO</text>
<text class="table__head-cell table__col-start" x-t>INÍCIO</text>
<text class="table__head-cell table__col-act-2" x-t>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" x-t:title>↗</button>
<button class="btn btn--dep-logs" type="button" x-show="!deployment.terminal" @click="abortDeployment(deployment.id)" title="Cancelar" x-t:title>⏹</button>
<button class="btn btn--dep-del" type="button" x-show="deployment.terminal" @click="removeDeployment(deployment.id)" title="Remover" x-t:title>✕</button>
</row>
</row>
<hr class="rule" />
</column>
</template>
<text class="u-muted" x-fallback="deployments" style="padding:16px" x-t>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'" x-t>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()" x-t>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 + $t(' linhas · ao vivo')"></text>
<button class="btn btn--sm-ghost" type="button" @click="store.logWin = true" x-t>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" x-t>
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)" x-t:placeholder />
<text class="u-muted" style="margin-top:8px" x-text="$t('Run args atuais: ') + runArgsText"></text>
<row class="env__add" style="margin-top:14px">
<button class="btn btn--primary" type="button" @click="saveAdvanced()" x-t>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="$t('FILA DE PRÉ-DEPLOY (') + preDeployChecks.length + ')'"></text>
<text class="u-muted" x-t>
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" x-t>
Nenhum check configurado — deploy segue direto.
</text>
</column>
<text class="u-label-cap" style="margin-top:14px" x-t>ADICIONAR À FILA</text>
<row class="env__add">
<select class="field__input" x-model="advancedPreDeployCheckAddJobId">
<option value="" x-t>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()" x-t>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()" x-t>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" x-t:aria-label
@click="$store.app.dismissToast(toast.id)">×</button>
<span class="toast__bar"></span>
</row>
</template>
</column>
</div>
</body>
</html>