feat(organize,web): sidecars follow selected videos; /console/organize?ids= prefill (FE-4 follow-ups)

- planImport (sourcePaths mode): when a selected source is a video file,
  same-directory subtitles / external audio whose stem (language tags
  stripped) matches it join the source list, so the organize wizard, which
  expands a directory into individual videos, no longer drops them.
  Unrelated sidecars stay out; pathGuard applies to the added files.
- Organize records page honours ?ids= (wizard multi-group redirect): shows
  just those records via /files/records, refreshes while running, clear chip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:52:33 +08:00
1 parent 363f85b1a1
commit 2ae780d2a1
5 files changed
+310 -7

No files matched your search

@@ -22,6 +22,7 @@ import {
type PathBinding as DomainBinding, type PathBinding as DomainBinding,
type ExistingPolicy, type ExistingPolicy,
type ExistingTarget, type ExistingTarget,
fileKindOf,
findOtherVersions, findOtherVersions,
type NewBinding, type NewBinding,
type OrganizePlan, type OrganizePlan,
@@ -32,6 +33,7 @@ import {
type QualityFacts, type QualityFacts,
resolveTargetDirectory, resolveTargetDirectory,
type SourceFile, type SourceFile,
sidecarStem,
} from "@umt/domain"; } from "@umt/domain";
import { organizationStatusFromFiles, organizePlanToFileResults, toDomainPathBinding } from "@umt/mapping"; import { organizationStatusFromFiles, organizePlanToFileResults, toDomainPathBinding } from "@umt/mapping";
import { withAttemptToken } from "@umt/tasks"; import { withAttemptToken } from "@umt/tasks";
@@ -192,6 +194,48 @@ async function walk(
} }
} }
/**
* 选中的是单个视频文件时,把同目录里主干名(剥掉 .chs/.eng 等语言标签后)相同的字幕 / 外挂音轨补进源文件列表;
* 配对规则与 planOrganize 的附属文件一致(同目录、主干唯一匹配),这里只负责把它们带进来。
* 目录源已由 walk 递归带上附属文件;已在列表里的不重复加。
*/
async function addSidecarSiblings(
sources: string[],
out: string[],
limit: number,
guard?: ((p: string) => Promise<string>) | undefined,
): Promise<void> {
const have = new Set(out);
const byDir = new Map<string, Set<string>>();
for (const src of sources) {
if (fileKindOf(src) !== "video" || !have.has(src)) continue;
const dir = dirname(src);
const stems = byDir.get(dir) ?? new Set<string>();
stems.add(
basename(src)
.replace(/\.[^.]+$/, "")
.toLowerCase(),
);
byDir.set(dir, stems);
}
for (const [dir, stems] of byDir) {
const names = (await readdir(dir).catch(() => [] as string[])).sort();
for (const n of names) {
if (out.length >= limit) return;
if (n.startsWith(".")) continue;
const kind = fileKindOf(n);
if (kind !== "subtitle" && kind !== "audio") continue;
const p = join(dir, n);
if (have.has(p) || !stems.has(sidecarStem(n).stem)) continue;
const st = await statSafe(p);
if (st.kind !== "ok" || !st.facts.isFile) continue;
if (guard) await guard(p);
out.push(p);
have.add(p);
}
}
}
function libraryKinds(kind: MediaEntityRow["kind"]): string[] { function libraryKinds(kind: MediaEntityRow["kind"]): string[] {
if (kind === "movie") return ["movie", "mixed"]; if (kind === "movie") return ["movie", "mixed"];
if (kind === "variety") return ["variety", "series", "mixed"]; if (kind === "variety") return ["variety", "series", "mixed"];
@@ -302,6 +346,8 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
try { try {
// 多走一个用来判断是否超限 // 多走一个用来判断是否超限
for (const p of req.sourcePaths) await walk(p, paths, max + 1, req.pathGuard); for (const p of req.sourcePaths) await walk(p, paths, max + 1, req.pathGuard);
// 直接选中的视频文件(整理向导把目录展开成逐个视频):同目录同主干的字幕 / 外挂音轨随行进计划
await addSidecarSiblings(req.sourcePaths, paths, max + 1, req.pathGuard);
} catch (err) { } catch (err) {
if (err instanceof PathGuardError) throw err; if (err instanceof PathGuardError) throw err;
return fail("source_missing", { message: errMessage(err) }); return fail("source_missing", { message: errMessage(err) });
@@ -176,6 +176,38 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
await h.db.updateTable("automation_grant").set({ status: "revoked" }).where("id", "=", grant.id).execute(); await h.db.updateTable("automation_grant").set({ status: "revoked" }).where("id", "=", grant.id).execute();
}); });
it("organize:选中的视频带上同目录同主干的字幕 / 外挂音轨,无关字幕不带", async () => {
const dir = join(h.dl, "Some.Show.S01.1080p.WEB-DL");
await writeFile(join(dir, "Some.Show.S01E02.1080p.WEB-DL-GRP.chs.ass"), "sub-ep2");
await writeFile(join(dir, "Some.Show.S01E03.1080p.WEB-DL-GRP.mka"), "audio-ep3");
await writeFile(join(dir, "unrelated.srt"), "noise");
// 整理向导把目录展开成逐个视频后提交
const items = ["Some.Show.S01E02.1080p.WEB-DL-GRP.mkv", "Some.Show.S01E03.1080p.WEB-DL-GRP.mkv"].map((n) => ({
root: h.dlRoot,
path: `Some.Show.S01.1080p.WEB-DL/${n}`,
mediaEntityId: show,
season: 1,
}));
const dry = await h.api("POST", "/files/organize", {
idempotencyKey: key(),
items,
action: "copy",
dryRun: true,
minSizeBytes: 0,
});
expect(dry.status).toBe(200);
const g = dry.body.data.groups[0];
expect(g.error).toBeNull();
const byKind = (k: string) =>
g.files.filter((f: { kind: string }) => f.kind === k).map((f: { source: string; target: string }) => f);
expect(byKind("subtitle")).toHaveLength(1);
expect(byKind("subtitle")[0].source).toBe(join(dir, "Some.Show.S01E02.1080p.WEB-DL-GRP.chs.ass"));
expect(byKind("subtitle")[0].target).toMatch(/S01E02.*\.chi\.zh-cn\.ass$/);
expect(byKind("audio")).toHaveLength(1);
expect(byKind("audio")[0].target).toMatch(/S01E03.*\.mka$/);
expect(JSON.stringify(g)).not.toContain("unrelated.srt");
});
it("organize:路径逃逸同样拒绝", async () => { it("organize:路径逃逸同样拒绝", async () => {
const r = await h.api("POST", "/files/organize", { const r = await h.api("POST", "/files/organize", {
idempotencyKey: key(), idempotencyKey: key(),
+24 -7
View File
@@ -4,6 +4,7 @@ import { EventType } from "@umt/contracts";
import { FileStack, FolderInput, X } from "lucide-react"; import { FileStack, FolderInput, X } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { useFileRecords } from "~/api/files.ts";
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecords } from "~/api/records.ts"; import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecords } from "~/api/records.ts";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
@@ -89,6 +90,12 @@ export default function OrganizePage() {
const [qInput, setQInput] = useState(""); const [qInput, setQInput] = useState("");
const [q, setQ] = useState(""); const [q, setQ] = useState("");
const entityId = params.get("mediaEntityId") ?? undefined; const entityId = params.get("mediaEntityId") ?? undefined;
// 整理向导执行多组后跳来:?ids=<记录 id,…> 只看这几条(执行中每 3 秒刷新)
const ids = (params.get("ids") ?? "")
.split(",")
.map((x) => x.trim())
.filter(Boolean);
const byIds = useFileRecords({ ids }, ids.length > 0);
const navigate = useNavigate(); const navigate = useNavigate();
const qc = useQueryClient(); const qc = useQueryClient();
useEffect(() => { useEffect(() => {
@@ -101,9 +108,13 @@ export default function OrganizePage() {
mediaEntityId: entityId, mediaEntityId: entityId,
q: q || undefined, q: q || undefined,
}); });
useSse([EventType.organizationChanged], () => void qc.invalidateQueries({ queryKey: ["records"] })); useSse([EventType.organizationChanged], () => {
const rows = records.data?.pages.flatMap((p) => p.items); void qc.invalidateQueries({ queryKey: ["records"] });
const filtered = !!(status || trigger || q || entityId); if (ids.length) void qc.invalidateQueries({ queryKey: ["files", "records"] });
});
const pinned = ids.length > 0;
const rows = pinned ? byIds.data?.items : records.data?.pages.flatMap((p) => p.items);
const filtered = !!(status || trigger || q || entityId || pinned);
return ( return (
<> <>
@@ -148,16 +159,22 @@ export default function OrganizePage() {
<X /> <X />
</Button> </Button>
) : null} ) : null}
{pinned ? (
<Button variant="outline" size="sm" onClick={() => setParams({})}>
本次整理的 {ids.length} 条
<X />
</Button>
) : null}
</FilterBar> </FilterBar>
<DataTable <DataTable
columns={COLUMNS} columns={COLUMNS}
rows={rows} rows={rows}
rowKey={(r) => r.id} rowKey={(r) => r.id}
loading={records.isPending} loading={pinned ? byIds.isPending : records.isPending}
error={records.error} error={pinned ? byIds.error : records.error}
onRetry={() => void records.refetch()} onRetry={() => void (pinned ? byIds.refetch() : records.refetch())}
onRowClick={(r) => navigate(`/console/organize/${r.id}`)} onRowClick={(r) => navigate(`/console/organize/${r.id}`)}
hasMore={records.hasNextPage} hasMore={!pinned && records.hasNextPage}
onLoadMore={() => void records.fetchNextPage()} onLoadMore={() => void records.fetchNextPage()}
loadingMore={records.isFetchingNextPage} loadingMore={records.isFetchingNextPage}
empty={{ empty={{
+1
View File
@@ -28,6 +28,7 @@ const STATIC_ROUTES = [
"/console/library?filter=unidentified", "/console/library?filter=unidentified",
"/console/library?view=identities", "/console/library?view=identities",
"/console/organize", "/console/organize",
"/console/organize?ids=00000000-0000-4000-8000-000000000000",
"/console/files", "/console/files",
// 文件管理 v2(FE-4):筛选/排序组合与演示数据(?mock=1 只在开发构建生效,随后 ?mock=0 关掉,免得影响后面的页) // 文件管理 v2(FE-4):筛选/排序组合与演示数据(?mock=1 只在开发构建生效,随后 ?mock=0 关掉,免得影响后面的页)
"/console/files?f=video&sort=size&order=desc&hidden=1", "/console/files?f=video&sort=size&order=desc&hidden=1",
Generated
+207
View File
@@ -45,6 +45,7 @@
"npm:react-dom@^19.2.4": "[email protected]", "npm:react-dom@^19.2.4": "[email protected]",
"npm:[email protected]": "[email protected][email protected]", "npm:[email protected]": "[email protected][email protected]",
"npm:react@^19.2.4": "19.3.0", "npm:react@^19.2.4": "19.3.0",
"npm:recharts@^3.10.1": "[email protected][email protected][email protected]_@[email protected][email protected]",
"npm:shadcn@^3.8.5": "3.8.5_@[email protected][email protected]", "npm:shadcn@^3.8.5": "3.8.5_@[email protected][email protected]",
"npm:sonner@^2.0.7": "2.0.8_@[email protected][email protected][email protected]", "npm:sonner@^2.0.7": "2.0.8_@[email protected][email protected][email protected]",
"npm:tailwind-merge@^3.5.0": "3.7.0", "npm:tailwind-merge@^3.5.0": "3.7.0",
@@ -2110,6 +2111,23 @@
"typescript" "typescript"
] ]
}, },
"@reduxjs/[email protected][email protected][email protected]_@[email protected]": {
"integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
"dependencies": [
"@standard-schema/spec",
"@standard-schema/utils",
"immer",
"react",
"react-redux",
"redux",
"redux-thunk",
"reselect"
],
"optionalPeers": [
"react",
"react-redux"
]
},
"@remix-run/[email protected]": { "@remix-run/[email protected]": {
"integrity": "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==" "integrity": "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA=="
}, },
@@ -2292,6 +2310,12 @@
"@stablelib/[email protected]": { "@stablelib/[email protected]": {
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="
}, },
"@standard-schema/[email protected]": {
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
},
"@standard-schema/[email protected]": {
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
},
"@tailwindcss/[email protected]": { "@tailwindcss/[email protected]": {
"integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
"dependencies": [ "dependencies": [
@@ -2414,6 +2438,42 @@
"assertion-error" "assertion-error"
] ]
}, },
"@types/[email protected]": {
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
},
"@types/[email protected]": {
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"@types/[email protected]": {
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
},
"@types/[email protected]": {
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": [
"@types/d3-color"
]
},
"@types/[email protected]": {
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
},
"@types/[email protected]": {
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"dependencies": [
"@types/d3-time"
]
},
"@types/[email protected]": {
"integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
"dependencies": [
"@types/d3-path"
]
},
"@types/[email protected]": {
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
},
"@types/[email protected]": {
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
"@types/[email protected]": { "@types/[email protected]": {
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="
}, },
@@ -2461,6 +2521,9 @@
"@types/[email protected]": { "@types/[email protected]": {
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==" "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="
}, },
"@types/[email protected]": {
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
},
"@types/[email protected]": { "@types/[email protected]": {
"integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==" "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="
}, },
@@ -2956,6 +3019,61 @@
"[email protected]": { "[email protected]": {
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
}, },
"[email protected]": {
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": [
"internmap"
]
},
"[email protected]": {
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
},
"[email protected]": {
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
},
"[email protected]": {
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="
},
"[email protected]": {
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": [
"d3-color"
]
},
"[email protected]": {
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="
},
"[email protected]": {
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": [
"d3-array",
"d3-format",
"d3-interpolate",
"d3-time",
"d3-time-format"
]
},
"[email protected]": {
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": [
"d3-path"
]
},
"[email protected]": {
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": [
"d3-time"
]
},
"[email protected]": {
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": [
"d3-array"
]
},
"[email protected]": {
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
}, },
@@ -2977,6 +3095,9 @@
"ms" "ms"
] ]
}, },
"[email protected]": {
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==" "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="
}, },
@@ -3137,6 +3258,9 @@
"es-errors" "es-errors"
] ]
}, },
"[email protected]": {
"integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"optionalDependencies": [ "optionalDependencies": [
@@ -3189,6 +3313,9 @@
"[email protected]": { "[email protected]": {
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
}, },
"[email protected]": {
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==" "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="
}, },
@@ -3612,6 +3739,9 @@
"[email protected]": { "[email protected]": {
"integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==" "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q=="
}, },
"[email protected]": {
"integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dependencies": [ "dependencies": [
@@ -3622,6 +3752,9 @@
"[email protected]": { "[email protected]": {
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"[email protected]": {
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==" "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w=="
}, },
@@ -4596,6 +4729,23 @@
"scheduler" "scheduler"
] ]
}, },
"[email protected]": {
"integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA=="
},
"[email protected]_@[email protected][email protected][email protected]": {
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"dependencies": [
"@types/react",
"@types/use-sync-external-store",
"react",
"redux",
"use-sync-external-store"
],
"optionalPeers": [
"@types/react",
"redux"
]
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==" "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="
}, },
@@ -4672,12 +4822,43 @@
"tslib" "tslib"
] ]
}, },
"[email protected][email protected][email protected][email protected]_@[email protected][email protected]": {
"integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
"dependencies": [
"@reduxjs/toolkit",
"clsx",
"decimal.js-light",
"es-toolkit",
"eventemitter3",
"immer",
"react",
"react-dom",
"react-is",
"react-redux",
"reselect",
"tiny-invariant",
"use-sync-external-store",
"victory-vendor"
]
},
"[email protected][email protected]": {
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"dependencies": [
"redux"
]
},
"[email protected]": {
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
}, },
"[email protected]": { "[email protected]": {
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
}, },
"[email protected]": {
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
}, },
@@ -5195,6 +5376,12 @@
"@types/react" "@types/react"
] ]
}, },
"[email protected][email protected]": {
"integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
"dependencies": [
"react"
]
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
@@ -5213,6 +5400,25 @@
"[email protected]": { "[email protected]": {
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"[email protected]": {
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"dependencies": [
"@types/d3-array",
"@types/d3-ease",
"@types/d3-interpolate",
"@types/d3-scale",
"@types/d3-shape",
"@types/d3-time",
"@types/d3-timer",
"d3-array",
"d3-ease",
"d3-interpolate",
"d3-scale",
"d3-shape",
"d3-time",
"d3-timer"
]
},
"[email protected]_@[email protected][email protected]": { "[email protected]_@[email protected][email protected]": {
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
"dependencies": [ "dependencies": [
@@ -5446,6 +5652,7 @@
"npm:react-dom@^19.2.4", "npm:react-dom@^19.2.4",
"npm:[email protected]", "npm:[email protected]",
"npm:react@^19.2.4", "npm:react@^19.2.4",
"npm:recharts@^3.10.1",
"npm:shadcn@^3.8.5", "npm:shadcn@^3.8.5",
"npm:sonner@^2.0.7", "npm:sonner@^2.0.7",
"npm:tailwind-merge@^3.5.0", "npm:tailwind-merge@^3.5.0",