Skip to content

Public dashboard v1 (closed — split into smaller PRs)#67

Closed
ZukwiZ wants to merge 19 commits into
masterfrom
feature/public-dashboard-v1
Closed

Public dashboard v1 (closed — split into smaller PRs)#67
ZukwiZ wants to merge 19 commits into
masterfrom
feature/public-dashboard-v1

Conversation

@ZukwiZ
Copy link
Copy Markdown
Collaborator

@ZukwiZ ZukwiZ commented May 27, 2026

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.

byskov and others added 19 commits May 25, 2026 14:32
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>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 25b7e29. Configure here.

@ZukwiZ ZukwiZ closed this May 27, 2026
@ZukwiZ ZukwiZ deleted the feature/public-dashboard-v1 branch May 27, 2026 16:49
@ZukwiZ ZukwiZ changed the title Public Reverse Watch dashboard (v1) Public dashboard v1 (closed — split into smaller PRs) May 27, 2026
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.

2 participants