Skip to content

Latest commit

Β 

History

646 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Acta Diurna

Acta Diurna

A self-hosted, AI-native reporting platform that serves reports as polished web documents

License Version Status SvelteKit Node PostgreSQL Docker


Acta Diurna replaces the slide deck for recurring reporting. A report is one declarative, versioned JSON document, rendered by a web server into something a PowerPoint file can never be: readable alone from an email link, presentable fullscreen in a meeting, and built once, refilled on every cycle.

The tool carries the skeleton - structure, templates, rendering, sharing, access control. Connected AI assistants build the content and data into it: an agent authors natively over MCP, or an author generates a draft outline-first, both through the same validated document model. Acta Diurna were the daily public gazettes of ancient Rome: the original recurring report.

Status: feature-complete, not yet production-hardened. Every capability below is implemented: the document model and renderer, the block catalogue, templates and data binding, the REST API and MCP server, outline-first AI generation, magic-link sharing, the SMTP-gated single / multi-author model with per-author tenancy, multi-audience reading and governance (audience levels, presenter view, access audit and retention, data freshness, theme selection), in-report drill-down (internal links to hidden detail pages), recurring report series with automatic issue-over-issue diffing (a workspace "what changed" view, numeric KPI deltas baked at publish, and an opt-in audience-aware reader change summary), and an in-browser WYSIWYG editor (block palette, per-block field editing, in-editor data binding, audience-aware live preview, autosave, and in-tab undo / redo) over the one validate-on-write service. The codebase has been through a full security and quality audit with no high-severity findings outstanding, and its multi-author and reader-verification flows run end to end in CI. The report-series and WYSIWYG-editor work is implemented and in review (not yet signed off). It is not yet production-ready - live-deploy hardening (reverse-proxy / TLS posture, an optional reader-session TTL) is still ahead. Two honest gaps: the Excel parser returns a "not enabled" stub (CSV / JSON are supported), and AI generation is tested against a mocked model in CI. See the Roadmap. The product brief lives in docs/brief.md.

Key Features

πŸ“œ One declarative document

  • A report is a structured, versioned JSON document (Zod schema, single source of truth): sections, blocks, theme, data bindings
  • The schema is published with examples (/api/v1/schema, static/schema/v1.json), designed so any LLM can produce a valid report in one shot
  • Every producer converges on one model: templates, the REST API, MCP agents, and AI generation all write through the same validate-on-write service - an invalid document is rejected with actionable RFC 9457 problem-details naming the offending block, never half-saved
  • Additive, version-aware evolution: a new block type does not break an existing document, and N/N-1 compatibility is tracked in a version registry

🎨 The reader render

  • Hybrid renderer - sections navigate as fullscreen slides, content scrolls within them, annexes stay out of the way; keyboard and touch navigation, table of contents, deep links
  • Audience levels - one report serves the whole room: a reader switches between summary / full / technical instantly, all levels server-rendered and toggled by CSS (near-zero added JS), with a matching author per-level preview. Tags are a reading-comfort filter, not a confidentiality boundary (the share is the boundary)
  • Presenter view - an author-only local presenter window runs a meeting from the same published document the readers received: current section, author-private speaker notes (stripped server-side before any reader sees the document), next-section preview, an elapsed timer, and a meeting mode that hides annexes from the flow
  • Internal links / drill-down detail pages - a finding in a table (or a matrix finding, or a phrase in prose) drills down to its own dedicated page: full evidence, reproduction, remediation, one click away. That detail page is part of the same document but hidden from the main flow and the table of contents - reachable only via the internal link, revealed by pure CSS (:target, near-zero added JS), with a "back to where you were" affordance. The linkTo reference is validated against the document (a dangling link is a validation error, never a dead click) and renders as an escaped in-page anchor, renderer-pure. "Reachable only via a link" is navigation, not a confidentiality boundary - the share is the boundary
  • Data freshness - a data-bound block shows an unobtrusive "Data as of " caption (the explicit data-as-of, else the injection time), baked onto the binding server-side so the renderer stays pure, and omitted rather than showing a misleading date
  • Server-side SVG charts (d3-scale / d3-shape) with zero hydration - the reader path ships static HTML/SVG, no client charting library
  • Theme selection - four built-in themes (Modern Gazette, Midnight, Cool Aurora, Warm Meridian) as complete design-token sets; an author picks one per report, an unknown reference falls back to the default with a workspace warning, self-hosted fonts under a strict Content-Security-Policy (no CDN, no third-party asset)
  • WCAG AAA report content (AA chrome floor, every built-in theme contrast-verified), gated by axe-core in CI; reader-path JS held under a 200 KB budget (currently ~75 KB), enforced on every build

🧱 Block catalogue

  • Core blocks: text, table, chart, KPI, image
  • Reporting primitives (generic, not domain-bound): comparison matrix (findings x sources with conditional formatting), set-membership / UpSet (coverage by source combination, derived from a matrix with zero re-entry), field grid, and legend
  • General report bricks: status badges and conditional table formatting (a scale value rendered as colour plus label), callouts (tinted info / success / warning / danger / neutral admonitions), code blocks (escaped, with an inline-code mark), a curated icon set (inline SVG, no icon font, no CDN), card grids (icon + title + description), structured lists and steps (native ordinals), timelines (milestones with per-milestone date and status), and a field-grid meta-strip header variant
  • Document-level categorical scales - severity and source colours/labels declared once and resolved at render by the matrix, legend, UpSet, status badges, and timelines, so a report stays legible even with zero authored colours
  • Every block is renderer-pure - escaped output only, no raw HTML, no $lib/server in the render path, zero hydration

πŸ€– AI-native authoring

  • MCP server at /api/mcp - Claude or any MCP-capable agent discovers the schema, skeletons, and reports, then authors natively (create / update / publish / unpublish / delete), pushes CSV / JSON data and auto-rebinds blocks (push_data_set), and runs outline-first generation (generate_outline / generate_report), at full parity with the REST API over the same service layer
  • Outline-first generation - the author (or an agent) states an intent, the model proposes an outline, the outline is reviewed and approved, then the model fills a schema-valid draft. Approval is bound to the exact outline by a content hash, so content is never generated from an unapproved structure. Driveable from the workspace, the REST API (POST /api/v1/reports/generate/outline + /fill), and MCP, all over the one two-stage generation service
  • Bring your own endpoint - point LLM_BASE_URL at any OpenAI-compatible base (the OpenAI API, a local Ollama / llama.cpp runtime, or an Anthropic-compatible proxy). No default cloud endpoint, no phone-home; the API key is redacted everywhere
  • Two explicit gates - the connector makes an outbound call only when the endpoint is configured AND AI_GENERATION_ENABLED=true. Untrusted model output is always validated on write and never executed

✏️ In-browser WYSIWYG editor

  • Edit a report visually in the workspace, against an authoritative live preview rendered by the SAME pure renderer the reader gets - what you edit is what ships, not a lookalike. Just another producer over the one validate-on-write service: an invalid edit is the same RFC 9457 problem-details at the failing block, and a published report loads read-only
  • Block palette + structural editing - add any block type or section, reorder and delete, all keyboard-accessible with managed focus; the palette is exhaustive over the block union by construction (a new block type is a compile error here, never a silent omission)
  • Per-block field editing for every block type - a table as a grid, a chart as series config, text as inline-formatted runs (bold / italic / inline-code / internal or external link, no contenteditable, no raw-HTML path), the reporting and rich blocks (comparison matrix, timeline, callout, card grid and the rest) as structured forms over their declared scales
  • Data binding from the editor - bind, rebind, and remap a table / chart / KPI to an uploaded data set at the block being edited, with the same green / amber / red diagnostics and closest-field remap the refill panel uses, reconciled against optimistic concurrency
  • Audience-aware live preview, tags, and speaker notes - switch the preview between summary / full / technical with the reader's own mechanism, set audience tags per section / block, and edit author-only speaker notes in place (stripped server-side, type-enforced never to reach a reader)
  • Autosave with an accessible status indicator (saving / saved / failed-retry), in-tab undo / redo (coalesced so a typing burst is one step, reseeded on every server baseline so undo can never resurrect pre-publish state), and optimistic-concurrency conflict handling that preserves in-memory edits. Workspace-only: the reader bundle ships zero editor code

πŸ”„ Report series & auto-diff

  • Recurring report series - starting the next issue links it to the previous one by an explicit lineage edge (a real, queryable predecessor chain, not a guess from titles or dates); a fresh report is its own one-issue series. Owner-scoped, a series never spans authors
  • Workspace "what changed since last issue" view - a readable changelog of the diff between an issue and its predecessor: sections and blocks added / removed / moved / kept, with data-changed and content-changed flags, from one pure isomorphic diff engine that matches strictly by stable id (no fabricated renames) and degrades to a neutral verdict on substantial drift
  • Numeric KPI deltas, baked at publish - a data-bound KPI shows its up / down / flat movement against the same figure in the previous issue (signed absolute and relative, a visually-hidden direction word so movement is never colour alone), computed server-side and baked onto the binding; the prior issue's raw data never ships to the reader, and a delta is omitted rather than shown misleading
  • Opt-in, audience-aware reader change summary - an author can optionally surface a "what changed" panel to readers, baked leak-safe at publish and filtered by the SAME audience CSS as the body, so a movement or section the reader's level hides is hidden in the summary too; off by default, omitted rather than misleading on a first issue or an unpublished predecessor

πŸ”Œ API & integration

  • REST API (/api/v1) - full report CRUD, publish lifecycle, duplicate, authenticated data push, and outline-first AI generation (propose an outline, then fill the approved outline into a draft), behaving identically to the workspace because every endpoint is a thin adapter over the same services
  • Personal access tokens - acta_pat_ bearer credentials, shown once, stored as a SHA-256 hash, revoke-only, managed from the workspace
  • OpenAPI 3.1 spec at /api/v1/openapi.json (public) with the document JSON Schema embedded as the single source of truth
  • Data ingestion - CSV / JSON file upload (streamed, capped) and authenticated API push; field inspection, binding to table / chart / KPI slots, automatic rebinding with drift diagnostics on the next refill

βœ‰οΈ Distribution & access

  • SMTP-gated single / multi-author - leave SMTP unconfigured and the instance runs single-author (one password author, unverified consultation-token shares); configure SMTP and it runs multi-author (email magic-link authors self-serving within AUTHOR_EMAIL_DOMAIN, password login disabled, verified reader sharing). The mode is resolved from the environment at boot, not a web-UI toggle
  • No reader accounts: passwordless magic links delivered over your own SMTP relay (an optional READER_EMAIL_DOMAINS allow-list restricts which destination domains may verify). Verification is prefetch-safe - the emailed link lands on a confirm interstitial and the single-use token is consumed only by the human's same-origin POST, so a mail-gateway link scanner that GET-prefetches the link cannot burn the token (which in multi-author mode would otherwise lock out every author)
  • Share hardening - high-entropy links (hashed at rest), optional expiry, restricted (per-recipient allow-list) or open mode, one-click revocation that cuts off live sessions immediately
  • Leak-free posture - revoked, expired, and unknown links serve one byte-for-byte identical neutral page; noindex and no-store on every reader route; no link-preview metadata; strict CSP with zero third-party assets
  • Access audit & retention - each verified access is recorded (one global reader identity per email, many access records), surfaced in an owner-scoped, cursor-paginated audit view with a "Load older" affordance (who read which report and when, filterable by report and reader, an author sees only their own; the trail is never silently cut off at a page boundary), with a configurable retention window (ACCESS_RECORD_RETENTION_DAYS, optional, unset = kept indefinitely) that auto-purges aged records for GDPR data minimization

πŸ”’ Security & privacy

  • Three strictly separated auth realms - author cookie, reader cookie, and API/MCP PAT bearer; a credential for one never authorizes another. In multi-author mode the author realm authenticates by email magic link in its own separate verification store
  • Per-author tenancy - one report = one author (owner). In multi-author mode every report, data set, share, and API token read and write is scoped to the authenticated author, so authors share an instance without seeing or touching each other's resources (closing the multi-author IDOR); single mode has one implicit owner and the predicate is a no-op
  • argon2id author password (single mode), SHA-256 hashing at rest for sessions, share, verification, and API tokens; secrets only in environment, never logged (pino redaction)
  • Rate limiting - per-IP token buckets with IP-independent global brakes on login, reader verification (with a per-share sub-brake), and API auth; per-session limits on AI generation and test-send
  • Enumeration-safety - neutral, timing-equivalent responses on the reader surface (NFR9); untrusted LLM output validated-on-write and never reaching a sink
  • Production env validation forces a https ORIGIN so session cookies are always Secure; a background sweep purges spent verification tokens and orphaned uploads

πŸ“¦ Deployment & operations

  • Docker image + docker-compose.yml (app + PostgreSQL); configure via environment, docker compose up
  • Boot migrations (Drizzle) with a bounded retry for the cold-start database race; /healthz endpoint; structured JSON logs to stdout
  • Trivy image scan in CI gating on fixable HIGH/CRITICAL CVEs; non-root container; tunable connection pool and purge cadence
  • Bring your own SMTP and (optionally) your own LLM endpoint - no vendor lock-in, no outbound call you did not configure

Quick Start

The target experience, three commands:

git clone https://github.com/Rwx-G/Acta-Diurna.git && cd Acta-Diurna
cp .env.example .env
docker compose up -d --build

Before exposing the instance, set the two CHANGE-ME values in .env and the author password hash:

# A session signing secret (>= 32 chars)
openssl rand -hex 32                  # paste into SESSION_SECRET

# A strong database password           # paste into POSTGRES_PASSWORD

# The author password, as an argon2id hash (never the password itself)
pnpm auth:hash -- 'your-strong-password'   # paste into AUTHOR_PASSWORD_HASH

The app listens on http://localhost:3000 by default. SMTP (for reader magic links) and the LLM endpoint (for AI generation) are optional and configured later from the same .env; the app boots without them and reports a misconfiguration only when the feature is used, never at startup. See docs/ops/deployment.md for the reverse-proxy contract and production hardening.

From source (development)

pnpm install
pnpm auth:hash -- 'dev-password'      # set AUTHOR_PASSWORD_HASH in .env
docker compose up -d db               # Postgres only
pnpm dev                              # app on http://localhost:5173

Useful scripts: pnpm test (Vitest), pnpm test:e2e (single-mode Playwright + ephemeral Postgres), pnpm test:e2e:multi (multi-author mode against an ephemeral Postgres + a Mailpit SMTP container, magic links intercepted over the Mailpit HTTP API), pnpm check (svelte-check), pnpm lint, pnpm reader:budget (reader JS budget gate).

Architecture

SvelteKit (Node 22, @sveltejs/adapter-node) over PostgreSQL via Drizzle ORM. One service layer is reached by four surfaces - the workspace (author cookie), the REST API and MCP server (PAT bearer), and the reader render (reader cookie) - so authoring behaves identically whoever drives it.

Module Path Role
schema src/lib/schema Zod document model, versioning, JSON Schema export, block catalogue
render src/lib/render Pure SSR renderer: slides + scroll, SVG charts, block components
ui src/lib/ui + workspace routes Authoring workspace and design system
ingestion src/lib/server/ingestion CSV / JSON parse, field inspection, binding, automatic rebind
sharing src/lib/server/sharing Share links, restricted / open modes, recipients, revocation
auth src/lib/server/auth Author / reader / PAT realms, sessions, rate limiting
mcp src/lib/server/mcp MCP server and tools (discovery + authoring)
ai src/lib/server/ai OpenAI-compatible connector and outline-first generation
mail src/lib/server/mail SMTP magic-link delivery
api src/routes/api REST /api/v1 endpoints and the /api/mcp transport
db src/lib/server/db Drizzle schema, boot migrations, connection pool

The renderer imports only a validated document and design tokens - never $lib/server, never raw HTML - so the reader bundle stays small and the report content cannot smuggle server state or script.

Configuration

Everything is configured from environment variables (see .env.example for the annotated full list). The essentials:

Variable Required Default Description
POSTGRES_PASSWORD yes - Database password (compose-managed)
SESSION_SECRET yes - Session signing secret, >= 32 chars (openssl rand -hex 32)
AUTHOR_PASSWORD_HASH yes - argon2id hash of the author password (pnpm auth:hash)
ORIGIN yes http://localhost:3000 Public URL readers use; must be https:// in production
PORT no 3000 Host port the app is published on
BODY_SIZE_LIMIT no 52428800 Max request body in bytes (50 MB, aligned with the upload cap)
DB_POOL_MAX no 10 Max PostgreSQL pool connections (1-100)
READER_SESSION_TTL no unset Reader session lifetime in days; unset = governed by the share
ACCESS_RECORD_RETENTION_DAYS no unset Access-audit retention in days (1-3650); unset = kept indefinitely
SMTP_* for sharing - Relay for magic links; its presence selects multi-author mode (all-or-nothing block)
AUTHOR_EMAIL_DOMAIN / INITIAL_OWNER_EMAIL for multi - Author sign-up domain and the owner that inherits existing reports (required when SMTP is set)
READER_EMAIL_DOMAINS no unset Reader destination allow-list (multi mode); unset = any verified reader
LLM_BASE_URL / LLM_MODEL for AI - OpenAI-compatible endpoint; no default, no phone-home
AI_GENERATION_ENABLED for AI false Second gate: the opt-in that lets the connector make a call

Authentication has two modes selected by the SMTP environment at boot: single (no SMTP - one password author) and multi (SMTP configured - email magic-link authors, password login disabled, per-author tenancy). See docs/ops/deployment.md for the env vars, fail-fast boot rules, the SMTP test, and the lockout/recovery procedure.

API & MCP

REST endpoints are served under /api/v1 and authenticate with an Authorization: Bearer <acta_pat_...> token (except the two public discovery endpoints). The MCP server shares the same PAT.

Method Path Auth Description
GET /api/v1/schema public Document version, JSON Schema, and example documents
GET /api/v1/openapi.json public OpenAPI 3.1 specification
GET /api/v1/whoami PAT Identify the calling token
GET POST /api/v1/reports PAT List reports / create a draft
GET PATCH DELETE /api/v1/reports/:id PAT Read / update / delete a report
POST /api/v1/reports/:id/publish PAT Publish (freeze a reader snapshot)
POST /api/v1/reports/:id/unpublish PAT Unpublish to keep editing
POST /api/v1/reports/:id/duplicate PAT Duplicate into a fresh draft
POST /api/v1/reports/generate/outline PAT Outline-first generation, stage 1: propose an outline + its approval hash (AI gated)
POST /api/v1/reports/generate/fill PAT Outline-first generation, stage 2: fill the approved outline into a draft (AI gated)
POST /api/v1/data-sets PAT Push CSV / JSON data onto a report and rebind
POST /api/mcp PAT MCP server (Streamable HTTP): discovery + authoring tools (incl. push_data_set for data and generate_outline / generate_report for AI generation)

Validation errors are RFC 9457 problem-details with the offending block path, field, and a fix hint - identical whether the write came from the workspace, the REST API, or an MCP agent.

Roadmap

Version Scope Status
v1 Document model + hybrid renderer, block catalogue, templates + data binding, file upload + API push, REST API, MCP server, outline-first AI generation, magic-link sharing with hardening, docker compose distribution Implemented
Phase 2 AI-native authoring (MCP + outline-first generation), the rich block catalogue (comparison matrix, UpSet, scales, callouts, code, lists, timelines and more), SMTP-gated identity & multi-author tenancy, and multi-audience reading and governance: audience levels (reader picks summary / full / technical), presenter view, access audit and retention, data freshness, theme selection Implemented
v2 Internal links + drill-down detail pages: a finding in a table drills down to its dedicated detail page, hidden from the main flow and the TOC, reachable only via the internal link, renderer-pure Implemented
v2 Report series + auto-diff: lineage chain, a workspace "what changed" view, numeric KPI deltas baked at publish, and an opt-in audience-aware reader change summary Implemented (in review)
v2 In-browser WYSIWYG editor: block palette, per-block field editing, in-editor data binding, audience-aware live preview, autosave, and in-tab undo / redo over the one validate-on-write service Implemented (in review)
Live deploy Live-deploy operational hardening: reverse-proxy / TLS posture, an optional reader-session TTL, the unencrypted-remote-DB note Next
Further Not yet drafted: viewer analytics, synced blocks, SQL connectors, multi-tenant spaces Planned

Documentation

  • docs/brief.md - product brief: vision, scope, kickoff decisions log
  • docs/ops/deployment.md - deployment hardening: reverse-proxy contract, ORIGIN, body size, secrets posture, pool sizing, authentication modes (single vs multi-author)
  • docs/ops/migrations.md - boot migration behavior, failure logs, and recovery runbook
  • CHANGELOG.md - notable changes per the Keep a Changelog format

License

Apache-2.0 - see LICENSE.

Author: Rwx-G

About

Self-hosted, AI-powered reporting platform. Reports are served as polished web documents - presentable in a meeting, readable from an email, adapted to each audience. A better alternative to PowerPoint for recurring reporting.

Topics

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages