feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)
生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。
- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
(metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)
同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距
依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8013196e3f
commit
ceb66f8c77
265 files changed
+17751
-2483
No files matched your search
@@ -43,6 +43,7 @@ import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { PaginatedList } from "~/components/console/paginated-list.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||
import { DROPPED_STATUS, droppedReason, PURPOSE_LABEL } from "~/components/downloads/plan-format.ts";
|
||||
@@ -143,8 +144,8 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) {
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{bindings.length ? (
|
||||
<ul className="space-y-2">
|
||||
{bindings.map((b) => {
|
||||
<PaginatedList rows={bindings} as="ul" label="目录绑定" resetKey={detail.entity.id} className="space-y-2">
|
||||
{(b) => {
|
||||
const root = rootOf(b.libraryRootId);
|
||||
return (
|
||||
<li key={b.id} className="rounded-lg border border-border/70 px-3 py-2">
|
||||
@@ -161,39 +162,41 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) {
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}}
|
||||
</PaginatedList>
|
||||
) : null}
|
||||
{detail.assets.map((a) => (
|
||||
<div key={a.asset.id} className="rounded-lg border border-border/70 px-3 py-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">{deliveryLine(a.asset) || "未知规格"}</span>
|
||||
{a.asset.status !== "active" ? <StatusBadge tone="warning">{a.asset.status}</StatusBadge> : null}
|
||||
{a.asset.verifiedAt ? null : <StatusBadge>未核验</StatusBadge>}
|
||||
<PaginatedList rows={detail.assets} label="文件资产" resetKey={detail.entity.id} className="space-y-4">
|
||||
{(a) => (
|
||||
<div key={a.asset.id} className="rounded-lg border border-border/70 px-3 py-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">{deliveryLine(a.asset) || "未知规格"}</span>
|
||||
{a.asset.status !== "active" ? <StatusBadge tone="warning">{a.asset.status}</StatusBadge> : null}
|
||||
{a.asset.verifiedAt ? null : <StatusBadge>未核验</StatusBadge>}
|
||||
</div>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{a.locations.map((l) => (
|
||||
<li key={l.id} className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{l.locationKind !== "library" ? (
|
||||
<Chip>{LOCATION_KIND[l.locationKind] ?? l.locationKind}</Chip>
|
||||
) : null}
|
||||
{l.status !== "available" ? <Tone v={LOCATION_STATUS[l.status]} /> : null}
|
||||
{l.libraryRootId ? (
|
||||
<span className="text-meta text-muted-foreground">
|
||||
{rootOf(l.libraryRootId)?.name ?? "媒体库目录"}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-0.5 font-mono text-meta break-all text-muted-foreground">
|
||||
{l.absolutePath ?? l.relativePath}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
{a.locations.length === 0 ? <li className="text-meta text-muted-foreground">没有登记位置</li> : null}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{a.locations.map((l) => (
|
||||
<li key={l.id} className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{l.locationKind !== "library" ? (
|
||||
<Chip>{LOCATION_KIND[l.locationKind] ?? l.locationKind}</Chip>
|
||||
) : null}
|
||||
{l.status !== "available" ? <Tone v={LOCATION_STATUS[l.status]} /> : null}
|
||||
{l.libraryRootId ? (
|
||||
<span className="text-meta text-muted-foreground">
|
||||
{rootOf(l.libraryRootId)?.name ?? "媒体库目录"}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-0.5 font-mono text-meta break-all text-muted-foreground">
|
||||
{l.absolutePath ?? l.relativePath}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
{a.locations.length === 0 ? <li className="text-meta text-muted-foreground">没有登记位置</li> : null}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</PaginatedList>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
@@ -226,11 +229,9 @@ function IdentityPanel({ detail }: { detail: LibraryDetail }) {
|
||||
className="py-8"
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{list.map((i) => (
|
||||
<IdentityRow key={i.id} i={i} />
|
||||
))}
|
||||
</ul>
|
||||
<PaginatedList rows={list} as="ul" label="外部身份" resetKey={detail.entity.id} className="space-y-2">
|
||||
{(i) => <IdentityRow key={i.id} i={i} />}
|
||||
</PaginatedList>
|
||||
)}
|
||||
<IdentityDialog
|
||||
entityId={detail.entity.id}
|
||||
@@ -530,8 +531,13 @@ function SlotsPanel({ detail, units }: { detail: LibraryDetail; units: ContentUn
|
||||
const assetOf = (id: string | null) => detail.assets.find((a) => a.asset.id === id);
|
||||
return (
|
||||
<Panel title="收录版本" description="每个目标单独判断硬条件、软偏好和升级">
|
||||
<div className="grid gap-3 xl:grid-cols-2">
|
||||
{detail.slots.map((s) => {
|
||||
<PaginatedList
|
||||
rows={detail.slots}
|
||||
label="收录版本"
|
||||
resetKey={detail.entity.id}
|
||||
className="grid gap-3 xl:grid-cols-2"
|
||||
>
|
||||
{(s) => {
|
||||
const cur = assetOf(s.currentAssetId);
|
||||
const unit = units.find((u) => u.id === s.contentUnitId);
|
||||
return (
|
||||
@@ -543,8 +549,8 @@ function SlotsPanel({ detail, units }: { detail: LibraryDetail; units: ContentUn
|
||||
candidates={detail.candidates.filter((c) => c.slotId === s.id).slice(0, 5)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
</PaginatedList>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -747,8 +753,14 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
}
|
||||
>
|
||||
{rows.length > 1 ? (
|
||||
<fieldset aria-label="按收录版本看状态" className="m-0 mb-3 flex min-w-0 flex-wrap gap-1.5 border-0 p-0">
|
||||
{rows.map((r, i) => (
|
||||
<PaginatedList
|
||||
rows={rows}
|
||||
as="fieldset"
|
||||
label="按收录版本看状态"
|
||||
resetKey={e.id}
|
||||
className="m-0 mb-3 flex min-w-0 flex-wrap gap-1.5 border-0 p-0"
|
||||
>
|
||||
{(r, i) => (
|
||||
<button
|
||||
key={r.slotId ?? "any"}
|
||||
type="button"
|
||||
@@ -762,8 +774,8 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
>
|
||||
{r.name}
|
||||
</button>
|
||||
))}
|
||||
</fieldset>
|
||||
)}
|
||||
</PaginatedList>
|
||||
) : null}
|
||||
{seasonNums.length === 0 ? (
|
||||
<EmptyState
|
||||
@@ -774,8 +786,8 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
className="py-8"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{seasonNums.map((n) => {
|
||||
<PaginatedList rows={seasonNums} label="季列表" resetKey={`${e.id}:${rowIdx}`} className="space-y-2">
|
||||
{(n) => {
|
||||
const su = seasonUnitOf(n);
|
||||
const eps = episodes.filter((u) => u.seasonNumber === n);
|
||||
const total = su?.episodeTotal ?? (eps.length || null);
|
||||
@@ -810,8 +822,15 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
{eps.length === 0 ? (
|
||||
<p className="text-meta text-muted-foreground">这一季还没有集信息。</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-border/60">
|
||||
{eps.map((u) => {
|
||||
<PaginatedList
|
||||
rows={eps}
|
||||
as="ul"
|
||||
compact
|
||||
label={`第 ${n} 季集列表`}
|
||||
resetKey={`${e.id}:${n}:${rowIdx}`}
|
||||
className="divide-y divide-border/60"
|
||||
>
|
||||
{(u) => {
|
||||
const v = (f: string) => finalOf(u, f)?.value;
|
||||
const str = (x: unknown) => (typeof x === "string" && x ? x : null);
|
||||
const num = (x: unknown) => (typeof x === "number" ? x : null);
|
||||
@@ -843,13 +862,13 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}}
|
||||
</PaginatedList>
|
||||
)}
|
||||
</SeasonSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
</PaginatedList>
|
||||
)}
|
||||
<UnplacedVersions versions={versions.data?.unplaced ?? []} />
|
||||
<UnitFieldDialog entityId={e.id} unit={editing} onOpenChange={(o) => !o && setEditing(null)} />
|
||||
@@ -949,8 +968,14 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
|
||||
{dropped.length ? (
|
||||
<div className={cn("px-4 py-3 sm:px-5", detail.downloads.length ? "border-t border-border/60" : "")}>
|
||||
<p className="mb-2 text-meta text-muted-foreground">没提交到下载器</p>
|
||||
<ul className="space-y-2">
|
||||
{dropped.map(({ plan, releaseTitle }) => {
|
||||
<PaginatedList
|
||||
rows={dropped}
|
||||
as="ul"
|
||||
label="未提交下载计划"
|
||||
resetKey={detail.entity.id}
|
||||
className="space-y-2"
|
||||
>
|
||||
{({ plan, releaseTitle }) => {
|
||||
const st = DROPPED_STATUS[plan.status as keyof typeof DROPPED_STATUS] ?? DROPPED_STATUS.failed;
|
||||
const why = droppedReason(plan.statusReason);
|
||||
return (
|
||||
@@ -977,15 +1002,15 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}}
|
||||
</PaginatedList>
|
||||
</div>
|
||||
) : null}
|
||||
{manual.length ? (
|
||||
<div className="border-t border-border/60 px-4 py-3 sm:px-5">
|
||||
<p className="mb-2 text-meta text-muted-foreground">其他整理记录</p>
|
||||
<ul className="space-y-1.5">
|
||||
{manual.map((r) => (
|
||||
<PaginatedList rows={manual} as="ul" label="其他整理记录" resetKey={detail.entity.id} className="space-y-1.5">
|
||||
{(r) => (
|
||||
<li key={r.id} className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<Tone v={ORG_STATUS[r.status]} />
|
||||
<Link
|
||||
@@ -1000,8 +1025,8 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
|
||||
{r.fileTotal} 个文件 · {formatDateTime(r.createdAt)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</PaginatedList>
|
||||
</div>
|
||||
) : null}
|
||||
</Panel>
|
||||
|
||||
Reference in new issue
Block a user