No description
  • CSS 67.9%
  • Jinja 14.3%
  • Python 13.8%
  • Shell 2.8%
  • Makefile 1.2%
Find a file
Mariusz Laszewski b1383d19b6 add: bin/publish-example.sh + SPEC author example sanitize
- Generic publish helper using env vars (no hardcoded hosts).
  Demonstrates --public token injection pattern that works with any
  reverse-proxied static file host (Caddy + Pangolin/Authelia/etc.).
- README section in 'Hosting & sharing' shows usage.
2026-05-15 09:13:53 +00:00
bin add: bin/publish-example.sh + SPEC author example sanitize 2026-05-15 09:13:53 +00:00
templates remove: v1 deployment scripts (contained internal infra references) 2026-05-15 09:13:15 +00:00
.gitignore initial: multi-template HTML report framework 2026-05-15 09:11:15 +00:00
CHANGELOG.md initial: multi-template HTML report framework 2026-05-15 09:11:15 +00:00
LICENSE initial: multi-template HTML report framework 2026-05-15 09:11:15 +00:00
Makefile initial: multi-template HTML report framework 2026-05-15 09:11:15 +00:00
README.md add: bin/publish-example.sh + SPEC author example sanitize 2026-05-15 09:13:53 +00:00
SPEC.md add: bin/publish-example.sh + SPEC author example sanitize 2026-05-15 09:13:53 +00:00
VERSION initial: multi-template HTML report framework 2026-05-15 09:11:15 +00:00

Report Framework

A small, opinionated system for generating beautiful HTML reports from Markdown — with four distinct visual personalities matched to four common deliverables: implementation plan, security audit, brainstorm (options + scoring), and executive brief.

Designed for LLM-assisted workflows. Drop in any local or hosted LLM (Ollama, llama.cpp, vLLM, LM Studio, Claude, GPT-4, …) — the LLM writes Markdown, this framework turns it into a polished, single-file HTML you can host on any static web server.

Why four templates instead of one? A sprint roadmap should not look like a CVSS audit, and an exec memo for the board should not look like a developer plan. Form follows function.


What you get

  • 4 type-specific templates (Jinja2) sharing one editorial design system (Fraunces + Inter Tight + JetBrains Mono).
  • Single CSS file with [data-theme="plan|audit|brainstorm|brief"] scoping — one ETag, one cache-bust, one stylesheet to ship.
  • Mobile-first — Pixel-grade responsive, 44×44 touch targets, A4 print-ready.
  • Lightbox — click any diagram or image to zoom; ESC / click-outside / × to close.
  • Mermaid diagrams — lazy-loaded from CDN only when used.
  • Type-specific components:
    • plan — sprint tables, ADR cards, acceptance checklists, cost/ROI grids.
    • audit — severity badges (critical/high/medium/low/info), finding cards, 5×5 risk matrix.
    • brainstorm — options grid, pros/cons cards, scoring widget, recommendation callout.
    • brief — KPI price cards, pullquotes, side-by-side trade-off grids.
  • Dark mode opt-in via <html data-mode="dark"> (light is canonical; auto-dark was disabled because the editorial accents are tuned for light surfaces).

Quick start

Prerequisites

  • Python 3.11+
  • jinja2, python-frontmatter, markdown
pip install jinja2 python-frontmatter markdown

Render your first report

git clone https://github.com/YOUR_ORG/report-framework.git
cd report-framework

# Renders the 4 example fixtures into out/*.html
make test-render

# Open them in a browser
open out/plan.html out/audit.html out/brainstorm.html out/brief.html

Render your own Markdown

Create body.md with YAML frontmatter:

---
type: plan
title: "Redis caching rollout"
summary: "4-sprint plan to introduce Redis for rate limiting and AI response cache."
author: Your Name
date: 2026-01-15
tags: [redis, infra, plan]
---

## 1. TL;DR

Deploy Upstash Redis in Sprint 0, wire rate-limit middleware in Sprint 1...

## 2. Sprint breakdown

| Sprint | Scope | Effort | Owner |
|--------|-------|--------|-------|
| 0 | Redis setup + health check | 2h | dev |
| 1 | Rate-limit middleware | 6h | dev |

Then render:

python3 bin/make_report.py \
  --type plan \
  --project myapp \
  --title "Redis caching rollout" \
  --summary "4-sprint plan..." \
  --tags "redis,infra,plan" \
  --content-file body.md \
  --out report.html

Open report.html in a browser. Done.


Type selection

When in doubt:

Trigger phrase Type
"implementation plan", "roadmap", "sprint plan" plan
"security audit", "compliance review", "OWASP" audit
"what are our options", "options + pros/cons" brainstorm
"executive brief", "1-pager for the board" brief

Each template's example.md is a realistic fixture — read it before writing your own.


Using with a local LLM

The framework is just a Jinja2 renderer with strong opinions about structure. Anything that emits Markdown can drive it. Common pairings:

Option 1 — Ollama (single host, simplest)

# Pull a capable instruct model (8B works; 30B+ better for technical writing)
ollama pull llama3.1:8b-instruct
# or: qwen2.5:14b, mistral-small, phi3.5, gemma2:9b, …

# Generate a plan-type report
PROMPT=$(cat <<'EOF'
You are writing an implementation plan for a SaaS feature. Output MUST be Markdown
with a YAML frontmatter. Required frontmatter keys: type (= "plan"), title, summary,
author, date (YYYY-MM-DD), tags (list).

Sections in this order:
1. TL;DR — 3-4 sentences (what, why, when, cost).
2. Decision / ADR — context, decision, alternatives rejected, consequences.
3. Architecture — mermaid diagram + component list.
4. Sprint breakdown — markdown table: sprint, scope, effort, owner, acceptance.
5. Cost — line items + total.
6. Risks & mitigations.
7. Definition of Done — checklist.

Feature to plan:
- Add Redis caching layer to a Next.js + Postgres SaaS
- Rate limit AI endpoints (cost protection)
- Cache dashboard aggregations
- Replace Vercel-timeout background jobs with a queue

Output the markdown only, no preamble.
EOF
)

ollama run llama3.1:8b-instruct "$PROMPT" > /tmp/plan-body.md

# Render
python3 bin/make_report.py \
  --type plan \
  --project myapp \
  --title "Redis rollout" \
  --summary "$(grep -m1 '^summary:' /tmp/plan-body.md | sed 's/summary: //')" \
  --tags "redis,plan" \
  --content-file /tmp/plan-body.md \
  --out /tmp/plan.html

Option 2 — llama.cpp HTTP server

# Start a local OpenAI-compatible server
./llama-server -m models/Qwen2.5-14B-Instruct.gguf -c 8192 --port 8080

# Generate via curl
curl -s http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen2.5-14b",
    "messages": [
      {"role": "system", "content": "You write technical reports in Markdown with YAML frontmatter. Follow user instructions exactly."},
      {"role": "user", "content": "Write an audit-type report for: SQL injection found in /api/search endpoint of a Next.js app. Include severity_summary, 2-3 finding cards, risk matrix mention, remediation steps. Use type: audit in frontmatter."}
    ]
  }' | jq -r '.choices[0].message.content' > /tmp/audit-body.md

python3 bin/make_report.py --type audit --project sec --title "API audit" \
  --summary "Pre-release security audit" --tags "owasp,audit" \
  --content-file /tmp/audit-body.md --out /tmp/audit.html

Option 3 — LM Studio / GPT4All / any OpenAI-compatible endpoint

Same pattern as llama.cpp — point your client at whatever local endpoint serves the OpenAI API. Frontmatter discipline matters: the LLM must emit a valid YAML block at top + Markdown body. A small system prompt fixes 90% of formatting drift.

Prompt templates per type

Bundle them with your generator:

prompts/
  plan.system.txt        # "You write implementation plans..."
  audit.system.txt       # "You write security audits..."
  brainstorm.system.txt  # "You explore options without picking a winner..."
  brief.system.txt       # "You write decision memos for non-technical execs..."

Each system prompt should enforce:

  1. YAML frontmatter required — list the keys per type (see templates/<type>/example.md).
  2. Sections in fixed order — copy from the example.md headings.
  3. Anti-patterns to avoid — e.g. for audit: "never soften language; if vulnerable, say VULNERABLE not 'could be improved'". For brief: "no code blocks; the audience is non-technical".
  4. HTML wrappers when needed — e.g. for plan, wrap the sprint table in <div class="sprint-table" markdown="1">...</div> to get the styled component.

Why local LLM is a good fit

  • Reports often contain internal data (architecture, costs, security findings) you do not want leaving your network.
  • Generation is bursty (a few reports per week, each ~2-10k tokens) — fits well on a single machine with a mid-size model.
  • The framework imposes structure, so even a 7-8B model produces readable output if the system prompt is solid.

Hosting & sharing (deployment patterns)

The renderer outputs standalone HTML — one file referencing one CSS via /_assets/style.css. Serve it any way you like:

Pattern A — local file (zero infra)

Open report.html in your browser. Send the file via Signal / Matrix / email. Done.

Pattern B — static web server

# nginx
location / { root /var/www/reports; index index.html; }

# Caddy
example.com {
  root * /var/www/reports
  file_server
}

Drop the rendered HTML in /var/www/reports/. Drop _shared/style.css in /var/www/reports/_assets/style.css.

This is the deployment the framework was forged in. Two ports, two visibility levels:

                  ┌─────────────────────────────┐
                  │  Reverse proxy w/ SSO       │  e.g. Pangolin / Authelia
External users ──▶│  (auth-gated path)          │       Cloudflare Access /
                  │  reports.example.com         │       oauth2-proxy / Caddy
                  └──────────────┬──────────────┘       forward_auth
                                 │
                                 ▼
                  ┌─────────────────────────────┐
                  │  Internal Caddy / nginx     │
                  │  :18080  authed reports     │  ──▶  /srv/reports/*.html
                  │  :18081  public share       │  ──▶  /srv/reports/_public/*.html
                  └─────────────────────────────┘
                                 ▲
                                 │  (no SSO — public token URL,
                                 │   only people with the link)
External users ─────────────────┘
                  share.example.com

How it works in practice (Pangolin example):

  • Main hub (reports.example.com) — Pangolin resource with SSO on. Only invited users (board members, leadership) can see the index of all reports. Behind that, an internal Caddy listens on port 18080 and serves the report directory.
  • Public share (share.example.com) — Pangolin resource with SSO off. Same Caddy instance, port 18081, serves only the _public/ subdirectory. URLs use an unguessable token (share.example.com/myapp-i27nq8zsb9l6.html), so only people you explicitly send the link to can open it. No login required.
  • When you render with --public, the bundled publisher script copies the report to _public/<token>.html, sed-injects that URL into the data-share-url attribute of the Share button, and the user copies the link with one click.

Pangolin (fosrl/pangolin) is open source — same pattern works with Authelia, Cloudflare Access, oauth2-proxy, Tailscale Funnel (for the public side), or any reverse proxy with forward-auth support.

Result: by default your reports are private unless you explicitly share the link. No accidental data leaks via "leaked Google Doc" or "wrong DM recipient".

Bundled example: bin/publish-example.sh

A minimal, infrastructure-agnostic publish script is included. It reads remote targets from environment variables (no hardcoded hosts) and demonstrates the --public token-injection pattern:

REPORTS_REMOTE=user@host:/srv/reports \
PUBLIC_DIR=user@host:/srv/reports/_public \
PUBLIC_URL_BASE=https://share.example.com \
  bin/publish-example.sh myapp /tmp/report.html --public

Adapt it to your stack (rsync, AWS CLI, gcloud storage, etc.).

Pattern D — object storage (S3 / R2 / GCS)

Render once, upload, share with a signed URL. Good for one-shot deliveries (e.g. send a brief to a customer).


Architecture

Inheritance

templates/_shared/base.html.j2          ← cover, TOC, prose grid, scripts
        ▲
        │ {% extends "_shared/base.html.j2" %}
        │
        ├── templates/plan/template.html.j2
        ├── templates/audit/template.html.j2
        ├── templates/brainstorm/template.html.j2
        └── templates/brief/template.html.j2

Each per-type template sets <html data-theme="X"> and may override blocks (hero, extra_head, extra_scripts).

Resolution order (type selection)

  1. Frontmatter type: in markdown (declarative — recommended).
  2. CLI --type flag (explicit override).
  3. config.json default_type (brief).
  4. Otherwise: ERROR.

File layout

report-framework/
├── README.md                # this file
├── LICENSE
├── VERSION                  # semver embedded in HTML <meta>
├── CHANGELOG.md
├── Makefile                 # test-render, lint, clean
├── SPEC.md                  # frontmatter spec + macros API
├── bin/
│   └── make_report.py       # the renderer
└── templates/
    ├── _shared/
    │   ├── base.html.j2
    │   ├── head.html.j2
    │   ├── share_btn.html.j2
    │   ├── scripts.html.j2
    │   ├── macros.j2
    │   └── style.css        # single file, all 4 themes
    ├── plan/        { template.html.j2, example.md }
    ├── audit/       { template.html.j2, example.md }
    ├── brainstorm/  { template.html.j2, example.md }
    └── brief/       { template.html.j2, example.md }

Common pitfalls (learned the hard way)

markdown="1" on <div> wrappers

Python-Markdown's md_in_html extension is enabled, but you still need the attribute to opt in:

<!-- WRONG — ### Context becomes literal text -->
<div class="adr-card">
### Context
</div>

<!-- RIGHT -->
<div class="adr-card" markdown="1">
### Context
</div>

Applies to all plan-specific wrappers (.adr-card, .sprint-table, .cost-roi-grid and its nested divs).

--public lives in two places

In the bundled publish-example.sh deployment script, --public is a flag that copies the file to a token-shareable location. If you've integrated something similar, the renderer needs its own --public flag too: that's what makes the Share button actually appear in the HTML ({% if public %} in share_btn.html.j2).

Hard refresh after CSS change

Browsers cache style.css aggressively. After redeploying, Ctrl+Shift+R (Linux/Windows) or Cmd+Shift+R (macOS).

Dark mode is opt-in, not auto

prefers-color-scheme: dark is intentionally disabled. The editorial accents (carmine, blueprint blue, severity traffic-light, brainstorm amber) are tuned for the cream-paper surface — auto-inverting to a dark background makes contrast worse, not better. Set <html data-mode="dark"> explicitly if you want it.


Versioning

Semver in VERSION:

  • MAJOR — breaking frontmatter spec, type removal.
  • MINOR — new type, new macro.
  • PATCH — CSS bugfix, typo.

Every render embeds <meta name="report-template-version" content="X.Y.Z"> for forensics and cache-busting.


Roadmap

  • research type (competitive analysis, benchmarks, sources)
  • postmortem type (incident timeline + RCA)
  • status type (sprint update, kanban-style)
  • ADR-only single-decision type
  • Print stylesheet polish for audit (multi-page risk matrix splitting)
  • Optional client-side search across published reports

License

MIT — see LICENSE.


Credits

Architecture: a hypothetical "solution architect" agent (Atlas). Content spec: a "business analyst" agent (Nika). Visual design: a "designer" agent (Pixel) — editorial tone inspired by Financial Times / Bloomberg analytical layouts. Implementation: a "senior full-stack" agent (Borys).

In practice: any LLM following the four agent roles can produce the four report types this framework is built to render. The agentic split is optional — what matters is that the structure per type stays disciplined.