199 lines
7.6 KiB
JavaScript
199 lines
7.6 KiB
JavaScript
(() => {
|
|
const strip = document.querySelector("[data-story-intelligence-global-strip]");
|
|
if (!strip || !window.signalR) {
|
|
return;
|
|
}
|
|
|
|
const title = strip.querySelector("[data-story-intelligence-global-title]");
|
|
const detail = strip.querySelector("[data-story-intelligence-global-detail]");
|
|
const bar = strip.querySelector("[data-story-intelligence-global-bar]");
|
|
let refreshTimer = null;
|
|
let startRetryTimer = null;
|
|
const diagnosticsEnabled =
|
|
location.hostname === "localhost"
|
|
|| location.hostname === "127.0.0.1"
|
|
|| location.hostname.startsWith("dev.");
|
|
const log = (message, data) => {
|
|
if (diagnosticsEnabled && window.console?.debug) {
|
|
window.console.debug(`[StoryIntelligenceGlobalProgress] ${message}`, data || "");
|
|
}
|
|
};
|
|
let lastSnapshotTime = Date.parse(strip.dataset.storyIntelligenceUpdatedUtc || "") || 0;
|
|
const readNumber = (value) => {
|
|
const parsed = Number.parseInt(value || "", 10);
|
|
return Number.isInteger(parsed) ? parsed : null;
|
|
};
|
|
let lastSnapshotScore =
|
|
(readNumber(strip.dataset.completedScenes) || 0)
|
|
+ (readNumber(strip.dataset.failedScenes) || 0);
|
|
let lastSnapshotKey = `${strip.dataset.projectId || ""}:${strip.dataset.bookId || ""}`;
|
|
|
|
const formatCount = (value) => new Intl.NumberFormat().format(Math.max(0, value || 0));
|
|
const estimateRemaining = (elapsedMs, completed, failed, total, status) => {
|
|
if (status === "Finalising") {
|
|
return "Finalising";
|
|
}
|
|
|
|
if (!total || !completed) {
|
|
return "Calculating";
|
|
}
|
|
|
|
const remainingScenes = Math.max(0, total - completed - failed);
|
|
if (remainingScenes === 0) {
|
|
return "Finalising";
|
|
}
|
|
|
|
const remainingMs = (elapsedMs / completed) * remainingScenes;
|
|
const remainingSeconds = Math.max(1, Math.round(remainingMs / 1000));
|
|
if (remainingSeconds >= 60) {
|
|
return `${Math.max(1, Math.round(remainingSeconds / 60))} min`;
|
|
}
|
|
|
|
return `${remainingSeconds} sec`;
|
|
};
|
|
|
|
const render = (progress) => {
|
|
const books = progress?.books || progress?.Books || [];
|
|
if (!books.length) {
|
|
strip.hidden = true;
|
|
strip.dataset.connection = "complete";
|
|
return;
|
|
}
|
|
|
|
const primary = books.find((book) => book.hasActiveRuns || book.HasActiveRuns) || books[0];
|
|
const activeBookCount = readNumber(progress.activeBookCount ?? progress.ActiveBookCount) || books.length;
|
|
const status = progress.status || progress.Status || primary.status || primary.Status || "";
|
|
const total = readNumber(progress.totalExpectedScenes ?? progress.TotalExpectedScenes)
|
|
?? readNumber(primary.totalExpectedScenes ?? primary.TotalExpectedScenes)
|
|
?? 0;
|
|
const completed = readNumber(progress.completedScenes ?? progress.CompletedScenes)
|
|
?? readNumber(primary.completedScenes ?? primary.CompletedScenes)
|
|
?? 0;
|
|
const failed = readNumber(progress.failedScenes ?? progress.FailedScenes)
|
|
?? readNumber(primary.failedScenes ?? primary.FailedScenes)
|
|
?? 0;
|
|
const active = readNumber(progress.activeScenes ?? progress.ActiveScenes)
|
|
?? readNumber(primary.activeScenes ?? primary.ActiveScenes)
|
|
?? 0;
|
|
const updatedRaw = progress.updatedUtc || progress.UpdatedUtc || primary.updatedUtc || primary.UpdatedUtc || "";
|
|
const updatedTime = Date.parse(updatedRaw);
|
|
const snapshotKey = `${primary.projectID ?? primary.projectId ?? ""}:${primary.bookID ?? primary.bookId ?? ""}`;
|
|
const snapshotScore = completed + failed;
|
|
if (snapshotKey !== lastSnapshotKey) {
|
|
lastSnapshotKey = snapshotKey;
|
|
lastSnapshotScore = 0;
|
|
lastSnapshotTime = 0;
|
|
}
|
|
|
|
if (snapshotScore < lastSnapshotScore) {
|
|
log("ignored backwards snapshot", { completed, failed, updatedUtc: updatedRaw, snapshotScore, lastSnapshotScore });
|
|
return;
|
|
}
|
|
|
|
if (Number.isFinite(updatedTime)) {
|
|
if (updatedTime < lastSnapshotTime || (updatedTime === lastSnapshotTime && snapshotScore < lastSnapshotScore)) {
|
|
log("ignored stale snapshot", { completed, failed, updatedUtc: updatedRaw, snapshotScore, lastSnapshotScore });
|
|
return;
|
|
}
|
|
|
|
lastSnapshotTime = updatedTime;
|
|
strip.dataset.storyIntelligenceUpdatedUtc = new Date(updatedTime).toISOString();
|
|
} else if (snapshotScore < lastSnapshotScore) {
|
|
return;
|
|
}
|
|
|
|
lastSnapshotScore = Math.max(lastSnapshotScore, snapshotScore);
|
|
strip.dataset.completedScenes = String(completed);
|
|
strip.dataset.failedScenes = String(failed);
|
|
strip.dataset.projectId = String(primary.projectID ?? primary.projectId ?? "");
|
|
strip.dataset.bookId = String(primary.bookID ?? primary.bookId ?? "");
|
|
|
|
strip.hidden = false;
|
|
strip.dataset.connection = "live";
|
|
const elapsedMs = readNumber(primary.totalDurationMs ?? primary.TotalDurationMs) || 0;
|
|
const percent = total > 0 ? Math.max(0, Math.min(100, Math.round((completed / total) * 100))) : 0;
|
|
|
|
if (bar) {
|
|
bar.style.width = `${percent}%`;
|
|
}
|
|
|
|
if (detail) {
|
|
const remaining = estimateRemaining(elapsedMs, completed, failed, total, status);
|
|
const failureText = failed > 0 ? ` \u00b7 ${formatCount(failed)} failed` : "";
|
|
const activeText = active > 0 ? ` \u00b7 ${formatCount(active)} active` : "";
|
|
detail.textContent = `${formatCount(completed)} of ${formatCount(total)} scenes analysed${failureText}${activeText} \u00b7 ${remaining} remaining`;
|
|
}
|
|
|
|
if (title && status) {
|
|
if (status === "Finalising") {
|
|
title.textContent = "Finalising Story Intelligence";
|
|
} else if (status === "Failed") {
|
|
title.textContent = "Story Intelligence needs attention";
|
|
} else if (status === "Cancelled") {
|
|
title.textContent = "Story Intelligence cancelled";
|
|
} else if (status === "Completed" || status === "CompletedWithWarnings") {
|
|
title.textContent = failed > 0 ? "Story Intelligence completed with notes" : "Story Intelligence complete";
|
|
} else if (activeBookCount > 1) {
|
|
title.textContent = `${formatCount(activeBookCount)} books analysing`;
|
|
} else {
|
|
title.textContent = primary.summaryText || primary.SummaryText || title.textContent;
|
|
}
|
|
}
|
|
log("applied snapshot", { completed, failed, total, active, status, updatedUtc: updatedRaw });
|
|
};
|
|
|
|
const refresh = () => {
|
|
connection.invoke("WatchGlobalStoryIntelligenceProgress")
|
|
.then((snapshot) => {
|
|
log("authoritative refresh received", snapshot);
|
|
render(snapshot);
|
|
})
|
|
.catch(() => {
|
|
strip.dataset.connection = "offline";
|
|
log("authoritative refresh failed");
|
|
});
|
|
};
|
|
|
|
const scheduleRefresh = () => {
|
|
window.clearTimeout(refreshTimer);
|
|
refreshTimer = window.setTimeout(refresh, 350);
|
|
};
|
|
|
|
const connection = new signalR.HubConnectionBuilder()
|
|
.withUrl("/hubs/story-intelligence")
|
|
.withAutomaticReconnect()
|
|
.build();
|
|
|
|
connection.on("StoryIntelligenceRunProgressChanged", scheduleRefresh);
|
|
connection.on("StoryIntelligenceGlobalProgressChanged", (snapshot) => {
|
|
log("live snapshot received", snapshot);
|
|
render(snapshot);
|
|
});
|
|
connection.onreconnected(() => {
|
|
log("SignalR reconnected");
|
|
refresh();
|
|
});
|
|
connection.onclose(() => {
|
|
strip.dataset.connection = "offline";
|
|
log("SignalR closed; retrying start");
|
|
startWithRetry();
|
|
});
|
|
|
|
const startWithRetry = () => {
|
|
window.clearTimeout(startRetryTimer);
|
|
connection.start()
|
|
.then(() => {
|
|
strip.dataset.connection = "live";
|
|
log("SignalR connected");
|
|
refresh();
|
|
})
|
|
.catch(() => {
|
|
strip.dataset.connection = "offline";
|
|
log("SignalR initial start failed; retrying");
|
|
startRetryTimer = window.setTimeout(startWithRetry, 2000);
|
|
});
|
|
};
|
|
|
|
startWithRetry();
|
|
})();
|