Limit and configure how many jobs the Jobs panels show
This commit is contained in:
+3
-1
@@ -68,7 +68,9 @@ TRUST_PROXY = _bool("TRUST_PROXY")
|
||||
START_THREADS = _bool("BINDERY_START_THREADS", "true")
|
||||
|
||||
SITE_DELAY = 5 # seconds between two jobs that go to a story site
|
||||
JOB_HISTORY = 60 * 60 # seconds that finished jobs stay in the job list
|
||||
JOB_LIST_MAX_HOURS = 24 * 30 # the highest allowed value of a "jobs from the last N hours" setting
|
||||
JOB_LIST_MAX_COUNT = 500 # the highest allowed value of a "to a maximum of N jobs" setting
|
||||
JOB_HISTORY = JOB_LIST_MAX_HOURS * 60 * 60 # seconds; finished jobs stay this long, for the settings above
|
||||
|
||||
DB_PATH = CONFIG_DIR / "bindery.db"
|
||||
UPLOAD_DIR = CONFIG_DIR / "uploads" # uploaded epubs wait here for their job, also over a restart
|
||||
|
||||
@@ -293,6 +293,24 @@ def edit_metadata(story_id: int, values: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"story": story_details(story_id)["story"], "job": queued}
|
||||
|
||||
|
||||
# Defaults for the job list settings. Each pair is how many hours of finished jobs the Jobs
|
||||
# panel shows, and the most it shows, before a queued job (which the panel always shows).
|
||||
JOB_LIST_DEFAULTS = {
|
||||
"story_jobs_hours": 36,
|
||||
"story_jobs_count": 10,
|
||||
"main_jobs_hours": 6,
|
||||
"main_jobs_count": 25,
|
||||
}
|
||||
|
||||
|
||||
def job_list_settings() -> dict[str, int]:
|
||||
result = {}
|
||||
for name, default in JOB_LIST_DEFAULTS.items():
|
||||
value = store.get_setting(None, name)
|
||||
result[name] = int(value) if value is not None else default
|
||||
return result
|
||||
|
||||
|
||||
def get_settings() -> dict[str, Any]:
|
||||
ini = config.PERSONAL_INI
|
||||
return {
|
||||
@@ -300,14 +318,31 @@ def get_settings() -> dict[str, Any]:
|
||||
"hold_new_links": bool(store.get_setting(None, "hold_new_links")),
|
||||
"pause_uploads": _uploads_paused(),
|
||||
"personal_ini": ini.read_text(encoding="utf-8") if ini.exists() else "",
|
||||
**job_list_settings(),
|
||||
}
|
||||
|
||||
|
||||
def _validate_job_list_value(value: Any, name: str, maximum: int) -> int:
|
||||
try:
|
||||
n = int(value)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError(f"{name} must be a whole number") from e
|
||||
if n < 0:
|
||||
raise ValueError(f"{name} cannot be negative")
|
||||
if n > maximum:
|
||||
raise ValueError(f"{name} cannot be more than {maximum}")
|
||||
return n
|
||||
|
||||
|
||||
def save_settings(
|
||||
recent_chapters: Any = None,
|
||||
personal_ini: str | None = None,
|
||||
hold_new_links: bool | None = None,
|
||||
pause_uploads: bool | None = None,
|
||||
story_jobs_hours: Any = None,
|
||||
story_jobs_count: Any = None,
|
||||
main_jobs_hours: Any = None,
|
||||
main_jobs_count: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Change the settings that are given. Return all settings, and FanFicFare's warnings about personal.ini."""
|
||||
warnings: list[str] = []
|
||||
@@ -327,6 +362,14 @@ def save_settings(
|
||||
if count < 0:
|
||||
raise ValueError("The number of recent chapters cannot be negative")
|
||||
store.set_setting(None, "recent_chapters", count)
|
||||
for name, value, maximum in (
|
||||
("story_jobs_hours", story_jobs_hours, config.JOB_LIST_MAX_HOURS),
|
||||
("story_jobs_count", story_jobs_count, config.JOB_LIST_MAX_COUNT),
|
||||
("main_jobs_hours", main_jobs_hours, config.JOB_LIST_MAX_HOURS),
|
||||
("main_jobs_count", main_jobs_count, config.JOB_LIST_MAX_COUNT),
|
||||
):
|
||||
if value is not None:
|
||||
store.set_setting(None, name, _validate_job_list_value(value, name, maximum))
|
||||
if personal_ini is not None:
|
||||
warnings = fff.check_personal_ini(personal_ini)
|
||||
# Write a new file, then replace the old one, so a job never reads half a file.
|
||||
|
||||
@@ -95,20 +95,37 @@ function jobItem(job) {
|
||||
);
|
||||
}
|
||||
|
||||
// A queued or running job always shows, however old or far down the list. A job that is done,
|
||||
// failed or cancelled shows only within the last `hours` hours, and at most `count` of them,
|
||||
// newest first.
|
||||
function visibleJobs(list, { hours, count }) {
|
||||
const byNewest = (a, b) => new Date(b.created || 0) - new Date(a.created || 0);
|
||||
const active = list.filter((j) => ACTIVE.has(j.status));
|
||||
const others = list.filter((j) => !ACTIVE.has(j.status));
|
||||
const cutoff = Date.now() - hours * 3600000;
|
||||
// A missing updated time never happens outside a test fixture. Showing the job is the safer default.
|
||||
const recent = others
|
||||
.filter((j) => !j.updated || new Date(j.updated).getTime() >= cutoff)
|
||||
.sort(byNewest)
|
||||
.slice(0, count);
|
||||
return [...active, ...recent].sort(byNewest);
|
||||
}
|
||||
|
||||
// A job list for a page that polls. The list keeps its scroll box and changes only the jobs in it, so an update
|
||||
// does not move the list back to the top. A poll without changes leaves the list alone.
|
||||
export function jobPanel() {
|
||||
export function jobPanel(limits = { hours: 24 * 30, count: 500 }) {
|
||||
const box = h('div', { class: 'jobs' });
|
||||
const empty = h('p', { class: 'empty' }, 'No jobs.');
|
||||
const el = h('div');
|
||||
let shown = null;
|
||||
let child = null;
|
||||
const render = (list) => {
|
||||
const key = JSON.stringify(list);
|
||||
const visible = visibleJobs(list, limits);
|
||||
const key = JSON.stringify(visible);
|
||||
if (key === shown) return;
|
||||
shown = key;
|
||||
fill(box, list.map(jobItem));
|
||||
const next = list.length ? box : empty;
|
||||
fill(box, visible.map(jobItem));
|
||||
const next = visible.length ? box : empty;
|
||||
if (child !== next) {
|
||||
child = next;
|
||||
fill(el, child);
|
||||
|
||||
@@ -6,11 +6,17 @@ import { globalRulesCard } from './rules.js';
|
||||
const INI_DOCS = 'https://github.com/JimmXinu/FanFicFare/wiki/INI-File';
|
||||
const DEFAULTS_INI = 'https://github.com/JimmXinu/FanFicFare/blob/main/fanficfare/defaults.ini';
|
||||
|
||||
export function settingsPage(main) {
|
||||
export function settingsPage(main, _params, config) {
|
||||
document.title = 'Settings · Bindery';
|
||||
const recent = h('input', { id: 'recent-chapters', type: 'number', min: 0, step: 1 });
|
||||
const holdNewLinks = checkbox(false, () => {});
|
||||
const pauseUploads = checkbox(false, () => {});
|
||||
const hoursField = (id) => h('input', { id, type: 'number', min: 0, max: config.job_list_max_hours, step: 1 });
|
||||
const countField = (id) => h('input', { id, type: 'number', min: 0, max: config.job_list_max_count, step: 1 });
|
||||
const storyJobsHours = hoursField('story-jobs-hours');
|
||||
const storyJobsCount = countField('story-jobs-count');
|
||||
const mainJobsHours = hoursField('main-jobs-hours');
|
||||
const mainJobsCount = countField('main-jobs-count');
|
||||
const ini = h('textarea', {
|
||||
class: 'ini',
|
||||
rows: 24,
|
||||
@@ -37,6 +43,10 @@ export function settingsPage(main) {
|
||||
holdNewLinks.checked = settings.hold_new_links;
|
||||
pauseUploads.checked = settings.pause_uploads;
|
||||
ini.value = settings.personal_ini;
|
||||
storyJobsHours.value = settings.story_jobs_hours;
|
||||
storyJobsCount.value = settings.story_jobs_count;
|
||||
mainJobsHours.value = settings.main_jobs_hours;
|
||||
mainJobsCount.value = settings.main_jobs_count;
|
||||
})
|
||||
.catch(showError);
|
||||
|
||||
@@ -97,6 +107,48 @@ export function settingsPage(main) {
|
||||
}, { class: 'primary' }),
|
||||
),
|
||||
),
|
||||
h(
|
||||
'section',
|
||||
{ class: 'card' },
|
||||
h('h2', {}, 'Jobs list'),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'controls' },
|
||||
h('label', { for: 'story-jobs-hours' }, "A story page's job list shows jobs from the last"),
|
||||
storyJobsHours,
|
||||
h('label', { for: 'story-jobs-count' }, 'hours, to a maximum of'),
|
||||
storyJobsCount,
|
||||
'jobs.',
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'controls' },
|
||||
h('label', { for: 'main-jobs-hours' }, "The story list's job list shows jobs from the last"),
|
||||
mainJobsHours,
|
||||
h('label', { for: 'main-jobs-count' }, 'hours, to a maximum of'),
|
||||
mainJobsCount,
|
||||
'jobs.',
|
||||
),
|
||||
h(
|
||||
'p',
|
||||
{ class: 'hint' },
|
||||
`A queued or running job always shows. The most either field allows is ${config.job_list_max_hours} hours ` +
|
||||
`and ${config.job_list_max_count} jobs.`,
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'controls' },
|
||||
button('Save', async () => {
|
||||
await api('settings', 'PUT', {
|
||||
story_jobs_hours: storyJobsHours.value,
|
||||
story_jobs_count: storyJobsCount.value,
|
||||
main_jobs_hours: mainJobsHours.value,
|
||||
main_jobs_count: mainJobsCount.value,
|
||||
});
|
||||
toast('Saved');
|
||||
}, { class: 'primary' }),
|
||||
),
|
||||
),
|
||||
globalRulesCard(),
|
||||
h(
|
||||
'section',
|
||||
|
||||
@@ -191,7 +191,7 @@ export function storiesPage(main, _params, config) {
|
||||
let stories = null;
|
||||
let filter = '';
|
||||
const table = h('div', {}, h('p', { class: 'empty' }, 'Loading…'));
|
||||
const jobs = jobPanel();
|
||||
const jobs = jobPanel({ hours: config.main_jobs_hours, count: config.main_jobs_count });
|
||||
|
||||
const render = () => {
|
||||
const q = filter.trim().toLowerCase();
|
||||
|
||||
@@ -88,7 +88,7 @@ export function storyPage(main, { storyId }, config) {
|
||||
const updates = h('div');
|
||||
const chapters = h('div');
|
||||
const selectAll = h('span', { class: 'chapter-select-all' });
|
||||
const jobs = jobPanel();
|
||||
const jobs = jobPanel({ hours: config.story_jobs_hours, count: config.story_jobs_count });
|
||||
const linked = h('div');
|
||||
const searchResults = h('div');
|
||||
|
||||
|
||||
+11
-1
@@ -211,6 +211,9 @@ def app_config() -> Any:
|
||||
"mode_labels": MODE_LABELS,
|
||||
"library_available": orchestrator.library_available(),
|
||||
"update_interval": config.UPDATE_INTERVAL_HOURS,
|
||||
"job_list_max_hours": config.JOB_LIST_MAX_HOURS,
|
||||
"job_list_max_count": config.JOB_LIST_MAX_COUNT,
|
||||
**orchestrator.job_list_settings(),
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +222,14 @@ def settings() -> Any:
|
||||
if request.method == "PUT":
|
||||
data = _json()
|
||||
return orchestrator.save_settings(
|
||||
data.get("recent_chapters"), data.get("personal_ini"), data.get("hold_new_links"), data.get("pause_uploads")
|
||||
data.get("recent_chapters"),
|
||||
data.get("personal_ini"),
|
||||
data.get("hold_new_links"),
|
||||
data.get("pause_uploads"),
|
||||
data.get("story_jobs_hours"),
|
||||
data.get("story_jobs_count"),
|
||||
data.get("main_jobs_hours"),
|
||||
data.get("main_jobs_count"),
|
||||
)
|
||||
return orchestrator.get_settings()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user