feat(discover): full reset — clear + rebuild suggestions

POST /api/discover/reset sets discover.resetRequested; the worker, at the
start of the next sweep, clears all PENDING suggestions + contributions +
seed throttles and nulls WatchedArtist.lastDiscoveredAt (keeping
followed/wanted/dismissed decisions), then regenerates everything with current
logic. A 'Reset' button on /discover (2-step confirm) triggers it.

worker 247, web 190 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-15 00:20:17 +02:00
parent 7e27654b5a
commit 9915bfbda2
6 changed files with 113 additions and 8 deletions
@@ -0,0 +1,17 @@
import { describe, it, expect } from "vitest";
import { prisma } from "@/lib/db";
import { POST } from "./route";
describe("POST /api/discover/reset", () => {
it("sets the reset + requested flags so the worker rebuilds on the next sweep", async () => {
const res = await POST();
expect(res.status).toBe(202);
expect((await res.json()).reset).toBe(true);
const cfg = Object.fromEntries(
(await prisma.config.findMany({ where: { key: { in: ["discover.resetRequested", "discover.requested"] } } }))
.map((c) => [c.key, c.value]),
);
expect(cfg["discover.resetRequested"]).toBe("true");
expect(cfg["discover.requested"]).toBe("true");
});
});
+17
View File
@@ -0,0 +1,17 @@
import { prisma } from "@/lib/db";
// POST /api/discover/reset — request a full rebuild: the worker clears the pending
// suggestions, contributions, and seed throttles at the start of the next sweep, then
// regenerates everything with the current logic. Kicks a sweep immediately.
export async function POST() {
await Promise.all(
["discover.resetRequested", "discover.requested"].map((key) =>
prisma.config.upsert({
where: { key },
create: { key, value: "true", secret: false },
update: { value: "true" },
}),
),
);
return Response.json({ reset: true }, { status: 202 });
}
+33 -7
View File
@@ -69,6 +69,7 @@ export function DiscoverClient() {
const [query, setQuery] = useState("");
const [search, setSearch] = useState<SearchResult | null>(null);
const [busy, setBusy] = useState(false);
const [confirmReset, setConfirmReset] = useState(false);
const [followedSimilar, setFollowedSimilar] = useState<Set<string>>(new Set());
const [albumModal, setAlbumModal] = useState<{ a: Album; artistName: string; artistMbid: string } | null>(null);
@@ -106,6 +107,18 @@ export function DiscoverClient() {
setRunning(true);
}
async function resetDiscover() {
setConfirmReset(false);
const res = await fetch("/api/discover/reset", { method: "POST" }).catch(() => null);
if (res && (res.ok || res.status === 202)) {
setRows([]); // clears immediately; the rebuild repopulates
setRunning(true);
toast("Rebuilding suggestions from scratch…");
} else {
toast("Couldn't reset");
}
}
async function runSearch(e: React.FormEvent) {
e.preventDefault();
if (!query.trim()) return;
@@ -169,13 +182,26 @@ export function DiscoverClient() {
<button className="btn accent" onClick={discoverNow} disabled={running}>
{running ? "Discovering…" : "Discover now"}
</button>
<span className="rmeta">
{running
? "Sweeping your followed artists for new suggestions…"
: lastRunAt
? `Last discovered ${agoLabel(lastRunAt)}${result ? ` · ${result}` : ""}`
: "Not yet run — press Discover now to sweep your followed artists"}
</span>
{confirmReset ? (
<>
<span className="rmeta">Clear all suggestions and rebuild from scratch?</span>
<button className="btn sm accent" onClick={resetDiscover}>Reset</button>
<button className="btn sm ghost" onClick={() => setConfirmReset(false)}>Cancel</button>
</>
) : (
<>
<button className="btn sm ghost" onClick={() => setConfirmReset(true)} disabled={running}>
Reset
</button>
<span className="rmeta">
{running
? "Sweeping your followed artists for new suggestions…"
: lastRunAt
? `Last discovered ${agoLabel(lastRunAt)}${result ? ` · ${result}` : ""}`
: "Not yet run — press Discover now to sweep your followed artists"}
</span>
</>
)}
</div>
<SectionHeader title="Find similar" />