Feature/v2/smartfolders rebuild (#6480)

Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
This commit is contained in:
Reece Browne
2026-06-10 11:18:14 +01:00
committed by GitHub
co-authored by aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
parent e0fc5061de
commit e2536daeb8
49 changed files with 9166 additions and 103 deletions
@@ -3203,7 +3203,7 @@ creditsRemaining = "{{current}} of {{total}} remaining"
creditsThisMonth = "Monthly credits"
current = "Current Plan"
customPricing = "Custom"
customSmartFolders = "Custom Smart Folders"
customWatchedFolders = "Custom Watched Folders"
dedicatedSupportSlas = "Dedicated support & SLAs"
enterpriseSubscription = "Enterprise"
everythingInCredits = "Everything in Credits, plus:"
@@ -3511,6 +3511,7 @@ addFiles = "Add Files"
[fileManager]
active = "Active"
addToWatchedFolder = "Add to Watched Folder"
addToUpload = "Add to Upload"
changesNotUploaded = "Changes not uploaded"
clearAll = "Clear All"
@@ -5773,6 +5774,7 @@ label = "SMTP Username"
[quickAccess]
access = "Access"
watchedFolders = "Watched Folders"
accessAddPerson = "Add another person"
accessBack = "Back"
accessCopyLink = "Copy link"
@@ -8150,6 +8152,184 @@ confirm = "Extract"
message = "This ZIP contains {{count}} files. Extract anyway?"
title = "Large ZIP File"
[watchedFolders]
alreadyInFolder = "Already in this folder"
deleteConfirmBody = "This will remove the folder and its run history. Files already downloaded are not affected."
deleteConfirmTitle = "Delete folder?"
defaultFolderWarning = "This is a default folder and will be recreated on next reload."
folderNotFound = "Folder not found"
newFolder = "New folder"
noFolders = "No watched folders yet"
sidebarTitle = "Watched Folders"
title = "Watched Folders"
sidebarFiles = "My Files"
[watchedFolders.modal]
automation = "Automation"
automationSaved = "Automation saved"
createFolder = "Create Folder"
stepsSaved = "Steps saved — click Create Folder to finish"
automationRequired = "Add at least one configured step before saving."
color = "Accent color"
createTitle = "New Watched Folder"
description = "Description"
descriptionPlaceholder = "What does this folder do?"
editTitle = "Edit Watched Folder"
icon = "Icon"
name = "Folder name"
namePlaceholder = "My Watched Folder"
nameRequired = "Folder name is required"
nameTooLong = "Folder name must be 50 characters or less"
saveChanges = "Save Changes"
saveFailed = "Failed to save folder. Please try again."
automationNameFallback = "Watched Folder automation"
retryLabel = "Auto-retry"
maxRetries = "Max retries"
maxRetriesDesc = "0 to disable"
retryDelay = "Delay (minutes)"
outputLabel = "Output"
outputModeVersion = "Replace original?"
outputModeVersionDesc = "Output becomes a new version of the input file rather than a separate file"
outputModeNewDesc = "Output is saved as a new separate file"
outputName = "Output filename prefix"
outputNameSuffix = "Suffix"
outputNamePlaceholderVersion = "Same as original"
sectionFolder = "Folder"
sectionSourceOutput = "Source & Output"
sectionSteps = "Steps"
inputSource = "Input source"
inputSourceBrowser = "Browser — drop files here"
inputSourceLocal = "Local folder (auto-scan)"
inputSourceLocalUnsupported = "Local folder (auto-scan) — Chrome/Edge only"
inputFolder = "Input folder"
inputFolderNotChosen = "No folder chosen — required for auto-scan"
changeFolder = "Change"
chooseFolder = "Choose"
clearFolder = "Clear"
autoScanHelp = "New PDF files in this folder are processed automatically every 10 seconds."
localOutputFolder = "Local output folder"
outputFolderUnsupported = "Not supported in this browser"
outputFolderNotSet = "Not set — outputs stay in app"
advanced = "Advanced"
replaceOriginal = "Replace original file"
autoNumber = "Auto-number"
autoNumberExample = "e.g. document.pdf → document (1).pdf"
outputNamePrefix = "Filename prefix"
positionPrefix = "Prefix"
positionSuffix = "Suffix"
[watchedFolders.home]
create = "Create your first Watched Folder"
dropHere = "Drop to process"
editFolder = "Edit folder"
empty = "No watched folders yet"
file = "file"
files = "files"
openFolder = "Open folder"
title = "Watched Folders"
subtitle = "Folders that automatically process PDFs with your configured pipeline"
emptyTitle = "Automate your PDF workflows"
emptyDesc = "Set up a Watched Folder once. Drop PDFs in and they're automatically compressed, OCR'd, split, merged — whatever your pipeline does."
addAnother = "Add another Watched Folder"
addAnotherDesc = "Automatically process files with a new pipeline"
resume = "Resume"
pause = "Pause"
deleteFolder = "Delete folder"
noSteps = "No automation steps configured"
[watchedFolders.card]
edit = "Edit folder"
delete = "Delete folder"
[watchedFolders.status]
done = "Done"
processing = "Processing"
paused = "Paused"
active = "Active"
[watchedFolders.howItWorks]
title = "How Watched Folders work"
step1Title = "Drop files"
step1Desc = "Drag PDFs onto any Watched Folder card — or send them from your file list"
step2Title = "Pipeline runs"
step2Desc = "Your configured tools process each file automatically"
step3Title = "Output ready"
step3Desc = "Download processed files from inside the folder"
[watchedFolders.workbench]
addFiles = "Add files"
inputs = "Inputs"
outputs = "Outputs"
failed = "Failed"
dataSaved = "Saved"
running = "Running"
dropToProcess = "Drop to process"
activity = "Activity"
noActivity = "No activity yet — drop a PDF to start"
noActivityMatch = "No matching activity"
retryAll = "Retry all"
retryIn = "retry in {{count}}m"
retryingSoon = "retrying…"
pause = "Pause"
resume = "Resume"
dayRunning = "day running"
daysRunning = "days running"
sortNewest = "Newest"
sortOldest = "Oldest"
sortNameAsc = "A → Z"
sortNameDesc = "Z → A"
filterAll = "All"
filterPending = "Pending"
search = "Search…"
retryMixedConfirm = "Some selected files have already completed and will be skipped. Only failed files will be retried. Continue?"
watching = "Watching: {{name}}"
noInputFolder = "No input folder — edit to configure"
countProcessing = "{{count}} processing"
countQueued = "{{count}} queued"
exportZip = "Export zip"
exportSeparately = "Export separately"
countSelected = "{{count}} selected"
selectAll = "Select all {{count}}"
delete = "Delete"
clearSelection = "Clear selection"
preview = "Preview"
export = "Export"
previewInput = "Preview input"
previewOutput = "Preview output"
directionIn = "in"
directionOut = "out"
queuedWaiting = "Queued — waiting to run"
allTime = "All time"
queued = "Queued"
inputSize = "Input size"
filesProcessedOverTime = "Files processed over time"
legendComplete = "Complete"
chartTooltipComplete = "{{count}} complete"
chartTooltipFailed = "{{count}} failed"
removeEntry = "Remove entry"
removeEntries = "Remove {{count}} entries"
deleteConfirmBody = "Remove notifications only clears the activity log. Delete outputs also removes the processed files from storage. Your original input files are never touched."
cancel = "Cancel"
removeNotificationsOnly = "Remove notifications only"
deleteOutputs = "Delete outputs"
previewLoadFailed = "Could not load file preview."
[watchedFolders.actions]
back = "Back"
dismiss = "Dismiss"
retry = "Retry"
view = "View"
download = "Download"
downloadInput = "Download input"
downloadOutput = "Download output"
collapse = "Collapse"
expand = "Expand"
[watchedFolders.time]
justNow = "just now"
minutesAgo = "{{count}}m ago"
hoursAgo = "{{count}}h ago"
daysAgo = "{{count}}d ago"
[addStamp]
tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original"
+88
View File
@@ -0,0 +1,88 @@
/**
* Service worker for Watched Folder retry scheduling.
*
* Reads the earliest pending retry from IndexedDB and sets a setTimeout for it.
* When the timer fires it posts PROCESS_DUE_RETRIES to all window clients so
* the main thread can atomically claim and process the due entries.
*
* Limitations:
* - Browsers may terminate idle service workers after ~30 s. The main thread
* therefore also drains due retries on mount and on visibilitychange as a
* fallback — no retries are lost, they may just fire slightly late.
* - Multiple clients each post a SCHEDULE_RETRY message; the SW deduplicates
* by resetting the timer each time, so only one notification is sent.
*/
const DB_NAME = "stirling-pdf-retry-schedule";
const STORE_NAME = "retries";
let retryTimer = null;
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim().then(scheduleNextTimer));
});
self.addEventListener("message", (event) => {
if (event.data?.type === "SCHEDULE_RETRY") {
scheduleNextTimer();
}
});
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(new Error("SW: failed to open retry DB"));
// Create store if this SW activates before the main thread has opened the DB
req.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
store.createIndex("dueAt", "dueAt", { unique: false });
}
};
});
}
async function getEarliestDueAt() {
try {
const db = await openDB();
return new Promise((resolve) => {
const tx = db.transaction([STORE_NAME], "readonly");
const req = tx.objectStore(STORE_NAME).index("dueAt").openCursor();
req.onsuccess = () => resolve(req.result ? req.result.value.dueAt : null);
req.onerror = () => resolve(null);
});
} catch {
return null;
}
}
async function notifyClients() {
const clients = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
});
for (const client of clients) {
client.postMessage({ type: "PROCESS_DUE_RETRIES" });
}
}
async function scheduleNextTimer() {
if (retryTimer !== null) {
clearTimeout(retryTimer);
retryTimer = null;
}
const earliest = await getEarliestDueAt();
if (earliest === null) return;
const delay = Math.max(0, earliest - Date.now());
retryTimer = setTimeout(async () => {
retryTimer = null;
await notifyClients();
// Re-schedule for any remaining entries that were not yet due
await scheduleNextTimer();
}, delay);
}