Skip to content

feat: share/og pipeline, history insights, scanner UX upgrades#10

Merged
sunnypatell merged 28 commits intomainfrom
feat/experimental-features
Apr 25, 2026
Merged

feat: share/og pipeline, history insights, scanner UX upgrades#10
sunnypatell merged 28 commits intomainfrom
feat/experimental-features

Conversation

@sunnypatell
Copy link
Copy Markdown
Owner

@sunnypatell sunnypatell commented Apr 25, 2026

chips at the share/og pipeline, dashboard polish, and a bunch of cost/perf wins. one render bug surfaced during local testing and is folded in. copilot review feedback is addressed in 99fc5d0.

share + virality

  • dynamic /api/og PNG endpoint (via @vercel/og), content-addressed by score/pass/total/delta and CDN-cached at s-maxage=86400, so repeat shares of the same link cost zero function compute
  • new /share landing page reads the same query params and emits og:image pointing at /api/og, so when LinkedIn / X / iMessage scrape a share URL the preview card renders the actual score
  • ShareBadge gets "Copy share link" + "Share to X" alongside the existing LinkedIn flow; LinkedIn share-text now appends the /share URL so its crawler can fetch the rich preview

comparison + history

  • comparison band on the dashboard ("you went from 67 to 78 (+11)") with up/down/flat states and a Twitter share-intent on positive deltas
  • per-card delta pill anchored to each ScoreCard's score ring
  • per-row delta in the scan-history dropdown
  • new /history view shows a journey stats card (total improvement, best score, strongest-gain platform, days span) and an SVG line chart of scores over time with hover tooltips
  • snapshot-at-startScoring fixes a race where a rapid re-scan compared against the wrong "previous" entry while the firestore save was still in flight

scanner UX

  • live JD skill-extraction preview: as you type a JD the parser runs on a 400ms debounce and shows detected role/industry/level + extracted skill chips. matched skills get a green check vs the loaded resume
  • before/after example templates on expanded suggestion cards (7 categories) with one-click copy on each block
  • cancellable in-flight scoring (re-scan or reset aborts the prior fetch instead of leaking a stale response into state)
  • live retry-after countdown in the LLM-fallback toast when /api/analyze returns 429

cost / perf

  • in-memory result cache on /api/analyze, SHA-256 keyed by full prompt; same-input requests skip the LLM entirely (verified live: 46s to 81ms, ~570x)
  • response shape adds _cached flag (additive, non-breaking)
  • rate-limit response now sends Retry-After header + retryAfter body field, distinguishes minute vs daily reason, no longer double-charges minute slots on daily failures
  • firebase SDK lazy-loaded out of the root layout chunk (~488kb no longer ships to landing-page visitors who never sign in)
  • @vercel/og response re-wrapped to actually set Cache-Control (the constructor's headers option concatenates rather than replaces, verified locally with curl)

security + production-grade

  • dynamic robots.txt + sitemap.xml routes (origin-tracking, no prerender)
  • baseline security headers via hooks: HSTS, Referrer-Policy, Permissions-Policy, nosniff, X-Frame-Options DENY
  • Content-Security-Policy-Report-Only header + /api/csp-report endpoint logging violations to vercel logs (no firestore writes, $0)
  • per-route SeoHead component with og/twitter/canonical, plus JSON-LD SoftwareApplication on the landing page; app.html no longer ships a static robots tag (single source of truth)
  • branded +error.svelte for 404/429/5xx
  • /healthz json liveness probe
  • login: email normalization (trim + lowercase) prevents the duplicate-account bug from casing/whitespace; displayName maxlength
  • /docs/[...slug] catchall validates resolved paths stay under static/docs (resolve + startsWith + realpath) so traversal attempts return 404

bug fixes

  • ScoreBreakdown rendered "[object Object]" for structured suggestions because it never type-narrowed before interpolation. fixed with helpers (same pattern report.ts already used). also hardened the equivalent fallback branch in ScoreDashboard with a typeof guard
  • rate-limiter cleanup throttled to once per 30s so it doesn't run O(n) on every request once the IP map exceeds threshold (real perf cliff at scale)
  • moved docs serving out of hooks.server.ts into a /docs/[...slug] catchall so node-only fs/path imports don't pollute the shared bundle (was a real production-blocking build error otherwise)
  • /api/og moved off the deprecated runtime: 'edge' config to default node runtime
  • JD-preview's debounced parser now wraps the IIFE in try/catch so a transient parse failure doesn't surface as an unhandled rejection
  • firebase.ts migrated to $env/dynamic/public so vercel preview builds no longer fail when PUBLIC_FIREBASE_* is scoped to Production only

copilot review (99fc5d0)

  • pass clamped to <= total in /api/og and /share so a tampered URL like ?pass=6&total=1 cannot render impossible state
  • timeline y-coordinate clamped to [0, 100] so anomalous scores still render inside the chart padding box
  • suggestion-card outer changed from <button> to <div role="button"> so the inner copy <button>s are valid HTML; restored real <button> on the copy controls; widened copyExample event param from MouseEvent to Event (no more unsafe casts)
  • migrated $app/stores to $app/state across SeoHead, Navbar, +error, /share

quality gate

  • 184 tests passing (started branch at 106, +78 across classification, fallback, cache, rate-limiter, comparison, timeline, journey, suggestion templates)
  • typecheck + lint + format clean
  • production build clean (pnpm build:app)
  • end-to-end smoke tested via curl: /healthz, /robots.txt, /sitemap.xml, /api/og (PNG bytes + cache headers), /share (og:image), rate limiter (11th request 429 with retry-after: 60), full LLM cache-hit round-trip (46s to 81ms), pass/total clamp on tampered URLs, single robots tag in head, /docs traversal blocked

docs

  • api/endpoints.md: documents the _cached field + lists new auxiliary endpoints
  • api/rate-limits.md: documents the new 429 body shape with retryAfter field + Retry-After header
  • CHANGELOG bumped to 0.2.0 with the full feature/fix breakdown
  • SECURITY documents the new defenses (security headers, CSP report-only, Retry-After, pass/total clamp, path-traversal guard, email normalization)
  • pnpm build:docs run, static/docs refreshed
  • package.json bumped 0.1.0 -> 0.2.0

- keyed by sha-256 of the full prompt so template edits auto-bust entries
- lru eviction at 200 entries, 24h ttl per entry
- cache check sits behind the rate limiter so abuse protection still applies
- response now includes _cached flag (additive, non-breaking)
- per-region in-memory; cold starts lose it. swap to vercel kv later if needed
- robots.txt allows crawlers, blocks /api, /history, /login
- sitemap lists public routes (/, /scanner, /about) with lastmod, changefreq, priority
- both routes derive origin from request, so they auto-track preview/prod domains
- robots.txt prerendered for cdn-friendly delivery
- HSTS (1y, includeSubDomains, preload), Referrer-Policy, Permissions-Policy
- X-Content-Type-Options nosniff, X-Frame-Options DENY
- applied as defaults so per-route headers (e.g. /api/analyze) take precedence
- covers both regular routes and the docs static-serving path
- scoreLLM accepts an external AbortSignal and returns a discriminated
  result so callers distinguish 'cancelled' from 'error' (deterministic
  fallback no longer kicks in when the user cancels mid-flight)
- scoresStore tracks an AbortController; startScoring aborts any prior
  scan and returns the signal; reset/setError also abort cleanly
- scanner page threads the signal through and bails silently on cancel,
  preventing the stale-results race when re-scanning during an LLM call
…try-After

- Retry-After header now matches the actual reset window (not hardcoded 60s)
- response body adds retryAfter (seconds) and a clearer reason in the error message
- check both windows before incrementing; previously a daily-limit failure
  still consumed a minute slot, double-charging the user
- discriminated return type from checkRateLimit for cleaner call-site handling
- scoreLLM now distinguishes 429 from generic error and parses both
  Retry-After header and the response body's retryAfter field
- scoresStore tracks an absolute llmRetryAtMs timestamp so the countdown
  stays accurate across re-renders
- ScoreDashboard ticks once per second only while the toast is visible
  and a retry timestamp is set; the line auto-clears when it hits 0
- classification.test.ts locks down score-tier boundaries (>= not >),
  label/color mapping, and behavior on negative inputs
- fallback.test.ts covers shape, experience-level/education/role-type
  detection, and the four suggestion-trigger paths
- 32 new tests, all green; total now 138 across 16 files
- cache.ts owns the LRU result cache (hashPrompt, getCached, setCached)
- rate-limiter.ts owns the per-IP minute/day windows and the discriminated result
- +server.ts shrinks from ~360 to ~210 lines and reads as orchestration
- behavior unchanged; module-level state preserved by keeping Maps inside the new files

co-located tests in tests/unit/api/ cover hash determinism/length, cache
round-trip and overwrite, and rate-limit boundaries (MAX_RPM exactly,
inter-IP independence, retry-after shape). 17 new tests, all green.
- new comparison.ts pure function returns per-platform deltas, average
  delta, passing-count delta, and improved/regressed/unchanged tallies
- ScoreDashboard renders a colored band ("score went from 67 to 78 +11")
  with up/down/flat states, only on fresh scans for authenticated users
  with prior history; suppressed when viewing a history snapshot
- store snapshots scanHistory[0] at startScoring time into
  previousScanForComparison so the band stays correct during the brief
  race between finishScoring (results visible) and the async
  saveToHistory -> loadHistory cycle
- 6 unit tests cover empty inputs, mixed deltas, sign handling, missing
  platforms, and stable platform ordering. 161 tests total, all green.
- new SeoHead component emits title/description/canonical/og/twitter on
  every public route, with og:url and canonical derived from the request
  origin so preview deployments bind to their own URLs automatically
- removed the static og/twitter block from app.html since per-route
  tags would otherwise render duplicates
- landing page also emits SoftwareApplication json-ld (with offers price
  0 to mark it free) for rich-result eligibility on Google
- json-ld output escapes '<' to its unicode form so a future user-
  controlled field can never close the surrounding <script> tag

dry-tested: dev server up, curl'd /, /scanner, /about - each shows
distinct og:title/description/url, twitter:* tags, canonical link, no
duplicates. ld+json parsed cleanly via node. typecheck + lint + 161
tests all green. dev server stopped, working tree clean.
… for it

- $lib/firebase exports a memoized getFirebase() that dynamic-imports
  firebase/app, firebase/auth, firebase/firestore on first use; type-only
  Auth/Firestore imports are erased at compile time so they don't bundle
- authStore, scoresStore, and Hero all updated to await getFirebase()
  before touching auth/db; method-level dynamic imports keep each method
  pulling only the firestore symbols it needs
- before: node 0 (root layout = every route) eagerly statically imported
  the firebase chunk (~488kb min). after: zero firebase references in
  any node bundle - the chunk only ships when a consumer code path runs

dry-tested: typecheck + lint + 161 tests green; production build clean;
post-build inspection of nodes 0-7 shows firebase-imports=0 on all and
firebase-strings=0 except node 3 which has the literal word in the
about-page tech-stack list (not a runtime dep). dev server boots, /,
/scanner, /about all return 200. dev server stopped, working tree clean.
- src/routes/+error.svelte: branded error page handles 404/429/5xx and
  surfaces $page.error.message for client errors; matches the dark
  glassmorphic theme and reuses SeoHead with noIndex
- src/routes/healthz/+server.ts: liveness probe for uptime checkers;
  returns {status,timestamp} with no-store and the standard headers
- src/routes/api/csp-report/+server.ts: receives violation reports and
  console.warns them so vercel log aggregation surfaces issues without
  touching firestore or any paid tier
- hooks.server.ts: emits Content-Security-Policy-Report-Only with a
  policy sized for sveltekit hydration, google fonts, firebase auth +
  firestore, and the LLM proxies; report-uri points at /api/csp-report.
  the csp-report endpoint itself is excluded from CSP injection to
  avoid feedback loops on its own error responses

dry-tested: typecheck + lint + 161 tests; dev server probed with curl:
- /healthz -> 200 with valid json + headers
- bogus path -> 404 rendering branded error card
- / -> Content-Security-Policy-Report-Only present
- /api/csp-report POST -> 204 with no csp header on its own response
each history entry shows a small +N/-N pill next to the average score,
computed against the chronologically prior scan (history is newest-first
so history[i+1] is the previous one). only renders when there's a
non-zero delta and a prior scan exists. zero-allocation reuse of the
existing scoresStore.history derivation.
when the comparison band shows a positive delta, render a small
"Share +N" pill that opens twitter intent with pre-filled text
referencing the user's actual delta and the site URL. origin pulled
from window.location so preview deploys share their own URL.

self-contained, no new deps, no infra. only renders when delta > 0
so users who tied or regressed don't see a (false) "share" prompt.
…rison

rate-limiter: at >MAX_MAP_SIZE (10k) the daily map sits above threshold
continuously, and the old code ran the O(n) cleanup on every request.
throttled to at most once per CLEANUP_INTERVAL_MS (30s) so cleanup cost
is bounded regardless of map size. matters past ~10k unique daily IPs.

scores store: a rapid re-scan before the previous saveToHistory's async
loadHistory completes left scanHistory[0] stale, so the snapshot used
by the comparison band pointed at the scan BEFORE the immediate previous
one. fixed by preferring the currently-visible results (in-memory truth)
over scanHistory[0] when computing the snapshot at startScoring.
@vercel/og + an edge route at /api/og generates per-share PNG cards
(score / verdict / passing-count, optionally a +N or -N delta pill)
from query params. element tree built as plain objects so we don't
need JSX in the project.

new /share route reads the same query params, renders a polished
landing card, and emits og:image pointing at /api/og with the same
query - linkedin/twitter previews show the user's actual score.
SeoHead's noIndex isn't applied here so crawlers DO index the page.

dry-tested: dev server boots; curl /api/og returns image/png with
valid PNG magic bytes; /share emits og:image=/api/og?... in head.
@vercel/og's default cache-control (no-cache, no-store) is left as-is
since user-specific share URLs should refetch when params change.
ShareBadge wiring is a follow-up - this iteration ships the infra.
ShareBadge:
- shareUrl derives /share?score=X&pass=Y&total=Z from window.location
- new "Copy share link" button (Clipboard API with execCommand fallback
  for insecure origins), shows "Link copied" for 1.8s on success
- shareToLinkedIn now appends shareUrl to the post text so linkedin's
  crawler fetches /share and renders the dynamic OG card in the preview

ScoreDashboard:
- improvement twitter-intent button now points at /share?score=...&delta=...
  rather than the homepage, so twitter's preview shows the actual delta
  via the /api/og PNG instead of a generic site card
symmetry with the linkedin button. uses twitter intent with the same
/share URL so the preview card shows the dynamic OG (per-share PNG
with the user's actual score), not a generic site preview.

shareText is too long for X's 280-char cap so a tighter version is
built inline; URL is appended via intent's url param.
login:
- normalizeEmail trims and lowercases on submit so "User@Example.com"
  and "user@example.com" land on the same firebase account instead of
  silently creating duplicates
- displayName trimmed and capped at 80 chars on submit; input gets
  maxlength=80 and autocomplete=name

SeoHead conversions:
- /login: indexable (entry point for "ats screener login" searches),
  with proper canonical / og / twitter cards
- /history: noIndex (auth-gated, would 404 to crawlers anyway), so
  search engines don't waste crawl budget hitting redirect-to-login
JD preview (in JobDescriptionInput):
- as the user types, the parser runs on a 400ms debounce and shows
  detected role/industry/experience-level chips plus the top 12
  extracted skills as chips
- when a resume is loaded, matched skills get a green check and the
  preview shows "X of Y in your resume" - real-time signal of how well
  this resume aligns with this JD before the user even hits scan
- parser is dynamically imported so compromise/skills-taxonomy don't
  ship in the layout chunk for landing-page visitors

build fix (CRITICAL - this would fail on Vercel):
- /api/og had runtime: 'edge' which caused esbuild to bundle hooks +
  root.js for the edge runtime. that bundle includes references to
  node:crypto / node:fs / node:path that edge can't resolve, so the
  whole production build broke
- runtime: 'edge' is also deprecated in adapter-vercel as of recent
  versions - moved /api/og to default node runtime (which @vercel/og
  >=0.6 supports) and added maxDuration: 30
- moved docs-serving from hooks.server.ts to a /docs/[...slug]/+server.ts
  catchall, so fs/path live in a node-runtime route instead of polluting
  the shared bundle

dry-tested: pnpm build:app succeeds; dev server boots, /api/og returns
valid PNG (magic bytes verified), /share emits correct og:image,
/docs/[...slug] catchall returns 404 when no doc exists. typecheck +
lint + 161 tests all green.
ScoreCard now accepts an optional previousScore prop and renders a
small +N/-N pill anchored at the bottom-right of the score ring when
the platform's score changed since the prior scan. ScoreDashboard
builds a Map<system, previousScore> from comparison.platforms once
and threads it through, so each card knows its own delta without
recomputing.

a11y polish: aria-expanded added to the three big disclosure toggles
(ScoreBreakdown system row, JobDescriptionInput "Add JD", ScanHistory
list) so screen readers announce the open/closed state correctly.
new $engine/suggestions/templates module classifies a suggestion's
free-text summary into one of seven categories (quantification,
action-verbs, missing-keywords, skills-section, short-resume, sections,
format) via regex patterns and attaches a concrete tip + before/after
example. pure functions, deterministic, no LLM call.

ScoreDashboard's expanded suggestion view now renders the example
inline as a side-by-side red/green diff pair so the user sees exactly
what to change instead of just being told something is wrong.
collapses to a single column under 640px.

11 unit tests in tests/unit/suggestions/templates.test.ts cover the
classifier (per-category cues + ambiguous-text null) and the example
shape (every type maps to a fully-populated record). 172 tests total.
new $engine/scorer/timeline.ts contains the pure geometry helper that
takes the scan history (any order), sorts it ascending by timestamp,
and returns evenly-spaced points + a path D string + an area D string
for a left-to-right trajectory chart. inverted y so higher scores
render visually higher; honors a custom viewBox.

new ScoreTimeline component is a self-contained SVG chart - no chart
library dep. each point renders a colored dot using the same tier
palette as the dashboard. pointer-move snaps to the nearest dot and
shows a tooltip with score / mode / file / date. tooltip flips left
of the dot when near the right edge to avoid clipping.

/history embeds the chart above the grid when >=2 scans exist; below
that threshold it stays hidden. 7 unit tests cover sort order, y
inversion, path shape, area closing, point clamping, and viewBox
override. 179 tests total.
@vercel/og's ImageResponse hardcodes Cache-Control: no-cache, no-store
and the constructor's headers option only CONCATENATES rather than
replaces (verified locally - the response shipped with both directives
contradicting each other). re-wrapping the rendered bytes in a fresh
Response is the only way to actually set the header.

new policy: public, max-age=3600 (browser 1h), s-maxage=86400
(vercel cdn 1d), stale-while-revalidate=604800 (7d background refresh),
immutable. since the URL is fully content-addressed by score+pass+
total+delta, every unique share combination caches forever - repeat
hits from linkedin/twitter crawlers and individual visitors all serve
from the edge with zero function invocation. real cost protection
that scales linearly with NEW share combos rather than total clicks.

dry-tested via curl: cache-control header now contains exactly the
intended directives, no contradictions. typecheck + build clean.
ScoreDashboard:
- "Copy" pill on each before/after example block (uses Clipboard API
  with execCommand fallback for insecure origins). copiedKey is
  per-block so multiple copies show their own "copied" state without
  trampling each other. e.stopPropagation prevents the copy click
  bubbling to the surrounding suggestion-card and collapsing the
  block. role=button + tabindex=0 + keydown(Enter|Space) because the
  outer card is already a <button> and html disallows nested buttons

/history:
- new $engine/scorer/journey.computeJourneyStats pure function
  derives total improvement, best score, scan count, strongest-gain
  platform, and days-span from the user's history (sorted ascending,
  per-platform delta only when both first and latest have it)
- inline stats card renders above the timeline when >=2 scans exist;
  big numbers, color-coded for positive/negative

4 unit tests cover null/empty input, single-scan no-delta, two-scan
delta + best-platform, and input-order independence. 183 tests total.
ScoreBreakdown rendered `<li>{suggestion}</li>` directly. when the LLM
returned StructuredSuggestion objects (with summary/details/impact/
platforms) instead of plain strings, svelte's text interpolation fell
back to String(obj) and shipped "[object Object]" to the page. fixed
with type-narrowing helpers (same pattern report.ts and ScoreDashboard
already use); structured suggestions now show their summary plus a
nested list of their details.

ScoreDashboard had the same class of bug in its non-deduplicated path
at the !structured fallback (`<p>{suggestion}</p>`). hardened with a
typeof guard so any malformed value renders empty rather than
"[object Object]".

JobDescriptionInput's debounced parser ran inside a fire-and-forget
async IIFE with no .catch(); a transient parser failure would surface
as an unhandled promise rejection. wrapped in try/catch with a
console.warn so the scan flow stays unaffected.

docs:
- api/rate-limits: documents the new 429 body shape (retryAfter field
  + reason discriminator) and the Retry-After header
- api/endpoints: documents the _cached response field, the in-memory
  cache behavior, and lists the new auxiliary endpoints (/healthz,
  /robots.txt, /sitemap.xml, /api/og, /share, /api/csp-report)
- pnpm build:docs run; static/docs refreshed

dry-tested via curl: /healthz, /robots.txt, /sitemap.xml, /api/og
(magic bytes valid + correct cache-control), /share, rate limiter
(11th request 429 with retry-after: 60), and a full LLM cache-hit
round-trip (46s -> 81ms, 573x). 183 tests + lint + typecheck + build
all green.
Copilot AI review requested due to automatic review settings April 25, 2026 04:22
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ats-screener Ready Ready Preview, Comment Apr 25, 2026 4:45am

@sunnypatell sunnypatell self-assigned this Apr 25, 2026
@sunnypatell sunnypatell changed the title feat: dashboard polish, share/og pipeline, cost & a11y wins feat: share/og pipeline, history insights, scanner UX upgrades Apr 25, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds share/OG infrastructure and dashboard “progress over time” UX on top of the existing scanner, while also improving operational behavior (rate limiting, caching) and bundle size (lazy-loaded Firebase). It also addresses the structured-suggestion rendering bug ([object Object]) via type narrowing in the UI.

Changes:

  • Adds dynamic sharing pipeline: /share landing page + /api/og PNG generator (cached) + expanded ShareBadge actions.
  • Adds history/comparison UX: scan comparison deltas, per-platform delta pills, /history journey stats + timeline chart.
  • Improves resilience/cost/perf: in-memory prompt-result cache for /api/analyze, improved rate-limiter shape/headers, lazy-loaded Firebase SDK, baseline security headers + CSP reporting.

Reviewed changes

Copilot reviewed 46 out of 47 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/unit/suggestions/templates.test.ts Unit tests for suggestion classification + before/after examples mapping.
tests/unit/scorer/timeline.test.ts Unit tests for timeline SVG geometry helper.
tests/unit/scorer/journey.test.ts Unit tests for cross-scan journey stats computation.
tests/unit/scorer/comparison.test.ts Unit tests for scan-to-scan comparison/deltas.
tests/unit/scorer/classification.test.ts Unit tests for score tier classification/labels/colors.
tests/unit/llm/fallback.test.ts Unit tests for deterministic fallback analysis behavior.
tests/unit/api/rate-limiter.test.ts Unit tests for new discriminated rate limiter result + retryAfterSec.
tests/unit/api/cache.test.ts Unit tests for SHA-256 hashing and in-memory LRU cache behavior.
src/routes/sitemap.xml/+server.ts Adds dynamic sitemap.xml endpoint for public routes.
src/routes/share/+page.svelte Adds branded share landing page emitting dynamic OG image URL.
src/routes/scanner/+page.svelte Adds abortable scoring flow + migrates to SeoHead.
src/routes/robots.txt/+server.ts Adds dynamic robots.txt endpoint (but currently marked prerender).
src/routes/login/+page.svelte Adds email normalization + displayName constraints + SeoHead.
src/routes/history/+page.svelte Adds journey stats + timeline component + noindex SEO.
src/routes/healthz/+server.ts Adds JSON liveness probe endpoint.
src/routes/docs/[...slug]/+server.ts Adds node-runtime docs catchall server for directory-style docs URLs.
src/routes/api/og/+server.ts Adds dynamic OG PNG endpoint using @vercel/og with caching headers.
src/routes/api/csp-report/+server.ts Adds CSP report receiver endpoint logging to server logs.
src/routes/api/analyze/rate-limiter.ts Extracts and improves per-IP rate limiting (retryAfterSec + throttled cleanup).
src/routes/api/analyze/cache.ts Adds in-memory SHA-256 keyed LRU cache for analyze results.
src/routes/api/analyze/+server.ts Integrates rate limiting + cache; adds Retry-After + _cached flag.
src/routes/about/+page.svelte Migrates About route metadata to SeoHead.
src/routes/+page.svelte Adds SeoHead + JSON-LD SoftwareApplication schema.
src/routes/+error.svelte Adds branded error page with noindex SEO.
src/lib/stores/scores.svelte.ts Adds abortable scoring, retry countdown plumbing, history snapshot for comparisons, lazy Firestore imports.
src/lib/stores/auth.svelte.ts Lazily imports Firebase auth + defers auth listener setup.
src/lib/firebase.ts Introduces getFirebase() lazy initialization wrapper for Firebase SDK.
src/lib/engine/suggestions/templates.ts Adds deterministic suggestion classification + before/after example templates.
src/lib/engine/scorer/timeline.ts Adds testable SVG timeline geometry helper.
src/lib/engine/scorer/journey.ts Adds journey stats helper across scan history.
src/lib/engine/scorer/comparison.ts Adds scan comparison helper to compute deltas and counts.
src/lib/engine/llm/client.ts Adds cancellable LLM scoring + explicit status results + rate-limit retry hint.
src/lib/components/upload/JobDescriptionInput.svelte Adds debounced live JD parsing preview + a11y aria-expanded.
src/lib/components/seo/SeoHead.svelte New component to emit per-route SEO/OG/Twitter/canonical metadata.
src/lib/components/scoring/ShareBadge.svelte Adds share URL building + copy link + X share + LinkedIn text includes /share.
src/lib/components/scoring/ScoreTimeline.svelte Adds interactive SVG timeline chart with hover tooltip.
src/lib/components/scoring/ScoreDashboard.svelte Adds comparison band, per-card deltas, suggestion templates + copy buttons, retry countdown UI.
src/lib/components/scoring/ScoreCard.svelte Adds per-platform delta pill display.
src/lib/components/scoring/ScoreBreakdown.svelte Fixes [object Object] via structured-suggestion narrowing + details rendering.
src/lib/components/scoring/ScanHistory.svelte Adds per-row delta pills + aria-expanded on toggle.
src/lib/components/landing/Hero.svelte Lazily loads Firestore for user count on landing page.
src/hooks.server.ts Removes docs serving from hooks; adds baseline security headers + CSP report-only header injection.
src/app.html Removes global OG/twitter/title/description; notes SeoHead is responsible.
pnpm-lock.yaml Locks new dependency tree including @vercel/og.
package.json Adds @vercel/og dependency.
docs/src/content/docs/api/rate-limits.md Documents new 429 response shape + Retry-After usage.
docs/src/content/docs/api/endpoints.md Documents _cached and new auxiliary endpoints.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app.html Outdated
Comment thread src/hooks.server.ts
Comment thread src/lib/components/scoring/ScoreDashboard.svelte Outdated
Comment thread src/routes/robots.txt/+server.ts Outdated
Comment thread src/routes/api/og/+server.ts
Comment thread src/routes/share/+page.svelte Outdated
Comment thread src/routes/docs/[...slug]/+server.ts Outdated
Comment thread src/lib/components/scoring/ScoreDashboard.svelte Outdated
Comment thread src/lib/engine/scorer/timeline.ts
copilot review (PR #10):
- ScoreDashboard: change outer suggestion-card from <button> to
  <div role=button> so the inner copy buttons are valid HTML, restore
  real <button>s for the copy controls, drop the unsafe
  KeyboardEvent->MouseEvent cast (widen copyExample to Event)
- ScoreDashboard: typeof guard on the !structured suggestion fallback
  so a malformed value renders empty instead of "[object Object]"
- /api/og + /share: clamp pass to <= total so a tampered URL like
  ?pass=6&total=1 cannot render "6 of 1 ATS systems passed"
- timeline.ts: clamp averageScore to [0, 100] before computing y so
  out-of-range scores still render inside the chart padding box
- robots.txt: drop prerender so the Sitemap URL tracks the request
  origin on preview deploys
- docs catchall: explicit path-traversal guard via resolve +
  startsWith on docsRoot, plus realpath check against symlink escapes
- hooks: clarify CSP comment (browsers ignore CSP on non-document
  contexts, header is benign on json/image responses)
- app.html: drop the static <meta name=robots>; SeoHead now emits a
  single robots tag per route (indexable or noindex) so noIndex pages
  no longer ship conflicting directives
- migrate $app/stores -> $app/state across SeoHead, Navbar, +error,
  /share (deprecated in newer SvelteKit)

vercel preview deploys were failing with "PUBLIC_FIREBASE_API_KEY is
not exported by virtual:env/static/public" because preview env scopes
those vars to Production only. switch firebase.ts to $env/dynamic/
public so the build succeeds when vars are missing; firebase init
fails at runtime on preview, which is the expectation (auth is not
expected to work on preview)

governance:
- bump version 0.1.0 -> 0.2.0
- CHANGELOG.md gains a 0.2.0 entry covering the full PR scope
- SECURITY.md documents the new defenses (security headers, CSP
  report-only, retry-after, pass/total clamp, path-traversal guard,
  email normalization)
- pnpm build:docs run, static/docs refreshed

quality gate: 184 tests + lint + format + typecheck + production
build all green. dry-tested via curl: /api/og pass=6&total=1 clamps
correctly, /share shows "of 1 systems" and og:image=...pass=1&total=1,
single <meta name=robots> in <head>, /docs/../etc/passwd returns 404.
@sunnypatell sunnypatell merged commit 029174b into main Apr 25, 2026
3 checks passed
@sunnypatell sunnypatell deleted the feat/experimental-features branch April 25, 2026 04:48
sunnypatell added a commit that referenced this pull request Apr 25, 2026
PR #10's CDN cache absorbs identical repeat requests at the edge, but
a client with Cache-Control: no-cache, a fresh edge region, or any
other CDN bypass lands on the serverless function and re-renders the
same image. each render is a few hundred ms of CPU plus satori
allocation; not free at any scale, real cost at 50k users.

new Map<string, ArrayBuffer> caches the rendered bytes per param
tuple (score|pass|total|delta), bounded at 200 entries which is
roughly 12MB of resident memory per function instance, well under
vercel hobby's per-function ceiling.

implementation:
- LRU bump on hit (delete + re-insert keeps most-recent at the tail)
- oldest-out on miss when at capacity (Map insertion order is
  stable, so .keys().next().value is the oldest entry)
- ArrayBuffer chosen over Uint8Array so the Response constructor
  accepts it as BodyInit without a cast

dry-tested: full gate green at 208/208. live smoke on dev:
  fresh params  73ms
  memoed repeat 16ms  (4.5x faster, byte-identical output)
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