Skip to content

feat: web GUI report editor (Go + Templ + HTMX)#73

Merged
WZ merged 30 commits into
mainfrom
feat/web-gui-editor
Mar 24, 2026
Merged

feat: web GUI report editor (Go + Templ + HTMX)#73
WZ merged 30 commits into
mainfrom
feat/web-gui-editor

Conversation

@WZ
Copy link
Copy Markdown
Owner

@WZ WZ commented Mar 23, 2026

Summary

  • Add web-based weekly report editor that runs alongside the Slack bot in a single binary
  • Managers can preview AI-classified items, reclassify between sections, edit/delete, preview markdown, and generate reports via HTMX-powered UI
  • Non-managers get read-only view of their own items (matches Slack behavior)

Architecture

  • Go + Templ + HTMX — no npm, no React, no separate frontend build
  • chi router with Slack OAuth, CSRF middleware (gorilla/csrf), HMAC-signed session cookies
  • SQLite WAL mode for concurrent web + Slack bot access
  • deps.go pattern (matches existing codebase convention, no service layer)
  • Report generation mutex prevents concurrent Slack + web writes

Key design decisions (from 4-review pipeline: CEO + Design + Eng + Codex)

  • Authoritative corrections: Manual reclassifications override LLM decisions (Codex finding)
  • Editor shows real report: Uses BuildReportsFromLast() output including carry-overs (Codex finding)
  • ReportWeekRange() instead of CurrentWeekRange() to match Slack's Monday cutoff (Codex finding)
  • Role derived per-request via config.IsManagerID(), not stored in cookie (Eng review)
  • Polling for generation progress, not SSE (Eng review: pipeline is synchronous)

Files

  • 28 new files (handlers, middleware, auth, templates, CSS, tests)
  • 9 modified files (db.go, report_builder.go, config.go, app.go, config.yaml, README, CLAUDE.md, go.mod, go.sum)
  • 31 new test functions across 3 test suites

Test plan

  • CGO_ENABLED=1 go test ./... — all 14 test suites pass
  • CGO_ENABLED=1 go build -o reportbot . — binary builds
  • Run with web_enabled: true — web UI at localhost:8080
  • Slack OAuth flow → session cookie set
  • Report editor loads with items grouped by section
  • Reclassify item → verify DB write + section swap
  • Preview markdown → verify rendered output
  • Generate report → verify polling + file written
  • All Slack commands still work unchanged
  • Non-manager login → read-only view

Codex added 30 commits March 23, 2026 03:01
Add a web-based weekly report editor that runs alongside the Slack bot
in the same binary. Managers can preview items grouped by AI-classified
sections, reclassify items between sections, edit/delete items, preview
markdown, and generate reports — all through an HTMX-powered interface.

Key changes:
- SQLite WAL mode for concurrent web + Slack access
- Authoritative corrections: manual reclassifications override LLM
- chi router with Slack OAuth, CSRF protection, session cookies
- Report editor shows real BuildReportsFromLast output (WYSIWYG)
- Non-managers see only their own items (matches Slack behavior)
- Generation mutex prevents concurrent Slack + web report writes
- deps.go pattern matching existing codebase convention
- 31 new tests across 3 test suites
Security:
- Validate web_session_secret/client_id/client_secret when web_enabled
- Fix XSS in GenerateStatus: escape jobID and message with html.EscapeString
- Derive Secure cookie flag from WebBaseURL scheme (not hardcoded false)
- Filter items by author in /preview for non-managers (was leaking all items)

Correctness:
- Fix data race on generateJob: add sync.Mutex with read/update methods
- Implement BuildResult cache (was declared but never used, every page hit LLM)
- Extract week param before goroutine (don't capture *http.Request in closure)
- Distinguish sql.ErrNoRows from DB errors in item lookups
- Skip mutation controls for zero-ID items in templates
- Pass week parameter to /generate endpoint from template

Reliability:
- Log Render() errors instead of silently discarding
- Log DB errors for corrections/historical items instead of discarding with _
- Verify web server bind before logging "listening" (fail fast on port conflict)
- Use Shutdown() instead of Close() for graceful web server draining
- Handle fs.Sub error at startup (was silently nil)
- Check os.MkdirAll error for report output directory
- Don't leak raw error strings to users in preview
…panic

The goroutine spawned by GenerateReport was calling real GetRecentCorrections
and GetClassifiedItemsWithSections after the test's defer restore() reset deps,
causing a nil pointer dereference on the db parameter in CI.
- Add Caddy reverse proxy with self-signed TLS (works with IP addresses
  on internal networks, no domain or internet required)
- Expose port 8082 in Dockerfile
- Update docker-compose.yaml with caddy service and web env vars
- Add Caddyfile with tls internal directive
- Document Docker Compose + Caddy deployment in README and CLAUDE.md
- Preview Markdown shows spinner + "Loading preview..." while LLM classifies
- Generate Report button shows spinner and disables during generation
- Reclassify dropdown dims while request is in flight
- Convert report markdown to readable HTML (headings, bullets, bold)
- Widen preview panel from 420px to 520px
- Replace raw <pre> with styled rendered HTML
Page load now uses existing classifications from DB (fast, no LLM).
LLM classification only runs on explicit "Classify Items" button click.

- Add GetLatestClassificationsForItems batch query to sqlite
- buildSectionsFromDB groups items by existing DB classifications
- classifyWithLLM only called when ?classify=1 parameter is set
- Unclassified items shown in "Unclassified" section with yellow highlight
- "Classify Items" button appears when items lack classifications
- "Re-classify" button available after initial classification
UI/UX Pro Max recommendations applied:
- Color: Blue data (#2563EB) + Slate text (#1E293B) + Amber highlights
- Focus states: visible 2px blue outline for keyboard navigation (a11y)
- Row hover: smooth slate highlight transition
- Sections: subtle box-shadow + left border for needs-review
- Tabular nums: confidence badges and stats use tabular figures
- Form inputs: blue focus ring with subtle shadow
- Responsive: stacks to single column below 1024px
- Transitions: 150ms on hover/focus states (within 150-300ms guideline)
- Nav: active state uses primary blue, hover transitions
- Sort sections by section_id to match report template ordering
  (template sections first, then custom, then Unclassified last)
- Add "+ Category" button for managers to create custom sections
- Custom section IDs use CUSTOM_{timestamp} prefix
- Reclassify dropdown shows all known sections (including empty ones)
- Add GetAllSectionLabels batch query for section discovery
gorilla/csrf was causing 403 on all HTMX POST requests due to complex
token masking and HttpOnly cookie requirements that conflicted with
JavaScript-based token injection.

New approach: simple double-submit cookie pattern:
- GET sets a readable _csrf cookie (not HttpOnly)
- HTMX reads cookie via document.cookie and sends X-CSRF-Token header
- POST validates header matches cookie
- No gorilla dependency needed for CSRF
- Switch from base64 to hex tokens (no special chars that break cookie parsing)
- Simpler JS cookie reader (no decodeURIComponent needed)
- Add server-side log for CSRF rejections showing header vs cookie values
- Verified all 6 POST endpoints pass CSRF validation
- Delete: item removed in-place (no scroll)
- Edit save: item row re-rendered in-place
- Rename: section name span swapped in-place
- Add category: new section card appended to sections list
- Reclassify: still reloads (structural change across sections)
- Rename cancel: restores original name via GET endpoint
When a section has subcategories (e.g., "Release and Support > FAZ-BD 7.6.2"),
the parent section ("Release and Support") is hidden from the reclassify
dropdown. Only the subcategories appear. Also deduplicates sections by name
to prevent duplicate entries in the dropdown.
@WZ WZ merged commit ed7a2dd into main Mar 24, 2026
1 check passed
@WZ WZ deleted the feat/web-gui-editor branch March 24, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant