Public dashboard v1 (closed — split into smaller PRs)#67
Conversation
Adds the v1 planning docs (PRD, handoff, AI session helpers, design
mockups) under docs/dashboard, plus the v1 backend for the public
Reverse Watch dashboard:
- GET /api/v1/stats/summary three KPI counts in one call,
60s in-process cache
- GET /api/v1/stats/reversals/daily 30/60/90-day buckets, zero-filled
in Go, UTC date keys
- GET /api/v1/reversals/recent latest reversals, slim public
projection, no pagination yet
All three endpoints are public and IP-rate-limited via the existing
ratelimit package. No schema changes; the existing Reversal model
already has every field needed.
See docs/dashboard/PRD.md and HANDOFF.md for the full spec.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- internal/devseed/fixtures/reversals_seed.csv: 98 of the original 100 rows from "Reverse Watch - Studio Results 2026-05-22 11:13". Two rows dropped due to steam_id precision loss in the source sheet (see HANDOFF section 10). - internal/devseed/sheet.go: parses the CSV into []*models.Reversal, validating the header layout. Insert uses ON CONFLICT (id) DO NOTHING, so re-running the seed is a no-op. - cmd/seed/main.go: CLI (go run ./cmd/seed). Refuses to run unless ENVIRONMENT=development. Co-authored-by: Cursor <cursoragent@cursor.com>
- v1 ships as ONE GitHub PR to Zach, not piecemeal. "PR #1 / PR #2" in HANDOFF.md are local scoping milestones, not separate PRs. - Explicit "csfloat/reverse-watch is private code I don't own" rule to enforce extra caution around force-push / history rewrites. Co-authored-by: Cursor <cursoragent@cursor.com>
http.ServeFile / http.FileServer were truncating every static
response at exactly 512 bytes (first TCP segment) on local macOS
dev. JSON endpoints were unaffected because render.JSON writes
directly to the ResponseWriter.
Replace the static handlers with small read-into-memory variants:
- GET / -> serveStaticFile("static/index.html", ...)
- GET /static/* -> staticDirHandler("static") with path-traversal
rejection and mime.TypeByExtension fallback.
Static payloads on this site are tiny (HTML + a logo, maybe a few
icons in v1.1), so the read-once cost is negligible.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces the single-purpose Steam-ID lookup page with the full
public dashboard per docs/dashboard/PRD.md:
- Hero with CSFloat logo, title, lede, restyled search.
- Search-result chip (clear/flagged) below the search input;
background tinting follows the verdict.
- Three KPI cards (Traders Indexed / Flagged / Flagged 24h)
populated from GET /api/v1/stats/summary.
- 30-day reversal-volume line chart (uPlot, loaded via CDN)
with a custom hover tooltip showing day + count.
- "Recently Reported Reversals" table populated from
GET /api/v1/reversals/recent?limit=100, paginated client-side
in 10-row chunks via "Load More" (server pagination = v1.1).
- Footer with "What is reverse.watch?" + "Want to contribute?"
blocks and a Powered-by-CSFloat lockup.
- Hardcoded marketplace slug -> {name, iconKey} map (D9).
- Mobile responsive reflow based on the one mobile mockup we have;
refines once Razvan ships the missing default + flagged mocks.
Adds static/csfloat-logo.png (116x36, 3.9KB) for the hero and
footer lockups.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Mention the public dashboard at / - Add createdb + postgres superuser steps required by pgtestdb - Document `go run ./cmd/seed` for loading local fixture data - Add table of the four public read endpoints - Link to docs/dashboard/PRD.md and HANDOFF.md Co-authored-by: Cursor <cursoragent@cursor.com>
Captures everything from May 24 to 26 so the next session's kick-off is one read away. Notes the 7 commits ahead of master, the static-file truncation workaround, and flags PR #3 (PostHog) as the recommended next step. Co-authored-by: Cursor <cursoragent@cursor.com>
GenerateSynthetic builds ~9,800 reversals over 180 days with a sinusoidal baseline, ~5% spike days, ~10% quiet days, and at least one row per day. Marketplace mix is 80% csfloat with smaller slices of tradeit/skinport/swap.gg so the table view exercises the unknown-slug fallback. ~5% rows use source=related_user (with valid related_steam_id), ~5% user_report, ~1.5% expunged. cmd/seed gains -synthetic to switch from the CSV fixture to the generator. Both modes stay idempotent via ON CONFLICT (id) DO NOTHING, and InsertReversals now chunks at 1,000 rows per round trip to stay under Postgres's 65,535 bound-parameter limit. Enables the dashboard's upcoming period picker (7d / 30d / 3mo / 6mo / 1y) to render meaningful curves instead of a sparse 3-day window. Co-authored-by: Cursor <cursoragent@cursor.com>
Extends the daily-counts allow-list from {30, 60, 90} to
{7, 30, 60, 90, 180, 365} to back the dashboard's new period picker.
Updates the error string accordingly and rewrites the invalid-days
test to use values that are still out of range. Adds a positive test
that asserts the response always returns exactly `days` zero-filled
buckets for every accepted value.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Rename first KPI label from "Traders Indexed" to "Steam IDs Searched". The underlying /api/v1/stats/summary contract stays put; only the user-facing string changes. - Add a segmented chart period picker (7d / 30d / 3m / 6m / 1y) that re-fetches /api/v1/stats/reversals/daily with the selected window. Race-safe via a fetch sequence counter so rapid clicks always settle on the latest selection. Axis label format collapses to month-only for windows beyond 60 days so the 6m and 1y views stay legible. - Remove the static "· Last 30 days" subtitle alongside the title and rephrase the chart subtitle accordingly. - Update PRD §3.1 to list the new label and picker, §6.2 to note the UI-only rename, §6.3 to enumerate the expanded `days` allow-list, and §3.5 to reflect that synthetic seed data is now a supported local mode (reversing the earlier "no synthetic data" stance). Co-authored-by: Cursor <cursoragent@cursor.com>
…seed) Co-authored-by: Cursor <cursoragent@cursor.com>
Chart axis:
- Pass uPlot a UTC `tzDate` hook so tick splits land at UTC midnight
instead of local midnight. Without this, in any non-UTC zone the
"May 24" tick was off by 1–12 hours from the May-24 data point and
the tick label would read "May 23" while the tooltip read "May 24".
- Force whole-day-or-coarser `incrs`. Default uPlot allows 8/12-hour
ticks, which collapsed to three identical "May 20" labels in the
7d view once the formatter only rendered the date.
- Make `incrs` period-aware: month-only label windows (> 60 days)
restrict to ≥30d increments so 3m doesn't pick 14-day ticks that
render as "Apr Apr May May Jun Jun".
Trader column:
- Was rendering the marketplace slug (e.g. "CSFloat", "tradeit"),
which conflates marketplace with trader identity. Razvan's mockup
shows the trader's Steam display name there.
- Replace with `fakeTraderName(steamId)` — a deterministic djb2 hash
into ~13.5k adjective+noun+suffix combinations. Same steam_id
always renders the same name; ~71% of synthetic ids get a unique
name, the rest collide which matches real-world Steam naming.
- Person glyph replaces the storefront/verified marketplace icons.
- CSS classes renamed from `.marketplace-{icon,name}` to
`.trader-{avatar,name}` to match the new semantics.
- Removed the now-unused MARKETPLACES map + marketplaceFor helper.
Open question filed for Zach (PRD §6.4, §14 D-open-4, HANDOFF §3):
how real display names get sourced — Steam GetPlayerSummaries with a
cache, a new steam_users table, or punt to v1.1. SESSION-LOG #3 also
updated.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds Pricempire-style annotation chips above the Reversal Graph at the date of CS2 events that may explain reversal spikes. The data source is a flat JSON file (`static/cs2-events.json`); editing it and refreshing the browser is the entire authoring loop — no rebuild and no backend involvement. Each entry needs `date` (YYYY-MM-DD, UTC) and `title`; `description` and `url` are optional and surface in the hover popover. Chips only render for events that fall inside the currently-selected period (7d / 30d / 3m / 6m / 1y). When two chips would overlap horizontally they row-stack (up to 3 rows); beyond that the oldest is dropped with a console warning so the author knows to space events out. PRD §6.3 documents the feature; SESSION-LOG #3 logs the work. If the event list ever outgrows manual curation it becomes its own endpoint, but v1 is editorial-on-disk by design. Co-authored-by: Cursor <cursoragent@cursor.com>
Chips disappeared after switching chart periods because renderEventChips ran synchronously immediately after `new uPlot(...)`. On a fresh uPlot instance the canvas bbox / scale state isn't guaranteed to be settled in the same tick, so `valToPos` returned positions that fell outside the visible container and the chips were either off-screen or zero-width. On initial load this was masked by a second, late render fired from loadCS2Events's own callback after the fetch resolved — that one happened after layout had settled, so chips appeared. Period switches don't get that second pass, hence the disappearance. Split renderEventChips into a public deferred entrypoint and a private paintEventChips body, scheduling the paint via requestAnimationFrame (cancelling any in-flight frame so rapid period switches collapse to one render). Also replaces the placeholder "Armory Holiday Drop" event with the real Retake Update (2025-10-22) — visible in the 1y view. Co-authored-by: Cursor <cursoragent@cursor.com>
Backend hygiene + frontend correctness + reviewer-facing summary doc. Backend: - Drop GORM tags from domain/dto/stats.go; scan via SQL alias matching GORM's auto-snake (TradersFlagged24h -> traders_flagged24h). JSON contract unchanged. - Collapse the local bucket struct in repository/public/reversal.go DailyCounts; scan straight into []dto.DailyCount. - Switch api/v1/stats allowedDays from map to slice + slices.Contains. - Collapse defaultLimit/maxLimit in listRecentHandler into one const. - Fix serveStaticFile docstring (was "read once", actually per-request). - Trim Phase 1/2/3 narration in internal/devseed/synthetic.go and dedupe chunk-size comment in sheet.go. - Use cmp.Diff in one stats handler test for consistency. Frontend (static/index.html): - chartInstance.destroy() before re-render (uPlot instance leak). - Attach chart mouseleave listener once at boot, not on every render. - formatDate uses timeZone: 'UTC' so table dates match chart axis. - Drop orphan CSS custom properties (--bg-secondary, --bg-input, --shadow), unused class rules (.csfloat-lockup, .block-meta), unreferenced @Keyframes fadeIn, dead element IDs (kpiGrid, reversalsLoadingCell), captured-but-unread prevIcon. - Strip ~15 narration comments / section banners; keep the ~6 high-value "why" comments (UTC tzDate, RAF defer, race guard, etc). - 1772 -> 1705 lines. Docs: - Appended "Project Summary (for a reviewer's first pass)" section to docs/dashboard/ENVIRONMENT.md with file map, mermaid request flow, design decisions, open items, run commands. - Logged Session #5 in SESSION-LOG.md. Tests: go test ./... green. Co-authored-by: Cursor <cursoragent@cursor.com>
- swap the legacy steam-id chip for avatar + display name + Steam/CSFloat icon links - stack chip vertically on mobile so the verdict pill drops below the user info Co-authored-by: Cursor <cursoragent@cursor.com>
…aimer) - wrap search input, helper text, and result chip in a single thin-bordered card - drop the chip's own card chrome; subtle nested fill + rounded corners instead - remove the colored top divider on flagged/clear chip states - center KPI card content - align "Powered by" logo with text in hero and footer (line-height + flex) - add "Reverse.Watch 2026. Not affiliated with Valve Corp." footer disclaimer - copy: "Recent Reversals", trimmed chart/table subtitles, "CSFloat extension" Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 25b7e29. Configure here.
| - Don't make large multi-file refactors without my explicit OK first. | ||
| - Don't install new Go dependencies (`go get`) without asking. | ||
| - Don't run "fix everything" commands (`go mod tidy` is fine; mass auto-format across the repo is not). | ||
| - Don't surprise-commit. Always show the proposed message first (see Git workflow above). |
There was a problem hiding this comment.
AI workflow and personal docs committed to public repo
Medium Severity
Several files are AI session-management prompts and personal workflow notes that belong in a local workspace, not a public repository. ABOUT-MORTEN.md contains internal team Discord handles, personal working preferences, and AI prompting instructions. START-CHAT.md and END-CHAT.md are Cursor session templates. SESSION-LOG.md logs internal process details including PIDs, config passwords, and Linear tickets. The HANDOFF.md itself notes these docs should be deleted once v1 is live.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 25b7e29. Configure here.


Closed and split into multiple smaller, reviewable PRs. Internal docs and dev fixtures have been removed from the working branch. See follow-up PRs for the actual scope.