feat: web GUI report editor (Go + Templ + HTMX)#73
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Architecture
Key design decisions (from 4-review pipeline: CEO + Design + Eng + Codex)
BuildReportsFromLast()output including carry-overs (Codex finding)ReportWeekRange()instead ofCurrentWeekRange()to match Slack's Monday cutoff (Codex finding)config.IsManagerID(), not stored in cookie (Eng review)Files
Test plan
CGO_ENABLED=1 go test ./...— all 14 test suites passCGO_ENABLED=1 go build -o reportbot .— binary buildsweb_enabled: true— web UI at localhost:8080