feat(web): rebuild home as "The Floor" on the Pressing Plant system

Home reads /api/requests + /api/wanted + /api/artists: summary tiles
(on the press / pressed / wanted / watching), the on-the-press job list
(state -> stripe + chip + stepped bar via describeJob; needs-attention shows
a literal recovery note), a request form (test hooks preserved), an empty
state, and a recently-pressed deadwax list. Verified in the real app (dark
desktop + mobile); light theme matches the approved comp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 14:09:42 +02:00
parent fcbe34b027
commit 7f1fe52a7c
2 changed files with 96 additions and 22 deletions
+1 -7
View File
@@ -1,11 +1,5 @@
import { Queue } from "./queue"; import { Queue } from "./queue";
export default function Home() { export default function Home() {
return ( return <Queue />;
<main>
<h1>Lyra</h1>
<p><a href="/artists">Artists</a> · <a href="/discover">Discover</a> · <a href="/wanted">Wanted</a> · <a href="/settings">Settings</a></p>
<Queue />
</main>
);
} }
+95 -15
View File
@@ -1,24 +1,47 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { describeJob } from "./_ui/status";
import { StatTiles } from "./_ui/stat-tiles";
import { SectionHeader } from "./_ui/section-header";
import { JobRow } from "./_ui/job-row";
import { PressedList } from "./_ui/pressed-list";
type Row = { type Row = {
id: string; id: string;
artist: string; artist: string;
album: string; album: string;
status: string; status: string;
createdAt?: string;
job: { state: string; currentStage: string } | null; job: { state: string; currentStage: string } | null;
}; };
function jobOf(r: Row) {
return r.job ?? { state: "requested", currentStage: "intake" };
}
function pressedMark(r: Row): string {
if (!r.createdAt) return "Lyra";
const d = new Date(r.createdAt);
return `Lyra · ${d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" }).toUpperCase()}`;
}
export function Queue() { export function Queue() {
const [rows, setRows] = useState<Row[]>([]); const [rows, setRows] = useState<Row[]>([]);
const [wanted, setWanted] = useState(0);
const [watching, setWatching] = useState(0);
const [artist, setArtist] = useState(""); const [artist, setArtist] = useState("");
const [album, setAlbum] = useState(""); const [album, setAlbum] = useState("");
async function refresh() { async function refresh() {
const res = await fetch("/api/requests"); const [reqRes, wantRes, artRes] = await Promise.all([
const body = await res.json(); fetch("/api/requests"),
setRows(body.requests); fetch("/api/wanted"),
fetch("/api/artists"),
]);
setRows((await reqRes.json()).requests ?? []);
setWanted(((await wantRes.json()).wanted ?? []).length);
setWatching(((await artRes.json()).artists ?? []).length);
} }
useEffect(() => { useEffect(() => {
@@ -40,21 +63,78 @@ export function Queue() {
refresh(); refresh();
} }
const active = rows.filter((r) => jobOf(r).state !== "imported");
const pressed = rows.filter((r) => jobOf(r).state === "imported");
return ( return (
<div> <div>
<form onSubmit={submit}> <StatTiles
<input aria-label="artist" placeholder="Artist" value={artist} onChange={(e) => setArtist(e.target.value)} /> items={[
<input aria-label="album" placeholder="Album" value={album} onChange={(e) => setAlbum(e.target.value)} /> { k: "On the press", v: active.length },
<button type="submit">Request</button> { k: "Pressed", v: pressed.length, accent: true },
{ k: "Wanted", v: wanted },
{ k: "Watching", v: watching, unit: "artists" },
]}
/>
<SectionHeader title="On the Press" note={`${active.length} running`} />
<form className="request-form" onSubmit={submit}>
<label className="field">
<span>Artist</span>
<input aria-label="artist" placeholder="Artist" value={artist} onChange={(e) => setArtist(e.target.value)} />
</label>
<label className="field">
<span>Album</span>
<input aria-label="album" placeholder="Album" value={album} onChange={(e) => setAlbum(e.target.value)} />
</label>
<button type="submit" className="btn accent">
Request
</button>
</form> </form>
<ul>
{rows.map((r) => ( {active.length === 0 ? (
<li key={r.id}> <p className="empty">The press is quiet. Queue a release above, or send one over from Wanted or Discover.</p>
{r.artist} {r.album} · <strong>{r.job?.state ?? r.status}</strong> ) : (
{r.job ? ` (${r.job.currentStage})` : ""} <section className="floor">
</li> {active.map((r) => {
))} const j = jobOf(r);
</ul> const d = describeJob(j.state, j.currentStage);
const attention = d.kind === "attention";
return (
<JobRow
key={r.id}
artist={r.artist}
album={r.album}
kind={d.kind}
label={d.label}
step={d.step}
total={d.totalSteps}
indeterminate={j.state === "matching" || j.state === "matched"}
note={
attention
? "No source met the quality cutoff, or the pipeline stalled. Retry the request, or lower the cutoff for this release."
: undefined
}
/>
);
})}
</section>
)}
{pressed.length > 0 ? (
<>
<SectionHeader title="Recently Pressed" note={`${pressed.length} in library`} />
<PressedList
items={pressed.map((r) => ({ id: r.id, artist: r.artist, album: r.album, matrix: pressedMark(r) }))}
/>
</>
) : null}
<div className="colophon">
<span>Lyra self-hosted pressing plant</span>
<span>MusicBrainz · Qobuz · Soulseek · YouTube</span>
</div>
</div> </div>
); );
} }