Context
apps/gittensory-miner-extension/options.js currently persists rankedCandidates to chrome.storage.local whenever a contributor pastes a new discover-run JSON blob, but it doesn't record when that save happened. opportunity-badge.js renders the badge from rankedCandidates but has no way to tell the contributor how stale that data is, unlike ORB's shared RefreshMeta component, which already gives users a relative-time trust signal elsewhere in the product.
Dependencies
None -- independently shippable. This is distinct from all five already-ticketed extension items (#4860-#4863, #4866); none of those touch data-freshness UX, so this can land in any order relative to them.
Requirements
- In apps/gittensory-miner-extension/options.js, when a contributor pastes a new discover-run JSON blob and rankedCandidates is saved to chrome.storage.local, also write a savedAt timestamp (e.g. Date.now()) to the same storage entry/key structure.
- Ensure savedAt is updated every time rankedCandidates is overwritten (re-paste), not just on first save.
- In opportunity-badge.js, read savedAt from chrome.storage.local alongside rankedCandidates.
- Render a relative-time label (e.g. 'last synced 3m ago', 'last synced 2h ago') next to the badge, using the same relative-time formatting/trust-signal visual pattern as ORB's shared RefreshMeta component (mirror its thresholds/format, e.g. seconds/minutes/hours/days buckets).
- Handle the case where savedAt is missing entirely (e.g. data saved before this change shipped, or storage was cleared) -- the badge must degrade gracefully (e.g. omit the label or show a neutral 'sync time unknown' state) rather than rendering NaN or crashing.
- This is UI/storage-schema plumbing only -- do not touch any ranking/scoring logic, governor state, or attempt state; rankedCandidates' contents and meaning must be unchanged, only the accompanying savedAt metadata is new.
Deliverables / Acceptance Criteria
Test Coverage Requirements
This PR must ship with full test coverage for every changed line and branch -- the repo's Codecov patch gate requires 99%+ coverage and the house standard is to aim for 100%, including both sides of every conditional/nullish-coalescing branch introduced. Add: (1) unit tests covering options.js writing savedAt on both first-save and re-paste/overwrite paths, and opportunity-badge.js rendering the label correctly across multiple relative-time buckets (seconds/minutes/hours/days), (2) an invariant test asserting the badge never mutates rankedCandidates itself when reading/deriving the savedAt label (this change must stay read-only with respect to ranking data), and (3) a regression-style test explicitly covering the missing-savedAt case (pre-existing storage without the new field) rendering the graceful fallback instead of a broken/NaN label.
Codecov visibility note: packages/gittensory-miner/**, apps/gittensory-miner-ui/**, and apps/gittensory-miner-extension/** currently sit entirely outside vitest's coverage.include glob, so codecov/patch cannot measure changes there yet — closing that gap is exactly what #4864 and #4865 (in this same milestone) do. Any part of this change under packages/gittensory-engine/src/** or the repo's own src/** remains fully Codecov-instrumented as usual. Either way, this does not lower the bar: treat the 100%-including-invariants-and-regression target above as the enforced house standard regardless of what Codecov can currently see, and it becomes gate-enforced for real once #4864/#4865 ship.
Expected Outcome
A contributor looking at the extension's opportunity badge can immediately tell how fresh their pasted discover-run data is, instead of having no signal at all about whether the ranked candidates they're looking at are current or stale.
Links & Resources
Context
apps/gittensory-miner-extension/options.js currently persists rankedCandidates to chrome.storage.local whenever a contributor pastes a new discover-run JSON blob, but it doesn't record when that save happened. opportunity-badge.js renders the badge from rankedCandidates but has no way to tell the contributor how stale that data is, unlike ORB's shared RefreshMeta component, which already gives users a relative-time trust signal elsewhere in the product.
Dependencies
None -- independently shippable. This is distinct from all five already-ticketed extension items (#4860-#4863, #4866); none of those touch data-freshness UX, so this can land in any order relative to them.
Requirements
Deliverables / Acceptance Criteria
Test Coverage Requirements
This PR must ship with full test coverage for every changed line and branch -- the repo's Codecov patch gate requires 99%+ coverage and the house standard is to aim for 100%, including both sides of every conditional/nullish-coalescing branch introduced. Add: (1) unit tests covering options.js writing savedAt on both first-save and re-paste/overwrite paths, and opportunity-badge.js rendering the label correctly across multiple relative-time buckets (seconds/minutes/hours/days), (2) an invariant test asserting the badge never mutates rankedCandidates itself when reading/deriving the savedAt label (this change must stay read-only with respect to ranking data), and (3) a regression-style test explicitly covering the missing-savedAt case (pre-existing storage without the new field) rendering the graceful fallback instead of a broken/NaN label.
Codecov visibility note:
packages/gittensory-miner/**,apps/gittensory-miner-ui/**, andapps/gittensory-miner-extension/**currently sit entirely outside vitest'scoverage.includeglob, socodecov/patchcannot measure changes there yet — closing that gap is exactly what #4864 and #4865 (in this same milestone) do. Any part of this change underpackages/gittensory-engine/src/**or the repo's ownsrc/**remains fully Codecov-instrumented as usual. Either way, this does not lower the bar: treat the 100%-including-invariants-and-regression target above as the enforced house standard regardless of what Codecov can currently see, and it becomes gate-enforced for real once #4864/#4865 ship.Expected Outcome
A contributor looking at the extension's opportunity badge can immediately tell how fresh their pasted discover-run data is, instead of having no signal at all about whether the ranked candidates they're looking at are current or stale.
Links & Resources
discoveryIndexUrlfield #4861, Add icons to the extension manifest #4862, Add storage-size validation for pasted JSON #4863, Give the browser extension a real package.json and CI wiring #4866 (related but distinct ticketed extension items)