From 99a7a221de58469b04eb2821361fb06b169ee01a Mon Sep 17 00:00:00 2001 From: Dev-2 Date: Tue, 3 Nov 2026 10:50:16 +0000 Subject: [PATCH] client: a locked Bot card with no tasks draws no heading A read-only window's Bot settings card dropped its New task row but kept the "Scheduled tasks" heading, so a Bot with no tasks showed a heading over an empty body. The details line and a group chat's section already draw nothing in the same state (#2246, P-482). --- changelog.d/2286-locked-empty-card.fixed.md | 1 + core/client.js | 1 + core/src/client/schedules.js | 5 ++++ core/test/schedules-client.test.js | 28 +++++++++++++++++++ docs/subsystems/core-client.md | 2 +- patches/ds-bot/0001-core-fixes.patch | 7 ++++- patches/ds-bot/0008-client-bundle.patch | 3 +- .../files/test/schedules-client.test.js | 28 +++++++++++++++++++ 8 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 changelog.d/2286-locked-empty-card.fixed.md diff --git a/changelog.d/2286-locked-empty-card.fixed.md b/changelog.d/2286-locked-empty-card.fixed.md new file mode 100644 index 000000000..0777ef9ea --- /dev/null +++ b/changelog.d/2286-locked-empty-card.fixed.md @@ -0,0 +1 @@ +A Bot's **Scheduled tasks** card in a read-only window draws nothing when the Bot has no scheduled tasks, instead of the bare heading over an empty body; a writable window keeps its **New task** row, and a locked card that has tasks keeps its heading and rows ([#2286](https://github.com/ThinkFlowLab/FlyBot/issues/2286)). diff --git a/core/client.js b/core/client.js index 907ee5f45..f079fe294 100644 --- a/core/client.js +++ b/core/client.js @@ -5924,6 +5924,7 @@ function BotSchedules({ bot, roster, actions }) { } if (view === null) return null; const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; + if (locked && view.available && tasks.length === 0) return null; return (0, import_react6.createElement)( "section", { className: "bt-set-section" }, diff --git a/core/src/client/schedules.js b/core/src/client/schedules.js index bf63ccb72..84c8cfaa3 100644 --- a/core/src/client/schedules.js +++ b/core/src/client/schedules.js @@ -447,6 +447,11 @@ export function BotSchedules({ bot, roster, actions }) { } if (view === null) return null const tasks = view.available ? view.tasks.filter(task => task.botId === bot.id) : [] + // A locked card with no tasks drew the heading over an empty body: it adds no row, and + // TaskList draws nothing without one. The details line hides this whole block in that state + // (#2232, P-457), so the settings card says nothing either. The Automation-tasks-off branch + // below is a different state and keeps its hint and button. + if (locked && view.available && tasks.length === 0) return null return h('section', { className: 'bt-set-section' }, h('h3', null, t('Scheduled tasks')), !view.available diff --git a/core/test/schedules-client.test.js b/core/test/schedules-client.test.js index fe2ca3188..16859a701 100644 --- a/core/test/schedules-client.test.js +++ b/core/test/schedules-client.test.js @@ -813,3 +813,31 @@ test('P-457: a read-only details line offers no Add one, and a writable one does const writable = render(ScheduleSummary({ bot: { id: 'b1' }, roster: ROSTER, actions })) assert.deepEqual(buttonsOf(writable).map(labelOf), ['No scheduled tasks yet. Add one'], 'a writable details line keeps its Add one') }) + +// P-482: after #2246 the same "locked, no tasks" state drew three shapes — ScheduleSummary hid +// the whole block, RoomSchedules drew nothing, and a Bot's card kept its "Scheduled tasks" +// heading over an empty body (TaskList returns null with no rows, and a locked card adds no row). +// A locked card with no tasks now draws nothing; the writable card and a locked card with tasks +// stay as they are. +const sectionsOf = tree => all(tree).filter(node => node.type === 'section') + +test('P-482: a locked Bot card with no tasks draws no bare "Scheduled tasks" heading', () => { + seeded.push({ available: true, tasks: [] }) + const locked = render(BotSchedules({ bot: { id: 'b1' }, roster: LOCKED_ROSTER, actions: scheduleActions() })) + assert.equal(sectionsOf(locked).length, 0, `a locked zero-task card still draws a section, saw ${text(locked).join('|')}`) + assert.ok(!text(locked).includes('Scheduled tasks'), `a locked zero-task card still says "Scheduled tasks", saw ${text(locked).join('|')}`) + // The same fixture in a writable window is the control: the rule is the lock, not an empty list. + seeded.push({ available: true, tasks: [] }) + const writable = render(BotSchedules({ bot: { id: 'b1' }, roster: ROSTER, actions: scheduleActions() })) + assert.equal(sectionsOf(writable).length, 1, 'a writable zero-task card keeps its section') + assert.ok(text(writable).includes('Scheduled tasks'), 'a writable zero-task card keeps its heading') + assert.ok(buttonsOf(writable).map(labelOf).includes('New task'), 'a writable zero-task card keeps the New task row') +}) + +test('P-482: a locked Bot card with tasks keeps the heading and the rows', () => { + seeded.push({ available: true, tasks: [ACTIVE] }) + const locked = render(BotSchedules({ bot: { id: 'b1' }, roster: LOCKED_ROSTER, actions: scheduleActions() })) + assert.equal(sectionsOf(locked).length, 1, 'the rule must not hide a locked card that has tasks') + assert.ok(text(locked).includes('Scheduled tasks'), 'the heading stays with tasks') + assert.ok(text(locked).includes('Briefing'), 'the task row stays') +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index cb3511324..935e74ad7 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -45,7 +45,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl | `core/src/client/panes.js` | `PANE_MIN_WIDTH_CSS` (#705): every pane the multi-pane row flexes gets `min-width:0`, the conversation column is matched by its DSH class fragment, and the panel's long values (About text, `4.1M tok · Cache hit …`) wrap with `overflow-wrap:anywhere`, so a value that cannot wrap never widens the panel past the space reserved for it. The same sheet clips horizontal overflow on the document, the conversation column and the app frame (`overflow-x:clip`, #1086), and `watchShellLevel` / `levelShell` put a leftover `scrollLeft` back, so an error toast or dialog cannot leave the window shifted. Tests: `core/test/shell-level.test.js`. | | `core/src/shared/approval-waiting.js` | The one rule for "this hold still waits for the owner's decision" (#734, #754): `isApprovalWaiting(status)` is true for `pending` and `pending_approval` and false for every settled spelling (`approved`, `auto`, `failed`, `rejected`, `stale`, missing). The host writes `pending`; Bot Social's own outbound hold arrives as `pending_approval`. Host and client both import this one file — the held-reply card, the approvals overlay and the chat bubble on this side, `hooks.js` (`decide()`, `heldOf()`, the cap, the in-flight filter, the cleanup) and `channel.js` (a row's wait badge) on the other — and the overlay asks the host for `[...WAITING_STATUSES, 'failed']`, so the two spellings cannot drift apart again on one side only. Tests: `core/test/approval-waiting.test.js`, `core/test/host-approval-status.test.js`. | | `core/src/shared/held-retry.js` | The one rule for "this failure can still be retried" (#968): `canRetry(record)` is false for `retry: false`, for `noRetry: true`, and for a record whose stored `error`/`code` names a terminal reason (`no such remote bot` / `is not shared with your Bots` / `is no longer shared with you` / `has not granted chat.public` / `bot_not_shared` / `bot_not_found`; a bare `capability_not_granted` is not one, #988), true otherwise. The host states the same answer in two spellings (`noRetry` on an approval, `retry: false` on a held reply), so both cards and Bot Social's own dock ask this one function instead of keeping a copy each; a hold written before the flag existed is read from its stored sentence at render time, with no state migration. Tests: `core/test/held-retry.test.js`. | -| `core/src/client/schedules.js` | Settings → Scheduled tasks, a Bot's card and the details line. Each row has **Edit** (pencil; the row turns into the same form that creates a task, filled in, with **Save changes**; a new task opens with no target chosen — **Send to** reads "Pick a Bot or group chat" and **Create task** stays disabled until one is picked, so one click cannot create the task for the first Bot (#1334); an edited task keeps its time zone, a cron rule it cannot show is kept), **Copy** (`CopyRow`, #1287: the row turns into the create form's target picker, minus the chat the task already runs in, and **Copy** posts `schedule-create` with `copyFrom: { sessionId, id }` — the host writes the row, so the copy keeps the source's next run) and **Pause** / **Resume**; a paused row reads **Paused** (#354). A row with a recorded run adds one line under it: "Running since …", "Last run …", "Last run … · Stopped" or, in red, "Last run … · Failed: "; a failed run of a task that is not paused gets **Retry**, which runs the task once now (`actions.scheduleRetry`, #473). With Automation tasks off, the hint reads "Switch on Automation tasks on the Plugins page." and the button **Open Plugins** closes the Settings dialog and opens DSH's Plugins panel (`actions.selectPanel('plugins')`); the Harness settings dialog has no such switch (#378). The page opens with the team's **Quiet hours** card (#1166): **Off** with a **Turn on** button, or **On, 22:00–07:00** with the window's two time fields and **Save window** (the settings write refuses an invalid window, and the host's named refusal appears on the form's error line, as a `role="alert"` so it is announced and not only printed (#2232)). While quiet hours are on and an `every`/`cron` task exists, the view's `quietUnchecked` flag draws one line saying such a task can still run inside the window: that flag is the only place the rule is computed — the page does not scan the task list itself (#1199) — and it asks for the view again right after the settings write, so the line does not wait for the 30-second poll. Never a silent claim of silence, and no suppressed count. A repeat whose interval is not a whole number of minutes is stated as seconds (`Every 90 seconds`, #1651): the edit form opens holding that exact value in the **seconds** unit, and saving a task whose repeat was not touched sends the stored interval back unchanged instead of a rounded number of minutes. The details panel's line (`ScheduleSummary`) lists every task of that Bot, paused and finished ones included, because a Bot whose tasks are all paused has tasks while a Bot with none is the only empty state; each row answers when — `Next: