Limit and configure how many jobs the Jobs panels show

This commit is contained in:
tim
2026-09-27 23:38:28 +02:00
parent a7a89b145c
commit cc0f070661
12 changed files with 225 additions and 16 deletions
+66 -3
View File
@@ -15,6 +15,7 @@ let api, main, details, cleanups;
const config = {
metadata_fields: { title: 'Title', authors: 'Authors', comments: 'Description', series: 'Series', pubdate: 'Published', tags: 'Tags', languages: 'Language' },
mode_labels: { new: 'New chapters' }, library_available: true, update_interval: 2,
job_list_max_hours: 720, job_list_max_count: 500,
};
const rule = { pattern: 'before', replacement: 'after', flags: 'i', note: '', enabled: true };
const emptyRules = { rules: [], paragraphs: null, global_paragraphs: true };
@@ -49,7 +50,7 @@ const writes = () => api.requests.filter((r) => r.method !== 'GET');
for (const value of ['7', '']) {
test(`settings save converts ${JSON.stringify(value)} to a count`, async () => {
settingsPage(main);
settingsPage(main, {}, config);
await settle();
api.on('PUT', 'settings', {});
field(main, 'recent-chapters').value = value;
@@ -60,7 +61,7 @@ for (const value of ['7', '']) {
}
test('personal.ini save reports warnings and clears them after a clean save', async () => {
settingsPage(main);
settingsPage(main, {}, config);
await settle();
const card = section(main, 'FanFicFare settings (personal.ini)');
field(main, 'personal.ini').value = '[defaults]\nis_adult: true';
@@ -74,7 +75,7 @@ test('personal.ini save reports warnings and clears them after a clean save', as
});
test('failed settings saves retain edits and re-enable the action', async () => {
settingsPage(main);
settingsPage(main, {}, config);
await settle();
const card = section(main, 'FanFicFare settings (personal.ini)');
field(main, 'personal.ini').value = 'invalid ini';
@@ -85,6 +86,28 @@ test('failed settings saves retain edits and re-enable the action', async () =>
assert.match(ui.toasts.textContent, /Invalid settings/);
});
test('the jobs list settings load and save all four fields', async () => {
api.on('GET', 'settings', {
recent_chapters: 0, personal_ini: '',
story_jobs_hours: 36, story_jobs_count: 10, main_jobs_hours: 6, main_jobs_count: 25,
});
settingsPage(main, {}, config);
await settle();
const card = section(main, 'Jobs list');
assert.equal(field(main, 'story-jobs-hours').value, 36);
assert.equal(field(main, 'main-jobs-count').value, 25);
field(main, 'story-jobs-hours').value = '48';
field(main, 'story-jobs-count').value = '20';
field(main, 'main-jobs-hours').value = '12';
field(main, 'main-jobs-count').value = '50';
api.on('PUT', 'settings', {});
await click(card, 'Save');
assert.deepEqual(api.sent('PUT', 'settings')[0].body, {
story_jobs_hours: '48', story_jobs_count: '20', main_jobs_hours: '12', main_jobs_count: '50',
});
assert.match(ui.toasts.textContent, /Saved/);
});
test('adding stories keeps input on rejection and clears it after a mixed result', async () => {
await mountStories({ ...config, library_available: false });
const urls = nodes(main).find((el) => el.tagName === 'textarea');
@@ -491,6 +514,46 @@ test('chapter comparisons and revision changes send the chosen revision numbers'
assert.equal(api.sent('GET', 'chapters/2/diff?source=1').length, 1);
});
test('a job panel shows a queued job however old, but limits others by age', () => {
const { el, render } = jobPanel({ hours: 1, count: 10 });
const now = Date.now();
const old = new Date(now - 2 * 3600000).toISOString();
const recent = new Date(now - 30 * 60000).toISOString();
render([
{ id: 'a', status: 'queued', title: 'Old queued', updated: old, created: old },
{ id: 'b', status: 'done', title: 'Old done', updated: old, created: old },
{ id: 'c', status: 'done', title: 'Recent done', updated: recent, created: recent },
]);
assert.match(el.textContent, /Old queued/);
assert.doesNotMatch(el.textContent, /Old done/);
assert.match(el.textContent, /Recent done/);
});
test('a job panel caps jobs other than queued by count, newest first', () => {
const { el, render } = jobPanel({ hours: 24, count: 2 });
const now = Date.now();
const at = (minsAgo) => new Date(now - minsAgo * 60000).toISOString();
render([
{ id: 'a', status: 'done', title: 'Oldest', updated: at(30), created: at(30) },
{ id: 'b', status: 'done', title: 'Middle', updated: at(20), created: at(20) },
{ id: 'c', status: 'done', title: 'Newest', updated: at(10), created: at(10) },
]);
assert.match(el.textContent, /Newest/);
assert.match(el.textContent, /Middle/);
assert.doesNotMatch(el.textContent, /Oldest/);
});
test('a job panel shows a running job however old, like a queued job', () => {
const { el, render } = jobPanel({ hours: 1, count: 10 });
const old = new Date(Date.now() - 2 * 3600000).toISOString();
render([
{ id: 'a', status: 'running', title: 'Old running', updated: old, created: old },
{ id: 'b', status: 'done', title: 'Old done', updated: old, created: old },
]);
assert.match(el.textContent, /Old running/);
assert.doesNotMatch(el.textContent, /Old done/);
});
test('job cancellation posts the job ID and refreshes polling', async () => {
startPolling();
await settle();
+5 -2
View File
@@ -45,7 +45,10 @@ const elements = (node) => node instanceof Element ? [node, ...node.children.fla
const text = (node) => node instanceof Element ? node.children.map(text).join('') : String(node);
const find = (root, tag, label) => elements(root).find((el) => el.tagName === tag && text(el) === label);
const settle = () => new Promise((resolve) => setImmediate(resolve));
const config = { metadata_fields: { title: 'Title' }, mode_labels: {}, library_available: true, update_interval: 0 };
const config = {
metadata_fields: { title: 'Title' }, mode_labels: {}, library_available: true, update_interval: 0,
job_list_max_hours: 720, job_list_max_count: 500,
};
const story = {
id: 1, title: 'Story', author: 'Author', url: 'https://example.com/story', site: 'example.com',
locked_fields: [], pinned: { title: 'Edited title' }, site_values: { title: 'Site title' },
@@ -101,7 +104,7 @@ test('button variants and attributes do not need a base class', () => {
});
for (const [name, render] of [
['settings', (main) => settingsPage(main)],
['settings', (main) => settingsPage(main, {}, config)],
['stories', (main) => storiesPage(main, {}, config)],
['story', (main) => storyPage(main, { storyId: '1' }, config)],
['chapter', (main) => chapterPage(main, { storyId: '1', chapterId: '2' })],
+2 -1
View File
@@ -7,6 +7,7 @@ const { settingsPage } = await import('../../bindery/static/js/settings.js');
const { storyPage } = await import('../../bindery/static/js/story.js');
const { storiesPage } = await import('../../bindery/static/js/stories.js');
const config = { job_list_max_hours: 720, job_list_max_count: 500 };
const book = { id: 7, uuid: 'book-7', title: 'Library Title', authors: ['Writer'] };
const details = (story, extra = {}) => ({
story: { id: 1, title: 'Story', author: 'Writer', url: 'https://example.com/story', site: 'example.com',
@@ -40,7 +41,7 @@ test('the upload switches load and save on the settings page', async (t) => {
backend.on('GET', 'rules', { rules: [], paragraphs: false });
backend.on('GET', 'stories', []);
const main = new Element('main');
settingsPage(main);
settingsPage(main, {}, config);
await settle();
const card = section(main, 'Uploads to CWA');
const [hold, pause] = nodes(card).filter((el) => el.attributes.type === 'checkbox');
+14
View File
@@ -289,6 +289,10 @@ def test_settings(client, monkeypatch, tmp_path):
"hold_new_links": False,
"pause_uploads": False,
"personal_ini": "[defaults]\nis_adult:true\n",
"story_jobs_hours": 36,
"story_jobs_count": 10,
"main_jobs_hours": 6,
"main_jobs_count": 25,
}
saved = client.put("/api/settings", json={"recent_chapters": 3}, headers=H).json
assert (saved["recent_chapters"], saved["warnings"]) == (3, [])
@@ -301,6 +305,16 @@ def test_settings(client, monkeypatch, tmp_path):
assert bad.status_code == 400 and "line 1" in bad.json["error"]
assert ini.read_text() == "[www.notasite.com]\nx:1\n"
assert client.put("/api/settings", json={"recent_chapters": -1}, headers=H).status_code == 400
saved = client.put(
"/api/settings",
json={"story_jobs_hours": 48, "story_jobs_count": 20, "main_jobs_hours": 12, "main_jobs_count": 50},
headers=H,
).json
assert (saved["story_jobs_hours"], saved["story_jobs_count"]) == (48, 20)
assert (saved["main_jobs_hours"], saved["main_jobs_count"]) == (12, 50)
assert client.put("/api/settings", json={"story_jobs_hours": -1}, headers=H).status_code == 400
assert client.put("/api/settings", json={"story_jobs_hours": 721}, headers=H).status_code == 400
assert client.put("/api/settings", json={"main_jobs_count": 501}, headers=H).status_code == 400
def test_metadata_edit(client):
+5 -1
View File
@@ -44,7 +44,11 @@ def test_add_list_download_and_cancel_jobs(client, story):
assert result.status_code == 200
assert result.json["force_fields"] == ["title"]
assert result.json["mode"] == "metadata"
assert client.get("/api/config").json["metadata_fields"]["title"]
app_config = client.get("/api/config").json
assert app_config["metadata_fields"]["title"]
assert (app_config["story_jobs_hours"], app_config["story_jobs_count"]) == (36, 10)
assert (app_config["main_jobs_hours"], app_config["main_jobs_count"]) == (6, 25)
assert (app_config["job_list_max_hours"], app_config["job_list_max_count"]) == (720, 500)
def test_settings_and_inclusion_persist_and_delete_cascades(client, story):