fix(tasks): name upgrade tasks, humanise dead instance slugs, keep icons
Three visible faults on the Tasks page, one shared root.
Upgrade tasks rendered as their raw command — "libreportal updater
upgrade rocketchat 8.7.1" beside properly named neighbours. The title
table had rows for updater check/apply/apply-all/rollback and none for
upgrade, because the upgrade command is assembled in task-actions.js
rather than task-commands.js — and lp-task-names, the guard built to
catch exactly this, only read task-commands.js. It certified 16 commands
and reported that as the whole surface; the surface was 29. The guard
now reads both dispatch sites (JS ${expr} interpolations become sample
placeholders; commented-out prose mentioning commands in backticks is
skipped, or it reports fictional commands), and all 29 pass.
A removed instance's tasks outlive it, and its slug rendered as a tech
identifier: "Bookstack_uitest - Remove Instance". getAppDisplayName
cannot help — it capitalises as its own fallback, so unknown is
indistinguishable from known-and-plain. The formatter now does the same
membership test the helper uses internally: slug absent from
window.apps, prefix before the underscore present -> render the way
live instances are shown, "Bookstack · uitest".
Same story for the icon: bookstack_uitest.svg is deleted with the
instance, and onerror="display:none" left a bare gap in the row. Now a
fallback chain — the TYPE's icon (which survives), then the LibrePortal
logo. Verified live: the dead instance's rows show bookstack.svg with
the fallback marker set, everything else keeps its own icon.
Verified in a real browser session — full render, zero console errors.
The 'add' verb also joins the app-action map so "Add Application" is
deliberate wording rather than the blind "<Verb> Application" compose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
035efa948b
commit
785df3dcd8
@ -11,7 +11,26 @@ Object.assign(TasksManager.prototype, {
|
||||
// Adding a new task: just append one row here. Add the matching command
|
||||
// shape in the WebUI submission site and you're done — no new branch
|
||||
// needed in this function.
|
||||
const displayName = (slug) => window.getAppDisplayName ? window.getAppDisplayName(slug) : (slug.charAt(0).toUpperCase() + slug.slice(1));
|
||||
const displayName = (slug) => {
|
||||
const cap = (x) => x.charAt(0).toUpperCase() + x.slice(1);
|
||||
if (!window.getAppDisplayName) return cap(slug);
|
||||
// getAppDisplayName never says "unknown" — it capitalises as its own
|
||||
// fallback — so telling a real app from a leftover slug needs the same
|
||||
// membership test it uses internally. The case this exists for: a
|
||||
// REMOVED instance's tasks outlive it, and its slug (bookstack_uitest)
|
||||
// is no longer in window.apps, so it rendered as a tech identifier.
|
||||
// When the prefix before the underscore is still a real app, render the
|
||||
// way live instances are shown: "Bookstack · uitest".
|
||||
const apps = window.apps || [];
|
||||
const known = (x) => apps.some(a => (a.command || '').endsWith(` ${x}`) || a.name?.toLowerCase() === x.toLowerCase());
|
||||
if (known(slug)) return window.getAppDisplayName(slug);
|
||||
const us = slug.indexOf('_');
|
||||
if (us > 0 && known(slug.slice(0, us))) {
|
||||
return `${window.getAppDisplayName(slug.slice(0, us))} · ${slug.slice(us + 1)}`;
|
||||
}
|
||||
return window.getAppDisplayName(slug);
|
||||
};
|
||||
|
||||
|
||||
const PATTERNS = [
|
||||
// -- System / setup ----------------------------------------------------
|
||||
@ -29,6 +48,10 @@ Object.assign(TasksManager.prototype, {
|
||||
{ match: /^libreportal updater apply-all\b/, title: 'Apps - Update All' },
|
||||
{ match: /^libreportal updater apply (\S+)/, title: (m) => `${displayName(m[1])} - Update` },
|
||||
{ match: /^libreportal updater rollback (\S+)/, title: (m) => `${displayName(m[1])} - Roll Back` },
|
||||
// Stepped version upgrade — built in task-actions.js rather than
|
||||
// task-commands.js, which is how it escaped lp-task-names' guard and
|
||||
// shipped title-less: the row showed the raw command.
|
||||
{ match: /^libreportal updater upgrade (\S+)(?:\s+(\S+))?/, title: (m) => `${displayName(m[1])} - Upgrade${m[2] ? ` to ${m[2]}` : ''}` },
|
||||
{ match: /^libreportal artifact apply (\S+)/, title: (m) => `Hotfix ${m[1]} - Apply` },
|
||||
{ match: /^libreportal artifact revert (\S+)/, title: (m) => `Hotfix ${m[1]} - Revert` },
|
||||
|
||||
@ -125,6 +148,7 @@ Object.assign(TasksManager.prototype, {
|
||||
const action = libreportalMatch[1];
|
||||
const appName = libreportalMatch[2];
|
||||
const actionMap = {
|
||||
'add': 'Add Application',
|
||||
'install': 'Install Application',
|
||||
'uninstall': 'Uninstall Application',
|
||||
'restart': 'Restart Application',
|
||||
|
||||
@ -280,7 +280,16 @@ Object.assign(TasksManager.prototype, {
|
||||
const isSystemSentinel = task.app === 'system' || task.app === 'updater';
|
||||
if (task.app && !isSystemSentinel) {
|
||||
const appIconPath = this.getAppIconPath(task);
|
||||
return `${typeIcon}<img src="${appIconPath}" alt="${task.app}" class="task-app-icon" onerror="this.style.display='none'">`;
|
||||
// Fallback chain instead of vanishing. A removed instance's tasks outlive
|
||||
// its icon file (bookstack_uitest.svg is deleted with the instance), and
|
||||
// display:none left a bare gap in the row. Try the TYPE's icon first —
|
||||
// an instance slug is <type>_<name>, and the type's icon survives — then
|
||||
// the LibrePortal logo, which every install ships.
|
||||
const baseSlug = String(task.app).includes('_') ? String(task.app).split('_')[0] : '';
|
||||
const fb1 = baseSlug ? `/core/icons/apps/${baseSlug}.svg` : '/core/icons/libreportal.svg';
|
||||
return `${typeIcon}<img src="${appIconPath}" alt="${task.app}" class="task-app-icon" `
|
||||
+ `onerror="if(!this.dataset.fb){this.dataset.fb=1;this.src='${fb1}';}`
|
||||
+ `else{this.onerror=null;this.src='/core/icons/libreportal.svg';}">`;
|
||||
}
|
||||
if (isSystemSentinel || this.isLibrePortalSystemTask(task)) {
|
||||
return `${typeIcon}<img src="/core/icons/libreportal.svg" alt="LibrePortal" class="task-app-icon">`;
|
||||
|
||||
@ -41,6 +41,7 @@ import sys
|
||||
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
FRONTEND = os.path.join(ROOT, "containers", "libreportal", "frontend")
|
||||
COMMANDS = os.path.join(FRONTEND, "core", "tasks", "js", "task-commands.js")
|
||||
ACTIONS = os.path.join(FRONTEND, "core", "tasks", "js", "task-actions.js")
|
||||
FORMAT = os.path.join(FRONTEND, "components", "tasks", "js", "tasks-format.js")
|
||||
|
||||
# Stand-ins for the {placeholders} in a command template. Values only have to be
|
||||
@ -54,6 +55,25 @@ SAMPLES = {
|
||||
"backupId": "backup_20260101",
|
||||
"toolId": "list_users",
|
||||
"args": "",
|
||||
# task-actions.js interpolations (see actions_templates below)
|
||||
"app": "bookstack",
|
||||
"slug": "bookstack",
|
||||
"serviceName": "bookstack-db",
|
||||
"v": "1.2.3",
|
||||
"cmd": "",
|
||||
"id": "hotfix-1",
|
||||
"flags.join(' ')": "",
|
||||
"backupFile": "backup_20260101.tar.gz",
|
||||
"changes": "CFG_X=1",
|
||||
"flag": "",
|
||||
"list": "bookstack,navidrome",
|
||||
"list.join(',')": "bookstack,navidrome",
|
||||
"deleteRemote1": "false",
|
||||
"deleteRemote2": "false",
|
||||
"action": "install",
|
||||
"toolName": "list_users",
|
||||
"version": "1.2.3",
|
||||
"config.replace(/'/g, \"'\\\\''\")": "",
|
||||
}
|
||||
|
||||
|
||||
@ -104,6 +124,37 @@ def command_templates():
|
||||
return out
|
||||
|
||||
|
||||
def actions_templates():
|
||||
"""Every backtick `libreportal …` template built in task-actions.js.
|
||||
|
||||
This is the file the original guard did not read, and precisely where the
|
||||
gap it exists to catch actually happened: `libreportal updater upgrade
|
||||
<app> <version>` is assembled here, never appears in task-commands.js, and
|
||||
shipped with no title row — every upgrade task rendered as its raw command.
|
||||
A guard that only reads one of the two dispatch sites certifies half the
|
||||
surface and reports it as all of it.
|
||||
|
||||
JS ${expr} interpolations become {expr} placeholders and go through the
|
||||
same SAMPLES fill as the {name} ones in task-commands.js. A template using
|
||||
an expression SAMPLES does not know is a die(), not a skip — an unknown
|
||||
placeholder is a command the guard cannot vouch for.
|
||||
"""
|
||||
src = open(ACTIONS, encoding="utf-8").read()
|
||||
# Strip comment lines first: prose mentions commands in backticks too
|
||||
# ("// index via `libreportal app add` as a task"), and reading those as
|
||||
# dispatch sites reports fictional commands as unnamed.
|
||||
src = "\n".join(l for l in src.splitlines() if not l.strip().startswith("//"))
|
||||
out = []
|
||||
for m in re.finditer(r"`(libreportal [^`]*)`", src):
|
||||
tmpl = re.sub(r"\$\{([^}]+)\}", r"{\1}", m.group(1))
|
||||
# conditional-suffix templates (`… ${v ? … : …}`) collapse to their bare
|
||||
# form: the guard cares that the COMMAND SHAPE has a title row, and the
|
||||
# regex rows match on prefix.
|
||||
tmpl = re.sub(r"\{[^}]*\?[^}]*\}", "", tmpl)
|
||||
out.append(("task-actions.js", tmpl.strip()))
|
||||
return out
|
||||
|
||||
|
||||
def fill(template):
|
||||
def sub(m):
|
||||
key = m.group(1)
|
||||
@ -122,6 +173,15 @@ def main():
|
||||
cases = [(key, fill(tpl)) for key, tpl in command_templates()]
|
||||
if not cases:
|
||||
die("no command templates found in task-commands.js — has its shape changed?")
|
||||
# Second dispatch site — the one the guard originally missed (see
|
||||
# actions_templates). Dedupe by filled command so shapes both files build
|
||||
# are not reported twice.
|
||||
seen = {c for _, c in cases}
|
||||
for key, tpl in actions_templates():
|
||||
filled = fill(tpl)
|
||||
if filled not in seen:
|
||||
seen.add(filled)
|
||||
cases.append((key, filled))
|
||||
|
||||
harness = os.path.join("/tmp", f"lp-task-names-{os.getpid()}.js")
|
||||
with open(harness, "w", encoding="utf-8") as fh:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user