Commit graph

99 commits

Author SHA1 Message Date
d5912080fb feat(search): async endpoint + SSE streaming for initial results
Add GET /api/search/async that returns HTTP 202 immediately and streams
scrape results via SSE to avoid nginx 120s timeouts on slow eBay searches.

Backend:
- New GET /api/search/async endpoint submits scraping to ThreadPoolExecutor
  and returns {session_id, status: "queued"} before scrape begins
- Background worker runs same pipeline as synchronous search, pushing
  typed SSE events: "listings" (initial batch), "update" (enrichment),
  "market_price", and None sentinel
- Existing GET /api/updates/{session_id} passes new event types through
  as-is (already a generic pass-through); deadline extended to 150s
- Module-level _search_executor (max_workers=4) caps concurrent scrape sessions

Frontend (search.ts):
- search() now calls /api/search/async instead of /api/search
- loading stays true until first "listings" SSE event arrives
- _openUpdates() handles new typed events: "listings", "market_price",
  "update"; legacy untyped enrichment events still handled
- cancelSearch() now also closes any open SSE stream

Tests: tests/test_async_search.py (6 tests) covering 202 response,
session_id registration in _update_queues, empty query path, UUID format,
and no-Chromium guarantee. All 159 pre-existing tests still pass.

Closes #49. Also closes Forgejo issue #1 (SSE enrichment streaming, already
implemented; async search completes the picture).
2026-04-20 10:57:32 -07:00
df4610c57b feat(search): normalize eBay listing + checkout URLs as item lookup
When the user pastes an eBay listing URL (www.ebay.com/itm/...) or an
eBay checkout URL (pay.ebay.com/rxo?itemId=...) into the search field,
extract the numeric item ID and use it as the search query.

Supported URL patterns:
- https://www.ebay.com/itm/Title-Slug/123456789012
- https://www.ebay.com/itm/123456789012
- https://ebay.com/itm/123456789012
- https://pay.ebay.com/rxo?action=view&sessionid=...&itemId=123456789012
- https://pay.ebay.com/rxo/view?itemId=123456789012

Closes #42
2026-04-20 10:49:17 -07:00
349cff8c50 chore: ignore .worktrees/ directory 2026-04-20 10:45:39 -07:00
90f72d6e53 feat(config): add CF_APP_NAME for cf-orch analytics attribution 2026-04-20 07:03:18 -07:00
e539427bec fix: catch sqlite3.OperationalError in search post-processing
Under high concurrency (100+ users), shared_db write contention causes
database is locked errors in the unguarded post-scrape block. These were
surfacing as 500s because there was no exception handler after line 663.

Now catches OperationalError and returns raw listings with empty trust
scores/sellers (degraded mode) instead of crashing. The SSE queue entry
is cleaned up on this path so no orphaned queue accumulates.

Root cause: shared_db (sellers, market_comps) is SQLite; at 100 concurrent
writers the WAL write queue exceeds the 30s busy timeout. Long-term fix
is migrating shared state to Postgres (see snipe#NN).

Refs: infra#12 load test Phase 2 spike findings
2026-04-19 21:26:20 -07:00
ed6d509a26 fix: authenticate eBay public key fetch + add webhook health endpoint
Fixes recurring `400 Missing access token` errors in Snipe logs.
`_fetch_public_key()` was making unauthenticated GET requests to
eBay's Notification API (`/commerce/notification/v1/public_key/{kid}`),
which requires an app-level Bearer token (client_credentials grant).

Wires in the existing `EbayTokenManager` as a lazy module-level
singleton so every public key fetch carries a valid OAuth token.

Also adds `GET /api/ebay/webhook-health` for Uptime Kuma compliance
monitoring — returns 200 + status dict when all five required env vars
are present, 500 with missing var names otherwise.

Runbook: circuitforge-plans/snipe/ebay-webhook-compliance-runbook.md
Kuma monitor: id=19 on heimdall status page (Snipe group)
2026-04-18 22:20:29 -07:00
16cd32b0db fix: body background follows theme tokens + Plausible analytics
- theme.css: add background: var(--color-surface) to body so it responds
  to theme changes (was hardcoded #0d1117 via FOFT guard in index.html,
  causing mixed dark/light on light theme)
- index.html: add Plausible analytics snippet (cookie-free, self-hosted,
  skips localhost; reports to hostname + circuitforge.tech rollup)
- index.html: clarify FOFT guard comment — bundle overrides both html
  and body once loaded
2026-04-17 03:00:16 -07:00
dbe9aaa00b feat: add Plausible analytics to Vue SPA and docs
Some checks failed
CI / Python tests (push) Has been cancelled
CI / Frontend typecheck + tests (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
2026-04-16 21:15:56 -07:00
9c2a2d6bf2 chore: bump changelog for v0.5.1
Some checks failed
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
2026-04-16 13:37:03 -07:00
66ae9eb0b8 feat: reported sellers tracking + community blocklist opt-in
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
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
2026-04-16 13:28:57 -07:00
7005be02c2 test: aggregator coverage for zero_feedback, long_on_market, significant_price_drop
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
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.
2026-04-16 13:14:20 -07:00
873b9a1413 feat: structured auth logging + log analytics foundation
Some checks failed
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
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).
2026-04-16 13:04:46 -07:00
8e40e51ed3 fix: prefix /api/session and /api/preferences with VITE_API_BASE
Some checks are pending
CI / Frontend typecheck + tests (push) Waiting to run
CI / Python tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
Release / release (push) Waiting to run
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.
2026-04-16 12:51:39 -07:00
e428c1446e docs: release v0.5.0 changelog
Some checks failed
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
2026-04-16 12:43:55 -07:00
af51de4cec feat: landing hero copy polish + frontend test suite
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
- 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
2026-04-16 12:39:54 -07:00
9734c50c19 feat: explicit dark/light theme override with Settings toggle
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.
2026-04-16 11:52:10 -07:00
c90061733c feat: listing detail page with trust score ring, signal breakdown, seller panel
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
2026-04-16 11:48:30 -07:00
29922ede47 ci: register browser pytest marker
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
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
2026-04-15 20:30:26 -07:00
f53aae1ae0 ci: add GitHub Actions CI for public credibility badge
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
Lean self-contained workflow — no Forgejo-specific secrets.
circuitforge-core installs from Forgejo git (public repo).
Forgejo (.forgejo/workflows/ci.yml) remains the canonical CI.

Backend: ruff + pytest (browser tests skipped) | Frontend: vue-tsc + vitest
2026-04-15 20:20:13 -07:00
8b6f4b5b90 docs: release v0.4.0 changelog
Some checks failed
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
2026-04-14 16:18:59 -07:00
f8dd1d261d feat: preferences store, community signals, a11y + API fixes
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
- Store: add save_community_signal, get/set_user_preference, get_all_preferences
- App.vue: move skip link before nav (correct a11y order); bootstrap preferences store after session
- useTrustFeedback: prefix fetch URL with VITE_API_BASE (fixes menagerie routing)
- search.ts: guard v-model.number empty-string from cleared price inputs
- Python: isort import ordering pass across adapters, trust, tasks, tests
2026-04-14 16:15:09 -07:00
af1ffa1d94 feat: wire Search with AI to cf-orch → Ollama (llama3.1:8b)
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
- Add app/llm/router.py shim — tri-level config lookup:
  repo config/llm.yaml → ~/.config/circuitforge/llm.yaml → env vars
- Add config/llm.cloud.yaml — ollama via cf-orch, llama3.1:8b
- Add config/llm.yaml.example — self-hosted reference config
- compose.cloud.yml: mount llm.cloud.yaml, set CF_ORCH_URL,
  add host.docker.internal:host-gateway (required on Linux Docker)
- api/main.py: use app.llm.router.LLMRouter (shim) not core directly
- .env.example: update LLM section to reference config/llm.yaml.example
- .gitignore: exclude config/llm.yaml (keep example + cloud yaml)

End-to-end tested: 3.2s for "used RTX 3080 under $400, no mining cards"
via cloud container → host.docker.internal:11434 → Ollama llama3.1:8b
2026-04-14 13:23:44 -07:00
c0a92315d9 fix: rename Build with AI → Search with AI, sync panel CSS to amber theme
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
- All copy: "Build with AI" → "Search with AI", "Build Search" → "Search",
  "Building…" → "Searching…", "Filters updated" → "Filters ready"
- Remove ✦ sparkle from toggle button
- Replace all var(--color-accent) (purple) with var(--app-primary) (amber)
- Fix var(--color-surface-1) → var(--color-surface) (surface-1 does not exist)
- Toggle muted at rest, amber on hover/open — matches sidebar toolbar style
- SettingsView aria-label updated to match
2026-04-14 12:32:33 -07:00
55b278ff8e fix: replace @/ path aliases with relative imports (no alias configured in vite.config)
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-14 12:18:43 -07:00
b5779224b1 fix: sync store.filters and store.query into SearchView local state after LLM populate
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
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.
2026-04-14 12:08:18 -07:00
0919ebc76a feat: LLM query builder — natural language → eBay search filters (snipe#29)
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-14 12:01:31 -07:00
37e34ac820 feat: mount LLMQueryPanel in SearchView and add auto-run setting to SettingsView 2026-04-14 11:50:57 -07:00
53ede9a4c5 feat: LLMQueryPanel collapsible panel with a11y wiring and theme-aware styles 2026-04-14 11:50:22 -07:00
b143962ef6 feat: useLLMQueryBuilder composable with buildQuery, autoRun, and status tracking 2026-04-14 11:49:48 -07:00
65ddd2f4fa feat: add llm_query_builder to SessionFeatures and populateFromLLM to search store 2026-04-14 11:49:22 -07:00
cdc4e40775 feat: POST /api/search/build endpoint with tier gate and category cache wiring 2026-04-14 11:46:15 -07:00
93f989c821 feat: add llm_query_builder tier gate (paid+) to tiers.py and SessionFeatures 2026-04-14 11:44:53 -07:00
7720f1def5 feat: QueryTranslator with domain-aware system prompt and category hint injection 2026-04-14 11:43:19 -07:00
3c54a65dda feat: SearchParamsResponse dataclass and JSON parser for LLM query builder 2026-04-14 11:40:44 -07:00
15718ab431 feat: community category federation in EbayCategoryCache.refresh()
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).
2026-04-14 11:38:12 -07:00
0b8cb63968 feat: EbayCategoryCache refresh from eBay Taxonomy API with bootstrap fallback 2026-04-14 11:14:52 -07:00
7c73186394 feat: EbayCategoryCache get_relevant and get_all_for_prompt 2026-04-14 11:06:13 -07:00
099943b50b feat: EbayCategoryCache init, is_stale, bootstrap seed 2026-04-14 10:49:17 -07:00
979ddd8cce docs: remove Ultra tier, switch to whole-dollar pricing (/0/mo)
Some checks are pending
CI / Python tests (push) Waiting to run
CI / Frontend typecheck + tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-14 10:42:46 -07:00
254fc482db feat: add ebay_categories migration for LLM query builder category cache 2026-04-14 10:40:20 -07:00
3c531f86c4 docs(env): add COMMUNITY_DB_URL to .env.example with guidance 2026-04-14 08:36:58 -07:00
3a4b33d5dd feat: wire community module + corrections router (#31 #32 #33)
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.
2026-04-14 08:33:00 -07:00
72b86834d8 docs: add link to docs.circuitforge.tech/snipe in README
Some checks are pending
CI / Frontend typecheck + tests (push) Waiting to run
CI / Python tests (push) Waiting to run
Mirror / mirror (push) Waiting to run
2026-04-14 08:06:11 -07:00
2dda26a911 feat: infra/devops batch — CI/CD, installer, nginx docs, cf-orch agent (v0.3.0)
Some checks failed
CI / Frontend typecheck + tests (push) Waiting to run
CI / Python tests (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Closes #15, #22, #24, #25. Closes #1 and #27 (already shipped in 0.2.0).

## CI/CD (#22)
- .forgejo/workflows/ci.yml — Python lint (ruff) + pytest + Vue typecheck + vitest
  on every PR/push. Installs cf-core from GitHub mirror for the CI runner.
- .forgejo/workflows/release.yml — Docker build/push (api + web) to Forgejo registry
  on v* tags; git-cliff changelog; multi-arch amd64+arm64.
- .forgejo/workflows/mirror.yml — push to GitHub + Codeberg mirrors.

## Self-hosted installer (#25)
- install.sh rewritten to match CF installer pattern: coloured output, named
  functions, --docker / --bare-metal / --help flags, auto-detect Docker/conda/
  Python/Node/Chromium/Xvfb, license key prompting with format validation.

## Nginx docs (#24)
- docs/nginx-self-hosted.conf — sample nginx config: SPA fallback, SSE proxy
  (proxy_buffering off), long-term asset cache headers.
- docs/getting-started/installation.md — bare-metal install section with nginx
  setup, Chromium/Xvfb note, serve-ui.sh vs nginx trade-off.

## cf-orch agent (#15)
- compose.override.yml — cf-orch-agent sidecar service (profiles: [orch]).
  Starts only with docker compose --profile orch. Registers with coordinator at
  CF_ORCH_COORDINATOR_URL (default 10.1.10.71:7700).
- .env.example — CF_ORCH_URL / CF_ORCH_COORDINATOR_URL comments expanded.

## Docs
- mkdocs.yml + full docs/ tree (getting-started, reference, user-guide) staged
  from prior session work.

Bump version 0.2.0 → 0.3.0.
2026-04-14 06:19:25 -07:00
6d5ceac0a1 docs(screenshots): retake all screenshots post css color-mix fix
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.
2026-04-13 22:48:36 -07:00
d5651e5fe8 fix(css): replace hardcoded rgba colors with color-mix(var(--token)) for light/dark parity
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
2026-04-13 20:56:07 -07:00
c93466c037 feat(mcp): Snipe MCP server for Claude Code integration (#27)
Three tools: snipe_search (GPU-scored trust-ranked), snipe_enrich (deep BTF scraping),
snipe_save (persist search to Snipe UI). GPU inference scoring uses VRAM + arch tier
weighted composite. LLM-condensed output trims verbose listing dicts to trust/price/GPU/url.

Configure via ~/.claude.json with SNIPE_API_URL env var pointing at local or cloud API.
2026-04-13 19:33:47 -07:00
fb81422c54 feat: snipe beta backlog batch (tickets #22/#28/#30/#34/#35/#36/#37/#38)
Cloud/session:
- fix(_extract_session_token): return "" for non-JWT cookie strings (snipe_guest=uuid was
  triggering 401 → forced login redirect for all unauthenticated cloud visitors)
- fix(affiliate): exclude guest: and anonymous users from pref-store writes (#38)
- fix(market-comp): use enriched comp_query for market comp hash so write/read keys match (#30)

Frontend:
- feat(SearchView): unauthenticated landing strip with free-account CTA (#36)
- feat(SearchView): aria-pressed on filter toggles, aria-label on icon buttons, focus-visible
  rings on all interactive controls, live region for result count (#35)
- feat(SearchView): no-results empty-state hint text (#36)
- feat(SEO): og:image 1200x630, summary_large_image twitter card, canonical link (#37)
- feat(OG): generated og-image.png (dark tactical theme, feature pills) (#37)
- feat(settings): TrustSignalPref view wired to /settings route (#28)
- fix(router): /settings route added; unauthenticated access redirects to home (#34)

CI/CD:
- feat(ci): Forgejo Actions workflow (ruff + pytest + vue-tsc + vitest) (#22)
- feat(ci): mirror workflow (GitHub + Codeberg on push to main/tags) (#22)
- feat(ci): release workflow (Docker build+push + git-cliff changelog) (#22)
- chore: git-cliff config (.cliff.toml) for conventional commit changelog (#22)
- chore(pyproject): dev extras (pytest/ruff/httpx), ruff config with ignore list (#22)

Lint:
- fix: remove 11 unused imports across api/, app/, tests/ (ruff F401 clean)
2026-04-13 19:32:50 -07:00
aff5bdda39 chore: bump version to 0.2.0, add CHANGELOG 2026-04-13 05:06:06 -07:00
5006a03603 feat(community): TrustFeedbackButtons + useTrustFeedback -- trust signal UI on ListingCard [MIT]
Files: web/src/composables/useTrustFeedback.ts, web/src/components/TrustFeedbackButtons.vue, web/src/components/ListingCard.vue

- useTrustFeedback composable: FeedbackState machine (idle/sending/confirmed/disputed), fail-soft fetch, always confirms regardless of network outcome
- TrustFeedbackButtons.vue: "This score looks right / This score is wrong" button pair with calm "Thanks, noted." confirmation; uses --trust-high/--trust-low theme CSS vars; aria-live="polite", aria-busy, focus-visible, prefers-reduced-motion, no countdown timers
- ListingCard.vue: TrustFeedbackButtons slotted after trust badge inside .card__score-col
2026-04-12 22:09:33 -07:00