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-uikeeps 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-componentscreates 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.
- what
Writing views that survive a UI switch¶
Documentation¶
- The Theme Guide's Writing views that survive a UI switch covers:
- what
project:switch-uichanges and leaves alone, including that it overwrites the default theme's layout files andstyle.cssand never touches the application's views; - which components adapt on their own and which do not:
Datatablewith its default$bootstrap = truedraws a Bootstrap 3 column picker,Dateusesform-control, andcomponent_classesis 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.
- what
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 scaffoldedtheme.html.phpcalls it above[MODULE], and every scaffolded stylesheet stylespf-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-uiwrote the default theme's layout files andstyle.cssover whatever the application had made of them, with no copy and no word. A file whose contents it changes is now copied tovar/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-srcto 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.$bootstrapnow followsscaffold_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
initdoes not use had them. All three now printNavItem::badgeHtml(), apf-nav-badgestyled by every stylesheet. - The Help menu item took its floor from app.php's
adminblock only. An application that is the area, throughAdminArea::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
themelog; get_search_form()readssearchform.php;front-pageis an element by name. It was written with a comma for its arrow.
- a layout without
- Tailwind
.form-control. The Tailwind stylesheet stylesform-control, the classHtml\Datecarries. - Docblocks:
NavItem::$iconis an icon name, not a CSS class;AdminArea::reset()has its own docblock;FieldStyles::for()has its own;scaffold_theme'splain-cssis said to resolve to theplainpreset.
- Examples. The Components Guide's
Fieldexamples follow the UI system, withFieldStyles::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, andadmin.themeandadmin.default_controllerapply.
Fixed¶
AdminArea::serve()called frominit()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 nowsURLitself whensURLalready 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, withAdminArea::resetRequest().
- The navigation linked to the site, because it is built in
Testing an application's JavaScript with the framework's runner and helpers¶
Added¶
test:js:node --testover the application's*.test.js, fromjs_tests_dirinapp.php(defaulttests/js). The framework's helpers are onNODE_PATH.lint:js: ESLint with the framework's rules, which are defects only. It uses the application'seslint.config.mjswhen there is one, and that config extendsresources/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()andinstallApiHeaders();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_sectionsinapp.phpandController::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
abilityis 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.jsgives a section its mount element and a router under its base.lib/page.jsgives it the layout's flash, the title, and the side panel's record.- Every
frontend/sections/<name>/main.jsis an entry of the scaffolded Vite build. - The scaffolded
lib/api.jssends 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 classesDataTable,Pagination,ConfirmDialog,Field,OmniboxandHelpPaneldraw 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 fromscaffold_theme, so a section inside a Bootstrap or plain-css page draws in that system's classes and followsproject:switch-ui.pf-ui-*hooks in the scaffoldedbootstrapandplain-cssstylesheets cover what those systems have no class for: layout, the modal, the side panel, a sticky table header.
Fixed¶
- The Svelte
Omniboxshowed nothing beside a group from a source with'count' => false. It shows5+, as the server-rendered search box does.
The side panel's buttons and look are the theme's¶
Added¶
- Openers.
pf-assist.jstreats 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, andPanel::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 itspf-assist-*classes, which every scaffolded stylesheet styles.initwrites 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 thedatabasechannel stored.list()returns a page, unread only or not, by group.unreadCount()andunreadByGroup()return the counts.markRead()andmarkAllRead()touch only that person's rows.- Groups come from
'notifications' => ['groups' => …](by class) or from the storedgroup. Feed::badge()is a NavItem badge.
NotificationsController(/Notifications): the feed as JSON with the page's session, with the CSRF token on writes.initwrites the wrapper withauth.Html\NotificationBelland its behaviour inpf-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:refreshand 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 screenscreate:crudgenerates asklib/ui.jsfor their parts, like the shared components already did. So a Svelte SPA in abootstraporplain-cssproject 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.inlineandtable.zebra. - Sizes and shapes combine with a variant:
ui('btn.primary', 'size.sm').
- New parts include
frontend/ui.css, imported byapp.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).
initwrites it, andbootstrapis in a Svelte SPA'spackage.jsonwhatever the system.
- The SPA shell reads
scaffold_themefrom app/app.php when it runs. It publishes it aswindow.__PRAMNOS__.ui. project:switch-uirewrites a Svelte SPA'sui.css, keeping the previous copy. It then says what is left to do:- run
npm run build; - in an
app.cssthat registers daisyUI itself, replace@plugin "daisyui";with@import "./ui.css";; - in a project without the package,
npm install -D bootstrap.
- run
- A framework test fails on a Svelte stub that writes a class two systems define (
btn,card,table, …) instead of askingui().
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¶
LogControllerbuilds every link fromlogsRoute(): breadcrumbs, redirects, the toolbar, "Go back", the viewer's address and thestats/dashboardviews. By default that is the subclass's class name;protected ?string $logsRoutesets another.- So an application with a
Logscontroller of its own mounts the viewer asclass Logfiles extends LogController, with nothing to override. - Under any address but
Logs, the bundledlogsviews come before the application's.
- So an application with a
Html\SearchBox::$value: the term already in the box, escaped.
What breaks in subclasses¶
- A
LogControllersubclass not namedLogsthat is still served at/Logsnow links to its own name. Addprotected ?string $logsRoute = 'Logs';.
Documentation¶
- The Logging Guide has a new section, Under another address. In the HTML Components Guide,
SearchBoxshows$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.cssholds the styles of the framework'spf-*components and no page rules: the search box, side panel, bell, badges, flash, help button, datatable actions, breadcrumbs, pagination and stat tiles.style.cssimports it, andinitandproject:switch-uiwrite it besidestyle.css.- An administration with a Tailwind build of its own imports it from
vendor/and keeps its own layout.
- The
framework_scriptshealth check also namespf-*.jscopies that differ from the framework's (outdated), which is what acomposer updateleaves 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
.btnin theutilitieslayer; - the notification bell is now among an admin theme's options, replacing a line that said there was no feed.
- a new section, An area with a Tailwind build of its own, which covers daisyUI's
- In the Health Guide,
framework_scriptscovers outdated copies.
Every UI system has its components.css¶
Added¶
scaffolding/themes/bootstrap/components.cssandscaffolding/themes/plain-css/components.cssstyle the same framework components as the tailwind one, and each theme'sstyle.cssimports its own.initandproject:switch-uiwrite it besidestyle.css.- The
pf-ui-*hooks of the SPA's shared components stay instyle.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.cssimports its file before any rule.
- the three files style the same
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
chartjswhere it is registered.
- the data as JSON in a
pf-utils.jsdraws each figure:- in the theme's colours (
--color-*,--bs-*or--primary-color), again whendata-themeor 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.
- in the theme's colours (
- Every
components.cssstylespf-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 madegetTpl()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,$tpland$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
0before it.Field::readSubmitted()read anything but an empty string as on, so the hidden0became'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_tokenor theX-CSRF-Tokenheader), asController::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.jsreports what a page hits. It sends uncaught errors and unhandled rejections to the address in<meta name="pf-client-errors">, withsendBeacon().- 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.phpwritten byinitcarries 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.initwrites the wrapper.Logs\ClientErrorsstores 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.
- It accepts the older
- A
javascripterrorsmigration 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 asbroadcastOnRequest. - 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,…::pagesand…::images; thepagesone 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_themetailwindorplain-css,Html\Datatablegives 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\TokenBucketon Redis passed the time and the rate to its script as PHP floats. phpredis writes a float as text by theprecisionini 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\Widgetdescribes one card: id, title,allowed(), size, whether a new dashboard has it, a link, andrender().WidgetRegistryholds the widgets: the framework's four (active users, API, database, health), those listed under'dashboard' => ['widgets' => […]], and those registered in code.'framework' => falseleaves the framework's four out.Layoutskeeps each person's layout per area inuser_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 actionssavelayoutandresetlayoutare POST-only.- In all three themes,
admin_dashboardis 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.jssaves each change at once. pramnos-testing'screateDom()hasnextSiblingandpreviousSibling.- 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
searchand 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>.
- The same held for the table's
Html\Datatable: the footer's text search filters again¶
Fixed¶
- Typing in a footer search box sent no request.
Html\Datatablealways draws withscrollX, 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
stateSavetable 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.
- It is a
- 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 compacttable-sm. Before, it wastable-sm: 12px text, centred cells. - The Theme Guide now says to import
components.cssoutside any layer. In@layer components, these rules lose to daisyUI'sutilitieslayer.
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\Clientreturns them apart:finalis the answer alone andstepsthe texts beside tool calls.replyis unchanged: the whole text, as before.Chat\Conversationsstoresfinalas the message and the steps inextra.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,stepsand 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.jsfound the figures once, at load, so this happened to a dashboard card fetched after a change. Figures are now drawn as they arrive, andwindow.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-figureis 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
kindisask(a question asked again, as before) orreminder: the owner's own words sent back as a notification, with no model asked (Chat\TaskReminder, orchat.reminder_notification).cadencecan now also beonce, at arun_aton the asker's clock orin_minutesfrom now, ormonthly_weekday: the second Tuesday, the last Friday.
- Runs claimed, not just found.
llm:routinesmoves 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.
- With
- A history of runs in
ai_task_runs, throughRoutines::runs($id). Chat\TaskPolicy(chat.task_policy) holdsrefusal,scope,timezone,stillAllowed,audience,link,onRanandmaxFailures. Its defaults keep what the assistant already decides.Routines::ownedBy()andtransfer()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_tableshealth check names the new tables and columns untilmigratehas 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_taskschedules a reminder too. A reminder iskindreminder, once or recurring; "remind me in an hour about the invoice" iscadenceoncewithin_minutes60. It links back to the conversation it was asked in, throughRoutines::toolsFor($scope, $zone, $conversationId).update_scheduled_taskchanges a task by id: its words, time or cadence, or pauses and resumes it (Routines::update()).PUT …/routines/{id}takes the same fields;activealone pauses and resumes as before.
list_scheduled_tasksshows 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_runswith its steps and who was told.
- Shared with an organisation or the installation.
ownerorganizationorglobal, gated byTaskPolicy::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 byTaskPolicy::canSee(): a member of the organisation, of the maker's standing or more. notifyrelevant. The model decides through areport_relevancetool, 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_typeandresource_id.TaskPolicy::resourceLabel()names it in the notification. WhenresourceExists()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_reportandunsubscribe_report.schedule_tasktakeskindreport,ownerandnotify. - API:
GET /chat/routines/{id}/runs,GET /chat/runs/{id},POST /chat/runs/{id}/continue,GET /chat/reports, andPOST/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 handlesadmin/andapi/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'sHelpPanel.svelte,ConfirmDialog.svelte,Pagination.svelteandllm/*, which now importlib/ui.js,lib/section.jsandlib/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, withlevel($role)andatLeast($user, $role), and aroleskey inapp.php. The defaults aremanager80,admin90,super98 androot99.- 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 followadmin. - A widget's default
allowed()and one user's tokens followmanager.
- The administration screens' floor and superuser follow
DevPanelController::developerFloor(), the one answer the panel, the debug bar and Adminer share.RolesTest::testNoBundledCodeWritesAUsertypeNumberfails 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
adminAbilitywas already raised toAdminAccess::defaultUsertype()(98) when it was built. Controller::adminFloor()reads 0 as that floor, never as "anybody".
- Their 80 and 90 were never in force: a screen with an
- The DevPanel in development and the debug bar's grants now default to root (99), from 90.
DebugGrantController::DEFAULT_MIN_USERTYPEis 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 theadmin_*_usertypesettings.
Documentation¶
- The Authentication Guide has a new subsection, Moving the lines:
roles. - There is a new row in the Upgrade Guide.