Rebrand the project as Tinyforge to reflect its evolution from a Docker container watcher into a self-hosted mini CI/deployment platform. Rename covers: Go module path, Docker labels, DB/config filenames, JWT issuer, Dockerfile binary, docker-compose, CI workflows, frontend i18n, README with static sites docs, and all code comments.
This commit is contained in:
@@ -25,6 +25,29 @@
|
||||
let logContainer: HTMLDivElement | undefined = $state();
|
||||
let eventSource: EventSource | null = null;
|
||||
|
||||
// Batch incoming SSE log lines to avoid per-line re-renders.
|
||||
let pendingLines: string[] = [];
|
||||
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function flushPendingLines() {
|
||||
flushTimer = null;
|
||||
if (pendingLines.length === 0) return;
|
||||
let updated = [...lines, ...pendingLines];
|
||||
pendingLines = [];
|
||||
if (updated.length > tailCount * 2) {
|
||||
updated = updated.slice(-tailCount);
|
||||
}
|
||||
lines = updated;
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function enqueueLine(line: string) {
|
||||
pendingLines.push(line);
|
||||
if (!flushTimer) {
|
||||
flushTimer = setTimeout(flushPendingLines, 150);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
loading = true;
|
||||
error = '';
|
||||
@@ -49,12 +72,7 @@
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.line) {
|
||||
lines = [...lines, data.line];
|
||||
// Trim to max lines.
|
||||
if (lines.length > tailCount * 2) {
|
||||
lines = lines.slice(-tailCount);
|
||||
}
|
||||
scrollToBottom();
|
||||
enqueueLine(data.line);
|
||||
}
|
||||
} catch { /* ignore parse errors */ }
|
||||
};
|
||||
@@ -69,6 +87,9 @@
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
// Flush any buffered lines before stopping.
|
||||
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
||||
flushPendingLines();
|
||||
following = false;
|
||||
}
|
||||
|
||||
@@ -90,7 +111,10 @@
|
||||
// Load on mount.
|
||||
$effect(() => { loadLogs(); });
|
||||
|
||||
onDestroy(() => { stopFollowing(); });
|
||||
onDestroy(() => {
|
||||
stopFollowing();
|
||||
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-page)] shadow-lg">
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
|
||||
$effect(() => {
|
||||
let cancelled = false;
|
||||
let inflight = false;
|
||||
|
||||
async function load() {
|
||||
if (inflight) return; // Skip if previous request still pending.
|
||||
inflight = true;
|
||||
try {
|
||||
const result = await api.fetchContainerStats(projectId, stageId, instanceId);
|
||||
if (!cancelled) {
|
||||
@@ -31,13 +34,15 @@
|
||||
if (!cancelled) {
|
||||
error = true;
|
||||
}
|
||||
} finally {
|
||||
inflight = false;
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
// Poll every 10 seconds.
|
||||
const interval = setInterval(load, 10_000);
|
||||
// Poll every 30 seconds (reduced from 10s to limit concurrent connections).
|
||||
const interval = setInterval(load, 30_000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Lucide-based SVG icon components for Docker Watcher.
|
||||
* Lucide-based SVG icon components for Tinyforge.
|
||||
* Task 2: Inline SVGs from Lucide icon set as Svelte components.
|
||||
*
|
||||
* Each icon is a standalone .svelte component accepting size and class props.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Docker Watcher",
|
||||
"name": "Tinyforge",
|
||||
"version": "v0.1"
|
||||
},
|
||||
"health": {
|
||||
@@ -508,7 +508,7 @@
|
||||
"password": "Password"
|
||||
},
|
||||
"login": {
|
||||
"title": "Docker Watcher",
|
||||
"title": "Tinyforge",
|
||||
"subtitle": "Sign in to your account",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
@@ -819,7 +819,7 @@
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS Records",
|
||||
"description": "View and manage DNS records created by Docker Watcher.",
|
||||
"description": "View and manage DNS records created by Tinyforge.",
|
||||
"wildcardActive": "Wildcard DNS Mode Active",
|
||||
"wildcardActiveDesc": "DNS records are managed externally via wildcard DNS. Disable wildcard DNS in Settings to manage records individually.",
|
||||
"refresh": "Refresh",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Docker Watcher",
|
||||
"name": "Tinyforge",
|
||||
"version": "v0.1"
|
||||
},
|
||||
"health": {
|
||||
@@ -508,7 +508,7 @@
|
||||
"password": "Пароль"
|
||||
},
|
||||
"login": {
|
||||
"title": "Docker Watcher",
|
||||
"title": "Tinyforge",
|
||||
"subtitle": "Войдите в свой аккаунт",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
@@ -819,7 +819,7 @@
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS-записи",
|
||||
"description": "Просмотр и управление DNS-записями, созданными Docker Watcher.",
|
||||
"description": "Просмотр и управление DNS-записями, созданными Tinyforge.",
|
||||
"wildcardActive": "Режим Wildcard DNS активен",
|
||||
"wildcardActiveDesc": "DNS-записи управляются внешне через wildcard DNS. Отключите wildcard DNS в настройках для индивидуального управления записями.",
|
||||
"refresh": "Обновить",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* SSE client helper with auto-reconnect and exponential backoff.
|
||||
*
|
||||
* Provides type-safe event handling for Docker Watcher's real-time
|
||||
* Provides type-safe event handling for Tinyforge's real-time
|
||||
* event streams (deploy logs and instance status changes).
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Simple pub/sub bus for SSE event_log payloads.
|
||||
*
|
||||
* The layout component publishes events from the single global SSE connection.
|
||||
* Pages (e.g. /events) subscribe without opening a duplicate SSE connection.
|
||||
*/
|
||||
|
||||
import type { EventLogSSEPayload } from '$lib/sse';
|
||||
|
||||
type Listener = (payload: EventLogSSEPayload) => void;
|
||||
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
export function subscribeEventLog(fn: Listener): () => void {
|
||||
listeners.add(fn);
|
||||
return () => { listeners.delete(fn); };
|
||||
}
|
||||
|
||||
export function publishEventLog(payload: EventLogSSEPayload): void {
|
||||
for (const fn of listeners) {
|
||||
fn(payload);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
import { fetchEventLog, fetchEventLogStats, clearAllEvents, deleteEvent } from '$lib/api';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import { connectGlobalEvents, type SSEConnection, type EventLogSSEPayload } from '$lib/sse';
|
||||
import { subscribeEventLog } from '$lib/stores/event-log-bus';
|
||||
import type { EventLogSSEPayload } from '$lib/sse';
|
||||
import type { EventLogEntry, EventLogStats } from '$lib/types';
|
||||
import EventLogEntryComponent from '$lib/components/EventLogEntry.svelte';
|
||||
import EventLogFilter from '$lib/components/EventLogFilter.svelte';
|
||||
@@ -35,7 +36,7 @@
|
||||
const PAGE_SIZE = 50;
|
||||
let offset = $state(0);
|
||||
|
||||
let sseConnection: SSEConnection | null = null;
|
||||
let unsubscribeEventLog: (() => void) | null = null;
|
||||
let listEl: HTMLDivElement | undefined = $state();
|
||||
let showClearConfirm = $state(false);
|
||||
|
||||
@@ -198,16 +199,15 @@
|
||||
loadEvents();
|
||||
loadStats();
|
||||
|
||||
sseConnection = connectGlobalEvents({
|
||||
onEventLog(payload) {
|
||||
handleSSEEvent(payload);
|
||||
}
|
||||
// Subscribe to event_log events from the global SSE connection (no duplicate connection).
|
||||
unsubscribeEventLog = subscribeEventLog((payload: EventLogSSEPayload) => {
|
||||
handleSSEEvent(payload);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
sseConnection?.close();
|
||||
sseConnection = null;
|
||||
unsubscribeEventLog?.();
|
||||
unsubscribeEventLog = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
let showDeleteConfirm = $state(false);
|
||||
|
||||
const projectId = $derived($page.params.id);
|
||||
const projectId = $derived($page.params.id!); // always present on [id] route
|
||||
|
||||
async function loadProject() {
|
||||
if (!project) loading = true;
|
||||
@@ -188,21 +188,22 @@
|
||||
}
|
||||
instancesByStage = mapped;
|
||||
|
||||
try {
|
||||
const allDeploys = await api.listDeploys(20);
|
||||
deploys = allDeploys.filter((d) => d.project_id === projectId);
|
||||
} catch {
|
||||
deploys = [];
|
||||
}
|
||||
// Fetch deploys, settings, and images in parallel (independent of each other).
|
||||
const [deploysResult, settingsResult, imagesResult] = await Promise.allSettled([
|
||||
api.listDeploys(20),
|
||||
api.getSettings(),
|
||||
api.listProjectImages(projectId)
|
||||
]);
|
||||
|
||||
try {
|
||||
const settings = await api.getSettings();
|
||||
settingsDomain = settings.domain ?? '';
|
||||
} catch { /* non-critical */ }
|
||||
|
||||
try {
|
||||
localImages = await api.listProjectImages(projectId);
|
||||
} catch { localImages = []; }
|
||||
deploys = deploysResult.status === 'fulfilled'
|
||||
? deploysResult.value.filter((d) => d.project_id === projectId)
|
||||
: [];
|
||||
settingsDomain = settingsResult.status === 'fulfilled'
|
||||
? (settingsResult.value.domain ?? '')
|
||||
: settingsDomain;
|
||||
localImages = imagesResult.status === 'fulfilled'
|
||||
? imagesResult.value
|
||||
: [];
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : $t('projectDetail.loadFailed');
|
||||
} finally {
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
<h3 class="text-base font-semibold text-[var(--text-primary)]">{$t('settingsAuth.oidcConfig')}</h3>
|
||||
<div class="mt-4 space-y-4">
|
||||
{#each [
|
||||
{ id: 'issuer', label: $t('settingsAuth.issuerUrl'), type: 'url', key: 'oidc_issuer_url', placeholder: 'https://auth.example.com/application/o/docker-watcher/' },
|
||||
{ id: 'issuer', label: $t('settingsAuth.issuerUrl'), type: 'url', key: 'oidc_issuer_url', placeholder: 'https://auth.example.com/application/o/tinyforge/' },
|
||||
{ id: 'client_id', label: $t('settingsAuth.clientId'), type: 'text', key: 'oidc_client_id', placeholder: '' },
|
||||
{ id: 'client_secret', label: $t('settingsAuth.clientSecret'), type: 'password', key: 'oidc_client_secret', placeholder: '' },
|
||||
{ id: 'redirect', label: $t('settingsAuth.redirectUrl'), type: 'url', key: 'oidc_redirect_url', placeholder: 'https://watcher.example.com/api/auth/oidc/callback' }
|
||||
|
||||
Reference in New Issue
Block a user