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
|
// 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
|
// shape in the WebUI submission site and you're done — no new branch
|
||||||
// needed in this function.
|
// 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 = [
|
const PATTERNS = [
|
||||||
// -- System / setup ----------------------------------------------------
|
// -- System / setup ----------------------------------------------------
|
||||||
@ -29,6 +48,10 @@ Object.assign(TasksManager.prototype, {
|
|||||||
{ match: /^libreportal updater apply-all\b/, title: 'Apps - Update All' },
|
{ match: /^libreportal updater apply-all\b/, title: 'Apps - Update All' },
|
||||||
{ match: /^libreportal updater apply (\S+)/, title: (m) => `${displayName(m[1])} - Update` },
|
{ match: /^libreportal updater apply (\S+)/, title: (m) => `${displayName(m[1])} - Update` },
|
||||||
{ match: /^libreportal updater rollback (\S+)/, title: (m) => `${displayName(m[1])} - Roll Back` },
|
{ 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 apply (\S+)/, title: (m) => `Hotfix ${m[1]} - Apply` },
|
||||||
{ match: /^libreportal artifact revert (\S+)/, title: (m) => `Hotfix ${m[1]} - Revert` },
|
{ match: /^libreportal artifact revert (\S+)/, title: (m) => `Hotfix ${m[1]} - Revert` },
|
||||||
|
|
||||||
@ -125,6 +148,7 @@ Object.assign(TasksManager.prototype, {
|
|||||||
const action = libreportalMatch[1];
|
const action = libreportalMatch[1];
|
||||||
const appName = libreportalMatch[2];
|
const appName = libreportalMatch[2];
|
||||||
const actionMap = {
|
const actionMap = {
|
||||||
|
'add': 'Add Application',
|
||||||
'install': 'Install Application',
|
'install': 'Install Application',
|
||||||
'uninstall': 'Uninstall Application',
|
'uninstall': 'Uninstall Application',
|
||||||
'restart': 'Restart Application',
|
'restart': 'Restart Application',
|
||||||
|
|||||||
@ -280,7 +280,16 @@ Object.assign(TasksManager.prototype, {
|
|||||||
const isSystemSentinel = task.app === 'system' || task.app === 'updater';
|
const isSystemSentinel = task.app === 'system' || task.app === 'updater';
|
||||||
if (task.app && !isSystemSentinel) {
|
if (task.app && !isSystemSentinel) {
|
||||||
const appIconPath = this.getAppIconPath(task);
|
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)) {
|
if (isSystemSentinel || this.isLibrePortalSystemTask(task)) {
|
||||||
return `${typeIcon}<img src="/core/icons/libreportal.svg" alt="LibrePortal" class="task-app-icon">`;
|
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__), "..", ".."))
|
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
FRONTEND = os.path.join(ROOT, "containers", "libreportal", "frontend")
|
FRONTEND = os.path.join(ROOT, "containers", "libreportal", "frontend")
|
||||||
COMMANDS = os.path.join(FRONTEND, "core", "tasks", "js", "task-commands.js")
|
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")
|
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
|
# Stand-ins for the {placeholders} in a command template. Values only have to be
|
||||||
@ -54,6 +55,25 @@ SAMPLES = {
|
|||||||
"backupId": "backup_20260101",
|
"backupId": "backup_20260101",
|
||||||
"toolId": "list_users",
|
"toolId": "list_users",
|
||||||
"args": "",
|
"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
|
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 fill(template):
|
||||||
def sub(m):
|
def sub(m):
|
||||||
key = m.group(1)
|
key = m.group(1)
|
||||||
@ -122,6 +173,15 @@ def main():
|
|||||||
cases = [(key, fill(tpl)) for key, tpl in command_templates()]
|
cases = [(key, fill(tpl)) for key, tpl in command_templates()]
|
||||||
if not cases:
|
if not cases:
|
||||||
die("no command templates found in task-commands.js — has its shape changed?")
|
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")
|
harness = os.path.join("/tmp", f"lp-task-names-{os.getpid()}.js")
|
||||||
with open(harness, "w", encoding="utf-8") as fh:
|
with open(harness, "w", encoding="utf-8") as fh:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user