--!strict
-- format/jobs.luau — formata Job/JobSummary/JobRun (tarefas one-shot via
-- docker-compose) pra exibição: tela global "Schedules" (sidebar) e aba
-- "Jobs" do detalhe do projeto. Recorrência é estruturada (Interval/Daily/
-- Weekly, ou nil = só manual) — resumida em texto curto pra tabela.
local Time = require("time")
local Util = require("util")
local M = {}
local WEEKDAYS = { "seg", "ter", "qua", "qui", "sex", "sáb", "dom" }
-- Resumo textual de uma `Recurrence?` (serde externally-tagged: `nil` = só
-- manual; `{IntervalHours=6}` / `{Daily={hour=,minute=}}` / `{Weekly={...}}`).
function M.recurrence_label(recurrence: any): string
if recurrence == nil then
return i18n.tr("manual")
end
if recurrence.IntervalHours ~= nil then
return i18n.tr("a cada ") .. tostring(recurrence.IntervalHours) .. "h"
end
if recurrence.Daily ~= nil then
return string.format(
i18n.tr("diariamente às %02d:%02d"),
recurrence.Daily.hour,
recurrence.Daily.minute
)
end
if recurrence.Weekly ~= nil then
local weekday_name = i18n.tr(WEEKDAYS[(recurrence.Weekly.weekday % 7) + 1] or "?")
return string.format(
i18n.tr("semanalmente %s às %02d:%02d"),
weekday_name,
recurrence.Weekly.hour,
recurrence.Weekly.minute
)
end
return "—"
end
-- Rótulo/cor (mesmo esquema de StateCell) da última execução de um job.
-- `run == nil` (nunca rodou) / `run.success == nil` (rodando agora) /
-- `true`/`false` (ok/falhou).
function M.run_status(run: any): (string, string)
if run == nil then
return i18n.tr("nunca rodou"), "muted"
end
if run.success == nil then
return i18n.tr("rodando…"), "warn"
end
if run.success then
return "ok", "ok"
end
return i18n.tr("falhou"), "bad"
end
-- Aquecimento de um serviço com build (`ServiceWarmStatus`); `nil` = sem
-- build, sem selo. Mesmas cores do job.
function M.service_warm_status(warm: any): (string, string)
if warm == nil then
return "", "muted"
end
return M.warm_status(warm)
end
-- Rótulo/cor (mesmo esquema de StateCell) do aquecimento de um job. `warm` é o
-- `JobWarmStatus` do daemon (fonte única da verdade): aqui só se formata,
-- nunca se deduz. `nil` (daemon antigo, sem o campo) conta como frio.
function M.warm_status(warm: any): (string, string)
if warm ~= nil and warm.warming then
return i18n.tr("aquecendo…"), "warn"
end
if warm ~= nil and warm.warm then
return i18n.tr("quente"), "ok"
end
return i18n.tr("frio"), "muted"
end
-- Tela global "Schedules": uma linha por job, de todos os projetos.
-- `inflight` (opcional): `State.jobs_inflight` — job_id → true entre o click
-- em "Rodar agora" e a confirmação do servidor (ver handlers/jobs.luau::
-- job_run_now). Sem isso, `running` só reflete o snapshot já confirmado
-- pelo daemon, deixando o botão clicável durante todo o round-trip da RPC.
function M.summaries(
job_summaries: { any }?,
inflight: { [string]: boolean }?
): (string, number)
local column_budgets = Util.column_budgets()
local rows = {}
for _, job_summary in ipairs(job_summaries or {}) do
local job = job_summary.job
local status_label, status_kind = M.run_status(job_summary.last_run)
local warm = job_summary.warm
local warm_label, warm_kind = M.warm_status(warm)
table.insert(rows, {
id = job.id,
name = Util.ellipsis(job.name, column_budgets.svc),
name_full = job.name,
project = job_summary.project_name,
trigger_service = job_summary.trigger_service_name,
-- trigger_service_name é nil quando o job é autônomo (sem
-- serviço gatilho) — mostra só o projeto nesse caso.
owner = job_summary.trigger_service_name and Util.ellipsis(
tostring(job_summary.project_name)
.. " / "
.. tostring(job_summary.trigger_service_name),
column_budgets.proj
) or Util.ellipsis(
tostring(job_summary.project_name),
column_budgets.proj
),
recurrence = M.recurrence_label(job.recurrence),
enabled = job.enabled,
enabled_label = job.enabled and "Pausar" or "Ativar",
last_run_label = status_label,
last_run_kind = status_kind,
-- `running`: usado pra desativar/trocar o botão "Rodar agora"
-- enquanto o job_run em voo não termina. `status_kind == "warn"`
-- é o estado CONFIRMADO pelo servidor; `inflight[job.id]` cobre o
-- intervalo entre o click e essa confirmação chegar.
running = status_kind == "warn"
or (inflight ~= nil and inflight[job.id] == true),
last_run_id = job_summary.last_run and job_summary.last_run.id
or "",
next_run_at = job.next_run_at and Time.date_day_month_hour_minute(
job.next_run_at
) or "—",
is_warm = warm ~= nil and warm.warm == true,
is_warming = warm ~= nil and warm.warming == true,
warm_label = warm_label,
warm_kind = warm_kind,
warm_reason = warm ~= nil and warm.reason or "",
warmed_at_label = warm ~= nil
and warm.warmed_at
and Time.date_day_month_hour_minute(warm.warmed_at)
or "",
})
end
return Util.encode_array(rows), #rows
end
-- Fila de checks de pré-deploy do serviço (aba Advanced), na ORDEM
-- configurada, com o nome resolvido a partir dos Jobs do projeto — mesmo
-- idioma de retrocompat do `ServiceSpec::pre_deploy_checks` no lado Rust
-- (`Util.pre_deploy_checks`, que cai no `pre_deploy_job_id` legado quando a
-- fila nova está vazia). Um job apagado por fora (Schedules) mas ainda
-- referenciado aparece com rótulo "(job removido)" em vez de sumir — assim
-- dá pra removê-lo da fila pela própria UI.
function M.pre_deploy_queue(spec: any, jobs: { any }?): string
local by_id = {}
for _, job in ipairs(jobs or {}) do
by_id[job.id] = job.name
end
local rows = {}
for position, job_id in ipairs(Util.pre_deploy_checks(spec)) do
table.insert(rows, {
id = job_id,
name = by_id[job_id] or (i18n.tr("(job removido: ") .. job_id .. ")"),
index = tostring(position),
})
end
return Util.encode_array(rows)
end
-- Jobs do projeto que AINDA NÃO estão na fila — opções do `<select>` de
-- "adicionar à fila" (evita duplicar o mesmo job na sequência).
function M.pre_deploy_available(spec: any, jobs: { any }?): string
local in_queue = {}
for _, job_id in ipairs(Util.pre_deploy_checks(spec)) do
in_queue[job_id] = true
end
local rows = {}
for _, job in ipairs(jobs or {}) do
if not in_queue[job.id] then
table.insert(rows, { id = job.id, name = job.name })
end
end
return Util.encode_array(rows)
end
-- Histórico de execuções (`Command::JobRunHistory`) — usado pra listar runs e
-- abrir "Ver logs" (log_window apontando pro `job_run_id`).
function M.runs(runs: { any }?): string
local rows = {}
for _, run in ipairs(runs or {}) do
local label, kind = M.run_status(run)
table.insert(rows, {
id = run.id,
started_at = Time.date_day_month_hour_minute_second(
run.started_at
),
finished_at = run.finished_at
and Time.date_day_month_hour_minute_second(run.finished_at)
or "—",
exit_code = run.exit_code ~= nil and tostring(run.exit_code)
or "—",
status_label = label,
status_kind = kind,
})
end
return Util.encode_array(rows)
end
return M