Skip to content

15 October 2026

Thirty-five entries today:

  • An administration area theme, in the Theme Guide
  • Writing views that survive a UI switch
  • Moving a separate admin application into the area
  • "Saved." is shown
  • project:switch-ui keeps what it replaces
  • Smaller things the documentation review found
  • An administration that is its own application, declared in its config
  • Testing an application's JavaScript with the framework's runner and helpers
  • A SPA section inside a server-rendered page
  • The SPA's shared components follow the UI system
  • The side panel's buttons and look are the theme's
  • The notification feed, and a bell for it
  • A whole Svelte SPA follows the UI system
  • The log screens under another address, and a search box with its term
  • The tailwind theme's component styles in a file of their own
  • Every UI system has its components.css
  • Charts with no inline script, in the theme's colours
  • A template cannot change the render that included it
  • Settings forms no longer turn checkboxes on or blank settings
  • Errors in the browser are recorded
  • The service worker can replace an application's own
  • Data tables draw with daisyUI or the plain stylesheet
  • A Redis token bucket refills whatever the float precision
  • A dashboard each administrator arranges
  • Html\Datatable: values in its script are encoded (XSS)
  • Html\Datatable: the footer's text search filters again
  • Html\Datatable: a column picker and a readable size under tailwind
  • The chat folds what the model said on the way
  • Charts added after load, and on a phone
  • Scheduled AI tasks: reminders, one-offs, runs that cannot run twice
  • The model schedules, changes and cancels tasks itself
  • Shared AI reports, subscribed to and continued in Ask
  • The service worker leaves the administration area and the API alone
  • project:resync --spa-components creates what an updated component imports
  • Roles: who counts as an administrator, a superuser or root, said once

An administration area theme, in the Theme Guide

Documentation

  • The Theme Guide's An administration area theme covers:
    • what 'admin' => ['theme' => …] changes;
    • that the admin screens and their partials are not the theme's;
    • a checklist of what the theme must provide (the layout, [MODULE], the UI system's CSS, the palette tokens, renderCss()/renderJs(), pf-utils.js, the admin menu and the flashed messages) and what goes wrong without each;
    • the optional parts (search box, side panel, badges);
    • a minimal four-file theme that reuses the site's head and footer.

Writing views that survive a UI switch

Documentation

  • The Theme Guide's Writing views that survive a UI switch covers:
    • what project:switch-ui changes and leaves alone, including that it overwrites the default theme's layout files and style.css and never touches the application's views;
    • which components adapt on their own and which do not: Datatable with its default $bootstrap = true draws a Bootstrap 3 column picker, Date uses form-control, and component_classes is one map for every system;
    • what stays tied to a system;
    • a recommended approach, a before/after example, and a decision rule for a project with many templates.

Moving a separate admin application into the area

Documentation

  • The Routing Guide's Moving a separate admin application into the area covers:
    • the two arrangements compared;
    • six steps (merging the config, controllers, views and their links, the application class's overrides, the front controller and .htaccess, and what carried the application's name);
    • running both under two prefixes during a transition, and why one prefix cannot serve both;
    • a table of requests to check after the move.

"Saved." is shown

Fixed

  • The bundled admin screens flash "Saved." and redirect, and the screen they land on does not print the flash; neither did any scaffolded header. The message was written, carried across the redirect, and never seen. Html\Flash::render() draws the flash, escaped and taken so it is shown once. Every scaffolded theme.html.php calls it above [MODULE], and every scaffolded stylesheet styles pf-flash.
  • A view that shows the flash itself takes it first, so the layout repeats nothing. ApplicationsController, which read it without taking it, now takes it.

A project scaffolded before adds the one line to its layout. See the Upgrade Guide.

project:switch-ui keeps what it replaces

Fixed

  • project:switch-ui wrote the default theme's layout files and style.css over whatever the application had made of them, with no copy and no word. A file whose contents it changes is now copied to var/switch-ui/<time>/ first, and the output names each one.
  • It installed into www/ whatever the project served from. It now uses the project's web root (public/, …).
  • It names the views copied with project:publish-views, which keep the old system's markup, and how to take the new copy.
  • Switching to Tailwind with no CSP style-src to update now says what to add, instead of reporting the CSP as updated.

Smaller things the documentation review found

Fixed

  • Html\Datatable's column picker. It was a Bootstrap 3 dropdown under every UI system, so it opened nothing under Bootstrap 5 or Tailwind. $bootstrap now follows scaffold_theme, and the Bootstrap dropdown carries both 3's and 5's attributes.
  • The scaffolded Tailwind header filed a child whose parent the visitor may not see under that missing parent, and never printed it. It is promoted to the top level.
  • Unread badges. The scaffolded headers drew none: only template files init does not use had them. All three now print NavItem::badgeHtml(), a pf-nav-badge styled by every stylesheet.
  • The Help menu item took its floor from app.php's admin block only. An application that is the area, through AdminArea::serve(), listed it to every member. It takes the area's floor.
  • Theme:
    • a layout without [MODULE] lost its first eight characters to the foot and had no head; it is now all head;
    • a missing theme layout is logged to the theme log;
    • get_search_form() reads searchform.php;
    • front-page is an element by name. It was written with a comma for its arrow.
  • Tailwind .form-control. The Tailwind stylesheet styles form-control, the class Html\Date carries.
  • Docblocks:
    • NavItem::$icon is an icon name, not a CSS class;
    • AdminArea::reset() has its own docblock;
    • FieldStyles::for() has its own;
    • scaffold_theme's plain-css is said to resolve to the plain preset.
  • Examples. The Components Guide's Field examples follow the UI system, with FieldStyles::configured(), instead of naming one.

An administration that is its own application, declared in its config

Added

  • 'admin' => ['serve' => true, 'prefix' => 'admin', 'min_usertype' => 90] in the config of an administration with its own front controller: the application serves the area from its constructor on. Its navigation and the area's language are built inside it, src/Admin/ is in scope as for an area, and admin.theme and admin.default_controller apply.

Fixed

  • AdminArea::serve() called from init() had four faults, each fixed:
    • The navigation linked to the site, because it is built in init() before the call. serve() now builds it again, and logs that the config is the place for it.
    • src/Admin/ was never in scope. It now is.
    • A front controller in www/admin/ linked to /admin/admin/…. The area's URL is now sURL itself when sURL already ends in the prefix.
    • From the second request one process served, the area was forgotten. So was a sign-in page set with useLogin(). Application::beginRequest() now resets only the request's state, with AdminArea::resetRequest().

Testing an application's JavaScript with the framework's runner and helpers

Added

  • test:js: node --test over the application's *.test.js, from js_tests_dir in app.php (default tests/js). The framework's helpers are on NODE_PATH.
  • lint:js: ESLint with the framework's rules, which are defects only. It uses the application's eslint.config.mjs when there is one, and that config extends resources/js/pramnos-testing/eslint.base.mjs.
  • require('pramnos-testing'):
    • createDom(), a small DOM whose events bubble to the document and window;
    • loadScript(), which runs a classic page script in it;
    • stubFetch() and installApiHeaders();
    • installJquery(), a jQuery-shaped stub for legacy code.

They are shipped in the package, under resources/js/, so an application uses them rather than copying them.

A SPA section inside a server-rendered page

Added

  • spa_sections in app.php and Controller::spaSection($name, $props): one part of a server-rendered application runs as a single-page section inside the theme's page.
    • The controller returns a mount element (base, API configuration and props as attributes, no inline script) and the entry's tags: dev server, manifest, or the file.
    • A declared ability is checked first.
    • The page is never page-cached.
    • Every address under the controller reaches display(), so deep links and reloads open the section where they point.
  • lib/section.js gives a section its mount element and a router under its base. lib/page.js gives it the layout's flash, the title, and the side panel's record.
  • Every frontend/sections/<name>/main.js is an entry of the scaffolded Vite build.
  • The scaffolded lib/api.js sends the page's CSRF token from <meta name="csrf">, so a section's writes are accepted with the page's session.
  • pramnos-testing: window.dispatchEvent.

The SPA's shared components follow the UI system

Added

  • lib/ui.js: the classes DataTable, Pagination, ConfirmDialog, Field, Omnibox and HelpPanel draw with, per UI system: daisyUI, Bootstrap 5, or plain-css. Each part is asked for by name, and the system is the page's (window.__PRAMNOS__.ui). A SPA section takes it from scaffold_theme, so a section inside a Bootstrap or plain-css page draws in that system's classes and follows project:switch-ui.
  • pf-ui-* hooks in the scaffolded bootstrap and plain-css stylesheets cover what those systems have no class for: layout, the modal, the side panel, a sticky table header.

Fixed

  • The Svelte Omnibox showed nothing beside a group from a source with 'count' => false. It shows 5+, as the server-rendered search box does.

The side panel's buttons and look are the theme's

Added

  • Openers.
    • pf-assist.js treats every [data-pf-assist-open] as an opener; ="help" or ="ask" opens the panel on that tab.
    • Panel::opener($tab, $label, $class, $attributes) draws one such button, and Panel::template($ask, $askUrl) draws the panel without one.
    • Analyst::canAsk() says whether to offer Ask.
  • 'assist' => ['styles' => 'theme'] drops the panel's inline styles. It then carries only its pf-assist-* classes, which every scaffolded stylesheet styles. init writes the key; a project without it keeps the inline look.

The notification feed, and a bell for it

Added

  • Pramnos\Notification\Feed: one person's notifications, read back from what the database channel stored.
    • list() returns a page, unread only or not, by group.
    • unreadCount() and unreadByGroup() return the counts.
    • markRead() and markAllRead() touch only that person's rows.
    • Groups come from 'notifications' => ['groups' => …] (by class) or from the stored group.
    • Feed::badge() is a NavItem badge.
  • NotificationsController (/Notifications): the feed as JSON with the page's session, with the CSRF token on writes. init writes the wrapper with auth.
  • Html\NotificationBell and its behaviour in pf-utils.js.
    • Polling runs only while the tab is visible.
    • A click on an item marks it read, then follows its link. Read-all and tabs by group are there too.
    • pf:notifications:refresh and a realtime channel refresh it at once.
    • The scaffolded stylesheets style pf-notify-*.
  • pramnos-testing: contains().

Documentation

  • The Notifications Guide's Showing the feed replaces "reading is the application's job".

A whole Svelte SPA follows the UI system

Added

  • The shell App.svelte, the admin, chat, scheduled-report and consent screens, and the screens create:crud generates ask lib/ui.js for their parts, like the shared components already did. So a Svelte SPA in a bootstrap or plain-css project draws with that system's classes.
    • New parts include navbar, brand, nav.link, card.raised, card.title, alert.success, alert.warning, badge.*, input.inline, select.inline and table.zebra.
    • Sizes and shapes combine with a variant: ui('btn.primary', 'size.sm').
  • frontend/ui.css, imported by app.css, is the stylesheet of the project's UI system:
    • tailwind: daisyUI.
    • bootstrap: Bootstrap from npm, plus the theme's tokens and stylesheet.
    • plain-css: the theme's tokens and stylesheet.
    • The last two also include the daisyUI components neither has (chat, dock, stats, join, divider, tabs, loading).
    • init writes it, and bootstrap is in a Svelte SPA's package.json whatever the system.
  • The SPA shell reads scaffold_theme from app/app.php when it runs. It publishes it as window.__PRAMNOS__.ui.
  • project:switch-ui rewrites a Svelte SPA's ui.css, keeping the previous copy. It then says what is left to do:
    • run npm run build;
    • in an app.css that registers daisyUI itself, replace @plugin "daisyui"; with @import "./ui.css";;
    • in a project without the package, npm install -D bootstrap.
  • A framework test fails on a Svelte stub that writes a class two systems define (btn, card, table, …) instead of asking ui().

Documentation

  • In the Application Styles Guide, The whole SPA follows the UI system replaces the section on the shared components alone.

The log screens under another address, and a search box with its term

Added

  • LogController builds every link from logsRoute(): breadcrumbs, redirects, the toolbar, "Go back", the viewer's address and the stats/dashboard views. By default that is the subclass's class name; protected ?string $logsRoute sets another.
    • So an application with a Logs controller of its own mounts the viewer as class Logfiles extends LogController, with nothing to override.
    • Under any address but Logs, the bundled logs views come before the application's.
  • Html\SearchBox::$value: the term already in the box, escaped.

What breaks in subclasses

  • A LogController subclass not named Logs that is still served at /Logs now links to its own name. Add protected ?string $logsRoute = 'Logs';.

Documentation

  • The Logging Guide has a new section, Under another address. In the HTML Components Guide, SearchBox shows $value. There is a new row in the Upgrade Guide.

The tailwind theme's component styles in a file of their own

Added

  • scaffolding/themes/tailwind/components.css holds the styles of the framework's pf-* components and no page rules: the search box, side panel, bell, badges, flash, help button, datatable actions, breadcrumbs, pagination and stat tiles.
    • style.css imports it, and init and project:switch-ui write it beside style.css.
    • An administration with a Tailwind build of its own imports it from vendor/ and keeps its own layout.
  • The framework_scripts health check also names pf-*.js copies that differ from the framework's (outdated), which is what a composer update leaves in the web root.

Documentation

  • In the Theme Guide:
    • a new section, An area with a Tailwind build of its own, which covers daisyUI's .btn in the utilities layer;
    • the notification bell is now among an admin theme's options, replacing a line that said there was no feed.
  • In the Health Guide, framework_scripts covers outdated copies.

Every UI system has its components.css

Added

  • scaffolding/themes/bootstrap/components.css and scaffolding/themes/plain-css/components.css style the same framework components as the tailwind one, and each theme's style.css imports its own. init and project:switch-ui write it beside style.css.
  • The pf-ui-* hooks of the SPA's shared components stay in style.css. They adjust the classes they sit beside (.dropdown-menu, .form-control, .btn-sm), so they have to come after them.
  • A framework test checks three things:
    • the three files style the same pf-* components;
    • none of them carries a page rule;
    • every style.css imports its file before any rule.

Documentation

  • In the Theme Guide, An area with a build of its own replaces the Tailwind-only section.

Charts with no inline script, in the theme's colours

Added

  • Html\Chart::render($type, $labels, $datasets, $options) renders a figure:
    • the data as JSON in a <script type="application/json">, which no CSP blocks;
    • a hidden canvas;
    • a table of the same numbers;
    • and it enqueues chartjs where it is registered.
  • pf-utils.js draws each figure:
    • in the theme's colours (--color-*, --bs-* or --primary-color), again when data-theme or the OS colour scheme changes;
    • with numbers in the page's language through Intl.NumberFormat;
    • and the table is kept for screen readers once the chart is drawn.
  • Every components.css styles pf-chart-*.

Changed

  • The log dashboard in all three themes draws its two charts with Html\Chart. It had an inline <script> with hard-coded colours.

Documentation

  • The HTML Components Guide has a new section, Chart.

A template cannot change the render that included it

Fixed

  • Before, a template was included inside View::getTpl(), so it could assign that method's locals. A template with $render = [...] of its own made getTpl() return the page instead of keeping it, and the page came out blank with nothing in any log.
  • Now a template runs in a scope of its own, with copies of $lang, $model, $_url, $doc, $tpl and $type. Its variables still reach the layout it declares.

Documentation

  • The Framework Guide has a new section, What a template sees.

Settings forms no longer turn checkboxes on or blank settings

Fixed

  • An unchecked checkbox was saved as checked. A checkbox is rendered with a hidden 0 before it. Field::readSubmitted() read anything but an empty string as on, so the hidden 0 became '1'. It now reads '' and '0' as off.
  • A rejected submission could blank every setting. SettingsForm::getData() returned [] when the form's own token was missing, and a caller that saved it wrote empty values over the stored ones. That included a page that embedded the fields in a form of its own and sent only the synchronizer token.
    • getData() now accepts the synchronizer token too (_csrf_token or the X-CSRF-Token header), as Controller::isVerifiedWrite() does.
    • When no token checks out, it throws Html\Form\TokenRejected.
    • Theme::saveSettings() catches it and saves nothing.

Documentation

  • The Theme Guide's Rendering and saving covers both tokens and TokenRejected.
  • There is a new row in the Upgrade Guide.

Errors in the browser are recorded

Added

  • pf-utils.js reports what a page hits. It sends uncaught errors and unhandled rejections to the address in <meta name="pf-client-errors">, with sendBeacon().
    • Each distinct error goes once per page, and at most ten per page.
    • window.pfReportError(error) reports one the page caught.
    • Every theme's head.php written by init carries the tag.
  • The scaffolded service worker reports its own errors to the same address.
  • ClientErrorsController (/ClientErrors) takes the reports. It is open to visitors who are not signed in, and accepts only a same-origin request, a body of up to 16 KB and 30 reports a minute per address. init writes the wrapper.
  • Logs\ClientErrors stores one row per distinct error, with a count, first and last seen, the latest page, browser and user, and the stack.
    • It accepts the older {error, agent} shape under the hash applications computed for it.
  • A javascripterrors migration creates the table. Where an application already has its six-column table, the migration adds only the new columns, so the rows and the screens written against them stay.
  • The log screens have a Browser Errors page, with delete and clear.

Documentation

  • The Logging Guide has a new section, Errors in the browser, with the steps for an existing project.

The service worker can replace an application's own

Added

  • An offline page. A navigation that fails for want of a network shows offline.html, or the page named with ?offline= in the registration. It is stored on install in a cache of its own. Pages themselves are still never stored.
  • An "offline" placeholder for an image with no cached copy and no network.
  • A {command: 'broadcast', message} message, relayed to every page the worker controls as broadcastOnRequest.
  • The worker reports its own errors to ClientErrors.

Fixed

  • Another worker's stale copy could be served. The worker read assets with caches.match(), which searches every cache on the origin, so a copy an earlier worker stored came first. It now reads its own cache only.
  • An earlier worker's caches are deleted on activation. These are the hand-versioned …::static, …::pages and …::images; the pages one holds signed-in pages.

Documentation

  • The Service Worker Guide has two new sections: The offline page, and an image with no network and Replacing a worker of your own.

Data tables draw with daisyUI or the plain stylesheet

Changed

  • Under scaffold_theme tailwind or plain-css, Html\Datatable gives DataTables that system's classes for the controls it writes itself:
    • the search box and the page-length select;
    • the page buttons, the processing note and Buttons' buttons.

It also uses DataTables' own pager renderer and the lfrtip layout, replacing a Bootstrap integration's on a page that loads one. Under Bootstrap, nothing changes. - The table gets table table-sm table-zebra (tailwind) or table table-hover (plain-css). It is wrapped in .pf-datatable. - Every components.css styles DataTables' DOM under .pf-datatable: the controls, the processing note, and sort marks that need no icon font.

Documentation

  • The HTML Components Guide has a new section, It draws with the project's UI system, under Datatable.

A Redis token bucket refills whatever the float precision

Fixed

  • Http\TokenBucket on Redis passed the time and the rate to its script as PHP floats. phpredis writes a float as text by the precision ini setting, so at a low setting the time reached Redis rounded to whole seconds, and a token due within the second did not come back. Both are now formatted to the microsecond.
  • The token bucket tests waited with one usleep(), which a signal can cut short. The suite has workers with async signals, and a refill test failed under a full run that way. They now wait until the time has actually passed.

A dashboard each administrator arranges

Added

  • Pramnos\Dashboard\Widget describes one card: id, title, allowed(), size, whether a new dashboard has it, a link, and render().
  • WidgetRegistry holds the widgets: the framework's four (active users, API, database, health), those listed under 'dashboard' => ['widgets' => […]], and those registered in code. 'framework' => false leaves the framework's four out.
  • Layouts keeps each person's layout per area in user_dashboards, from a new migration. A layout is cleaned of what its owner may not have whenever it is read or written.
  • DashboardController::display() draws the person's widgets in their order and widths. The new actions savelayout and resetlayout are POST-only.
  • In all three themes, admin_dashboard is arrangeable. "Customise" shows tools to move, resize, hide and add cards. They are buttons, so a keyboard can do all of it, and a mouse can drag. pf-utils.js saves each change at once.
  • pramnos-testing's createDom() has nextSibling and previousSibling.
  • A widget has the controller drawing it, as $this->controller (setController()), for a model.

Changed

  • The dashboard's figures are worked out once per request, and a view of the older shape still receives them.

Documentation

  • A new Dashboard Guide.

Html\Datatable: values in its script are encoded (XSS)

Fixed

  • Security: a column's search value was written into the table's script between single quotes, as it came. Pages often take it from the address, so a link such as …?typeSelect=');alert(document.cookie);// ran script on the page of the administrator who opened it.
    • The same held for the table's search and for its data address.
    • All three are now JSON string literals, with quotes, <, > and & escaped, so a value can end neither the string nor the <script>.

Html\Datatable: the footer's text search filters again

Fixed

  • Typing in a footer search box sent no request. Html\Datatable always draws with scrollX, under which DataTables moves the visible footer into a table of its own. What it leaves under the table is a copy, rebuilt on every draw, and the keyup handler was bound to that copy, so it was gone at the first draw.
    • The handler is now delegated from the table's wrapper.
    • Run against DataTables 1.13.8, typing in the visible box sent no request before the fix and one, with the column's search value, after it.
  • The state a stateSave table restores into its footer boxes is now looked up in the visible footer.
  • A column can set its own footer search minimum, as ['minSearchLength' => 1]. With the table's three, an id column with ids under 100 could never be searched. A column without its own minimum keeps the table's, which a phone column wants: every Greek mobile number starts with 69.

Html\Datatable: a column picker and a readable size under tailwind

Changed

  • The column picker is a "Show ▾" button with the columns in a list, under tailwind and plain-css. Before, it was a line of links in brackets, which grew into four lines of text over a table of sixty columns.
    • It is a <details> in the system's dropdown classes, so it opens with no script.
    • Its links keep their psh_… ids, so the show/hide handlers are unchanged.
  • Under tailwind a table is table table-zebra, at the page's size, with compact cell padding and its cells aligned to the top. $table->size = 'sm' gives the compact table-sm. Before, it was table-sm: 12px text, centred cells.
  • The Theme Guide now says to import components.css outside any layer. In @layer components, these rules lose to daisyUI's utilities layer.

The chat folds what the model said on the way

Changed

  • Lines the model writes while it calls tools ("Let me check…") were joined to the start of the answer. They were copied with it and replayed in the next question's history. A Greek question was answered after ten lines of English.
    • Llm\Client returns them apart: final is the answer alone and steps the texts beside tool calls. reply is unchanged: the whole text, as before.
    • Chat\Conversations stores final as the message and the steps in extra.steps.
    • The chat screens of all three themes, and the SPA's, show the steps in a closed "What it did on the way". A message stored before this shows as it did.
  • The assistants' prompt asks for anything said while calling tools to be in the question's language too.

Documentation

  • The LLM Guide explains final, steps and where the chat shows them.

Charts added after load, and on a phone

Fixed

  • A chart inserted after the page loaded stayed a table. pf-utils.js found the figures once, at load, so this happened to a dashboard card fetched after a change. Figures are now drawn as they arrive, and window.pfCharts.draw(root) draws them on request.
  • A chart widened the page on a phone. The table kept for screen readers is visually hidden, and a hidden table still lays out at its full width: a 30-day chart made a 390px page 481px wide. .pf-chart-figure is now relative and clipped, in every theme.

Scheduled AI tasks: reminders, one-offs, runs that cannot run twice

This is the first phase of scheduled AI tasks and reports, agreed with an application that already runs routines in production. Its existing rows run as they did.

Added

  • Reminders and one-offs. A task's kind is ask (a question asked again, as before) or reminder: the owner's own words sent back as a notification, with no model asked (Chat\TaskReminder, or chat.reminder_notification).
    • cadence can now also be once, at a run_at on the asker's clock or in_minutes from now, or monthly_weekday: the second Tuesday, the last Friday.
  • Runs claimed, not just found. llm:routines moves a task's next run on, and holds the task, in the statement that checks it is still due. A second tick or host claims nothing.
    • With chat.task_queue, claimed runs go to the queue (ai_task_run, Chat\TaskRunTask), and a worker runs them.
  • A history of runs in ai_task_runs, through Routines::runs($id).
  • Chat\TaskPolicy (chat.task_policy) holds refusal, scope, timezone, stillAllowed, audience, link, onRan and maxFailures. Its defaults keep what the assistant already decides.
  • Routines::ownedBy() and transfer() hand a leaving member's tasks to another. Each keeps its schedule, not its conversation; each is checked again as the new owner's, and the new owner is told.
  • ai_task_subscriptions, for the shared reports of the next phase.

Changed

  • Three failed runs in a row pause a task and tell its owner.
  • A run's question carries the day on the owner's clock, not UTC's.
  • The llm_tables health check names the new tables and columns until migrate has run.

Documentation

  • The LLM Guide has a new section, Scheduled tasks: questions, reminders, one-offs, which replaces Questions on a schedule.
  • There is a new row in the Upgrade Guide.

The model schedules, changes and cancels tasks itself

This is phase two of scheduled AI tasks.

Added

  • schedule_task schedules a reminder too. A reminder is kind reminder, once or recurring; "remind me in an hour about the invoice" is cadence once with in_minutes 60. It links back to the conversation it was asked in, through Routines::toolsFor($scope, $zone, $conversationId).
  • update_scheduled_task changes a task by id: its words, time or cadence, or pauses and resumes it (Routines::update()).
    • PUT …/routines/{id} takes the same fields; active alone pauses and resumes as before.
  • list_scheduled_tasks shows each task's kind, and why one stopped.
  • A duplicate is refused by the tool itself, with the id of the task that already does the same thing.

Changed

  • A reminder is refused where the assistant refuses scheduled questions (TaskPolicy::refusal()), so an assistant that schedules nothing, such as the admin analyst, gets no scheduling tools.
    • This replaces phase one's default, which refused no reminder.
    • A reminder still never counts against a limit on active tasks.
    • An application that offers reminders without scheduled questions returns null for them in its own TaskPolicy.

Shared AI reports, subscribed to and continued in Ask

This is phase three of scheduled AI tasks.

Added

  • A third kind, report. Conversations::report() asks it once, headless, as its maker:
    • no conversation is kept;
    • no write tool, memory or scheduling tool is offered, and the maker's memory is left out of the prompt;
    • the run is kept in ai_task_runs with its steps and who was told.
  • Shared with an organisation or the installation. owner organization or global, gated by TaskPolicy::canManage() (usertype 80 inside an organisation, 90 for the installation, by default).
  • Subscriptions. Routines::subscribe(), unsubscribe(), subscribers(), reports(). Readers are re-checked at every run by TaskPolicy::canSee(): a member of the organisation, of the maker's standing or more.
  • notify relevant. The model decides through a report_relevance tool, forced on the round before the last. Nothing new tells nobody, and the run is still kept; no decision tells everyone.
  • What it is about. resource_type and resource_id. TaskPolicy::resourceLabel() names it in the notification. When resourceExists() is false, the report is paused without counting a failure.
  • Continue in Ask. A report's notification links to chat/report/{run}, which opens the run as the reader's own conversation (Routines::continueRun(), Conversations::seed()). Opening it again continues the same conversation.
  • Chat tools list_reports, subscribe_report and unsubscribe_report. schedule_task takes kind report, owner and notify.
  • API: GET /chat/routines/{id}/runs, GET /chat/runs/{id}, POST /chat/runs/{id}/continue, GET /chat/reports, and POST/DELETE /chat/routines/{id}/subscribe.
  • Routines::forUser() answers what is kept about a person, for an export.

Changed

  • A failed report tells its maker only.
  • An unknown cadence is refused as "There is no such cadence. How often: daily, weekly, monthly, monthly_weekday (…) or once." A test matching the older "How often: …" text matches "no such cadence" now.

Documentation

  • The LLM Guide's scheduled-tasks section is now Scheduled tasks: questions, reminders, reports, with a new subsection, Shared reports, and new rows in the policy and API tables.
  • There is a new row in the Upgrade Guide.

The service worker leaves the administration area and the API alone

FW-131. A worker registered at the site root controlled the administration area too. It served admin.js stale-while-revalidate, so after a deploy an administrator could get the old script with the new page, which can break it.

Changed

  • The scaffolded worker (sw.js) no longer handles admin/ and api/ under its scope. Assets and pages there go straight to the browser, and the offline page does not stand in for them.
  • ?skip= in the registration names other prefixes, comma-separated and relative to the scope. ?skip= with nothing after it excludes nothing.

Documentation

  • The Service Worker Guide has a new section, Paths left to the browser.
  • There is a new row in the Upgrade Guide.

project:resync --spa-components creates what an updated component imports

Fixed

  • An updated component no longer breaks the build. Without --all, the resync updated a project's HelpPanel.svelte, ConfirmDialog.svelte, Pagination.svelte and llm/*, which now import lib/ui.js, lib/section.js and lib/page.js. It skipped those files as "not present", and the build failed with "Could not resolve ../lib/ui.js".
    • Now a file of the sync that a file the project has will import is created, and so is whatever it imports in turn.
    • A file nothing present imports is still only offered.

Documentation

  • The Console Guide's section on the shared components says so.

Roles: who counts as an administrator, a superuser or root, said once

FW-132. An installation that moved its administrators to 97 and kept 99 for a single root account could not say so in configuration. The lines were numbers written into each screen and tool, and four keys described one idea.

Added

  • Pramnos\User\Roles, with level($role) and atLeast($user, $role), and a roles key in app.php. The defaults are manager 80, admin 90, super 98 and root 99.
    • The administration screens' floor and superuser follow super.
    • The developer tools follow root: the DevPanel in development and production, its debug-bar link, Adminer and the debug bar's grants. A developer is root; there is no separate role for it.
    • Health's phpinfo, the GDPR screen's administrator view and the trusted-devices exclusion follow admin.
    • A widget's default allowed() and one user's tokens follow manager.
  • DevPanelController::developerFloor(), the one answer the panel, the debug bar and Adminer share.
  • RolesTest::testNoBundledCodeWritesAUsertypeNumber fails when bundled code compares a usertype with a literal.

Changed

  • Bundled screens declare $requiredUserType = 0, which means the administration floor.
    • Their 80 and 90 were never in force: a screen with an adminAbility was already raised to AdminAccess::defaultUsertype() (98) when it was built.
    • Controller::adminFloor() reads 0 as that floor, never as "anybody".
  • The DevPanel in development and the debug bar's grants now default to root (99), from 90. DebugGrantController::DEFAULT_MIN_USERTYPE is 99.
  • Each tool's existing key still wins over its role: devpanel.min_usertype, adminer_min_usertype, production_min_usertype, debug.grant_min_usertype, and the admin_*_usertype settings.

Documentation

  • The Authentication Guide has a new subsection, Moving the lines: roles.
  • There is a new row in the Upgrade Guide.