Complete 0.7.1 fixes

This commit is contained in:
hnzio committed 2026-10-26 09:35:19 +01:00
1 parent a08b02c34b
commit 64f55a8b1f
26 files changed
+610 -168

No files matched your search

+123 -33
View File
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import type Database from 'better-sqlite3';
import { createPiece } from './content.js';
import { getDatabase } from './database.js';
import { detectVideoProvider, type PresentationMode } from './presentation.js';
import { detectVideoProvider, type PresentationMode, type VideoProvider } from './presentation.js';
export const SHOW_STATUSES = ['draft', 'ready', 'live', 'completed'] as const;
export const USAGE_STATUSES = ['open', 'current', 'done', 'skipped'] as const;
@@ -10,6 +10,8 @@ export const USAGE_STATUSES = ['open', 'current', 'done', 'skipped'] as const;
export type ShowStatus = (typeof SHOW_STATUSES)[number];
export type EditableShowStatus = Exclude<ShowStatus, 'completed'>;
export type UsageStatus = (typeof USAGE_STATUSES)[number];
export type UsagePreparationStatus = 'preparing' | 'ready';
export type PreparationEffort = 'low' | 'medium' | 'high' | null;
export type ShowListItem = {
id: string;
@@ -28,7 +30,7 @@ export type PreparationUsage = {
showStartsAtUtc: string | null;
sectionName: string;
title: string;
preparationEffort: 'low' | 'medium' | 'high' | null;
preparationEffort: PreparationEffort;
topics: UsageTopic[];
};
@@ -53,6 +55,8 @@ export type PieceUsage = {
originalPieceId: string | null;
position: number;
status: UsageStatus;
preparationStatus: UsagePreparationStatus;
preparationEffort: PreparationEffort;
durationSeconds: number;
title: string;
outputTitle: string;
@@ -70,7 +74,7 @@ export type PieceUsage = {
presentationDescription: string;
presentationImageUrl: string;
presentationSiteName: string;
presentationProvider: string;
presentationProvider: VideoProvider | '';
presentationProviderId: string;
ownNotes: string;
sources: { id: string; label: string; url: string; timecodeSeconds: number | null }[];
@@ -186,6 +190,8 @@ export type PieceUsageUpdate = {
presentationSiteName: string;
ownNotes: string;
durationSeconds: number;
preparationStatus?: UsagePreparationStatus;
preparationEffort?: PreparationEffort;
tools?: { id: string; workUrl: string; note: string }[];
presentationMediaId?: string | null;
};
@@ -231,6 +237,8 @@ type UsageRow = {
original_piece_id: string | null;
position: number;
status: UsageStatus;
preparation_status_snapshot: UsagePreparationStatus;
preparation_effort_snapshot: PreparationEffort;
duration_seconds: number;
title_snapshot: string;
output_title_snapshot: string;
@@ -249,7 +257,7 @@ type UsageRow = {
presentation_description_snapshot: string;
presentation_image_url_snapshot: string;
presentation_site_name_snapshot: string;
presentation_provider_snapshot: string;
presentation_provider_snapshot: VideoProvider | '';
presentation_provider_id_snapshot: string;
own_notes: string;
};
@@ -470,6 +478,8 @@ function usageFromRow(database: Database.Database, row: UsageRow): PieceUsage {
originalPieceId: row.original_piece_id,
position: row.position,
status: row.status,
preparationStatus: row.preparation_status_snapshot,
preparationEffort: row.preparation_effort_snapshot,
durationSeconds: row.duration_seconds,
title: row.title_snapshot,
outputTitle: row.output_title_snapshot,
@@ -786,16 +796,16 @@ export function listPreparationUsages(): PreparationUsage[] {
.prepare(
`SELECT u.id AS usage_id, sh.id AS show_id, sh.title AS show_title,
sh.starts_at_utc AS show_starts_at_utc, s.name AS section_name,
u.title_snapshot, p.preparation_effort
u.title_snapshot, u.preparation_effort_snapshot
FROM piece_usages u
JOIN show_sections s ON s.id = u.show_section_id
JOIN shows sh ON sh.id = s.show_id
JOIN pieces p ON p.id = u.original_piece_id
WHERE sh.status IN ('draft', 'ready', 'live')
AND s.is_reserve_bank = 0 AND p.deleted_at_utc IS NULL AND p.status = 'preparing'
ORDER BY CASE p.preparation_effort
AND s.is_reserve_bank = 0 AND u.preparation_status_snapshot = 'preparing'
ORDER BY sh.starts_at_utc IS NULL, sh.starts_at_utc,
CASE u.preparation_effort_snapshot
WHEN 'high' THEN 0 WHEN 'medium' THEN 1 WHEN 'low' THEN 2 ELSE 3 END,
sh.starts_at_utc IS NULL, sh.starts_at_utc, s.position, u.position`
s.position, u.position`
)
.all() as {
usage_id: string;
@@ -804,7 +814,7 @@ export function listPreparationUsages(): PreparationUsage[] {
show_starts_at_utc: string | null;
section_name: string;
title_snapshot: string;
preparation_effort: PreparationUsage['preparationEffort'];
preparation_effort_snapshot: PreparationUsage['preparationEffort'];
}[];
const topics = database.prepare(
'SELECT name, icon_name FROM usage_topic_snapshots WHERE usage_id = ? ORDER BY position'
@@ -816,7 +826,7 @@ export function listPreparationUsages(): PreparationUsage[] {
showStartsAtUtc: row.show_starts_at_utc,
sectionName: row.section_name,
title: row.title_snapshot,
preparationEffort: row.preparation_effort,
preparationEffort: row.preparation_effort_snapshot,
topics: (topics.all(row.usage_id) as { name: string; icon_name: string }[]).map((topic) => ({
name: topic.name,
iconName: topic.icon_name
@@ -876,6 +886,7 @@ export function getShow(id: string): Show | null {
const usageRows = database
.prepare(
`SELECT u.id, u.show_section_id, u.original_piece_id, u.position, u.status,
u.preparation_status_snapshot, u.preparation_effort_snapshot,
u.duration_seconds, u.title_snapshot, u.output_title_snapshot, u.intro_snapshot, u.research_snapshot,
u.moderation_snapshot, u.interaction_snapshot, u.transition_snapshot, u.presentation_snapshot,
CASE WHEN u.presentation_image_only_snapshot = 1 THEN 'image' ELSE u.presentation_mode_snapshot END AS presentation_mode_snapshot,
@@ -1138,8 +1149,9 @@ export function duplicateCompletedShow(
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, presentation_image_only_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
presentation_provider_snapshot, presentation_provider_id_snapshot, presentation_image_only_snapshot,
preparation_status_snapshot, preparation_effort_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
for (const section of sections) {
const sectionId = randomUUID();
@@ -1182,6 +1194,8 @@ export function duplicateCompletedShow(
usage.presentation_provider_snapshot,
usage.presentation_provider_id_snapshot,
usage.presentation_image_only_snapshot,
usage.preparation_status_snapshot,
usage.preparation_effort_snapshot,
usage.own_notes,
usage.is_reserve
);
@@ -1361,7 +1375,8 @@ export function addPieceUsage(
COALESCE(ps.url, '') AS presentation_source_url,
ps.timecode_seconds AS presentation_source_timecode,
p.presentation_title, p.presentation_description, p.presentation_image_url,
p.presentation_site_name, p.presentation_provider, p.presentation_provider_id
p.presentation_site_name, p.presentation_provider, p.presentation_provider_id,
p.preparation_effort
FROM pieces p LEFT JOIN piece_sources ps ON ps.piece_id = p.id
AND ps.position = p.presentation_source_position WHERE p.id = ?`
)
@@ -1387,6 +1402,7 @@ export function addPieceUsage(
presentation_provider: string;
presentation_provider_id: string;
status: string;
preparation_effort: PreparationEffort;
}
| undefined;
if (!piece) throw new Error('NOT_FOUND');
@@ -1397,7 +1413,7 @@ export function addPieceUsage(
) {
throw new Error('NOT_FOUND');
}
if (piece.status !== 'ready') throw new Error('PIECE_NOT_READY');
if (!['preparing', 'ready'].includes(piece.status)) throw new Error('PIECE_NOT_READY');
const ids = (
database
.prepare('SELECT id FROM piece_usages WHERE show_section_id = ? ORDER BY position')
@@ -1414,8 +1430,9 @@ export function addPieceUsage(
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)`
presentation_provider_snapshot, presentation_provider_id_snapshot,
preparation_status_snapshot, preparation_effort_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)`
)
.run(
id,
@@ -1442,6 +1459,8 @@ export function addPieceUsage(
piece.presentation_site_name,
piece.presentation_provider,
piece.presentation_provider_id,
piece.status,
piece.preparation_effort,
input.sectionId === null ? 1 : 0
);
snapshotPieceRelations(database, id, input.pieceId);
@@ -1523,6 +1542,7 @@ export function duplicatePieceUsage(
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot,
preparation_status_snapshot, preparation_effort_snapshot,
own_notes, is_reserve FROM piece_usages WHERE id = ?`
)
.get(usageId) as Omit<UsageRow, 'id' | 'show_section_id' | 'position'> & {
@@ -1545,8 +1565,9 @@ export function duplicatePieceUsage(
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
presentation_provider_snapshot, presentation_provider_id_snapshot,
preparation_status_snapshot, preparation_effort_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
id,
@@ -1573,6 +1594,8 @@ export function duplicatePieceUsage(
source.presentation_site_name_snapshot,
source.presentation_provider_snapshot,
source.presentation_provider_id_snapshot,
source.preparation_status_snapshot,
source.preparation_effort_snapshot,
source.own_notes,
source.is_reserve
);
@@ -1601,7 +1624,8 @@ export function refreshPieceUsageFromOriginal(
COALESCE(ps.url, '') AS presentation_source_url,
ps.timecode_seconds AS presentation_source_timecode,
p.presentation_title, p.presentation_description, p.presentation_image_url,
p.presentation_site_name, p.presentation_provider, p.presentation_provider_id
p.presentation_site_name, p.presentation_provider, p.presentation_provider_id,
p.status, p.preparation_effort
FROM piece_usages u JOIN pieces p ON p.id = u.original_piece_id
LEFT JOIN piece_sources ps ON ps.piece_id = p.id
AND ps.position = p.presentation_source_position
@@ -1628,9 +1652,12 @@ export function refreshPieceUsageFromOriginal(
presentation_site_name: string;
presentation_provider: string;
presentation_provider_id: string;
status: string;
preparation_effort: PreparationEffort;
}
| undefined;
if (!source) throw new Error('ORIGINAL_NOT_FOUND');
if (!['preparing', 'ready'].includes(source.status)) throw new Error('PIECE_NOT_READY');
database
.prepare(
`UPDATE piece_usages SET title_snapshot = ?, output_title_snapshot = ?, duration_seconds = ?, intro_snapshot = ?,
@@ -1640,7 +1667,8 @@ export function refreshPieceUsageFromOriginal(
presentation_source_timecode_snapshot = ?, presentation_title_meta_snapshot = ?,
presentation_description_snapshot = ?, presentation_image_url_snapshot = ?,
presentation_site_name_snapshot = ?, presentation_provider_snapshot = ?,
presentation_provider_id_snapshot = ? WHERE id = ?`
presentation_provider_id_snapshot = ?, preparation_status_snapshot = ?,
preparation_effort_snapshot = ? WHERE id = ?`
)
.run(
source.title,
@@ -1663,6 +1691,8 @@ export function refreshPieceUsageFromOriginal(
source.presentation_site_name,
source.presentation_provider,
source.presentation_provider_id,
source.status,
source.preparation_effort,
usageId
);
snapshotPieceRelations(
@@ -1687,6 +1717,18 @@ export function updatePieceUsage(
const title = cleanText(input.title, 200, 'title', true);
const outputTitle = cleanText(input.outputTitle, 300, 'output_title');
const duration = validDuration(input.durationSeconds)!;
if (
input.preparationStatus !== undefined &&
!['preparing', 'ready'].includes(input.preparationStatus)
) {
throw new Error('INVALID_PREPARATION_STATUS');
}
if (
input.preparationEffort !== undefined &&
![null, 'low', 'medium', 'high'].includes(input.preparationEffort)
) {
throw new Error('INVALID_PREPARATION_EFFORT');
}
const values = {
intro: cleanText(input.intro, 50_000, 'intro'),
research: cleanText(input.research, 50_000, 'research'),
@@ -1697,7 +1739,7 @@ export function updatePieceUsage(
presentation: cleanText(input.presentation, 50_000, 'presentation'),
ownNotes: cleanText(input.ownNotes, 50_000, 'notes')
};
const presentation = validPresentation(
const submittedPresentation = validPresentation(
input.presentationMode,
input.presentationSourceLabel,
input.presentationSourceUrl,
@@ -1711,6 +1753,57 @@ export function updatePieceUsage(
return database.transaction(() => {
const result = bumpShow(database, showId, expectedRevision);
usageSection(database, showId, usageId);
const currentPreparation = database
.prepare(
`SELECT preparation_status_snapshot, preparation_effort_snapshot
FROM piece_usages WHERE id = ?`
)
.get(usageId) as {
preparation_status_snapshot: UsagePreparationStatus;
preparation_effort_snapshot: PreparationEffort;
};
let presentation = submittedPresentation;
if (presentation.mode === 'link' || presentation.mode === 'video') {
const source = database
.prepare(
`SELECT label, url, timecode_seconds FROM usage_source_snapshots
WHERE usage_id = ? AND url = ?`
)
.get(usageId, presentation.sourceUrl) as
{ label: string; url: string; timecode_seconds: number | null } | undefined;
if (!source) throw new Error('INVALID_PRESENTATION');
presentation = validPresentation(
presentation.mode,
source.label,
source.url,
source.timecode_seconds,
presentation.title,
presentation.description,
presentation.imageUrl,
presentation.siteName
);
}
const selectedMediaId =
input.presentationMediaId === undefined
? ((
database
.prepare(
'SELECT id FROM usage_media_snapshots WHERE usage_id = ? AND is_presentation = 1'
)
.get(usageId) as { id: string } | undefined
)?.id ?? null)
: input.presentationMediaId;
if (
selectedMediaId !== null &&
!database
.prepare('SELECT 1 FROM usage_media_snapshots WHERE id = ? AND usage_id = ?')
.get(selectedMediaId, usageId)
) {
throw new Error('INVALID_PRESENTATION_MEDIA');
}
if (presentation.mode === 'image' && !selectedMediaId) {
throw new Error('INVALID_PRESENTATION_MEDIA');
}
database
.prepare(
`UPDATE piece_usages SET title_snapshot = ?, output_title_snapshot = ?, intro_snapshot = ?, research_snapshot = ?,
@@ -1720,7 +1813,8 @@ export function updatePieceUsage(
presentation_title_meta_snapshot = ?, presentation_description_snapshot = ?,
presentation_image_url_snapshot = ?, presentation_site_name_snapshot = ?,
presentation_provider_snapshot = ?, presentation_provider_id_snapshot = ?,
own_notes = ?, duration_seconds = ? WHERE id = ?`
own_notes = ?, duration_seconds = ?, preparation_status_snapshot = ?,
preparation_effort_snapshot = ? WHERE id = ?`
)
.run(
title,
@@ -1744,6 +1838,10 @@ export function updatePieceUsage(
presentation.providerId,
values.ownNotes,
duration,
input.preparationStatus ?? currentPreparation.preparation_status_snapshot,
input.preparationEffort === undefined
? currentPreparation.preparation_effort_snapshot
: input.preparationEffort,
usageId
);
if (input.tools) {
@@ -1776,23 +1874,15 @@ export function updatePieceUsage(
}
}
if (input.presentationMediaId !== undefined) {
if (
input.presentationMediaId !== null &&
!database
.prepare('SELECT 1 FROM usage_media_snapshots WHERE id = ? AND usage_id = ?')
.get(input.presentationMediaId, usageId)
) {
throw new Error('INVALID_PRESENTATION_MEDIA');
}
database
.prepare('UPDATE usage_media_snapshots SET is_presentation = 0 WHERE usage_id = ?')
.run(usageId);
if (input.presentationMediaId !== null) {
if (selectedMediaId !== null) {
database
.prepare(
'UPDATE usage_media_snapshots SET is_presentation = 1 WHERE id = ? AND usage_id = ?'
)
.run(input.presentationMediaId, usageId);
.run(selectedMediaId, usageId);
}
}
return result;