Phase 2 (snipe#4): after bulk-reporting sellers to eBay T&S, Snipe now
persists which sellers were reported so cards show a muted "Reported to
eBay" badge and users aren't prompted to re-report the same seller.
- migration 012 adds reported_sellers table (user DB, UNIQUE on seller)
- Store.mark_reported / list_reported methods
- POST /api/reported + GET /api/reported endpoints
- reported store (frontend) with optimistic update + server persistence
- reportSelected wires into store after opening eBay tabs
Phase 3 prep (snipe#4): community blocklist share toggle
- Settings > Community section: "Share blocklist with community" toggle
(visible only to signed-in cloud users, default OFF)
- Persisted as community.blocklist_share user preference
- Backend community signal publish now gated on opt-in preference;
privacy-by-architecture: sharing is explicit, never implicit
Add 10 new tests covering the three previously untested flag paths:
- zero_feedback: flag fires + composite capped at 35 even with all-20 signals
- long_on_market: fires at >=5 sightings + >=14 days; NOT at <5 sightings or <14 days
- significant_price_drop: fires at >=20% below first-seen; NOT at <20% or no prior price
- established_retailer: duplicate_photo suppressed at feedback>=1000; fires below threshold
Also fix datetime.utcnow() deprecation in aggregator._days_since() and test helper
— replaced with timezone-aware datetime.now(timezone.utc) throughout.
Add _auth_label() classifier: local/anon/guest/authed — no PII, just
enough to distinguish traffic types in docker logs for log-based analytics.
Instrument /api/session: logs new_guest (with UUID) or auth=.../tier=...
on every session bootstrap. Instrument /api/search: expands existing
multi-search log line with auth=, tier=, adapter=, pages=, queries=,
listings= fields for grep/awk analysis of search behaviour by tier.
Add logging.basicConfig so app-level log.info() calls appear in docker
logs alongside the Uvicorn access log (previously suppressed by missing
root handler).
Both stores hardcoded /api/* paths without reading VITE_API_BASE, causing
/api/session to hit the menagerie root (no /snipe prefix) and receive a
302 redirect to circuitforge.tech/login instead of a session response.
Every other store already read VITE_API_BASE correctly.
- Rewrite landing hero subtitle with narrative opener ("Seen a listing
that looks almost too good to pass up?") — more universal than the
feature-list version, avoids category assumptions
- Update eBay cancellation callout CTA to "Search above to score
listings before you commit" — direct action vs. passive reminder
- Tile descriptions rewritten with concrete examples: "does this seller
actually know what they're selling?", "40% below median", quoted
"scratch and dent" pattern for instant recognition
- Sign-in strip: add specific page count ("up to 5 pages") and
"community-maintained" attribution for blocklist credibility
- Fix useTheme restore() to re-read from localStorage instead of using
cached module-level ref — fixes test isolation failure where previous
test's setMode('dark') leaked into the restore() system test
- Add 32-test Vitest suite: useTheme (7), searchStore (7),
ListingView component (18) — all green
Adds user-controlled theme selection independent of OS preference:
- useTheme composable: sets data-theme="dark"|"light" on <html>,
persisted to localStorage as snipe:theme. Follows the same pattern
as useSnipeMode.
- theme.css: [data-theme="dark"] and [data-theme="light"] explicit
attribute selectors override @media (prefers-color-scheme: light).
Media query updated to :root:not([data-theme="dark"]) so it has no
effect when the user has forced dark on a light-OS machine.
- App.vue: restoreTheme() called in onMounted alongside restoreSnipeMode.
- SettingsView: Appearance section with System/Dark/Light segmented
button group.
Replaces the Coming Soon placeholder. Clicking Details on any card opens
a full trust breakdown view:
- SVG score ring with composite score and colour-coded verdict label
- Auto-generated verdict text (identifies worst signals in plain English)
- Signal table with mini-bars: Feedback Volume/Ratio, Account Age,
Price vs Market, Category History — pending state shown for unresolved
- Red flag badges (hard vs soft) above the score ring
- Photo carousel with prev/next controls and img-error skip
- Seller panel (feedback count/ratio, account age, pending enrichment note)
- Block seller inline form wired to POST /api/blocklist
- Triple Red pulsing border easter egg carried over from card
- Not-found state for direct URL access (store cleared on refresh)
- Responsive: single-column layout on ≤640px
- ListingCard: adds Details RouterLink to price column
- search store: adds getListing(id) lookup helper
Silences PytestUnknownMarkWarning from '-m not browser' in GitHub
Actions CI. Any test requiring headed Chromium (Kasada bypass,
scraper integration) should be decorated with @pytest.mark.browser
so it is excluded from CI automatically.
Closes#40
After populateFromLLM() runs, the sidebar filter controls and search bar now
update to reflect the LLM-generated values. Adds two one-way watchers
(store.filters → local reactive, store.query → queryInput). Closes#39.
Adds optional community_store param to refresh(). Credentialed instances
publish leaf categories to the shared community PostgreSQL after a
successful Taxonomy API fetch. Credentialless instances pull from community
(requires >= 10 rows) before falling back to the hardcoded bootstrap.
Adds 3 new tests (14 total, all passing).
Corrections (#31):
- Add 010_corrections.sql migration (from cf-core CORRECTIONS_MIGRATION_SQL)
- Wire make_corrections_router() at /api/corrections (shared_db, product='snipe')
- get_shared_db() dependency aggregates corrections across all cloud users
Community module (#32#33):
- Init SnipeCommunityStore at startup when COMMUNITY_DB_URL is set
- Graceful skip if COMMUNITY_DB_URL is unset (local mode, community disabled)
- add_to_blocklist() publishes confirmed_scam=True seller_trust signal to
community postgres on every manual blocklist addition (fire-and-forget)
- BlocklistAdd gains flags[] field so active red-flag keys travel with signal
cf-orch community postgres (cf-orch#36) + cf-core module (cf-core#47) both merged.
Retake 01-hero, 02-results, 03-steal-badge, and hero.png after the
color-mix() refactor (d5651e5) so docs reflect corrected light/dark
tints on STEAL badge, flags, and trust score indicators.
All error-red and success-green rgba values were using dark-mode hex values directly.
In light mode those tokens shift (error #f85149→#dc2626, success #3fb950→#16a34a),
so the hardcoded tints stayed wrong. Replaced with color-mix() so tints follow the token.
Also:
- Add missing --space-5 (1.25rem) to spacing scale in theme.css
- Add --color-accent (purple) token for csv_import badge; adapts dark/light
- Wire blocklist source badges to use --color-info/accent/success tokens
After a search, the API now returns a session_id. If any trust scores are
partial (pending seller age or category data), the frontend opens a
Server-Sent Events stream to /api/updates/{session_id}. As the background
BTF (account age) and category enrichment threads complete, they re-score
affected listings and push updated TrustScore payloads over SSE. The
frontend patches the trustScores and sellers maps reactively so signal
dots light up without requiring a manual re-search.
Backend:
- _update_queues registry maps session_id -> SimpleQueue (thread-safe bridge)
- _trigger_scraper_enrichment accepts session_id/user_db/query, builds a
seller->listings map, calls _push_updates() after each enrichment pass
which re-scores, saves trust scores, and puts events on the queue
- New GET /api/updates/{session_id} SSE endpoint: polls queue every 500ms,
emits heartbeats every 15s, closes on sentinel None or 90s timeout
- search endpoint generates session_id and returns it in response
Frontend:
- search store adds enriching state and _openUpdates() / closeUpdates()
- On search completion, if partial scores exist, opens EventSource stream
- onmessage: patches trustScores and sellers maps (new Map() to trigger
Vue reactivity), updates marketPrice if included
- on 'done' event or error: closes stream, enriching = false
- SearchView: pulsing 'Updating scores...' badge in toolbar while enriching
SQLite's executescript() auto-commits each DDL statement, so a partial
migration failure leaves columns in the DB without marking the migration
done. On the next startup the runner retries and hits duplicate column errors.
Use ADD COLUMN IF NOT EXISTS (SQLite 3.35+, shipped in Python 3.11+)
so migrations 004 and 005 are safe to re-run in any partial state.
001_init.sql already defines first_seen_at in the CREATE TABLE statement.
On fresh installs, migration 004 failed with 'duplicate column name: first_seen_at'.
Remove the redundant ALTER TABLE; last_seen_at/times_seen/price_at_first_seen
are still added by 004 as before.