@gentbajko/slopify 3.17.0 → 3.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edge/http/actions.js +2 -2
- package/dist/edge/http/actor.js +10 -0
- package/dist/edge/http/app.js +21 -0
- package/dist/edge/http/checkpoints.js +14 -6
- package/dist/edge/http/external-reviews.js +95 -0
- package/dist/edge/http/projects.js +36 -10
- package/dist/edge/http/reviews.js +53 -18
- package/dist/edge/http/revisions.js +5 -1
- package/dist/edge/http/settings.js +9 -1
- package/dist/edge/http/starts.js +41 -0
- package/dist/edge/mcp/attribution.js +38 -0
- package/dist/edge/mcp/call-route.js +203 -0
- package/dist/edge/mcp/excluded-routes.js +95 -0
- package/dist/edge/mcp/file-tools.js +141 -0
- package/dist/edge/mcp/http.js +79 -0
- package/dist/edge/mcp/instructions.js +17 -0
- package/dist/edge/mcp/library-tools.js +143 -0
- package/dist/edge/mcp/remake-tools.js +90 -0
- package/dist/edge/mcp/review-tools.js +64 -0
- package/dist/edge/mcp/route-tool.js +61 -0
- package/dist/edge/mcp/server.js +48 -0
- package/dist/edge/mcp/state-shapes.js +60 -0
- package/dist/edge/mcp/tools-channels.js +153 -0
- package/dist/edge/mcp/tools-library.js +151 -0
- package/dist/edge/mcp/tools-planning.js +137 -0
- package/dist/edge/mcp/tools-projects.js +167 -0
- package/dist/edge/mcp/tools-publishing.js +148 -0
- package/dist/edge/mcp/tools-runs.js +94 -0
- package/dist/edge/mcp/tools-schedules.js +143 -0
- package/dist/edge/mcp/tools-setup.js +234 -0
- package/dist/edge/mcp/tools-storage.js +99 -0
- package/dist/edge/mcp/tools.js +21 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/db/migrations/0055-external-reviews.sql +56 -0
- package/dist/kernel/db/migrations/0056-agent-actions.sql +20 -0
- package/dist/kernel/db/migrations/0057-agent-action-once.sql +5 -0
- package/dist/kernel/db/migrations/0058-revision-work-admission.sql +3 -0
- package/dist/kernel/db/migrations/0059-external-review-canceled.sql +29 -0
- package/dist/kernel/db/migrations/0060-reservation-work.sql +4 -0
- package/dist/kernel/db/migrations/0061-project-control-counts.sql +4 -0
- package/dist/kernel/db/migrations/0062-told-failures.sql +8 -0
- package/dist/kernel/pipeline.js +6 -0
- package/dist/kernel/runner/graph.js +37 -2
- package/dist/kernel/runner/index.js +6 -3
- package/dist/main.js +29 -13
- package/dist/patch-notes/3.18.0.md +13 -0
- package/dist/patch-notes/3.18.1.md +11 -0
- package/dist/patch-notes/3.18.2.md +10 -0
- package/dist/patch-notes/index.json +18 -0
- package/dist/slices/admission/admitted-work.js +53 -0
- package/dist/slices/admission/heads.js +5 -0
- package/dist/slices/admission/repo.js +47 -29
- package/dist/slices/admission/schema.js +1 -0
- package/dist/slices/admission/stage-writes.js +21 -0
- package/dist/slices/admission/start.js +2 -1
- package/dist/slices/admission/state-words.js +110 -0
- package/dist/slices/agents/attention-work.js +233 -0
- package/dist/slices/agents/attention.js +168 -0
- package/dist/slices/agents/connection.js +15 -0
- package/dist/slices/agents/model.js +29 -0
- package/dist/slices/agents/repo.js +66 -0
- package/dist/slices/cancel/index.js +4 -1
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/episodes/summarize.js +2 -1
- package/dist/slices/estimate/index.js +6 -2
- package/dist/slices/images/run.js +2 -1
- package/dist/slices/library/transfer.js +69 -0
- package/dist/slices/narration/run.js +2 -1
- package/dist/slices/notifications/address.js +40 -0
- package/dist/slices/notifications/notifier.js +43 -10
- package/dist/slices/notifications/rules.js +111 -45
- package/dist/slices/notifications/server.js +14 -0
- package/dist/slices/notifications/settings.js +1 -1
- package/dist/slices/notifications/subject.js +30 -0
- package/dist/slices/notifications/told.js +33 -0
- package/dist/slices/play-drafts/readiness.js +9 -2
- package/dist/slices/play-drafts/schema.js +2 -1
- package/dist/slices/rebuild/narration-words.js +76 -0
- package/dist/slices/rebuild/recipe-model.js +1 -0
- package/dist/slices/rebuild/recipe-reviews.js +44 -23
- package/dist/slices/rebuild/recovery-repo.js +0 -20
- package/dist/slices/rebuild/recovery-selection.js +14 -3
- package/dist/slices/rebuild/recovery.js +5 -2
- package/dist/slices/rebuild/remake-outdated.js +27 -0
- package/dist/slices/rebuild/repo.js +7 -2
- package/dist/slices/rebuild/review-redo.js +12 -0
- package/dist/slices/rebuild/runtime-agent-review.js +121 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +26 -11
- package/dist/slices/rebuild/runtime-review.js +12 -2
- package/dist/slices/rebuild/runtime-run.js +11 -2
- package/dist/slices/rebuild/runtime-store.js +32 -5
- package/dist/slices/reruns/index.js +2 -1
- package/dist/slices/research/run.js +2 -1
- package/dist/slices/reviews/external-model.js +21 -0
- package/dist/slices/reviews/external-release.js +38 -0
- package/dist/slices/reviews/external-repo.js +109 -0
- package/dist/slices/reviews/external-rows.js +68 -0
- package/dist/slices/reviews/external-waiting.js +75 -0
- package/dist/slices/reviews/holds.js +144 -0
- package/dist/slices/reviews/model.js +14 -0
- package/dist/slices/reviews/repo.js +27 -10
- package/dist/slices/reviews/rules.js +5 -3
- package/dist/slices/reviews/verdict.js +18 -8
- package/dist/slices/revisions/repo.js +3 -0
- package/dist/slices/revisions/schema.js +1 -0
- package/dist/slices/schedules/agenda.js +47 -17
- package/dist/slices/schedules/prepare.js +6 -0
- package/dist/slices/schedules/schema.js +13 -1
- package/dist/slices/storage/arrange.js +21 -8
- package/dist/slices/storage/consequences.js +27 -0
- package/dist/slices/storage/portable.js +3 -0
- package/dist/slices/storage/project-files.js +66 -0
- package/dist/slices/storage/trim.js +6 -2
- package/dist/slices/subtitles/prepare.js +2 -1
- package/dist/slices/uploads/repo.js +12 -6
- package/dist/slices/uploads/rules.js +5 -0
- package/dist/slices/video/write-export.js +1 -1
- package/dist/slices/video/write-subtitles.js +1 -1
- package/dist/tutorials/Agents-MCP.md +168 -0
- package/dist/tutorials/Calendar.md +2 -1
- package/dist/tutorials/First-Launch-and-Welcome.md +1 -0
- package/dist/tutorials/Home-and-Projects.md +16 -9
- package/dist/tutorials/Home.md +1 -0
- package/dist/tutorials/Notifications.md +25 -6
- package/dist/tutorials/Project-Page.md +9 -3
- package/dist/tutorials/Publishing-to-YouTube.md +1 -0
- package/dist/tutorials/Recovery-and-Retries.md +17 -3
- package/dist/tutorials/Reviews-and-Checkpoints.md +22 -5
- package/dist/tutorials/Schedules.md +1 -1
- package/dist/tutorials/Settings-Reference.md +16 -1
- package/dist/tutorials/Troubleshooting.md +3 -1
- package/dist/tutorials/Where-Your-Files-Live.md +3 -3
- package/dist/tutorials/Your-First-Short.md +1 -0
- package/dist/tutorials/_Sidebar.md +1 -0
- package/dist/web/assets/{index-DpdXRssU.css → index-BKbZIxAq.css} +1 -1
- package/dist/web/assets/index-DASWFus0.js +326 -0
- package/dist/web/assets/{pdf-B42WW32R.js → pdf-CF97Fw5v.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CbmBh8V-.js +0 -325
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { actors } from "../agents/model.js";
|
|
3
|
+
import { reviewAnswerers, reviewStages } from "./model.js";
|
|
3
4
|
const rowSchema = z.object({
|
|
4
5
|
id: z.string(),
|
|
5
6
|
project_id: z.string(),
|
|
@@ -10,12 +11,14 @@ const rowSchema = z.object({
|
|
|
10
11
|
review_fingerprint: z.string(),
|
|
11
12
|
passed: z.number(),
|
|
12
13
|
reasons: z.string(),
|
|
13
|
-
outcome: z.enum(["passed", "flagged", "redo"]),
|
|
14
|
+
outcome: z.enum(["passed", "flagged", "redo", "auto-passed"]),
|
|
14
15
|
attempt: z.number().int(),
|
|
15
16
|
action: z.enum(["overruled", "redone"]).nullable(),
|
|
16
17
|
action_at: z.string().nullable(),
|
|
17
18
|
redo_state: z.enum(["pending", "started", "failed"]).nullable(),
|
|
18
19
|
redo_error: z.string().nullable(),
|
|
20
|
+
answered_by: z.enum(reviewAnswerers),
|
|
21
|
+
action_by: z.enum(actors).nullable(),
|
|
19
22
|
created_at: z.string(),
|
|
20
23
|
});
|
|
21
24
|
function recordOf(value) {
|
|
@@ -36,18 +39,21 @@ function recordOf(value) {
|
|
|
36
39
|
actionAt: row.action_at,
|
|
37
40
|
redoState: row.redo_state,
|
|
38
41
|
redoError: row.redo_error,
|
|
42
|
+
answeredBy: row.answered_by,
|
|
43
|
+
actionBy: row.action_by,
|
|
39
44
|
createdAt: row.created_at,
|
|
40
45
|
};
|
|
41
46
|
}
|
|
42
47
|
// One verdict per review step: the same review asked again (a retried step whose answer was
|
|
43
48
|
// saved but not published) replaces the earlier answer rather than adding a second one.
|
|
44
49
|
export function saveVerdict(db, record) {
|
|
45
|
-
db.prepare(`INSERT INTO review_verdicts(id,project_id,revision_id,item_key,stage,item_fingerprint,review_fingerprint,passed,reasons,outcome,attempt,action,action_at,redo_state,redo_error,created_at)
|
|
46
|
-
VALUES (
|
|
50
|
+
db.prepare(`INSERT INTO review_verdicts(id,project_id,revision_id,item_key,stage,item_fingerprint,review_fingerprint,passed,reasons,outcome,attempt,action,action_at,redo_state,redo_error,answered_by,action_by,created_at)
|
|
51
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
47
52
|
ON CONFLICT(project_id,review_fingerprint) DO UPDATE SET revision_id=excluded.revision_id,
|
|
48
53
|
item_fingerprint=excluded.item_fingerprint,passed=excluded.passed,reasons=excluded.reasons,
|
|
49
|
-
outcome=excluded.outcome,attempt=excluded.attempt,action=NULL,action_at=NULL,
|
|
50
|
-
redo_state=excluded.redo_state,redo_error=NULL,
|
|
54
|
+
outcome=excluded.outcome,attempt=excluded.attempt,action=NULL,action_at=NULL,action_by=NULL,
|
|
55
|
+
redo_state=excluded.redo_state,redo_error=NULL,answered_by=excluded.answered_by,
|
|
56
|
+
created_at=excluded.created_at`).run(record.id, record.projectId, record.revisionId, record.itemKey, record.stage, record.itemFingerprint, record.reviewFingerprint, record.passed ? 1 : 0, JSON.stringify(record.reasons), record.outcome, record.attempt, record.action, record.actionAt, record.redoState, record.redoError, record.answeredBy, record.actionBy, record.createdAt);
|
|
51
57
|
const saved = verdictByReview(db, record.projectId, record.reviewFingerprint);
|
|
52
58
|
if (saved === undefined)
|
|
53
59
|
throw new Error("Slopify hit an internal error (a review's verdict could not be saved). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
@@ -93,10 +99,13 @@ export function setRedoState(db, id, state, error = null) {
|
|
|
93
99
|
// A passed verdict has nothing to overrule. Overrule also calls off an automatic redo that is
|
|
94
100
|
// still waiting to start (what depends on the item then goes ahead with it as it is), but a
|
|
95
101
|
// redo already under way owns the item until it lands.
|
|
96
|
-
export function actOnVerdict(db, projectId, id, action, at) {
|
|
102
|
+
export function actOnVerdict(db, projectId, id, action, at, by = "person") {
|
|
97
103
|
const record = verdictById(db, projectId, id);
|
|
98
104
|
if (record === undefined)
|
|
99
105
|
return { ok: false, reason: "not-found" };
|
|
106
|
+
// The first decision stands: a second person or agent is told it was already made.
|
|
107
|
+
if (record.action !== null)
|
|
108
|
+
return { ok: false, reason: "decided", value: record };
|
|
100
109
|
if (action === "overruled" && record.passed)
|
|
101
110
|
return { ok: false, reason: "conflict" };
|
|
102
111
|
if (record.redoState === "started")
|
|
@@ -105,13 +114,21 @@ export function actOnVerdict(db, projectId, id, action, at) {
|
|
|
105
114
|
if (action !== "overruled")
|
|
106
115
|
return { ok: false, reason: "conflict" };
|
|
107
116
|
const called = db
|
|
108
|
-
.prepare("UPDATE review_verdicts SET action=?, action_at=?, redo_state=NULL WHERE id=? AND redo_state='pending'")
|
|
109
|
-
.run(action, at, id);
|
|
117
|
+
.prepare("UPDATE review_verdicts SET action=?, action_at=?, action_by=?, redo_state=NULL WHERE id=? AND redo_state='pending' AND action IS NULL")
|
|
118
|
+
.run(action, at, by, id);
|
|
110
119
|
if (Number(called.changes) !== 1)
|
|
111
120
|
return { ok: false, reason: "conflict" };
|
|
112
121
|
}
|
|
113
122
|
else {
|
|
114
|
-
|
|
123
|
+
const changed = db
|
|
124
|
+
.prepare("UPDATE review_verdicts SET action=?, action_at=?, action_by=? WHERE id=? AND action IS NULL")
|
|
125
|
+
.run(action, at, by, id);
|
|
126
|
+
if (Number(changed.changes) !== 1) {
|
|
127
|
+
const now = verdictById(db, projectId, id);
|
|
128
|
+
return now === undefined
|
|
129
|
+
? { ok: false, reason: "not-found" }
|
|
130
|
+
: { ok: false, reason: "decided", value: now };
|
|
131
|
+
}
|
|
115
132
|
}
|
|
116
133
|
const updated = verdictById(db, projectId, id);
|
|
117
134
|
return updated === undefined ? { ok: false, reason: "not-found" } : { ok: true, value: updated };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { imageReviewers, reviewModeOf, reviewRetriesMax, reviewRetriesMin, reviewStageLabels, reviewStages, visionStages, } from "./model.js";
|
|
1
|
+
import { imageReviewers, reviewedByAgent, reviewModeOf, reviewRetriesMax, reviewRetriesMin, reviewStageLabels, reviewStages, visionStages, } from "./model.js";
|
|
2
2
|
// A stage is reviewed only while it makes something: a provided article, uploaded images or
|
|
3
3
|
// narration Off leave nothing for a reviewer to look at, so a review left on there asks for
|
|
4
4
|
// nothing and holds nothing up.
|
|
@@ -39,7 +39,8 @@ export function reviewFields(draft) {
|
|
|
39
39
|
if (active.length === 0 || reviews === undefined)
|
|
40
40
|
return [];
|
|
41
41
|
const fields = [];
|
|
42
|
-
|
|
42
|
+
const byAgent = reviewedByAgent(reviews);
|
|
43
|
+
if (!byAgent && (reviews.provider.trim() === "" || reviews.model.trim() === ""))
|
|
43
44
|
fields.push({
|
|
44
45
|
field: "reviews.provider",
|
|
45
46
|
message: "Automatic reviews need a reviewer model. Choose one in the Reviews section, or set every review to Off.",
|
|
@@ -52,7 +53,8 @@ export function reviewFields(draft) {
|
|
|
52
53
|
message: `Enter a number of redos between ${reviewRetriesMin} and ${reviewRetriesMax} in the Reviews section.`,
|
|
53
54
|
});
|
|
54
55
|
const pictures = active.filter((stage) => visionStages.includes(stage));
|
|
55
|
-
if (
|
|
56
|
+
if (!byAgent &&
|
|
57
|
+
pictures.length > 0 &&
|
|
56
58
|
reviews.provider.trim() !== "" &&
|
|
57
59
|
!reviewerSeesImages(reviews.provider))
|
|
58
60
|
fields.push({
|
|
@@ -3,15 +3,28 @@ import { z } from "zod";
|
|
|
3
3
|
// at least one reason, since a reason is what the person reads beside it.
|
|
4
4
|
export const reviewReasonMax = 500;
|
|
5
5
|
export const reviewReasonsMax = 10;
|
|
6
|
-
const
|
|
6
|
+
export const verdictAnswerSchema = z
|
|
7
7
|
.object({
|
|
8
8
|
verdict: z.enum(["pass", "fail"]),
|
|
9
|
-
reasons: z
|
|
9
|
+
reasons: z
|
|
10
|
+
.array(z
|
|
11
|
+
.string()
|
|
12
|
+
.trim()
|
|
13
|
+
.min(1, "a reason can't be empty")
|
|
14
|
+
.max(reviewReasonMax, `a reason is at most ${String(reviewReasonMax)} characters`))
|
|
15
|
+
.max(reviewReasonsMax, `at most ${String(reviewReasonsMax)} reasons`),
|
|
10
16
|
})
|
|
11
17
|
.strict()
|
|
12
18
|
.refine((answer) => answer.verdict === "pass" || answer.reasons.length > 0, {
|
|
13
19
|
message: "a failed verdict names no reason",
|
|
14
20
|
});
|
|
21
|
+
// What was wrong with a verdict, where it was: " (reasons.0): a reason can't be empty". The
|
|
22
|
+
// reviewer's refused answer and an agent's refused verdict say it the same way.
|
|
23
|
+
export function verdictIssue(issues, otherwise) {
|
|
24
|
+
const issue = issues[0];
|
|
25
|
+
const where = issue === undefined || issue.path.length === 0 ? "" : ` (${issue.path.map(String).join(".")})`;
|
|
26
|
+
return `${where}: ${issue?.message ?? otherwise}`;
|
|
27
|
+
}
|
|
15
28
|
const answerRules = 'Answer with one JSON object and nothing else: {"verdict":"pass","reasons":[]} when the item is fine, or {"verdict":"fail","reasons":["..."]} with one short sentence per problem you found (at most 10). Name only real problems you can point to.';
|
|
16
29
|
export function reviewMessages(material) {
|
|
17
30
|
const attached = material.images.length === 0
|
|
@@ -55,15 +68,12 @@ export function parseVerdict(text) {
|
|
|
55
68
|
reason: `The reviewer's answer was not the JSON verdict Slopify asked for. ${retryHint}`,
|
|
56
69
|
};
|
|
57
70
|
}
|
|
58
|
-
const parsed =
|
|
59
|
-
if (!parsed.success)
|
|
60
|
-
const issue = parsed.error.issues[0];
|
|
61
|
-
const where = issue === undefined || issue.path.length === 0 ? "" : ` (${issue.path.join(".")})`;
|
|
71
|
+
const parsed = verdictAnswerSchema.safeParse(value);
|
|
72
|
+
if (!parsed.success)
|
|
62
73
|
return {
|
|
63
74
|
ok: false,
|
|
64
|
-
reason: `The reviewer's verdict was malformed${
|
|
75
|
+
reason: `The reviewer's verdict was malformed${verdictIssue(parsed.error.issues, "it did not match the verdict format")}. ${retryHint}`,
|
|
65
76
|
};
|
|
66
|
-
}
|
|
67
77
|
return {
|
|
68
78
|
ok: true,
|
|
69
79
|
value: { passed: parsed.data.verdict === "pass", reasons: parsed.data.reasons },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { agentRevisionIds } from "../agents/repo.js";
|
|
2
3
|
import { projectRevisionSchema } from "./schema.js";
|
|
3
4
|
export { insertManifestOutput, insertManifestPiece, outputsForRevision, piecesForRevision, selectOutputRecord, selectPieceRecord, } from "./manifest-repo.js";
|
|
4
5
|
const revisionRow = z.object({
|
|
@@ -37,6 +38,7 @@ export function currentRevisionId(db, projectId) {
|
|
|
37
38
|
}
|
|
38
39
|
export function listRevisionHistory(db, projectId) {
|
|
39
40
|
const head = currentRevisionId(db, projectId);
|
|
41
|
+
const byAgent = agentRevisionIds(db, projectId);
|
|
40
42
|
return db
|
|
41
43
|
.prepare("SELECT * FROM project_revisions WHERE project_id=? ORDER BY created_at DESC,id DESC")
|
|
42
44
|
.all(projectId)
|
|
@@ -49,6 +51,7 @@ export function listRevisionHistory(db, projectId) {
|
|
|
49
51
|
title: revision.config.title,
|
|
50
52
|
createdAt: revision.createdAt,
|
|
51
53
|
current: revision.id === head,
|
|
54
|
+
...(byAgent.has(revision.id) ? { byAgent: true } : {}),
|
|
52
55
|
};
|
|
53
56
|
});
|
|
54
57
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { derive } from "../../kernel/runner/graph.js";
|
|
2
|
+
import { derive, progressOf } from "../../kernel/runner/graph.js";
|
|
3
3
|
import { liveProject, stageStandingsByProject } from "../admission/repo.js";
|
|
4
|
+
import { readsByWaitBesideFailure } from "../admission/state-words.js";
|
|
4
5
|
import { queueEntries } from "../batch/index.js";
|
|
5
6
|
import { sampleIds } from "../onboarding/model.js";
|
|
6
7
|
import { readSampleRecord } from "../onboarding/state.js";
|
|
8
|
+
import { projectHolds } from "../reviews/holds.js";
|
|
7
9
|
import { limitWaitsByProject, listingWait } from "../run-cost/panel.js";
|
|
8
|
-
import { uploadedProjects } from "../uploads/repo.js";
|
|
10
|
+
import { setAsideProjects, uploadedAndSettled, uploadedProjects } from "../uploads/repo.js";
|
|
11
|
+
import { settledStatus } from "../uploads/rules.js";
|
|
9
12
|
import { nextOccurrence } from "./calendar.js";
|
|
10
13
|
import { preparedProject } from "./prepare.js";
|
|
11
14
|
import { scheduleRows } from "./repo.js";
|
|
@@ -109,21 +112,22 @@ const projectRow = z.object({
|
|
|
109
112
|
finished_at: z.string().nullable(),
|
|
110
113
|
});
|
|
111
114
|
// Projects the person has something to decide on in the current revision: a review
|
|
112
|
-
// checkpoint holding the run
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
+
// checkpoint holding the run for them (`reviews/holds.ts`: reached, and not a Before Video hold
|
|
116
|
+
// its schedule added, which waits for its day), or an automatic review that failed an item and
|
|
117
|
+
// waits for Overrule or Redo (its latest verdict, with no redo under way), as needs_attention
|
|
118
|
+
// reads them.
|
|
119
|
+
function awaitingReview(db, holds) {
|
|
120
|
+
const checkpoints = [...holds.holding]
|
|
121
|
+
.filter(([, held]) => held.some((hold) => hold.waitsFor === "person"))
|
|
122
|
+
.map(([id]) => id);
|
|
115
123
|
const ids = db
|
|
116
|
-
.prepare(`SELECT
|
|
117
|
-
JOIN project_heads h ON h.project_id=c.project_id AND h.revision_id=c.revision_id
|
|
118
|
-
WHERE c.state IN ('held','pending-review') AND c.approved_at IS NULL
|
|
119
|
-
UNION
|
|
120
|
-
SELECT v.project_id AS id FROM review_verdicts v
|
|
124
|
+
.prepare(`SELECT v.project_id AS id FROM review_verdicts v
|
|
121
125
|
JOIN project_heads h ON h.project_id=v.project_id AND h.revision_id=v.revision_id
|
|
122
126
|
WHERE v.passed=0 AND v.action IS NULL AND (v.redo_state IS NULL OR v.redo_state='failed')
|
|
123
127
|
AND NOT EXISTS (SELECT 1 FROM review_verdicts n WHERE n.project_id=v.project_id
|
|
124
128
|
AND n.item_key=v.item_key AND (n.created_at>v.created_at OR (n.created_at=v.created_at AND n.rowid>v.rowid)))`)
|
|
125
129
|
.all();
|
|
126
|
-
return new Set(ids.map((row) => z.string().parse(row.id)));
|
|
130
|
+
return new Set([...checkpoints, ...ids.map((row) => z.string().parse(row.id))]);
|
|
127
131
|
}
|
|
128
132
|
// Whether a finished project's settings make a video: projects stored before sources were
|
|
129
133
|
// recorded made one.
|
|
@@ -137,13 +141,20 @@ function sampleProjectIds(db) {
|
|
|
137
141
|
}));
|
|
138
142
|
}
|
|
139
143
|
// What a project on the calendar asks of the person, if anything. The same readings Home's
|
|
140
|
-
// Needs you uses: a failed
|
|
141
|
-
|
|
144
|
+
// Needs you uses: a failed run, a paused run, one a restart stopped (Continue the run), or one
|
|
145
|
+
// waiting for a review. A step failed beside a wait reads by that wait and is listed apart
|
|
146
|
+
// (`stepFailed`). One Kept as is asks nothing, nor does an uploaded one that reads Done
|
|
147
|
+
// (`uploadedAndSettled`).
|
|
148
|
+
function needsOf(state, ask) {
|
|
142
149
|
if (state === "failed")
|
|
143
150
|
return "failed";
|
|
144
151
|
if (state === "paused")
|
|
145
152
|
return "paused";
|
|
146
|
-
if (
|
|
153
|
+
if (ask.aside || ask.settled)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (ask.stopped)
|
|
156
|
+
return "stopped";
|
|
157
|
+
if (ask.reviewing && state !== "canceled")
|
|
147
158
|
return "review";
|
|
148
159
|
return undefined;
|
|
149
160
|
}
|
|
@@ -151,10 +162,13 @@ function needsOf(state, reviewing) {
|
|
|
151
162
|
// the range, with what each needs from the person and whether its video is ready to upload.
|
|
152
163
|
function projectsIn(deps, from, to) {
|
|
153
164
|
const standings = stageStandingsByProject(deps.db);
|
|
154
|
-
const
|
|
165
|
+
const settled = uploadedAndSettled(deps.db);
|
|
166
|
+
const holds = projectHolds(deps.db, { standings, settled });
|
|
167
|
+
const reviewing = awaitingReview(deps.db, holds);
|
|
155
168
|
const uploads = uploadedProjects(deps.db);
|
|
156
169
|
const samples = sampleProjectIds(deps.db);
|
|
157
170
|
const waits = limitWaitsByProject(deps.db);
|
|
171
|
+
const aside = setAsideProjects(deps.db);
|
|
158
172
|
const fromSchedule = new Map();
|
|
159
173
|
for (const row of deps.db
|
|
160
174
|
.prepare(`SELECT DISTINCT admitted.value AS project_id, schedule_runs.schedule_id
|
|
@@ -180,14 +194,22 @@ function projectsIn(deps, from, to) {
|
|
|
180
194
|
.all(to.toISOString(), from.toISOString())
|
|
181
195
|
.flatMap((row) => {
|
|
182
196
|
const project = projectRow.parse(row);
|
|
183
|
-
const state = derive(standings.get(project.id) ?? [], project.paused === 1);
|
|
197
|
+
const state = settledStatus(derive(standings.get(project.id) ?? [], project.paused === 1), settled.has(project.id));
|
|
184
198
|
const terminal = state === "done" || state === "partial" || state === "failed" || state === "canceled";
|
|
185
199
|
if (terminal) {
|
|
186
200
|
const ended = project.finished_at ?? project.created_at;
|
|
187
201
|
if (ended < from.toISOString() || ended >= to.toISOString())
|
|
188
202
|
return [];
|
|
189
203
|
}
|
|
190
|
-
|
|
204
|
+
// One Kept as is waits for nobody. A run parked on an agent's review holds no checkpoint
|
|
205
|
+
// of the person's, but an item already flagged still waits for their Overrule or Redo.
|
|
206
|
+
const waitingForAgent = holds.waitingForAgent.has(project.id);
|
|
207
|
+
const needs = needsOf(state, {
|
|
208
|
+
stopped: holds.stopped.has(project.id),
|
|
209
|
+
reviewing: reviewing.has(project.id),
|
|
210
|
+
aside: aside.has(project.id),
|
|
211
|
+
settled: settled.has(project.id),
|
|
212
|
+
});
|
|
191
213
|
const ready = (state === "done" || state === "partial") &&
|
|
192
214
|
makesVideo(project.video_source) &&
|
|
193
215
|
!uploads.has(project.id) &&
|
|
@@ -201,8 +223,16 @@ function projectsIn(deps, from, to) {
|
|
|
201
223
|
createdAt: project.created_at,
|
|
202
224
|
finishedAt: terminal ? (project.finished_at ?? project.created_at) : null,
|
|
203
225
|
scheduleId: fromSchedule.get(project.id) ?? null,
|
|
226
|
+
progress: progressOf(standings.get(project.id) ?? []),
|
|
204
227
|
...(needs === undefined ? {} : { needs }),
|
|
205
228
|
...(ready ? { readyToUpload: true } : {}),
|
|
229
|
+
...(aside.has(project.id) ? { setAside: true } : {}),
|
|
230
|
+
...(holds.prepared.has(project.id) ? { prepared: true } : {}),
|
|
231
|
+
...(waitingForAgent ? { waitingForAgent: true } : {}),
|
|
232
|
+
...(holds.stopped.has(project.id) ? { stopped: true } : {}),
|
|
233
|
+
...(readsByWaitBesideFailure(state, standings.get(project.id) ?? [])
|
|
234
|
+
? { stepFailed: true }
|
|
235
|
+
: {}),
|
|
206
236
|
...(waiting === undefined ? {} : { limitWaits: waiting.map(listingWait) }),
|
|
207
237
|
},
|
|
208
238
|
];
|
|
@@ -154,3 +154,9 @@ export function preparedTopics(deps, scheduleId) {
|
|
|
154
154
|
return found === undefined ? [] : [{ title, topic: topic.title, projectId: found.projectId }];
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
|
+
// A project a schedule prepared ahead whose Before Video hold is the schedule's own: that hold
|
|
158
|
+
// waits for its day, not for anyone (`reviews/holds.ts` reads it with the project's other
|
|
159
|
+
// checkpoints). The day deletes the row (`continuePrepared`).
|
|
160
|
+
export function heldForItsDay(alias) {
|
|
161
|
+
return `EXISTS (SELECT 1 FROM prepared_videos p WHERE p.project_id=${alias}.project_id AND p.added_checkpoint=1)`;
|
|
162
|
+
}
|
|
@@ -5,7 +5,7 @@ import { cadenceSchema, validTimeZone } from "./calendar.js";
|
|
|
5
5
|
const id = z.uuid();
|
|
6
6
|
// What a project on the calendar waits on the person for: a failed or paused run, or a review
|
|
7
7
|
// (a checkpoint holding the run, or an automatic review's failed item).
|
|
8
|
-
export const calendarNeeds = ["failed", "paused", "review"];
|
|
8
|
+
export const calendarNeeds = ["failed", "paused", "stopped", "review"];
|
|
9
9
|
const keywordName = z
|
|
10
10
|
.string()
|
|
11
11
|
.min(1)
|
|
@@ -242,10 +242,22 @@ export const calendarProjectSchema = z
|
|
|
242
242
|
createdAt: z.string(),
|
|
243
243
|
finishedAt: z.string().nullable(),
|
|
244
244
|
scheduleId: z.string().nullable(),
|
|
245
|
+
// The run's share done (`progressOf`), which tells a held run from a queued one. Older
|
|
246
|
+
// servers leave it out.
|
|
247
|
+
progress: z.number().min(0).max(1).default(0),
|
|
245
248
|
// What the project waits on the person for; left out when nothing.
|
|
246
249
|
needs: z.enum(calendarNeeds).optional(),
|
|
247
250
|
// Finished, makes a video, not marked uploaded and not a bundled sample.
|
|
248
251
|
readyToUpload: z.literal(true).optional(),
|
|
252
|
+
// What a held run waits for when it isn't the person (`reviews/holds.ts`), and a run
|
|
253
|
+
// Kept as is, as the Projects list says them.
|
|
254
|
+
setAside: z.literal(true).optional(),
|
|
255
|
+
prepared: z.literal(true).optional(),
|
|
256
|
+
waitingForAgent: z.literal(true).optional(),
|
|
257
|
+
stopped: z.literal(true).optional(),
|
|
258
|
+
// A step failed while the run still reads by a wait (`readsByWaitBesideFailure`): listed as a
|
|
259
|
+
// failed item beside what it waits for, as on Home. Older servers leave it out.
|
|
260
|
+
stepFailed: z.literal(true).optional(),
|
|
249
261
|
// Stages waiting for a CLI plan's limits to reset.
|
|
250
262
|
limitWaits: z
|
|
251
263
|
.array(z
|
|
@@ -5,7 +5,13 @@ import { backupsFolderName, projectDir, renderCacheFolder } from "./layout.js";
|
|
|
5
5
|
// The file in each project folder that says which project it is, for when the database and
|
|
6
6
|
// the folders disagree (a restored database, a folder renamed by hand). Reconcile keeps it.
|
|
7
7
|
export const projectMarker = ".slopify-project";
|
|
8
|
-
//
|
|
8
|
+
// The pictures a video or short shows. Every version's stay in Upload, an older version's under
|
|
9
|
+
// Images/Earlier/<date>/, never in History: making them again never gives the same ones.
|
|
10
|
+
const imageRoles = new Set(["image", "animated_image", "short_image"]);
|
|
11
|
+
// Raised when a role's place changes, so every project arranged by the older places is moved
|
|
12
|
+
// once: 2 put every version's images the video and shorts show into Upload (3.18.1).
|
|
13
|
+
const layoutVersion = 2;
|
|
14
|
+
// What makes a project's arrangement current: the layout, its title, head version and files. A project
|
|
9
15
|
// whose key is unchanged since it was last arranged is left alone.
|
|
10
16
|
export function arrangeKeys(db) {
|
|
11
17
|
const keys = new Map();
|
|
@@ -16,7 +22,10 @@ export function arrangeKeys(db) {
|
|
|
16
22
|
(SELECT count(*)||':'||COALESCE(max(r.id),'') FROM revision_outputs r WHERE r.project_id=p.id AND r.selected=1 AND r.revision_id=h.revision_id) AS current
|
|
17
23
|
FROM projects p LEFT JOIN project_heads h ON h.project_id=p.id`)
|
|
18
24
|
.all())
|
|
19
|
-
keys.set(String(row.id), [
|
|
25
|
+
keys.set(String(row.id), [
|
|
26
|
+
`layout ${layoutVersion}`,
|
|
27
|
+
...[row.title, row.revision_id, row.assets, row.legacy, row.current].map(String),
|
|
28
|
+
].join("|"));
|
|
20
29
|
return keys;
|
|
21
30
|
}
|
|
22
31
|
// Whether anything of the project runs or may start: its files stay where they are until then.
|
|
@@ -241,7 +250,9 @@ export function desiredPlaces(db, projectId) {
|
|
|
241
250
|
const place = placeFor(reference, order, titles);
|
|
242
251
|
out.set(reference.path, reference.current
|
|
243
252
|
? place
|
|
244
|
-
:
|
|
253
|
+
: imageRoles.has(reference.role)
|
|
254
|
+
? `Upload/Images/Earlier/${day(reference.createdAt)}/${place.replace(/^Upload\/(?:Images\/)?/u, "")}`
|
|
255
|
+
: `History/${day(reference.createdAt)}/${place.replace(/^Upload\//u, "")}`);
|
|
245
256
|
}
|
|
246
257
|
return out;
|
|
247
258
|
}
|
|
@@ -286,14 +297,16 @@ function placeFor(reference, order, titles) {
|
|
|
286
297
|
const key = /^image:(.+)$/u.exec(slot)?.[1];
|
|
287
298
|
const at = key === undefined ? undefined : order.get(key);
|
|
288
299
|
return at === undefined
|
|
289
|
-
? `
|
|
290
|
-
: `
|
|
300
|
+
? `Upload/Images/${file}`
|
|
301
|
+
: `Upload/Images/${pad(at + 1)}${extension}`;
|
|
291
302
|
}
|
|
292
303
|
case "short_image": {
|
|
293
304
|
const match = /^shorts:([0-9]+):image:([0-9]+)$/u.exec(slot);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
305
|
+
if (match === null)
|
|
306
|
+
return `Upload/Shorts/Images/${file}`;
|
|
307
|
+
// Beside the short's video, in a folder named like it: Shorts/3 - Title/image 1.png.
|
|
308
|
+
const title = titles.get(Number(match[1]));
|
|
309
|
+
return `Upload/Shorts/${match[1]}${title === undefined || title === "" ? "" : ` - ${clean(title)}`}/image ${match[2]}${extension}`;
|
|
297
310
|
}
|
|
298
311
|
case "figure_card":
|
|
299
312
|
return `Working/Cards/${file}`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// What a storage action removes and what it costs later, in the words its confirm dialog
|
|
2
|
+
// uses: shared by the web page (Settings → Backup & storage, a project's Free up space) and
|
|
3
|
+
// the agent's tool, so both state the same count and size.
|
|
4
|
+
// "512 B", "3.4 MB", "12 GB": binary units, one decimal under ten.
|
|
5
|
+
export function fileSize(bytes) {
|
|
6
|
+
if (bytes < 1024)
|
|
7
|
+
return `${String(bytes)} B`;
|
|
8
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
9
|
+
let value = bytes;
|
|
10
|
+
let unit = units[0];
|
|
11
|
+
for (const candidate of units) {
|
|
12
|
+
value /= 1024;
|
|
13
|
+
unit = candidate;
|
|
14
|
+
if (value < 1024 || candidate === units.at(-1))
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, "")} ${unit}`;
|
|
18
|
+
}
|
|
19
|
+
// What Delete old versions removes and what it costs later; without the numbers when they
|
|
20
|
+
// can't be read. `scope` is whose History it empties: every project's, or one project's.
|
|
21
|
+
export function oldVersionsConsequence(size, scope = "projects") {
|
|
22
|
+
const what = size === undefined
|
|
23
|
+
? "the older versions"
|
|
24
|
+
: `${String(size.files)} file(s) of older versions and frees ${fileSize(size.bytes)}`;
|
|
25
|
+
const where = scope === "project" ? "this project's History folder" : "the projects' History folders";
|
|
26
|
+
return `This deletes ${what}: what is in ${where} (earlier renders, narration before a change). The current video, shorts, thumbnails and everything in Upload stay, and so does anything you uploaded or put there yourself. Going back to one of those older versions makes its files again, which takes time and uses provider credits.`;
|
|
27
|
+
}
|
|
@@ -8,6 +8,7 @@ import { transact } from "../../kernel/db/tx.js";
|
|
|
8
8
|
import { liveProject } from "../admission/repo.js";
|
|
9
9
|
import { silenceGapSecondsMax } from "../admission/rules.js";
|
|
10
10
|
import { detectSlots } from "../admission/substitute.js";
|
|
11
|
+
import { agentConnectionKey } from "../agents/connection.js";
|
|
11
12
|
import { fontMaxBytes } from "../fonts/model.js";
|
|
12
13
|
import { readFontMetadata } from "../fonts/sfnt.js";
|
|
13
14
|
import { lintEntry, lintPrompt } from "../library/lint.js";
|
|
@@ -373,9 +374,11 @@ function portableSettings(settings) {
|
|
|
373
374
|
// So does the Studio extension's pairing: its token reads every project's upload pack.
|
|
374
375
|
// Which "What's new" tour and which patch notes this install has closed are about this
|
|
375
376
|
// install, not the data.
|
|
377
|
+
// When an agent last connected is this install's own fact.
|
|
376
378
|
if (key === "tutorial.session" ||
|
|
377
379
|
// Whether this install showed its first-run welcome belongs to this install.
|
|
378
380
|
key === "first-run.done" ||
|
|
381
|
+
key === agentConnectionKey ||
|
|
379
382
|
key === notificationUrlKey ||
|
|
380
383
|
key === studioPairingKey ||
|
|
381
384
|
// What waits to be filled in Studio belongs to this install's pairing.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { desiredPlaces } from "./arrange.js";
|
|
3
|
+
import { assetOf } from "./asset-name.js";
|
|
4
|
+
import { outputPath } from "./layout.js";
|
|
5
|
+
import { outputsOf, projectTitle } from "./repo.js";
|
|
6
|
+
// A project's files as its folder is arranged (`arrange.ts`): Upload (what is published),
|
|
7
|
+
// Working (what it was made from) and History (older versions). The place is where the file
|
|
8
|
+
// belongs even before the folder is arranged; `path` is where it is now and can move, so the
|
|
9
|
+
// `/files` URL is the handle to keep.
|
|
10
|
+
export const fileGroups = ["Upload", "Working", "History"];
|
|
11
|
+
export function groupOf(place) {
|
|
12
|
+
const first = place.split("/", 1)[0];
|
|
13
|
+
return first === "Upload" || first === "History" ? first : "Working";
|
|
14
|
+
}
|
|
15
|
+
const recordRow = z.object({
|
|
16
|
+
path: z.string(),
|
|
17
|
+
revision_id: z.string(),
|
|
18
|
+
id: z.string(),
|
|
19
|
+
role: z.string().nullable(),
|
|
20
|
+
});
|
|
21
|
+
// The newest version record of each stored path, for its `/files/:id/revisions/...` URL.
|
|
22
|
+
function revisionRecords(db, projectId) {
|
|
23
|
+
const records = new Map();
|
|
24
|
+
for (const row of db
|
|
25
|
+
.prepare(`SELECT a.path, o.revision_id, o.id, json_extract(o.descriptor,'$.role') AS role, o.created_at
|
|
26
|
+
FROM revision_outputs o JOIN project_assets a ON a.id=o.asset_id AND a.project_id=o.project_id
|
|
27
|
+
WHERE o.project_id=?
|
|
28
|
+
UNION ALL
|
|
29
|
+
SELECT a.path, p.revision_id, p.id, 'piece:'||COALESCE(json_extract(p.descriptor,'$.kind'),'other'), p.created_at
|
|
30
|
+
FROM revision_pieces p JOIN project_assets a ON a.id=p.asset_id AND a.project_id=p.project_id
|
|
31
|
+
WHERE p.project_id=?
|
|
32
|
+
ORDER BY 5`)
|
|
33
|
+
.all(projectId, projectId)) {
|
|
34
|
+
const record = recordRow.parse(row);
|
|
35
|
+
records.set(record.path, record);
|
|
36
|
+
}
|
|
37
|
+
return records;
|
|
38
|
+
}
|
|
39
|
+
export function projectFiles(deps, projectId) {
|
|
40
|
+
if (projectTitle(deps.db, projectId) === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const current = new Map(outputsOf(deps.db, projectId).map((output) => [output.path, output]));
|
|
43
|
+
const records = revisionRecords(deps.db, projectId);
|
|
44
|
+
return [...desiredPlaces(deps.db, projectId)]
|
|
45
|
+
.flatMap(([stored, place]) => {
|
|
46
|
+
const output = place.startsWith("History/") ? undefined : current.get(stored);
|
|
47
|
+
const record = records.get(stored);
|
|
48
|
+
const url = output !== undefined
|
|
49
|
+
? `/files/${projectId}/${assetOf(output)}`
|
|
50
|
+
: record === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: `/files/${projectId}/revisions/${record.revision_id}/${record.id}`;
|
|
53
|
+
if (url === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
group: groupOf(place),
|
|
58
|
+
place,
|
|
59
|
+
role: output?.role ?? record?.role ?? "other",
|
|
60
|
+
path: outputPath(deps.paths, projectId, stored),
|
|
61
|
+
url,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
})
|
|
65
|
+
.toSorted((a, b) => fileGroups.indexOf(a.group) - fileGroups.indexOf(b.group) || a.place.localeCompare(b.place));
|
|
66
|
+
}
|
|
@@ -5,8 +5,9 @@ import { liveProject, projectExists, projectPaused, stagesOf } from "../admissio
|
|
|
5
5
|
import { projectSettled } from "./arrange.js";
|
|
6
6
|
import { backupsFolderName, outputPath, projectDir } from "./layout.js";
|
|
7
7
|
import { pieceFile } from "./reconcile.js";
|
|
8
|
-
// Keep outputs only: a finished project drops the working files it was made from (
|
|
9
|
-
//
|
|
8
|
+
// Keep outputs only: a finished project drops the working files it was made from (narration
|
|
9
|
+
// parts, subtitle timing, render settings) and keeps what gets published, the images the
|
|
10
|
+
// video and shorts show included: making them again costs money and never gives the same ones. Two
|
|
10
11
|
// rules keep it safe. A file the user supplied is never removed, because nothing could make
|
|
11
12
|
// it again. And only files a project record names are removed: anything else in the folder
|
|
12
13
|
// is reconcile's business (`reconcile.ts`), which leaves the Backups folder alone.
|
|
@@ -28,6 +29,9 @@ export const outputRoles = [
|
|
|
28
29
|
"audio_export",
|
|
29
30
|
"subtitles_srt",
|
|
30
31
|
"subtitles_vtt",
|
|
32
|
+
"image",
|
|
33
|
+
"animated_image",
|
|
34
|
+
"short_image",
|
|
31
35
|
];
|
|
32
36
|
export function projectStorage(deps, projectId) {
|
|
33
37
|
const files = filesOf(deps.paths, projectId);
|
|
@@ -4,7 +4,8 @@ import { join, relative, sep } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
6
6
|
import { projectLanguage } from "../../kernel/ports/languages.js";
|
|
7
|
-
import { projectById
|
|
7
|
+
import { projectById } from "../admission/repo.js";
|
|
8
|
+
import { setStageProgress } from "../admission/stage-writes.js";
|
|
8
9
|
import { captionFont, captionFontDeps } from "../fonts/coverage.js";
|
|
9
10
|
import { resolveFont } from "../fonts/index.js";
|
|
10
11
|
import { outputPath, projectDir } from "../storage/layout.js";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { admittedUnfinished } from "../admission/admitted-work.js";
|
|
2
|
+
import { oneProject } from "../admission/repo.js";
|
|
1
3
|
export function markUploaded(db, projectId, uploaded, at) {
|
|
2
4
|
if (db.prepare("SELECT 1 FROM projects WHERE id=?").get(projectId) === undefined)
|
|
3
5
|
return { ok: false, reason: "not-found" };
|
|
@@ -18,15 +20,19 @@ export function uploadedProjects(db) {
|
|
|
18
20
|
.all()
|
|
19
21
|
.map((row) => [String(row.project_id), String(row.uploaded_at)]));
|
|
20
22
|
}
|
|
21
|
-
// Uploaded projects with nothing
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
23
|
+
// Uploaded projects with nothing left to run read done, not "Paused" or "Waiting": a step still
|
|
24
|
+
// allowed to run, or a held one admitted for the current version (`admittedUnfinished`, as
|
|
25
|
+
// `reviews/holds.ts` heldBesideReviews reads it), keeps one unsettled. What is left otherwise
|
|
26
|
+
// - a placeholder from an edit after the upload, a pause from a bulk Pause - is not work the
|
|
27
|
+
// person waits for. One project's alone when `projectId` is given.
|
|
28
|
+
export function uploadedAndSettled(db, projectId) {
|
|
29
|
+
const one = oneProject("u", projectId);
|
|
25
30
|
return new Set(db
|
|
26
31
|
.prepare(`SELECT u.project_id FROM project_uploads u WHERE NOT EXISTS(
|
|
27
32
|
SELECT 1 FROM revision_work w WHERE w.project_id=u.project_id
|
|
28
|
-
AND w.state IN ('pending','running')
|
|
29
|
-
|
|
33
|
+
AND w.state IN ('pending','running')
|
|
34
|
+
AND (w.dispatch_state!='held' OR ${admittedUnfinished("w")}))${one.sql}`)
|
|
35
|
+
.all(...one.params)
|
|
30
36
|
.map((row) => String(row.project_id)));
|
|
31
37
|
}
|
|
32
38
|
// "Keep as is": a waiting run the person leaves as it is, until the project's next edit. Set on
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// An uploaded, settled project (`uploadedAndSettled`) reads done where its run would read paused
|
|
2
|
+
// or pending: the Projects list, a project's page and the calendar.
|
|
3
|
+
export function settledStatus(status, settled) {
|
|
4
|
+
return settled && (status === "paused" || status === "pending") ? "done" : status;
|
|
5
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { transact } from "../../kernel/db/tx.js";
|
|
4
|
-
import { setStageProgress } from "../admission/
|
|
4
|
+
import { setStageProgress } from "../admission/stage-writes.js";
|
|
5
5
|
import { outputPath } from "../storage/layout.js";
|
|
6
6
|
import { deleteOutput, insertOutput, outputsOf } from "../storage/repo.js";
|
|
7
7
|
import { subtitleRoles } from "../subtitles/prepare.js";
|