|
1 | 1 | <script lang="ts"> |
2 | | - import type { Report, EnrichedLot, Spot } from '$lib/types' |
| 2 | + import type { Report, EnrichedLot } from '$lib/types' |
3 | 3 | import { verdict } from '$lib/types' |
4 | | - import { money, pct, oz, num, today } from '$lib/format' |
5 | | - import { api } from '$lib/api' |
| 4 | + import { money, pct, oz, num } from '$lib/format' |
6 | 5 | import Card from '$lib/components/ui/Card.svelte' |
7 | | - import Button from '$lib/components/ui/Button.svelte' |
8 | 6 | import Badge from '$lib/components/ui/Badge.svelte' |
9 | 7 | import StatCard from './StatCard.svelte' |
10 | | - import Composition from './Composition.svelte' |
11 | | - import StackByType from './StackByType.svelte' |
12 | | - import HuntYield from './HuntYield.svelte' |
13 | | - import HitRateGrid from './HitRateGrid.svelte' |
14 | | - import TrophyFeed from './TrophyFeed.svelte' |
15 | 8 | import { cn } from '$lib/utils' |
16 | 9 | import { Check, TriangleAlert, RadioTower } from 'lucide-svelte' |
17 | 10 |
|
18 | | - let { report, onRefresh }: { report: Report; onRefresh: () => void } = $props() |
| 11 | + // Overview owns the *glance + ledger* altitude only (ADR-012): what you can |
| 12 | + // read without interpreting — is the hunt costing money, what's it worth, is |
| 13 | + // the float square. Analysis (yield-by-bank, hit-rate, trophies, composition, |
| 14 | + // stack-by-type) lives in Insights; the spot *editor* lives in Settings. This |
| 15 | + // view stays read-only. |
| 16 | + let { report }: { report: Report } = $props() |
19 | 17 |
|
20 | 18 | const r = $derived(report) |
21 | 19 | const bullion = $derived(r.lots.filter((l) => l.activity !== 'crh')) |
|
31 | 29 | .map(([d, n]) => `${num(n)} ${d}`) |
32 | 30 | .join(', ') || 'none logged', |
33 | 31 | ) |
34 | | -
|
35 | | - // Inline spot editor. |
36 | | - let spotGold = $state(0) |
37 | | - let spotSilver = $state(0) |
38 | | - let spotPlat = $state(0) |
39 | | - let spotPall = $state(0) |
40 | | - let spotDate = $state('') |
41 | | - let spotBusy = $state(false) |
42 | | - let spotErr = $state('') |
43 | | - $effect(() => { |
44 | | - spotGold = r.spot.gold_usd |
45 | | - spotSilver = r.spot.silver_usd |
46 | | - spotPlat = r.spot.platinum_usd |
47 | | - spotPall = r.spot.palladium_usd |
48 | | - spotDate = r.spot.as_of || today() |
49 | | - }) |
50 | | -
|
51 | | - async function saveSpot() { |
52 | | - spotBusy = true |
53 | | - spotErr = '' |
54 | | - try { |
55 | | - const s: Spot = { |
56 | | - as_of: spotDate || today(), |
57 | | - gold_usd: Number(spotGold) || 0, |
58 | | - silver_usd: Number(spotSilver) || 0, |
59 | | - platinum_usd: Number(spotPlat) || 0, |
60 | | - palladium_usd: Number(spotPall) || 0, |
61 | | - source: 'manual', |
62 | | - } |
63 | | - await api.putSpot(s) |
64 | | - onRefresh() |
65 | | - } catch (e) { |
66 | | - spotErr = (e as Error).message |
67 | | - } finally { |
68 | | - spotBusy = false |
69 | | - } |
70 | | - } |
71 | 32 | </script> |
72 | 33 |
|
73 | 34 | <div class="space-y-6"> |
74 | | - <!-- meta line + spot freshness chip (ADR-007: a background poller refreshes spot) --> |
| 35 | + <!-- meta line + spot freshness chip (ADR-007: a background poller refreshes spot). |
| 36 | + Read-only here; the spot editor moved to Settings (ADR-012 §5). --> |
75 | 37 | <div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1"> |
76 | 38 | <p class="text-sm text-muted-foreground"> |
77 | 39 | As of <span class="font-medium text-foreground">{r.spot.as_of || '—'}</span> |
|
80 | 42 | </p> |
81 | 43 | <span |
82 | 44 | class="inline-flex items-center gap-1.5 rounded-full border bg-muted/40 px-2.5 py-0.5 text-xs text-muted-foreground" |
83 | | - title="Spot prices refresh in the background while the app runs (ADR-007). Manual entry is the offline fallback." |
| 45 | + title="Spot prices refresh in the background while the app runs (ADR-007). Manual entry is the offline fallback in Settings." |
84 | 46 | > |
85 | 47 | <RadioTower class="size-3" /> |
86 | 48 | <span>Spot: {r.spot.source || 'none'}</span> |
|
121 | 83 | <StatCard label="CRH net (cash)" value={money(r.crh_net_real)} sub="finds minus costs" tone={crhTone} /> |
122 | 84 | </div> |
123 | 85 |
|
124 | | - <!-- live composition snapshot --> |
125 | | - <Composition {report} /> |
126 | | - |
127 | | - <!-- unified inventory: stack by coin type (bought + found combined) --> |
128 | | - <StackByType {report} /> |
129 | | - |
130 | 86 | <!-- bullion --> |
131 | 87 | <section class="space-y-2"> |
132 | 88 | <div class="flex items-center justify-between"> |
|
260 | 216 | </div> |
261 | 217 | </section> |
262 | 218 |
|
263 | | - <!-- hunt yield by bank & box --> |
264 | | - {#if r.box_yields?.length} |
265 | | - <HuntYield {report} /> |
266 | | - {/if} |
267 | | - |
268 | | - <!-- greatest hits: finds flagged as trophies (ADR-006) --> |
269 | | - <TrophyFeed {report} /> |
270 | | - |
271 | | - <!-- hit-rate report: 1 per face $, per denom × category × source (ADR-006) --> |
272 | | - <HitRateGrid {report} /> |
273 | | - |
274 | 219 | <!-- realized (sold) --> |
275 | 220 | {#if r.realized?.length} |
276 | 221 | <section class="space-y-2"> |
|
318 | 263 | </Card> |
319 | 264 | </section> |
320 | 265 | {/if} |
321 | | - |
322 | | - <!-- spot updater --> |
323 | | - <section class="space-y-2"> |
324 | | - <h2 class="text-lg font-semibold">Spot prices</h2> |
325 | | - <Card class="flex flex-wrap items-end gap-4 p-4"> |
326 | | - <label class="flex flex-col gap-1 text-xs text-muted-foreground"> |
327 | | - Gold $/ozt |
328 | | - <input |
329 | | - type="number" |
330 | | - step="0.01" |
331 | | - bind:value={spotGold} |
332 | | - class="w-32 rounded-md border border-input bg-card px-2 py-1.5 text-sm text-foreground tnum focus:border-ring focus:outline-none" |
333 | | - /> |
334 | | - </label> |
335 | | - <label class="flex flex-col gap-1 text-xs text-muted-foreground"> |
336 | | - Silver $/ozt |
337 | | - <input |
338 | | - type="number" |
339 | | - step="0.01" |
340 | | - bind:value={spotSilver} |
341 | | - class="w-32 rounded-md border border-input bg-card px-2 py-1.5 text-sm text-foreground tnum focus:border-ring focus:outline-none" |
342 | | - /> |
343 | | - </label> |
344 | | - <label class="flex flex-col gap-1 text-xs text-muted-foreground"> |
345 | | - Platinum $/ozt |
346 | | - <input |
347 | | - type="number" |
348 | | - step="0.01" |
349 | | - bind:value={spotPlat} |
350 | | - class="w-32 rounded-md border border-input bg-card px-2 py-1.5 text-sm text-foreground tnum focus:border-ring focus:outline-none" |
351 | | - /> |
352 | | - </label> |
353 | | - <label class="flex flex-col gap-1 text-xs text-muted-foreground"> |
354 | | - Palladium $/ozt |
355 | | - <input |
356 | | - type="number" |
357 | | - step="0.01" |
358 | | - bind:value={spotPall} |
359 | | - class="w-32 rounded-md border border-input bg-card px-2 py-1.5 text-sm text-foreground tnum focus:border-ring focus:outline-none" |
360 | | - /> |
361 | | - </label> |
362 | | - <label class="flex flex-col gap-1 text-xs text-muted-foreground"> |
363 | | - As of |
364 | | - <input |
365 | | - type="date" |
366 | | - bind:value={spotDate} |
367 | | - class="rounded-md border border-input bg-card px-2 py-1.5 text-sm text-foreground focus:border-ring focus:outline-none" |
368 | | - /> |
369 | | - </label> |
370 | | - <Button onclick={saveSpot} disabled={spotBusy}>Update spot</Button> |
371 | | - {#if spotErr}<span class="text-sm text-destructive">{spotErr}</span>{/if} |
372 | | - </Card> |
373 | | - <p class="text-xs text-muted-foreground"> |
374 | | - Manual entry is the offline fallback; a live spot feed can be wired behind the same endpoint later. |
375 | | - </p> |
376 | | - </section> |
377 | 266 | </div> |
0 commit comments