diff --git a/publish/src/core/log.ts b/publish/src/core/log.ts new file mode 100644 index 0000000..6cc4201 --- /dev/null +++ b/publish/src/core/log.ts @@ -0,0 +1,106 @@ +export type RunMode = 'diff' | 'force-all' | 'post-single' + +export interface PostLog { + slug: string + status: 'success' | 'failed' | 'skipped-draft' + action?: 'new' | 'update' + event_id?: string + relays_ok?: string[] + relays_failed?: string[] + blossom_servers_ok?: string[] + images_uploaded?: number + duration_ms?: number + error?: string +} + +export interface RunLog { + run_id: string + started_at: string + ended_at: string + mode: RunMode + posts: PostLog[] + exit_code: number +} + +export interface SuccessArgs { + slug: string + action: 'new' | 'update' + eventId: string + relaysOk: string[] + relaysFailed: string[] + blossomServersOk: string[] + imagesUploaded: number + durationMs: number +} + +export interface FailedArgs { + slug: string + error: string + durationMs: number +} + +export interface LoggerOptions { + mode: RunMode + runId: string + print?: (line: string) => void + now?: () => Date +} + +export interface Logger { + postSuccess(args: SuccessArgs): void + postFailed(args: FailedArgs): void + postSkippedDraft(slug: string): void + finalize(exitCode: number): RunLog + writeJson(path: string, summary: RunLog): Promise +} + +export function createLogger(opts: LoggerOptions): Logger { + const print = opts.print ?? ((line: string) => console.log(line)) + const now = opts.now ?? (() => new Date()) + const posts: PostLog[] = [] + const startedAt = now().toISOString() + return { + postSuccess(a) { + posts.push({ + slug: a.slug, + status: 'success', + action: a.action, + event_id: a.eventId, + relays_ok: a.relaysOk, + relays_failed: a.relaysFailed, + blossom_servers_ok: a.blossomServersOk, + images_uploaded: a.imagesUploaded, + duration_ms: a.durationMs, + }) + print( + `✓ ${a.slug} (${a.action}) — relays:${a.relaysOk.length}ok/${a.relaysFailed.length}fail — ${a.durationMs}ms`, + ) + }, + postFailed(a) { + posts.push({ + slug: a.slug, + status: 'failed', + error: a.error, + duration_ms: a.durationMs, + }) + print(`✗ ${a.slug} — ${a.error}`) + }, + postSkippedDraft(slug) { + posts.push({ slug, status: 'skipped-draft' }) + print(`- ${slug} (draft, skipped)`) + }, + finalize(exitCode) { + return { + run_id: opts.runId, + started_at: startedAt, + ended_at: now().toISOString(), + mode: opts.mode, + posts, + exit_code: exitCode, + } + }, + writeJson(path, summary) { + return Deno.writeTextFile(path, JSON.stringify(summary, null, 2)) + }, + } +} diff --git a/publish/tests/log_test.ts b/publish/tests/log_test.ts new file mode 100644 index 0000000..dbc3868 --- /dev/null +++ b/publish/tests/log_test.ts @@ -0,0 +1,50 @@ +import { assertEquals } from '@std/assert' +import { createLogger } from '../src/core/log.ts' + +Deno.test('logger: sammelt post-einträge und schreibt summary', () => { + const sink: string[] = [] + const logger = createLogger({ + mode: 'force-all', + runId: 'run-1', + print: (line) => sink.push(line), + now: () => new Date('2026-04-16T10:00:00Z'), + }) + logger.postSuccess({ + slug: 's1', + action: 'new', + eventId: 'ev1', + relaysOk: ['wss://r1'], + relaysFailed: [], + blossomServersOk: [], + imagesUploaded: 0, + durationMs: 10, + }) + logger.postSkippedDraft('s2') + const summary = logger.finalize(0) + assertEquals(summary.run_id, 'run-1') + assertEquals(summary.mode, 'force-all') + assertEquals(summary.posts.length, 2) + assertEquals(summary.posts[0].status, 'success') + assertEquals(summary.posts[1].status, 'skipped-draft') + assertEquals(summary.exit_code, 0) + assertEquals(sink.some((s) => s.includes('s1')), true) +}) + +Deno.test('logger: writeJson schreibt datei', async () => { + const tmp = await Deno.makeTempDir() + try { + const logger = createLogger({ + mode: 'diff', + runId: 'run-2', + print: () => {}, + now: () => new Date('2026-04-16T10:00:00Z'), + }) + const summary = logger.finalize(0) + await logger.writeJson(`${tmp}/out.json`, summary) + const text = await Deno.readTextFile(`${tmp}/out.json`) + const parsed = JSON.parse(text) + assertEquals(parsed.run_id, 'run-2') + } finally { + await Deno.remove(tmp, { recursive: true }) + } +})