Skip to content

Commit 56a617e

Browse files
feat: improve operational logs viewer
1 parent 021790b commit 56a617e

3 files changed

Lines changed: 21 additions & 9 deletions

File tree

‎cmd/stackhost/logs.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ func (h *deploymentAPI) logsRoute(w http.ResponseWriter, r *http.Request, applic
4040
return
4141
}
4242
tail := 100
43-
if value, parseErr := strconv.Atoi(r.URL.Query().Get("tail")); parseErr == nil && value > 0 {
43+
if raw := r.URL.Query().Get("tail"); raw != "" {
44+
value, parseErr := strconv.Atoi(raw)
45+
if parseErr != nil || (value != 100 && value != 500 && value != 1000) {
46+
jsonError(w, http.StatusBadRequest, "invalid_tail", "Escolha 100, 500 ou 1000 linhas.")
47+
return
48+
}
4449
tail = value
4550
}
4651
logs, err := reader.Logs(r.Context(), request, service, tail)

‎docs/features-boost-progress.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ O build Docker pós-correções passou com a imagem `stackhost:features-boost`.
5252
- `go test ./...`, `go vet ./...` e `go mod verify` passaram após alinhar as dependências Go.
5353
- O runtime agora expõe health real do Docker SDK (`healthy`, `starting`, `unhealthy`, `no_healthcheck`) e uma amostra de métricas locais Docker (`/api/v1/applications/:id/metrics`) com retenção de 24 horas e UI resumida. Swarm permanece explicitamente `unknown` para métricas não locais. Notificações têm configuração protegida, teste de webhook, UI e entrega assíncrona para eventos publicados. Aplicações podem ser duplicadas preservando a origem criptografada e iniciando como `not_deployed`.
5454
- Backup local do sistema entregue em `POST/GET /api/v1/backups/system`, com SQLite via `VACUUM INTO`, certificados persistidos em tar.gz, download e remoção protegidos por admin, controles em Configurações, retenção e scheduler interno manual/diário/semanal. Destinos S3 continuam pendentes.
55+
- Ajuste incremental da fase de logs: `tail` agora aceita somente 100, 500 ou 1000; o viewer ganhou busca local, auto-scroll, cópia, download, limpeza visual e indicador de conexão.
5556

5657
## Último commit validado
5758

‎web/src/components/applications/LogsPanel.vue‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { onMounted, onUnmounted, ref, watch } from "vue";
2+
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
33
import { api } from "../../composables/useApi";
44
import { useToast } from "../../composables/useToast";
55
@@ -11,6 +11,10 @@ const tail = ref(100);
1111
const logs = ref("");
1212
const loading = ref(false);
1313
const following = ref(false);
14+
const query = ref("");
15+
const autoScroll = ref(true);
16+
const connected = ref(false);
17+
const viewer = ref<HTMLElement>();
1418
let events: EventSource | undefined;
1519
1620
async function loadServices() {
@@ -23,23 +27,25 @@ async function loadServices() {
2327
async function loadLogs() {
2428
if (!service.value) return;
2529
loading.value = true;
26-
try { const result = await api<{ logs: string }>(`/api/v1/applications/${props.applicationId}/logs?service=${encodeURIComponent(service.value)}&tail=${tail.value}`); logs.value = result.logs || ""; }
30+
try { const result = await api<{ logs: string }>(`/api/v1/applications/${props.applicationId}/logs?service=${encodeURIComponent(service.value)}&tail=${tail.value}`); logs.value = result.logs || ""; await nextTick(); if (autoScroll.value && viewer.value) viewer.value.scrollTop = viewer.value.scrollHeight; }
2731
catch (error) { toast.error(error instanceof Error ? error.message : "Não foi possível carregar os logs."); }
2832
finally { loading.value = false; }
2933
}
30-
function stopFollowing() { following.value = false; events?.close(); events = undefined; }
34+
function stopFollowing() { following.value = false; connected.value = false; events?.close(); events = undefined; }
3135
function toggleFollowing() {
3236
if (following.value) { stopFollowing(); return; }
3337
if (!service.value) return;
3438
events = new EventSource(`/api/v1/applications/${props.applicationId}/logs/stream?service=${encodeURIComponent(service.value)}`);
3539
events.onmessage = (event) => {
36-
try { const payload = JSON.parse(event.data) as { line?: string }; if (payload.line !== undefined) logs.value += `${logs.value ? "\n" : ""}${payload.line}`; }
40+
try { const payload = JSON.parse(event.data) as { line?: string }; if (payload.line !== undefined) { logs.value += `${logs.value ? "\n" : ""}${payload.line}`; void nextTick(() => { if (autoScroll.value && viewer.value) viewer.value.scrollTop = viewer.value.scrollHeight; }); } }
3741
catch { /* ignore malformed frames */ }
3842
};
39-
events.onerror = stopFollowing;
43+
events.onopen = () => { connected.value = true; };
44+
events.onerror = () => { connected.value = false; };
4045
following.value = true;
4146
}
4247
async function copyLogs() { await navigator.clipboard?.writeText(logs.value); toast.info("Logs copiados."); }
48+
function downloadLogs() { const link = document.createElement("a"); link.href = URL.createObjectURL(new Blob([logs.value], { type: "text/plain" })); link.download = `${service.value || "logs"}.txt`; link.click(); URL.revokeObjectURL(link.href); }
4349
watch(() => props.applicationId, async () => { stopFollowing(); logs.value = ""; await loadServices(); await loadLogs(); });
4450
watch(service, () => void loadLogs());
4551
onMounted(async () => { await loadServices(); await loadLogs(); });
@@ -48,9 +54,9 @@ onUnmounted(stopFollowing);
4854

4955
<template>
5056
<section class="logs-panel">
51-
<div class="section-head"><div><p class="kicker">LOGS</p><h2>Saída operacional</h2><p class="muted">Acompanhe o serviço sem expor containers arbitrários.</p></div><div class="log-actions"><button class="ghost" type="button" :disabled="!logs" @click="copyLogs">Copiar</button><button class="secondary" type="button" :class="{ active: following }" @click="toggleFollowing">{{ following ? "Pausar" : "Acompanhar" }}</button></div></div>
52-
<div class="logs-toolbar"><label>Serviço<select v-model="service"><option v-for="item in services" :key="item" :value="item">{{ item }}</option></select></label><label>Linhas<select v-model.number="tail"><option :value="100">100</option><option :value="500">500</option><option :value="1000">1000</option></select></label><button class="ghost" type="button" @click="loadLogs">Atualizar</button></div>
53-
<pre class="logs-viewer" :aria-busy="loading">{{ logs || "Nenhum log disponível para este serviço." }}</pre>
57+
<div class="section-head"><div><p class="kicker">LOGS</p><h2>Saída operacional</h2><p class="muted">{{ connected ? "Conectado em tempo real" : "Histórico do serviço" }}</p></div><div class="log-actions"><button class="ghost" type="button" :disabled="!logs" @click="copyLogs">Copiar</button><button class="ghost" type="button" :disabled="!logs" @click="downloadLogs">Baixar</button><button class="ghost" type="button" :disabled="!logs" @click="logs = ''">Limpar</button><button class="secondary" type="button" :class="{ active: following }" @click="toggleFollowing">{{ following ? "Pausar" : "Acompanhar" }}</button></div></div>
58+
<div class="logs-toolbar"><label>Serviço<select v-model="service"><option v-for="item in services" :key="item" :value="item">{{ item }}</option></select></label><label>Linhas<select v-model.number="tail"><option :value="100">100</option><option :value="500">500</option><option :value="1000">1000</option></select></label><label>Buscar<input v-model="query" type="search" placeholder="Filtrar linhas" /></label><label class="checkbox"><input v-model="autoScroll" type="checkbox" /> Auto-scroll</label><button class="ghost" type="button" @click="loadLogs">Atualizar</button></div>
59+
<pre ref="viewer" class="logs-viewer" :aria-busy="loading">{{ (logs.split("\n").filter((line) => !query || line.toLowerCase().includes(query.toLowerCase())).join("\n")) || "Nenhum log disponível para este serviço." }}</pre>
5460
</section>
5561
</template>
5662

0 commit comments

Comments
 (0)