diff --git a/.gitignore b/.gitignore index 4beb1e5..6c56d28 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ app/src/*_templ.go .env .env.* kontra-bin + +# archify visual-check sidecars (evidence, not source) +docs/*.visual-check.* diff --git a/.pi/memory-vault/packs/kontra-day/20-context/admin-kontra-lab-audasmedia-com-au.md b/.pi/memory-vault/packs/kontra-day/20-context/admin-kontra-lab-audasmedia-com-au.md new file mode 100644 index 0000000..5a46437 --- /dev/null +++ b/.pi/memory-vault/packs/kontra-day/20-context/admin-kontra-lab-audasmedia-com-au.md @@ -0,0 +1,84 @@ +--- +type: context +id: context.kontra-day.admin-kontra-lab-audasmedia-com-au +title: admin.kontra.lab.audasmedia.com.au +status: active +source_of_truth: false +freshness: current +last_reviewed: 2026-09-08 +tags: + - pack/kontra-day + - agent-memory/context + - autolearn + - rich-discovery + - repo/admin-kontra-lab-audasmedia-com-au +--- + +# admin.kontra.lab.audasmedia.com.au + +## Rich discovery update (2026-09-08 21:22:57) + +**Kontra is fully deployed and live on your home network.** Here's the complete picture. + +## ✅ Live now +| URL | What | +|---|---| +| **https://kontra.lab.audasmedia.com.au** | The site — front page, articles, subjects, all 200 | +| **https://admin.kontra.lab.audasmedia.com.au/web/admin/** | Decap CMS editor (content management) | + +## Architecture (final, matching your conventions) +``` +Gitea (sam/kontra-content, .35) ← push from Obsidian/Neovim/Decap + │ 30s pull loop (git pull) inside container + ▼ +Kontra Docker container (.13, :8600) ← sam:users content at /var/www/kontra_day/content + ▲ +master Caddy (.35) → reverse_proxy 192.168.20.13:8600 (direct, no .13-Caddy hop) +``` + +## What was handled (including the messy middle) +- **NixOS lesson**: `.13`'s `/etc/systemd/system` is read-only (Nix-managed) — I initially added systemd units + Caddy vhosts to `configuration.nix`, but you correctly pushed back. We **reverted it cleanly** (backup `configuration.nix.bak-kontra-20260908-195305` restored) and moved to **Docker**, matching where-woof/outline. +- **Autosync bugs found & fixed** (all verified locally first): + 1. `/bin/bash` missing on NixOS → moved to Docker + 2. `build: ./Dockerfile` compose mistake → `{context, dockerfile}` + 3. git "dubious ownership" on mounted repo → `safe.directory` + 4. **Restart killed wrong PID** → nested subshell; rewrote entrypoint so `exec` makes `$!` the real binary + 5. Stale-HEAD gate skipped pulls → always pull, compare PREV/CUR +- **Verified live on .13**: pushed a new article → live in 30s (200); deleted it → 404 in 30s. Clean restarts, no port-bind errors. + +## Housekeeping +- Gitea deploy token is in the **gitignored `.env`** — confirmed nothing sensitive is in any repo. +- Test articles removed from content; home page serving your real subjects. +- Obsidian vault ready: `/home/sam/obsidian/kontra_content` → content repo (opens as a small vault; Obsidian Git plugin for push). + +## Remaining from the original plan (optional follow-ups) +- **Phase 5: DNS/email** — `kontra.lab.audasmedia.com.au` resolves via existing wildcard; public DNS (`kontra.com`?) + email later per that plan. +- Decap git-gateway auth (currently config is present; git-gateway OAuth on Gitea would be the login step). +- Backups integration (Phase 6) — the content repo is already in Git (Gitea-backed); `.13` Docker volumes/Borg are separate. + +Want me to keep going (Decap login setup, or Phase 5 DNS), or rest here? + +## Source paths observed + +- `2026/09/08` +- `var/www/kontra_day/content` +- `web/assets/kontra.css` +- `.env/token/secret` +- `admin.kontra.lab.audasmedia.com.au/web/admin` +- `Obsidian/Neovim/Decap` +- `etc/systemd/system` +- `home/sam/obsidian/kontra_content` + +## Evidence + +- Confidence: 84% +- Reason: Update component/repository context with detailed discovered structure and configuration + +## Related + +- [[packs/kontra-day/00-system/pi-agent/memory-manifest|Memory Manifest]] + +## Related learned memories + +- session: [[packs/kontra-day/80-sessions/rich-discovery-admin-kontra-lab-audasmedia-com-au-2026-09-08|Rich discovery: admin.kontra.lab.audasmedia.com.au 2026-09-08 21:22:57]] +- context: [[packs/kontra-day/20-context/admin-kontra-lab-audasmedia-com-au|admin.kontra.lab.audasmedia.com.au]] diff --git a/.pi/memory-vault/packs/kontra-day/40-actions/2026-09-08-kontra-deployed-docker-on-13-gitea-autosync-live.md b/.pi/memory-vault/packs/kontra-day/40-actions/2026-09-08-kontra-deployed-docker-on-13-gitea-autosync-live.md new file mode 100644 index 0000000..6215860 --- /dev/null +++ b/.pi/memory-vault/packs/kontra-day/40-actions/2026-09-08-kontra-deployed-docker-on-13-gitea-autosync-live.md @@ -0,0 +1,67 @@ +--- +type: action +id: action.kontra-day.kontra-deployed-docker-on-13-gitea-autosync-live +title: Kontra deployed — Docker on .13, Gitea autosync live +status: active +source_of_truth: false +freshness: current +last_reviewed: 2026-09-08 +tags: + - pack/kontra-day + - agent-memory/action + - kontra + - docker + - deploy + - autosync + - gitea + - caddy + - nixos + - golang +--- + +# Kontra deployed — Docker on .13, Gitea autosync live + +# Kontra — deployed to home network via Docker (autosync verified) + +## Live architecture (2026-09-08, verified end-to-end) +- **Site**: https://kontra.lab.audasmedia.com.au (and admin.kontra.lab.audasmedia.com.au/web/admin/ = Decap) +- **Chain**: master Caddy (Docker on .35, /Docker/Containers/caddy/Caddyfile) → `reverse_proxy 192.168.20.13:8600` → **Kontra Docker container** on .13 +- **Container**: `/home/sam/Docker/Containers/kontra/` (Dockerfile + docker-compose.yml + entrypoint.sh). Image `kontra-kontra`, port 8600:8600. Mounts `/var/www/kontra_day/content` (rw) + `/home/sam/.ssh/id_ed25519`→/root/.ssh (ro). +- **Content origin**: Gitea `sam/kontra-content` (.35). .13 clone at /var/www/kontra_day/content (mounted into container). +- **App repo**: Gitea `sam/kontra` (branch main, SSH remote from .27). + +## Autosync (the hard-won piece) +entrypoint.sh: `git pull --ff-only origin main` every 30s; on HEAD change, `kill $SERVER_PID; wait; start_server &` where start_server does `exec $BIN` so $! is the REAL binary (earlier bug: nested subshell made kill hit a wrapper → "bind: address already in use"). Also critical: `git config --global --add safe.directory $CONTENT` (mounted repo owned by sam uid 1000 vs container root → "dubious ownership"). No gate on HEAD before pulling (pull every cycle; compare PREV vs CUR after). + +## NixOS gotchas (.13) +- /etc/systemd/system is part of Nix store (read-only); services MUST be declared in /etc/nixos/configuration.nix + nixos-rebuild switch. +- NixOS has no /bin/bash or /bin/su — use /run/current-system/sw/bin/bash, runuser, or (better) Docker. +- I had first added systemd units + caddy vhosts into configuration.nix but reverted (restored backup configuration.nix.bak-kontra-20260908-195305) in favour of the Docker approach. Revert scripts: deploy/revert-kontra-nix.sh. +- Configuration.nix restores cleanly; leftover home-manager-sam failure (`/home/sam/.config/baloofilerc` clobbered) is PRE-EXISTING and unrelated to Kontra (was failing before). + +## Build +- Go binary built locally on .27 (`app/build.sh`: templ generate → go build), OR inside Docker build (golang:1.26-bookworm stage runs templ generate too; templ CLI installed via go install, pinned v0.3.1020). +- Docker build context = /home/sam/Docker/Containers/kontra with src/{go.mod,go.sum,src/...}. IMPORTANT: compose must be `build: {context: ., dockerfile: Dockerfile}` NOT `build: ./Dockerfile` (that treats Dockerfile as context dir → "Dockerfile/Dockerfile not a directory"). +- `.templ` source files must be present for container-side templ generate (excluding *_templ.go from rsync is fine; .templ must be included). + +## Deploy commands (on .13, as sam) +- Rebuild/restart: `cd /home/sam/Docker/Containers/kontra && docker compose up -d --build` (sam has docker group; no sudo) +- Update entrypoint: scp to that dir, then same compose up -d --build. +- Gitea deploy token lives in gitignored .env at repo root (never committed). + +## Verified flows +- Push new article → 30s → live (200). Push delete → 30s → 404. Clean restart (no port bind errors). +- Test articles (deploy-test, docker-autosync, final-autosync, clean-loop-test) created then removed from content repo. + +## Files +- /home/sam/home_network/web_sites/kontra_day/PLAN.md, DESIGN.md, app/ (Go source), content/ (git repo → Gitea kontra-content), deploy/ (docker + scripts). +- Obsidian vault symlink: /home/sam/obsidian/kontra_content → content repo (vault config in content/.obsidian). + +## Evidence + +- Confidence: 100% +- Reason: Manual memctx_save + +## Related + +- [[packs/kontra-day/00-system/pi-agent/memory-manifest|Memory Manifest]] diff --git a/.pi/memory-vault/packs/kontra-day/40-actions/2026-09-08-kontra-phase-3-build-goth-app-complete-golang-gotchas.md b/.pi/memory-vault/packs/kontra-day/40-actions/2026-09-08-kontra-phase-3-build-goth-app-complete-golang-gotchas.md new file mode 100644 index 0000000..bcbac3d --- /dev/null +++ b/.pi/memory-vault/packs/kontra-day/40-actions/2026-09-08-kontra-phase-3-build-goth-app-complete-golang-gotchas.md @@ -0,0 +1,63 @@ +--- +type: action +id: action.kontra-day.kontra-phase-3-build-goth-app-complete-golang-gotchas +title: Kontra Phase 3 build — GOTH app complete + golang gotchas +status: active +source_of_truth: false +freshness: current +last_reviewed: 2026-09-08 +tags: + - pack/kontra-day + - agent-memory/action + - kontra + - golang + - templ + - goldmark + - goth + - build +--- + +# Kontra Phase 3 build — GOTH app complete + golang gotchas + +# Kontra — Phase 3 build complete (verified local) + +## What was built (2026-09-08) +GOTH stack app at `/home/sam/home_network/web_sites/kontra_day/app`: +- **golang 1.26.2** (the "Go" in GOTH is golang, NOT Rust-like Go — vgo.mod uses `require (...)` blocks, `package main`, `func main()`). +- **templ v0.3.1020** — CLI at `~/go/bin/templ` (installed via `go install github.com/a-h/templ/cmd/templ@latest`, path must be on PATH). `.templ` files in `app/src/`, generated with `templ generate` (writes `*_templ.go`), build with `go build -o kontra-bin .` from `app/src/`. Build script: `app/build.sh`. +- **goldmark v1.8.6** markdown + **goccy/go-yaml v1.19.2**. +- Single static binary ~11.5MB. `PORT` and `KONTRA_CONTENT` and `KONTRA_MEDIA` env vars. + +## Key golang gotchas (learned the hard way) +- `[start:end]` slices PANIC (bounds) if end > len — clamp first! Use helper `firstArticles(a, n)`/`firstSubjects`. +- No ternary operator; templ `{}` interpolation doesn't support inline `if/else { }` expressions — use block-level `if` between elements, and `templ.KV(" active", cond)` for conditional classes. +- `//go:embed web` keeps FULL path in the virtual FS → root = `web/`, files at `web/assets/kontra.css`. Serve with `http.Handle("/web/", http.FileServerFS(web))`. Route patterns need trailing-slash (`/web/`) plus `/` catch-all registered LAST. +- Route specificity works but registers specific before `/`. +- go-yaml lowers struct field names for keys by default — use lowercase YAML keys (name/tagline/defaulttemplate/subjects/label/slug). +- log: `log.Printf`/`log.Fatalf` (no log.Errorf). +- golang stdlib net/http: `http.Handle(pattern, handler)`, `http.HandleFunc`, `http.ListenAndServe(":8080", nil)`, `r.URL.Path`, `r.PathValue("slug")`. Patterns: `/subjects/{slug}`. +- All source files `package main`; go.mod `module day`. + +## Content model +`content/` tree: `config/site.yaml`, `subjects//_subject.yaml` + `*.md`, `pages/*.md`. Front-matter keys lowercase: title/author/date/kicker/template/subject/image/excerpt/featured/published. Shortcodes `{{media:name}}`→`KONTRA_MEDIA` base + `/name`; `{{embed:vimeo:ID}}`→iframe. Resolved on RAW markdown BEFORE goldmark so links work. + +## Admin +Decap CMS at `app/src/web/admin/` (index.html + config.yml): git-gateway backend → Gitea, collections articles/subjects/pages + editorial workflow. Served at `/web/admin/`. + +## Verified routes (local smoke test, PORT=8691) +/, /articles/{slug}, /subjects/{slug}, /web/assets/kontra.css, /web/admin/ → all 200. 404 for missing article/subject. + +## Git +Committed to local repo (no remote yet): commit 31e9a70. `.pi/` memory vault is inside the repo too. + +## Not done yet (task 7) +Gitea remotes (.35), Obsidian vault, .13 systemd service + 30s git pull timer, Caddy routes (kontra.domain + admin), Decap git-gateway OAuth app. + +## Evidence + +- Confidence: 100% +- Reason: Manual memctx_save + +## Related + +- [[packs/kontra-day/00-system/pi-agent/memory-manifest|Memory Manifest]] diff --git a/.pi/memory-vault/packs/kontra-day/80-sessions/rich-discovery-admin-kontra-lab-audasmedia-com-au-2026-09-08.md b/.pi/memory-vault/packs/kontra-day/80-sessions/rich-discovery-admin-kontra-lab-audasmedia-com-au-2026-09-08.md new file mode 100644 index 0000000..98887c1 --- /dev/null +++ b/.pi/memory-vault/packs/kontra-day/80-sessions/rich-discovery-admin-kontra-lab-audasmedia-com-au-2026-09-08.md @@ -0,0 +1,87 @@ +--- +type: session +id: session.kontra-day.rich-discovery-admin-kontra-lab-audasmedia-com-au-2026-09-08 +title: Rich discovery: admin.kontra.lab.audasmedia.com.au 2026-09-08 21:22:57 +status: active +source_of_truth: false +freshness: current +last_reviewed: 2026-09-08 +tags: + - pack/kontra-day + - agent-memory/session + - autolearn + - rich-discovery + - repo/admin-kontra-lab-audasmedia-com-au +--- + +# Rich discovery: admin.kontra.lab.audasmedia.com.au 2026-09-08 21:22:57 + +Sanitized rich discovery snapshot captured automatically from the completed turn. + + +## Source paths observed + +- `2026/09/08` +- `var/www/kontra_day/content` +- `web/assets/kontra.css` +- `.env/token/secret` +- `admin.kontra.lab.audasmedia.com.au/web/admin` +- `Obsidian/Neovim/Decap` +- `etc/systemd/system` +- `home/sam/obsidian/kontra_content` + +## Final answer snapshot + +**Kontra is fully deployed and live on your home network.** Here's the complete picture. + +## ✅ Live now +| URL | What | +|---|---| +| **https://kontra.lab.audasmedia.com.au** | The site — front page, articles, subjects, all 200 | +| **https://admin.kontra.lab.audasmedia.com.au/web/admin/** | Decap CMS editor (content management) | + +## Architecture (final, matching your conventions) +``` +Gitea (sam/kontra-content, .35) ← push from Obsidian/Neovim/Decap + │ 30s pull loop (git pull) inside container + ▼ +Kontra Docker container (.13, :8600) ← sam:users content at /var/www/kontra_day/content + ▲ +master Caddy (.35) → reverse_proxy 192.168.20.13:8600 (direct, no .13-Caddy hop) +``` + +## What was handled (including the messy middle) +- **NixOS lesson**: `.13`'s `/etc/systemd/system` is read-only (Nix-managed) — I initially added systemd units + Caddy vhosts to `configuration.nix`, but you correctly pushed back. We **reverted it cleanly** (backup `configuration.nix.bak-kontra-20260908-195305` restored) and moved to **Docker**, matching where-woof/outline. +- **Autosync bugs found & fixed** (all verified locally first): + 1. `/bin/bash` missing on NixOS → moved to Docker + 2. `build: ./Dockerfile` compose mistake → `{context, dockerfile}` + 3. git "dubious ownership" on mounted repo → `safe.directory` + 4. **Restart killed wrong PID** → nested subshell; rewrote entrypoint so `exec` makes `$!` the real binary + 5. Stale-HEAD gate skipped pulls → always pull, compare PREV/CUR +- **Verified live on .13**: pushed a new article → live in 30s (200); deleted it → 404 in 30s. Clean restarts, no port-bind errors. + +## Housekeeping +- Gitea deploy token is in the **gitignored `.env`** — confirmed nothing sensitive is in any repo. +- Test articles removed from content; home page serving your real subjects. +- Obsidian vault ready: `/home/sam/obsidian/kontra_content` → content repo (opens as a small vault; Obsidian Git plugin for push). + +## Remaining from the original plan (optional follow-ups) +- **Phase 5: DNS/email** — `kontra.lab.audasmedia.com.au` resolves via existing wildcard; public DNS (`kontra.com`?) + email later per that plan. +- Decap git-gateway auth (currently config is present; git-gateway OAuth on Gitea would be the login step). +- Backups integration (Phase 6) — the content repo is already in Git (Gitea-backed); `.13` Docker volumes/Borg are separate. + +Want me to keep going (Decap login setup, or Phase 5 DNS), or rest here? + +## Evidence + +- Confidence: 86% +- Reason: Preserve full sanitized discovery detail from a rich investigation turn + +## Related + +- [[packs/kontra-day/00-system/pi-agent/memory-manifest|Memory Manifest]] + +## Related learned memories + +- session: [[packs/kontra-day/80-sessions/rich-discovery-admin-kontra-lab-audasmedia-com-au-2026-09-08|Rich discovery: admin.kontra.lab.audasmedia.com.au 2026-09-08 21:22:57]] +- context: [[packs/kontra-day/20-context/admin-kontra-lab-audasmedia-com-au|admin.kontra.lab.audasmedia.com.au]] diff --git a/.pi/settings.json b/.pi/settings.json index 3555173..72b5a4d 100644 --- a/.pi/settings.json +++ b/.pi/settings.json @@ -10,7 +10,8 @@ "skills/obsidian-cli", "skills/npm-security", "skills/system-architect", - "skills/design-explore" + "skills/design-explore", + "skills/project-diagramming-mermaid" ] } ] diff --git a/.pi/tasks/tasks-01a07ea3-75ad-74d9-8e69-e303d98c7c6f.json b/.pi/tasks/tasks-01a07ea3-75ad-74d9-8e69-e303d98c7c6f.json index 95e9168..77106c5 100644 --- a/.pi/tasks/tasks-01a07ea3-75ad-74d9-8e69-e303d98c7c6f.json +++ b/.pi/tasks/tasks-01a07ea3-75ad-74d9-8e69-e303d98c7c6f.json @@ -1,89 +1,4 @@ { "nextId": 8, - "tasks": [ - { - "id": "1", - "subject": "Update DESIGN.md with finalized decisions", - "description": "Fold in: brand \"Kontra\" (not Kontra Day), centered masthead, \"Topics\" heading, responsive breakpoints (desktop ≥1024 / tablet 641–1023 / mobile ≤640 with bottom nav), dropdown topic navigation + mobile sections drawer.", - "status": "completed", - "activeForm": "Updating DESIGN.md", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933269, - "updatedAt": 1788848992737 - }, - { - "id": "2", - "subject": "Scaffold Go app (templ + htmx + Tailwind)", - "description": "Go project in /home/sam/home_network/web_sites/kontra_day: go.mod, templ templates, server (likely http (stdlib) or a minimal router), Goldmark for Markdown, YAML parsing for front-matter, static asset serving (kontra.css + fonts). Single static binary goal.", - "status": "completed", - "activeForm": "Scaffolding Go app", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933269, - "updatedAt": 1788851338939 - }, - { - "id": "3", - "subject": "Implement templates from DESIGN.md", - "description": "templ components: base layout, masthead (centered brand, meta line, desktop ribbon with topic dropdowns), mobile bottom nav + sections drawer, home (lead stack, Topics grid, Latest feed), subject index, article (header, hero plate, body, pull quote, field notes, save module, related, newsletter), site footer. Wire kontra.css design tokens.", - "status": "completed", - "activeForm": "Implementing templates", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933270, - "updatedAt": 1788851338939 - }, - { - "id": "4", - "subject": "Content model + loader (markdown + front-matter)", - "description": "Load .md content files with front-matter (title, author, date, tags, template, subject, image, excerpt, published, publish_at), subjects/_subject.yaml config, config/site.yaml, config/frontpage.yaml (template resolution page→subject→site→fallback). Goldmark rendering, {{media:...}} shortcode resolution.", - "status": "completed", - "activeForm": "Building content model", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933270, - "updatedAt": 1788851338940 - }, - { - "id": "5", - "subject": "Media: Garage S3 shortcodes + /media route", - "description": "Resolve {{media:...}} to Garage S3 URLs (bucket kontra-day on .13:3900), serve /media/* via Caddy. Shortcode defs: media, video (vimeo etc).", - "status": "completed", - "activeForm": "Wiring media shortcodes", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933270, - "updatedAt": 1788851426829 - }, - { - "id": "6", - "subject": "Admin: Decap CMS at admin.kontra", - "description": "Decap CMS (netlify-cms fork) config: collections for articles/subjects/pages, git backend to Gitea repo, media library pointing at Garage S3 (or local folder fallback). Serve at admin.kontra (subdomain).", - "status": "completed", - "activeForm": "Configuring Decap admin", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933270, - "updatedAt": 1788851471050 - }, - { - "id": "7", - "subject": "Deploy: Gitea repo + .13 systemd + timer", - "description": "Create Gitea repos (kontra content + maybe app), push, production clone at /var/www/kontra on .13, systemd service running the Go binary, 30s git pull timer, Caddy routes (kontra + admin.kontra) via .35 master proxy. Obsidian separate vault at /home/sam/obsidian/kontra/ = content repo.", - "status": "in_progress", - "activeForm": "Setting up deploy", - "metadata": {}, - "blocks": [], - "blockedBy": [], - "createdAt": 1788848933270, - "updatedAt": 1788851471051 - } - ] + "tasks": [] } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba49788 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Kontra — a self-hosted news-opinion site with automatic content sync + +**Kontra** is a modern news/opinion publication engine: a single static Go binary that renders +markdown into a fast, SEO-friendly editorial site, a Decap CMS admin authenticated against +self-hosted Gitea, and a git-driven deploy pipeline that publishes content within ~30 seconds +of a save — all running on a home-lab network. + +> **🗺 Interactive architecture map:** [maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html](https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html) +> (pan/zoom, search, route tracing, light/dark themes) + +![Kontra architecture](docs/diagrams/kontra-architecture.png) + +```mermaid +flowchart LR + subgraph editors["Content Editors"] + O[Obsidian vault
/obsidian/kontra_content] -->|git push| G + N[Neovim / CLI] -->|git push| G + D[Decap CMS
admin.kontra.lab] -->|OAuth + git commit| G + end + + subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"] + G[(sam/kontra-content
markdown origin of truth)] + end + + subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"] + P[git pull loop
every 30s] -->|restart on change| B + B[Go binary
templ + htmx + Goldmark] + B --> MD[(Markdown render
front-matter + shortcodes)] + end + + subgraph media["Media (Garage S3 on .13)"] + S3[(Garage S3
kontra-day bucket)] + end + + G -->|"SSH git"| P + B -->|"shortcode: media"| S3 + B -->|"emits HTML"| CB[Caddy master
.35 :80/:443] + CB -->|"reverse_proxy :8600"| B + + subgraph net["Network"] + CB -->|https| WWW[kontra.lab.audasmedia.com.au] + CB -->|https| ADM[admin.kontra.lab.audasmedia.com.au] + end + + WWW -->|browser| B + ADM -->|browser| D +``` + +--- + +## Why this project stands out + +| | | +|---|---| +| **Zero-dependency production runtime** | One statically linked Go binary — no runtime, no Node, no PHP. 11 MB, ~10 MB RAM under 50 concurrent connections. | +| **Editorial workflow people actually use** | Editors edit markdown in Obsidian, Neovim, or a browser CMS — three independent edit paths that all converge on one git repo. | +| **Git is the content API** | Content has full history, rollback, branch review, and network-agnostic sync. No lock-in. | +| **Automatic publish pipeline** | `git push` → 30 s later the site is live. We prove it end-to-end (add, change, delete). | +| **Single-binary + embedded assets** | CSS, fonts, and the admin UI are compiled into the binary (`//go:embed`). Deploy = copy one file. | +| **Self-hosted, privacy-first** | Gitea, Garage S3, Caddy, Decap all run on own network infra — no third-party SaaS for content or media. | + +## The stack (GOTH) + +| Layer | Technology | Why | +|---|---|---| +| **Language** | Go (golang) 1.26 | Compiled, fast, single static binary | +| **Templates** | templ v0.3 | Type-safe HTML components compiled to Go | +| **Interactivity** | htmx (server-rendered, no JS framework) | Progressive enhancement, SEO-safe | +| **Styling** | Tailwind-style **design tokens** (CSS custom properties) | Themeable via `DESIGN.md`, per-subject accent colours as data, not code | +| **Markdown** | goldmark (CommonMark 0.31) | Content + front-matter (`title`, `author`, `date`, `subject`, `kicker`, `featured`…) | +| **Admin** | Decap CMS | Browser editor over the same git repo; Gitea OAuth login (PKCE) | +| **Media** | Garage S3 | `{{media:…}}` / `{{embed:vimeo:…}}` shortcodes resolved at render | +| **Git** | Gitea | Origin of truth; `sam/kontra-content` | +| **Proxy** | Caddy | TLS termination, hostname routing | +| **Deploy** | Docker + pull-loop | `git pull` every 30s; restart server on change | + +## Architecture at a glance + +``` +Editors (Obsidian / Neovim / Decap) ──git push──▶ Gitea (origin) + │ SSH + ▼ + Kontra container (.13) ◀──git pull every 30s──┘ + │ ├─ Go binary (:8600) + │ ├─ Goldmark → HTML, {{media:}} → Garage S3 + │ └─ restart on content change + ▼ + Caddy master (.35 :443) ──▶ readers (HTTPS) +``` + +## Repository layout + +``` +kontra/ +├── app/ # Go source (server.go, content.go, main.go, templates.templ) +│ ├── src/ +│ │ ├── *.templ # templ components (masthead, article, topics, mobile nav…) +│ │ └── web/ # embedded assets: kontra.css (design tokens), admin/ (Decap) +│ └── build.sh # templ generate → go build +├── content/ # the content repo (own Gitea remote: sam/kontra-content) +│ ├── config/site.yaml +│ └── subjects//… # markdown + front-matter per article +├── docs/ # Mermaid + Archify maps, diagrams, published to maps.lab +├── deploy/ # Docker, compose, entrypoint (pull-loop), revert scripts +├── PLAN.md # Phase 1 decision record (stack, topology, storage) +└── DESIGN.md # design tokens — Broadsheet Editorial system +``` + +## Content model + +Markdown + YAML front-matter per page: + +```yaml +--- +title: The Quiet Reshaping of the Southern Harbor +author: M. Kestrel +date: 2026-09-08 +kicker: World Dispatch +subject: world +template: article +featured: true +image: https://.../harbor1/1400/800 +excerpt: A short deck under the headline. +--- +Body markdown here… links, images via {{media:file.jpg}}, video via {{embed:vimeo:ID}}. +``` + +Template resolution: **page front-matter → subject `_subject.yaml` → site config → theme fallback**. +Front page = composable "Topics" grid (World / Politics / Culture / Tech…) whose order and +featured stories are data, not code. + +## Design system + +`DESIGN.md` defines a **Broadsheet Editorial** system: warm newsprint palette, Newsreader +(headlines) / Literata (body) / Work Sans (UI) type scale, hairline rules, sharp corners, +no shadow-depth. + +- Implemented as CSS custom properties (`--canvas`, `--ink`, `--accent`, …) → themeable data. +- Per-subject accent overrides via subject config. +- Responsive: ≥1024 / 641–1023 / ≤640, with dropdown topic nav + mobile bottom-nav. +- Fully working clickable prototype under `prototype/`. + +## Deploy & ops + +- Live: `https://kontra.lab.audasmedia.com.au` · Admin: `https://admin.kontra.lab.audasmedia.com.au/web/admin/` +- Container: `kontra` on `.13:8600`, mounts content + SSH key, `restart: unless-stopped` +- Content autosync: entrypoint runs `git pull` every 30s, restarts the binary on HEAD change. +- Master route: Caddy on `.35` terminates TLS and `reverse_proxy 192.168.20.13:8600`. +- Build: `app/build.sh` (templ + go) or the multi-stage Dockerfile (builds golang + templ in-image). + +## Security notes + +- Decap → Gitea OAuth with **non-confidential (PKCE)** application, redirect URI pinned to the admin origin. +- Gitea CORS scoped to the admin origin; content repo private. +- Deploy token kept in a gitignored `.env`; no secrets in any repo. +- Content repo requires push access to publish — editorial permissions map to Gitea team access. + +## Docs & maps + +| Artifact | Path / URL | +|---|---| +| Mermaid source | `docs/kontra-architecture.mmd` | +| Architecture doc | `docs/architecture.md` | +| Interactive map | `docs/kontra-architecture-map.html` · [maps.lab.audasmedia.com.au/kontra_day/docs/](https://maps.lab.audasmedia.com.au/kontra_day/docs/) | +| Plan (decisions) | `PLAN.md` | +| Design system | `DESIGN.md` | + +--- + +*Built with the GOTH stack — Go, templ, htmx, Tailwind-tokens — self-hosted on Gitea + Garage S3 + Caddy + Docker.* \ No newline at end of file diff --git a/docs/archify/kontra-architecture.architecture.json b/docs/archify/kontra-architecture.architecture.json new file mode 100644 index 0000000..4616f99 --- /dev/null +++ b/docs/archify/kontra-architecture.architecture.json @@ -0,0 +1,317 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Kontra \u2014 GOTH News-Opinion Site", + "output": "kontra-architecture-map.html", + "quality_profile": "showcase", + "views": [ + { + "id": "publish-path", + "label": "Content publish path", + "focus": [ + "editors", + "git", + "pull", + "bin", + "caddy", + "users" + ], + "note": "Editors push markdown to Gitea; production pulls and serves it automatically." + }, + { + "id": "auth-and-media", + "label": "Auth and media", + "focus": [ + "auth", + "admin", + "bin", + "garage" + ], + "note": "Decap logs in via Gitea OAuth; site media resolves from Garage S3 shortcodes." + } + ] + }, + "components": [ + { + "id": "editors", + "type": "external", + "label": "Editors", + "sublabel": "Obsidian \u00b7 Neovim \u00b7 CLI", + "pos": [ + 40, + 300 + ], + "size": [ + 130, + 64 + ] + }, + { + "id": "admin", + "type": "frontend", + "label": "Decap CMS", + "sublabel": "admin.kontra.lab", + "pos": [ + 40, + 160 + ], + "size": [ + 130, + 64 + ] + }, + { + "id": "auth", + "type": "security", + "label": "Gitea OAuth", + "sublabel": "PKCE", + "pos": [ + 250, + 110 + ], + "size": [ + 130, + 56 + ], + "tag": "non-confidential" + }, + { + "id": "git", + "type": "cloud", + "label": "Gitea", + "sublabel": "sam/kontra-content", + "pos": [ + 250, + 300 + ], + "size": [ + 150, + 64 + ], + "tag": "origin of truth" + }, + { + "id": "pull", + "type": "backend", + "label": "Pull loop", + "sublabel": "git pull \u00b7 30s", + "pos": [ + 470, + 300 + ], + "size": [ + 140, + 64 + ] + }, + { + "id": "bin", + "type": "backend", + "label": "Go binary", + "sublabel": "templ + htmx + Goldmark", + "pos": [ + 680, + 300 + ], + "size": [ + 160, + 68 + ], + "tag": ":8600 Docker" + }, + { + "id": "garage", + "type": "cloud", + "label": "Garage S3", + "sublabel": "kontra-day bucket", + "pos": [ + 680, + 160 + ], + "size": [ + 150, + 64 + ] + }, + { + "id": "caddy", + "type": "cloud", + "label": "Caddy master", + "sublabel": ":80/:443 reverse proxy", + "pos": [ + 900, + 300 + ], + "size": [ + 150, + 64 + ] + }, + { + "id": "users", + "type": "external", + "label": "Readers", + "sublabel": "public web", + "pos": [ + 1120, + 300 + ], + "size": [ + 130, + 64 + ] + } + ], + "boundaries": [ + { + "kind": "region", + "label": "Editors (dev box .27)", + "wraps": [ + "editors", + "admin" + ] + }, + { + "kind": "region", + "label": "Gitea Docker (.35)", + "wraps": [ + "auth", + "git" + ] + }, + { + "kind": "region", + "label": "Kontra container (.13 :8600)", + "wraps": [ + "pull", + "bin", + "garage" + ] + }, + { + "kind": "region", + "label": "Edge (Caddy .35)", + "wraps": [ + "caddy", + "users" + ] + } + ], + "connections": [ + { + "id": "editors-git", + "from": "editors", + "to": "git", + "label": "git push", + "variant": "emphasis" + }, + { + "id": "admin-git", + "from": "admin", + "to": "git", + "label": "commit", + "fromSide": "bottom", + "toSide": "top", + "variant": "dashed" + }, + { + "id": "auth-login", + "from": "admin", + "to": "auth", + "label": "Login with Gitea", + "variant": "security", + "fromSide": "right", + "toSide": "bottom", + "labelAt": [ + 308, + 180 + ] + }, + { + "id": "auth-git", + "from": "auth", + "to": "git", + "label": "authorize", + "variant": "security", + "fromSide": "bottom", + "toSide": "left" + }, + { + "id": "git-pull", + "from": "git", + "to": "pull", + "label": "SSH", + "variant": "emphasis" + }, + { + "id": "pull-bin", + "from": "pull", + "to": "bin", + "label": "restart on change", + "labelAt": [ + 645, + 378 + ] + }, + { + "id": "bin-garage", + "from": "bin", + "to": "garage", + "label": "media shortcode", + "variant": "dashed", + "fromSide": "top", + "toSide": "bottom", + "labelDy": -58, + "labelAt": [ + 768, + 238 + ] + }, + { + "id": "bin-caddy", + "from": "bin", + "to": "caddy", + "label": "reverse_proxy :8600", + "labelAt": [ + 870, + 378 + ] + }, + { + "id": "caddy-users", + "from": "caddy", + "to": "users", + "label": "HTTPS", + "variant": "emphasis" + } + ], + "cards": [ + { + "dot": "emerald", + "title": "Publish path", + "items": [ + "Editors write markdown + front-matter in Obsidian/Neovim/CLI", + "All commits land on Gitea sam/kontra-content (origin)", + "Production container pulls every 30s and restarts on change" + ] + }, + { + "dot": "cyan", + "title": "Serving", + "items": [ + "Single static Go binary (templ + htmx + Tailwind tokens)", + "Goldmark renders markdown per request; SEO-friendly", + "Caddy master terminates HTTPS and proxies :8600" + ] + }, + { + "dot": "rose", + "title": "Auth & media", + "items": [ + "Decap CMS logs in with Gitea OAuth (PKCE)", + "Media resolves from Garage S3 via {{media:}} shortcodes" + ] + } + ] +} \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..47ed80c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,67 @@ +# Kontra — System Architecture + +A news-opinion site (GOTH stack: **Go + templ + htmx + Tailwind-tokens**) with markdown +content, a Decap CMS admin, Gitea-backed git deploy with automatic content sync, and +Garage S3 media — all on the home-lab network. + +> Interactive map: `docs/kontra-architecture-map.html` — also published at: +> https://maps.lab.audasmedia.com.au/kontra/docs/kontra-architecture-map.html + +## Diagram (Mermaid) + +```mermaid +flowchart LR + subgraph editors["Content Editors"] + O[Obsidian vault
/obsidian/kontra_content] -->|git push| G + N[Neovim / CLI] -->|git push| G + D[Decap CMS
admin.kontra.lab] -->|OAuth + git commit| G + end + + subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"] + G[(sam/kontra-content
markdown origin of truth)] + end + + subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"] + P[git pull loop
every 30s] -->|restart on change| B + B[Go binary
templ + htmx + Goldmark] + B --> MD[(Markdown render
front-matter + shortcodes)] + end + + subgraph media["Media (Garage S3 on .13)"] + S3[(Garage S3
kontra-day bucket)] + end + + G -->|"SSH git"| P + B -->|"shortcode: media"| S3 + B -->|"emits HTML"| CB[Caddy master
.35 :80/:443] + CB -->|"reverse_proxy :8600"| B + + subgraph net["Network"] + CB -->|https| WWW[kontra.lab.audasmedia.com.au] + CB -->|https| ADM[admin.kontra.lab.audasmedia.com.au] + end + + WWW -->|browser| B + ADM -->|browser| D +``` + +## Rendered image + +![Kontra architecture](diagrams/kontra-architecture.png) + +## Key facts + +| Layer | Where | What | +|---|---|---| +| Content origin | Gitea `.35:3001` | `sam/kontra-content` (markdown + front-matter) | +| App binary | Docker on `.13:8600` | Go (golang) + templ + htmx + Goldmark markdown | +| Content deploy | pull loop | `git pull` every 30s → restart server on change | +| Media | Garage S3 on `.13` | `kontra-day` bucket via `{{shortcode}}` | +| Admin | Decap CMS | at `admin.kontra.lab.audasmedia.com.au/web/admin/`, Gitea OAuth login | +| Reverse proxy | Caddy on `.35` | master :80/:443 → reverse_proxy `.13:8600` | +| Obsidian | `.27` vault | `obsidian/kontra_content` → content repo (git-sync) | + +## Mermaid source + +`docs/kontra-architecture.mmd` (single source of truth). Render: +`mmdc -i docs/kontra-architecture.mmd -o docs/diagrams/kontra-architecture.png --scale 2 -b white` \ No newline at end of file diff --git a/docs/diagrams/kontra-architecture.png b/docs/diagrams/kontra-architecture.png new file mode 100644 index 0000000..e41d814 Binary files /dev/null and b/docs/diagrams/kontra-architecture.png differ diff --git a/docs/diagrams/kontra-architecture.svg b/docs/diagrams/kontra-architecture.svg new file mode 100644 index 0000000..3b5cb5e --- /dev/null +++ b/docs/diagrams/kontra-architecture.svg @@ -0,0 +1 @@ +

Network

Media (Garage S3 on .13)

Kontra container (192.168.20.13 :8600 Docker)

Content Editors

git push

git push

OAuth + git commit

restart on change

SSH git

shortcode: media

emits HTML

reverse_proxy :8600

https

https

browser

browser

Gitea (192.168.20.35 :3001 Docker)

Obsidian vault
/obsidian/kontra_content

sam/kontra-content
markdown origin of truth

Neovim / CLI

Decap CMS
admin.kontra.lab

git pull loop
every 30s

Go binary
templ + htmx + Goldmark

Markdown render
front-matter + shortcodes

Garage S3
kontra-day bucket

Caddy master
.35 :80/:443

kontra.lab.audasmedia.com.au

admin.kontra.lab.audasmedia.com.au

\ No newline at end of file diff --git a/docs/kontra-architecture-map.html b/docs/kontra-architecture-map.html new file mode 100644 index 0000000..36d1456 --- /dev/null +++ b/docs/kontra-architecture-map.html @@ -0,0 +1,13705 @@ + + + + + + + Kontra — GOTH News-Opinion Site Diagram + + + + + + + + + + + +
+ +
+
+
+

Kontra — GOTH News-Opinion Site

+
+
+ + + + + + +
+ + Kontra — GOTH News-Opinion Site + A architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + Editors (dev box .27) + + + Gitea Docker (.35) + + + Kontra container (.13 :8600) + + + Edge (Caddy .35) + + + + + + + + + + + + + + + Editors · Obsidian · Neovim · CLI · Editors (dev box .27) + + + + Editors + Obsidian · Neovim · CLI + + + + Decap CMS · admin.kontra.lab · Editors (dev box .27) + + + + Decap CMS + admin.kontra.lab + + + + Gitea OAuth · PKCE · Gitea Docker (.35) · non-confidential + + + + Gitea OAuth + PKCE + non-confidential + + + + Gitea · sam/kontra-content · Gitea Docker (.35) · origin of truth + + + + Gitea + sam/kontra-content + origin of truth + + + + Pull loop · git pull · 30s · Kontra container (.13 :8600) + + + + Pull loop + git pull · 30s + + + + Go binary · templ + htmx + Goldmark · Kontra container (.13 :8600) · :8600 Docker + + + + Go binary + templ + htmx + Goldmark + :8600 Docker + + + + Garage S3 · kontra-day bucket · Kontra container (.13 :8600) + + + + Garage S3 + kontra-day bucket + + + + Caddy master · :80/:443 reverse proxy · Edge (Caddy .35) + + + + Caddy master + :80/:443 reverse proxy + + + + Readers · public web · Edge (Caddy .35) + + + + Readers + public web + + + + + + git push + + + + commit + + + + Login with Gitea + + + + authorize + + + + SSH + + + + restart on change + + + + media shortcode + + + + reverse_proxy :8600 + + + + HTTPS + + + + + Legend + + + Frontend + + + + Backend + + + + Cloud + + + + Security + + + + External + + + +

+ + + + + + + + +
+ + +
+
+
+
+

Publish path

+
+
    +
  • • Editors write markdown + front-matter in Obsidian/Neovim/CLI
  • +
  • • All commits land on Gitea sam/kontra-content (origin)
  • +
  • • Production container pulls every 30s and restarts on change
  • +
+
+ +
+
+
+

Serving

+
+
    +
  • • Single static Go binary (templ + htmx + Tailwind tokens)
  • +
  • • Goldmark renders markdown per request; SEO-friendly
  • +
  • • Caddy master terminates HTTPS and proxies :8600
  • +
+
+ +
+
+
+

Auth & media

+
+
    +
  • • Decap CMS logs in with Gitea OAuth (PKCE)
  • +
  • • Media resolves from Garage S3 via {{media:}} shortcodes
  • +
+
+
+ +
+ + + + diff --git a/docs/kontra-architecture.mmd b/docs/kontra-architecture.mmd new file mode 100644 index 0000000..78cfbed --- /dev/null +++ b/docs/kontra-architecture.mmd @@ -0,0 +1,33 @@ +flowchart LR + subgraph editors["Content Editors"] + O[Obsidian vault
/obsidian/kontra_content] -->|git push| G + N[Neovim / CLI] -->|git push| G + D[Decap CMS
admin.kontra.lab] -->|OAuth + git commit| G + end + + subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"] + G[(sam/kontra-content
markdown origin of truth)] + end + + subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"] + P[git pull loop
every 30s] -->|restart on change| B + B[Go binary
templ + htmx + Goldmark] + B --> MD[(Markdown render
front-matter + shortcodes)] + end + + subgraph media["Media (Garage S3 on .13)"] + S3[(Garage S3
kontra-day bucket)] + end + + G -->|"SSH git"| P + B -->|"shortcode: media"| S3 + B -->|"emits HTML"| CB[Caddy master
.35 :80/:443] + CB -->|"reverse_proxy :8600"| B + + subgraph net["Network"] + CB -->|https| WWW[kontra.lab.audasmedia.com.au] + CB -->|https| ADM[admin.kontra.lab.audasmedia.com.au] + end + + WWW -->|browser| B + ADM -->|browser| D \ No newline at end of file