diff --git a/.env.example b/.env.example index 07a27ea..542d6b8 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,5 @@ OPS_AGENT_URL="http://127.0.0.1:3099" REPO_PATHS="/srv/scrum4me/repos/scrum4me,/srv/ops/repos/ops-dashboard" # Comma-separated list of systemd unit names to show on the /systemd page (must match commands.yml allowed list) SYSTEMD_UNITS="scrum4me-web,ops-agent" +# Worker run-logs directory inside the container (read-only bind mount; see docker-compose.yml) +WORKER_LOGS_DIR="/var/worker-logs/idea" diff --git a/app/api/worker-logs/[name]/route.ts b/app/api/worker-logs/[name]/route.ts new file mode 100644 index 0000000..c0b8f28 --- /dev/null +++ b/app/api/worker-logs/[name]/route.ts @@ -0,0 +1,32 @@ +import { NextRequest } from 'next/server' +import { getCurrentUser } from '@/lib/session' +import { readRunLog, WorkerLogError } from '@/lib/worker-logs' +import { parseRunLog } from '@/lib/parse-worker-log' + +export const dynamic = 'force-dynamic' + +// GET /api/worker-logs/.log — full parsed timeline for one run-log. +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ name: string }> }, +) { + const user = await getCurrentUser() + if (!user) { + return Response.json({ error: 'unauthorized' }, { status: 401 }) + } + + const { name: rawName } = await params + const name = decodeURIComponent(rawName) + + try { + const raw = await readRunLog(name) + return Response.json(parseRunLog(raw, name)) + } catch (err) { + if (err instanceof WorkerLogError) { + const status = err.code === 'invalid' ? 400 : err.code === 'not-found' ? 404 : 500 + return Response.json({ error: err.message }, { status }) + } + const message = err instanceof Error ? err.message : 'failed to read worker log' + return Response.json({ error: message }, { status: 500 }) + } +} diff --git a/app/api/worker-logs/route.ts b/app/api/worker-logs/route.ts new file mode 100644 index 0000000..96c85d3 --- /dev/null +++ b/app/api/worker-logs/route.ts @@ -0,0 +1,25 @@ +import { NextRequest } from 'next/server' +import { getCurrentUser } from '@/lib/session' +import { listRunLogs } from '@/lib/worker-logs' + +export const dynamic = 'force-dynamic' + +// GET /api/worker-logs?limit=10 — newest-first run-log summaries for the table. +export async function GET(request: NextRequest) { + const user = await getCurrentUser() + if (!user) { + return Response.json({ error: 'unauthorized' }, { status: 401 }) + } + + const limitParam = request.nextUrl.searchParams.get('limit') + const limit = limitParam ? Number(limitParam) : 10 + + try { + const logs = await listRunLogs(limit) + return Response.json({ logs }) + } catch (err) { + // Surfaces a missing bind mount legibly (e.g. WORKER_LOGS_DIR not mounted). + const message = err instanceof Error ? err.message : 'failed to list worker logs' + return Response.json({ error: message }, { status: 500 }) + } +} diff --git a/app/caddy/_components/caddy-codemirror.tsx b/app/caddy/_components/caddy-codemirror.tsx new file mode 100644 index 0000000..15b010c --- /dev/null +++ b/app/caddy/_components/caddy-codemirror.tsx @@ -0,0 +1,25 @@ +'use client' +import CodeMirror from '@uiw/react-codemirror' +import { caddyfileLanguage } from '@/lib/codemirror/caddyfile-mode' +import { EditorView } from '@codemirror/view' + +type Props = { + value: string + onChange: (next: string) => void + readOnly?: boolean +} + +export default function CaddyCodeMirror({ value, onChange, readOnly }: Props) { + return ( + + ) +} diff --git a/app/caddy/_components/caddy-editor.tsx b/app/caddy/_components/caddy-editor.tsx index 14a4427..6d5f0d1 100644 --- a/app/caddy/_components/caddy-editor.tsx +++ b/app/caddy/_components/caddy-editor.tsx @@ -1,11 +1,21 @@ 'use client' import { useCallback, useEffect, useState } from 'react' +import dynamic from 'next/dynamic' import Link from 'next/link' import { useFlowRun } from '@/hooks/useFlowRun' import ConfirmDialog from '@/components/ConfirmDialog' import StreamingTerminal from '@/components/StreamingTerminal' +const CaddyCodeMirror = dynamic(() => import('./caddy-codemirror'), { + ssr: false, + loading: () => ( +
+ Loading editor… +
+ ), +}) + type Phase = 'edit' | 'writing' | 'validating' | 'validated' | 'saving' | 'saved' type DialogPending = 'validate' | 'save' | null @@ -106,17 +116,13 @@ export default function CaddyEditor({ initialContent, initialError }: Props) { )} -