- CSS 67.9%
- Jinja 14.3%
- Python 13.8%
- Shell 2.8%
- Makefile 1.2%
- 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. |
||
|---|---|---|
| bin | ||
| templates | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| SPEC.md | ||
| VERSION | ||
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:
- YAML frontmatter required — list the keys per type (see
templates/<type>/example.md). - Sections in fixed order — copy from the example.md headings.
- Anti-patterns to avoid — e.g. for
audit: "never soften language; if vulnerable, say VULNERABLE not 'could be improved'". Forbrief: "no code blocks; the audience is non-technical". - 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.
Pattern C — auth-gated hub (recommended for internal use)
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 thedata-share-urlattribute 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)
- Frontmatter
type:in markdown (declarative — recommended). - CLI
--typeflag (explicit override). config.jsondefault_type(brief).- 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
researchtype (competitive analysis, benchmarks, sources)postmortemtype (incident timeline + RCA)statustype (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.