fix(files): a file is added when it is done, not when it was asked for

A download that failed for days and then went through sat days down the library, under its
request date, and read as missing. A job now records when it became done (`ready_at`), stamped
where the expiry already is (`folders.stamp_done`, `finalize_if_running`) and on an insert that is
already done; a check constraint holds every path to done to it. The library sorts and dates its
"Added" by it; the queue keeps the request order.

The migration dates a file from before by its "ready to watch" notice where the inbox still holds
one, else by its request date, where it already sat.
This commit is contained in:
2026-10-12 22:19:37 +02:00
parent 132fb56428
commit 96509fe356
14 changed files with 204 additions and 21 deletions
@@ -11,6 +11,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { api, type FileFolder, type LibraryFile } from "../lib/api";
import { formatDateTime } from "../lib/format";
import {
ALL_FILES,
FilesActionsContext,
@@ -496,6 +497,32 @@ describe("Files · the view", () => {
expect(screen.queryByText("Más")).not.toBeNull();
expect(screen.queryByText("Harmadik")).toBeNull();
});
it("dates and sorts a file by when it was done, not when it was asked for", () => {
// Done in another order than asked: a download that failed for days and then went through is new.
mocks.files = [
file(2, { created_at: "2026-10-01T10:00:00Z", ready_at: "2026-10-05T10:00:00Z" }),
file(3, { created_at: "2026-10-02T10:00:00Z", ready_at: "2026-10-12T10:00:00Z" }),
file(4, { created_at: "2026-10-03T10:00:00Z", ready_at: "2026-10-03T10:00:00Z" }),
];
mocks.folders = [YT];
mount();
const order = () =>
[...document.querySelectorAll<HTMLElement>("[data-nav-index]")].map(
(r) => r.textContent?.match(/Clip \d/)?.[0],
);
const byDone = [
["Clip 4", "Clip 2", "Clip 3"],
["Clip 3", "Clip 2", "Clip 4"],
];
fireEvent.click(screen.getByRole("button", { name: /files\.columns\.added/ }));
expect(byDone).toContainEqual(order());
fireEvent.click(screen.getByRole("button", { name: /files\.columns\.added/ }));
expect(byDone).toContainEqual(order());
expect(
rowOf("Clip 3").querySelector(`[title="${formatDateTime("2026-10-12T10:00:00Z", "en")}"]`),
).not.toBeNull();
});
});
describe("Files · the grid", () => {
+2 -2
View File
@@ -402,9 +402,9 @@ export default function Files() {
align: "right",
nowrap: true,
sortable: true,
sortValue: (f) => f.shared_at ?? f.created_at ?? "",
sortValue: (f) => f.shared_at ?? f.ready_at ?? f.created_at ?? "",
render: (f) => {
const at = f.shared_at ?? f.created_at;
const at = f.shared_at ?? f.ready_at ?? f.created_at;
return (
<span className="text-xs text-muted" title={formatDateTime(at, lang)}>
{relativeTime(at, lang, t)}
+2 -1
View File
@@ -94,7 +94,8 @@ export interface DownloadJob {
error_code: string | null; // failure category, see backend downloads.errors.classify
error_detail: string | null; // host-specific failure-catalog entry, see downloads.errors.diagnose
queue_pos: number;
created_at: string | null;
created_at: string | null; // when it was asked for
ready_at?: string | null; // when it became done — the library's "Added"
source_kind: string;
source_ref: string;
source_url: string | null; // clean "downloaded from" URL (null for edit clips)