22 October 2026¶
Forty-four entries today:
- The rule editor offers templates and lists, and wears the theme
- Person events are kept in the change log
- A/B tests
- Person events drain where the change log's feature is off
- The audience screens, redesigned
- Ask about audiences
- The audience screens, second review
- Event transports, and events:status
- The event stream, on the database
- The event stream on the Queue screen
- The event stream on Redis Streams
- Conversations: the engine
- Conversations, live
- Conversations: files
- Conversations: search
- Conversations: the HTTP API
- Realtime: presence members and whispers in
pfRealtime - Conversations: the page, the dock and the files screen
- Conversations: the dock's windows, and a first review
- Conversations: posting in place of a chat webhook
- Conversations: references, commands, and a rule that tells the team
- Conversations:
@Ask - Conversations: the beacon, for when the server is down
- Conversations: an app that opens while the server is down
- Push: the site's worker, on a page with a worker of its own
- Conversations: a sidebar in sections, and a window of their own
- Conversations: their own window is the whole window
- Conversations: threads, activity, drafts and sent, saved, all unread
- Conversations: people who stopped signing in
- Beacon: no signing key, no beacon
- Ask: everybody's conversations, read only
- Ask: the page's own links behind a gear
- Ask: other people's conversations leave out the reader's own
- Ask in the conversations: a conversation of its own, and the reader's rights elsewhere
- Ask's answers reach the conversations from any worker
- Ask's charts drawn in the conversations, its card in the reader's language
- Tables in the conversations; a chart's long category names cut to fit
- Other people's Ask conversations: folded steps, drawn charts
- Help pages' headings are the levels they write
- Conversations: history from another system, written through the API
- Conversations: the application's own links at the foot of the list
- Conversations: bringing history in from another system is the application's
- Reactions: every emoji its own, on MySQL too
- Conversations: a person, as a client's card and profile show them
The rule editor offers templates and lists, and wears the theme¶
Changed¶
- The email action's template and unsubscribe list are choices in the rule editor: the mail templates by title, and the declared mail types' lists (FW-234).
Rules\Actions::templates($options, $fill): an application whose templates are not the framework'smailtemplatessays which it has and fills one for a person.- The rule editor's controls take the theme's field classes, and its rows line up.
- An application's template fill that returns
'emailtemplate' => ''is sent bare, not wrapped again in the installation's email layout.
Fixed¶
- The rule page's forms carry the session's token where the write-action contract looks for it.
Documentation¶
- Audience guide: Templates, under Actions.
Person events are kept in the change log¶
Changed¶
- Each occurrence of a person event is written to the change log's application events
(
pramnos.changelog_events, entityuser) through the write spool, instead of a table of its own: one history of what people did, partitioned, compressed and kept two years on TimescaleDB.person_events, the per-person summary, stays (FW-234 B). create_person_events_tablescreatesperson_eventsonly;audience.event_log_daysis gone.audience:prune-eventsprunes the rule run log and segment stays that ended more thanaudience.segment_history_daysago (365), and says so in one line when there is nothing to do.- Had it been on for an event-triggered rule reads the summary.
Added¶
- An index on
changelog_events (entity, event, created_at)(add_event_index_to_changelog_events,changelogfeature).
What changes for an application¶
- Windowed conditions ("N times in the last M days") and A/B goals need the
changelogfeature; without it they are refused or shown as not counted. Aperson_event_logmade by yesterday's migration is no longer written and can be dropped.
Documentation¶
- Audience guide: The summary, and the change log; Console guide; Upgrade Guide row.
A/B tests¶
Added¶
Pramnos\Audience\Experiments:variant($slug)gives a person or visitor one variant by share, kept for good, a visitor's carried over on sign-in;create,update,start,stop,decide, audited;results()with the rate, how sure the difference is and how many more people it needs (FW-234 D)./admin/Experiments(abilityadmin.experiments) in the three themes: the list, the form, the results with Start, Stop and Make it the winner.- A filter condition on a test's variant, and a rule split that follows a test.
- Migration
create_experiments_tables(core).
Documentation¶
- Audience guide: A/B tests.
Person events drain where the change log's feature is off¶
Fixed¶
ChangelogWriter::event()encodesdetailswhen it spools the row. A drain in a process where the changelog provider never booted — an application withchangelog_eventsand the feature off — had no transformer, and the column refused the array: every person event was kept back for the next run, so windowed conditions and A/B goals counted nobody (FW-237).- The A/B results' footnote escapes its
95%, whichsprintfread as a placeholder and printed a literal%sfor the visitors.
The audience screens, redesigned¶
Changed¶
- Segments, consent lists, rules and A/B tests share one design language in every theme: a page head with its actions, cards, tabs with counts and a search, tables whose row is one link, coloured status pills and ▲/▼ deltas, links to what uses a segment, empty states with a way in, a segmented control for a rule's status, and a footer that keeps a long form's buttons in reach (FW-234, design review round 1).
- The builder: a condition on one line; what it is about picked from a searchable list under the attributes' and events' headings; the word between rows switches all/any; the live panel sticks beside the conditions, with the breakdown as bars and each person of the sample with initials and a line under the name.
- The rule editor: when, who, what and until as numbered cards that fold to a one-line summary; the steps as cards joined by a line, added from a menu of kinds; events picked from the searchable list; sending hours on the 24-hour clock; limits as "at most [1] [email] per [1] days". A new step follows the last one that ends. The rule's page draws the same cards with how many went through each.
AudienceBuilder::classes()returns the screens' own classes in every theme.
Added¶
Attributes::sampleLine()andFilter::lines(): what a sample shows under each name.Segments::uses()andRules::linksUsingSegment(): what uses a segment, with links.data-pf-combobox(pf-audience.js) anddata-pf-filter-rows(pf-utils.js) for any form.Iconnames for the audience screens: plus, close, search, chevron, segment, rule, experiment, consent, clock, branch, split, goal, action, play, pause, shadow, stop.
Documentation¶
- Audience guide: the screens' design, the two form pieces, the sample's line.
Ask about audiences¶
Added¶
- Five tools for the administrators' Ask, offered where the audience tables are and behind each
screen's ability:
audience_segments,audience_rules,audience_experiments(read),audience_why(one account against a segment or a rule, taggedpersonal) andaudience_draft— drafts only: a segment as a link that opens the builder filled in, nothing saved; a rule saved as a draft (FW-234 D). /admin/Segments/edit?draft=…opens a new segment's form filled in from a draft.Rules::figures($id): entered over seven days, under way, goal reached — the Rules screen's numbers, shared.
Documentation¶
- LLM guide: the audience tools; Audience guide: Asking about audiences.
The audience screens, second review¶
Changed¶
- A sample's line says a date as "today", "yesterday", "5 days ago" or its day, and an empty one as "never" — never a raw Unix time.
- The words the screens show are their own phrases ("Group of conditions", "Rule status", "Goal of the test", "Test variant"…), not bare one-word keys an application may already translate in another sense.
- One of a thing is said in the singular: "1 step", "1 condition", "1 day".
- A step is named by its place and what it does ("2. Send an email") in the summaries, the editor's choices, the rule's page, its log and its test send — never by its id.
- A condition's row holds its fields at their own width with its remove button beside them; an empty filter says it is everybody; the live panel breaks down by the first list attribute at once; the end of the sending hours reads 21:00.
- The segment and group badges are equally legible, each with a border.
Event transports, and events:status¶
Added¶
- A queued listener's events go by a transport:
Pramnos\Event\Transport\EventTransport, with the queue (QueueTransport) as every installation's and the default.'events' => ['transport' => …, 'listeners' => [Class => …]]in app.php chooses one for the installation and per listener; one that is unknown or cannot carry events falls back to the queue, logged once (FW-235 P1). Nothing changes for an installation that sets nothing. bin/pramnos events:status: each queued listener's transport as set and as it would go — a dry run, with--json.Event::events(): the names something listens for.
Documentation¶
- Queue guide: Which transport carries a listener's events; Console guide:
events:status.
The event stream, on the database¶
Added¶
- The
streamtransport: an event is one entry inevent_log, written in the caller's transaction, for every queued listener set to it; each listener is a consumer group reading from its own position (event_consumers). One row per event instead of one task per listener (FW-235 P2). bin/pramnos events:consume: the daemon that reads the groups — one consumer per group by lease; failures retried with a growing wait and kept as dead past the listener's attempts (event_deliveries); folded listeners handed to the queue, so they fold as before; idempotency keys as on the queue.bin/pramnos events:prune(scheduled daily): entries pastevents.log_days(30) that every group has read.- Migration
create_event_log_tables(queuefeature).event_logandevent_deliveriesare declared personal data. QueuedEvents::payload()andQueuedEvents::arguments(): what travels for a firing, and the arguments built again — shared by the queue and the stream.
Documentation¶
- Queue guide: The stream: one entry per event; Console guide:
events:consume,events:prune.
The event stream on the Queue screen¶
Added¶
- Event streams on the Queue screen: per consumer group, entries waiting to be read, to be tried again and dead, the oldest waiting and the consumer holding it; Try the dead again and Delete the dead per group (FW-235 P4).
- The analyst's
queuetool answers the same figures understreams. StreamConsumer::status()andStreamConsumer::redrive().
Fixed¶
- Whether the stream's tables are here is remembered per database, not once per process.
Documentation¶
- Queue guide: watching the stream.
The event stream on Redis Streams¶
Added¶
'events' => ['stream' => 'redis', 'redis' => [...], 'max_length' => 100000]: the stream's groups read a Redis stream.event_logstays the outbox, written in the caller's transaction; a relay appends what is committed, at once outside a transaction and on every pass ofevents:consume, so nothing is lost between the commit and Redis. Consumer groups withXREADGROUP/XACK; what was read and never acknowledged is claimed again once it has waited; dead entries go toevent_deliveriesas on the database (FW-235 P3).- A Redis whose
maxmemory-policyisallkeys-*is refused for the queue, asked throughCONFIG GETor, where that is disabled,INFO memory. - On the Queue screen, a group's name is its class, the namespace beside it; with no consumer
holding any group in the last minute, the card says no
events:consumedaemon is reading.
Documentation¶
- Queue guide: On Redis Streams, and how an application adds a transport of its own — Kafka
among them — with
Transports::register()(FW-235 P5: the extension point, documented; a Kafka transport comes when an application's producer is brought into the framework).
Conversations: the engine¶
Added¶
- The
conversationsfeature: channels, private channels, direct and group conversations and system ones (Pramnos\Conversations\Conversations); messages with threads, mentions (<@id>,@all,@online), edits kept, deletions, reactions, pins, references, read state and what is unread (Messages); markdown-lite rendered safely (Markdown); imports written once bysourceandexternalid(FW-236, phase 1). - Events
conversation.created,conversation.message.posted,conversation.mention.received. - Access by the ability
admin.chat, with a usertype floor of its own (conversations.usertype), andadmin.chat.all— root's — reading everything. - Migration
create_conversations_tables; on TimescaleDB the messages are a hypertable compressed after 90 days and never dropped. Every table is declared personal data.
Documentation¶
- New: the Conversations guide.
Conversations, live¶
Added¶
- What the conversations tell browsers after the database has it: the message events on
private-conversation.{id},conversation.activityon each other member's own channel with whether it mentions them, andpresence-chatfor who of the team is connected (Pramnos\Conversations\Realtime). The feature's provider adds the channel rules; typing is a whisperedclient-typing; without broadcasting nothing is told and nothing fails (FW-236, phase 1).
Documentation¶
- Conversations guide: Live: what browsers are told.
Conversations: files¶
Added¶
Pramnos\Conversations\Files: files sent in conversations kept on disk undervar/chat-files/YYYY/MM/<id>-<name>, with only the relative path in the database; allowed by what they are (images, PDF, Office, OpenDocument, CSV, text, zip), never a program; at most 25 MB, from the settingconversations_max_file_mborconversations.max_file_mb; a thumbnail for an image; sent only to a reader of the conversation;usage()andprune()for the space, a removed file keeping its place in its message.Messages::post()takesfiles(FW-236, phase 1).
Documentation¶
- Conversations guide: Files.
Conversations: search¶
Added¶
Messages::search($user, $terms, ?$conversationId): every word, each at its start, through the full-text index (MySQL/MariaDBFULLTEXT, PostgreSQLto_tsvector('simple')), byLIKEwhen a word is shorter than three letters; only in what the searcher reads, never a deleted message.Conversations::readable()andConversations::names()(FW-236, phase 1).
Documentation¶
- Conversations guide: Search.
Conversations: the HTTP API¶
Added¶
/admin/Chat/…(Pramnos\Application\Controllers\Chat): the conversations' JSON for a browser — the sidebar, messages, threads, search, creating, joining, writing, editing, deleting, reactions, pins, read state, notification choices, uploads and protected downloads; behind sign-in andadmin.chat, every write a POST with the session's token (FW-236, phase 1).
Documentation¶
- Conversations guide: The HTTP API.
Fixed¶
/admin/Chatrefuses a write without the session's token with 403 and{"error": …}instead of a redirect to the site, whatever the request'sAcceptandX-Requested-Withsay;createanswers the new conversation as well as its id.
Realtime: presence members and whispers in pfRealtime¶
Added¶
window.pfRealtimehears a presence channel's members —pusher:subscription_succeeded,pusher:member_added,pusher:member_removed, each with the members as the server last said — and sends client events withchannel(name).whisper(event, data);stopListening()removes a listener. Through the shared worker a whisper goes on the one socket, and a tab joining a presence channel the worker already holds is told its members.
Documentation¶
- Realtime guide: presence channels and whispers in the page's live connection.
Conversations: the page, the dock and the files screen¶
Added¶
/admin/Conversations: the team's chat — channels and direct and group conversations in a sidebar with unread and mentions, a conversation with day lines and where the unread start, threads beside it, reactions, pins, edits and deletions from a toolbar over each message, system cards (title linked, text, fields, a colour), files picked, dropped or pasted,@and:suggestions, who is typing and who is here, search, and a menu entry. Drawn bypf-conversations.js, built from Svelte sources inresources/js/conversations/bynpm run build:conversationsand shipped ready: no build in the application (FW-236, phase 2).Conversations\Dock::render(): the same over every other page of the administration — a button with what is unread and a panel; in the scaffolded themes' footers.'conversations' => ['dock' => false]turns it off; the usertype floor keeps it, the page and the menu to whom it names./admin/ConversationFiles, behindadmin.chat.files(a superuser's): the space the files take, per conversation and month, the largest and the oldest; the largest file allowed; removing old files with a report, or one at a time.- The JSON gains
people, conversationtitles and a conversation'smembers;Conversations\Clientgives the client its start (props(),strings());People::search(). - A system message is signed by its card's
from, else itssource(jira→ Jira).
Changed¶
- The screen is
Controllers\Conversationsat/admin/Conversations;/admin/Chatreaches it still (Controllers\Chatextends it), except in an application with the AI chat page, whose site controller is alsoChat. A message's markdown classes arepf-conv-*(they werepf-chat-*, the AI chat page's prefix). - Cards side by side in
.pf-grid-2/.pf-grid-3are spaced by the grid alone, and a card after such a grid keeps its distance.
Documentation¶
- Conversations guide: The page and the dock, cards, and that a mention reaches members only.
- Theme guide: the dock in an administration theme's footer.
Conversations: the dock's windows, and a first review¶
Added¶
- The dock as the plan drew it: the faces of recent direct and group conversations above the launcher, each with its unread; a conversation in a small window at the foot of the page, with the whole page, minimise and close; as many windows side by side as fit; a new direct message opens its window by itself; the windows follow the reader from page to page (FW-236, phase 2).
project:resync --jsadds the scripts an enabled feature's pages load where they are missing —pf-conversations.jsforconversations— without--all.
Fixed¶
- Lists in a message show their bullets and numbers, also under a theme whose reset removes them.
- The toolbar over a message sits at its top edge instead of over the line above.
- An unread direct message counts in red in the sidebar, as a mention does.
- On a phone the page's title is not shown twice.
- The files screen without files is one empty state; its introduction is a hint.
- A conversation begins with a short line — its name, what it is for, who started it and when — only once its oldest message is in.
Conversations: posting in place of a chat webhook¶
Added¶
Conversations\SystemPost::post($channel, $text, $attachments, $options): what a chat webhook takes, posted to the conversations —#namea system channel made on first use, an account id (or@id:N, or a name the application resolves withresolvePeople()) that person's own conversation with the bot; the text and attachments become a message and a card (title,title_link,text,image_url,fields,colorby name or hue);usernamesigns it; retries and same-request repeats are written once (FW-236, phase 3).- A conversation kind
bot: one person and a named bot, listed with direct messages. A card shows its image; a card that only signs is no box.
Conversations: references, commands, and a rule that tells the team¶
Added¶
Conversations\References:register($type, $resolve)draws#type:idas a card for the reader — or nothing, where they may not see it;match($type, $pattern)makes other text a reference (a Jira key, an address). The browser reads every card on screen in one request (/admin/Conversations/refs) (FW-236, phase 3).Conversations\Commands:register($name, $description, $run)—/namein the composer, with suggestions; the answer for its writer alone (/admin/Conversations/command/{id}).- The rules' action Tell the team in a conversation (
conversation_post), where the application has the conversations: a channel or an administrator, the person as a linked card, once per run. Conversations\Client::props()gives the clientrefTypesandcommands.
Fixed¶
- A conversation opens on its newest message and stays there while cards and pictures load below; messages arriving while the reader is further up show a "new messages ↓" button.
- A deleted message without replies is gone from the conversation, as a deleted reply is from its thread; one with a thread keeps its place for it.
Conversations: @Ask¶
Added¶
@Askin a message puts its question to the administration's analyst as its writer; the answer comes in the message's thread, signed "Ask" — or why there is none (Conversations\Ask). The composer offers it to whoever may ask the analyst (FW-236, phase 3).
Conversations: the beacon, for when the server is down¶
Added¶
Conversations\Beacon: a second server with the same code, PHP and Redis that keeps an emergency channel and direct messages going while the primary does not answer, and gives them back once it does (FW-236, phase 3b)./beacon/*is served bywww/beacon.phpwithout the application or its database: the watcher's health (on the primary too) and alarms, the primary's sync, export and acknowledgement, and the browsers' session, messages and reactions.- Emergency keys: Ed25519, minted by the primary alone (
/admin/Conversations/emergencykey), checked by the beacon, thirty days, renewed daily; servers sign their requests with HMAC. conversations:beacon sync|import|test|keys; sync and import scheduled every minute.- The page and the dock turn to the beacon after three failed requests, and back when the server answers; a drill sends one person's page there for five minutes.
scaffolding/templates/beacon.php.stub.
Fixed¶
- The primary's health found its database only in
app/settings.php: it now takessettings_filefromapp/beacon.php, elseapp/config/settings.phporapp/settings.php, or the application's owndb_check— so a primary with its settings elsewhere is not reported down (FW-238). beacon.phprequiresapp/beacon.bootstrap.phpfirst when it is there, for the constants an application's autoloaded files need, and definesDS; it said nothing and answered an empty 200 where they were missing (FW-238).
Documentation¶
- Conversations guide: When the server is down — the beacon. Console guide:
conversations:beacon.
Conversations: an app that opens while the server is down¶
Added¶
conversations.app(name, icons, colours): the conversations installable as an app of their own —Conversations/manifest, opening on the page, standalone, without the administration around it (Client::app()).Conversations/worker: a worker scoped to the conversations page alone, beside the site's own. It keeps the page and what it is made of; when the server does not answer, the kept page opens cold and goes straight to the beacon with the key the device holds (FW-236).- The beacon's push carries
page(app/beacon.php), so tapping it opens the conversations — on the emergency channel while the server is down. - When the beacon does not answer either, the emergency screen says so — the addresses tried, the time of the last try, a countdown to the next — with Try now, instead of an empty pane.
Documentation¶
- Conversations guide: Opening cold — the conversations as an app.
Push: the site's worker, on a page with a worker of its own¶
Fixed¶
push.jschecked and subscribednavigator.serviceWorker.ready, which on the conversations page is the conversations' own worker: it said notifications were off while the site's worker was subscribed, and its button subscribed a worker with nopushhandler, leaving the server a dead second endpoint. It now uses the registration at the site's address, andreadyonly when there is none (FW-239).
Documentation¶
- Push guide: A page with a worker of its own.
Conversations: a sidebar in sections, and a window of their own¶
Added¶
- The sidebar is in sections — Starred, Channels, Direct messages, System — each folded or open and sorted by name or by the newest, kept per reader; a folded one still shows what is unread. Only conversations with unread, in the list's ⋯ menu (FW-240).
- Each conversation's own ⋯ menu in the sidebar: star, mute, mark as read, leave.
star/{id}andConversations::star();conversation_members.starred(migration2026_10_22_000003_add_starred_to_conversation_members). - Ctrl/⌘+K, a quick switcher; Alt+↑/↓ to the conversation above or below, Alt+Shift+↑/↓ to the next unread.
- ⧉ opens the conversations in a window of their own (
?window=1), from the page and the dock; a second click brings it forward. Install as an app where the browser offers it. While the window or the app is open, the docks open no small window for a new message. - A face in the dock is put away with its × until a newer message there; Faces above the button turns them off.
Documentation¶
- Conversations guide: The page and the dock — the sidebar, the keys, its own window, putting a face away.
- Upgrade Guide: the conversations' sidebar reads
conversation_members.starred— run the migrations.
Conversations: their own window is the whole window¶
Fixed¶
- In a window of their own — popped out, installed, or opened cold — the conversations were
squeezed into a 264px column: the page's body was given
pf-conv-app, the class of the conversations' box, a grid as wide as its sidebar. The body is nowpf-conv-alone, which each theme gives the whole window, and the box fills it without a border (FW-241).
Conversations: threads, activity, drafts and sent, saved, all unread¶
Added¶
- Views above the conversations, across all of the reader's (
Conversations\Inbox, FW-240): Threads (threads) with the replies new since the reader's own; Activity (activity) — mentions, reactions to what they wrote, replies in their threads, the new ones marked; Drafts and sent — drafts kept in the browser per conversation, andsent; Saved for later — 🔖 on a message (save/{id},saved, tableconversation_saved, migration2026_10_22_000004_create_conversation_saved_table, declared personal data); All unread (unreads) with Mark as read per conversation. - A conversation's files in its header (🗂,
files/{id},Files::inConversation()). Messages::many(): messages by id as a reader sees them, with their conversation's name.
Documentation¶
- Conversations guide: The page and the dock — the views above the conversations; The HTTP API — the new actions.
- Upgrade Guide: the conversations' views need
conversation_saved— run the migrations.
Conversations: people who stopped signing in¶
Added¶
conversations.inactive_after_days(0, the default, is off): somebody who has neither signed in nor registered within that many days is inactive — not offered in New conversation or an@unless their whole name is typed or Show inactive people too is ticked, not counted in@allor@online, greyed with their last sign-in where they already are, and a note above the composer of a direct conversation with them. Nothing is removed; signing in makes them active again (FW-242).People::inactive(),People::inactiveAfterDays();People::search()takes$inactive;peopletakesinactive; a conversation'smembersand the sidebar's conversations carryinactive.
Fixed¶
- Searching people matches each word in the first name, last name or username, so a whole name —
Νίκος Π.— finds its person; before, the phrase had to be inside one of them. - All unread's button has a key of its own,
Mark every conversation as read:Mark all as readis an application's word too, and came out in its wording.
Documentation¶
- Conversations guide: Access — people who stopped signing in.
Beacon: no signing key, no beacon¶
Fixed¶
- A primary with the beacon's addresses but no
signing_keystill told every page there was a beacon; the page asked for its emergency key andConversations/emergencykeyfailed with a 500 each time ("The beacon has no signing key here"). The browsers are now told of a beacon only when the primary can sign keys (Beacon\Config::ready(): a primary, its addresses, and a signing key that is one), andemergencykeyanswers 404 otherwise (FW-243).
Documentation¶
- Conversations guide: When the server is down — the beacon, keys — without a signing key there is no beacon to the browsers.
Ask: everybody's conversations, read only¶
Added¶
/admin/Ai/all: every administrator's Ask conversations for whoever hasadmin.ai.all— root alone in usertype mode — to debug what the analyst did for somebody else (FW-244). The list has the owner, title, date, message count and cost, filtered by owner or a word in the title; one opens as its owner sees it under "'s conversation — read only ", with each answer's tools and the model calls made for it (/admin/Ai/call/{id}, where the tools' output is). Nothing is written — no question box, no delete, no memory — but anai.conversation_viewedrow in the audit log for each view of somebody else's. Anybody else gets a 404.Analyst\Review(readsAll(),list(),owners(),open());AiController::all(),readsAll(),reader().
Documentation¶
- LLM guide: Other people's conversations, read only.
- Upgrade Guide:
AiControllergainsall(),readsAll()andreader().
Ask: the page's own links behind a gear¶
Changed¶
- The link to everybody's Ask conversations no longer sits beside New conversation as if it
were one of the page's main actions (FW-245): it and the settings are in a ⚙ menu under the
title (
<details class="pf-ask-gear">, named Ask page tools), each shown to whoever has it, and a reader with neither sees no gear. All three themes.
Documentation¶
- LLM guide: Other people's conversations, read only.
Ask: other people's conversations leave out the reader's own¶
Changed¶
/admin/Ai/allno longer lists the reader's own Ask conversations, nor offers them in the owner filter — they are in the Ask column, where they open normally (FW-246).Ai/all?c=with one of them redirects toAi?c=. The page and its ⚙ entry are now Other people's Ask conversations.Review::list()takes anexceptfilter andReview::owners()an owner to leave out.
Documentation¶
- LLM guide: Other people's conversations, read only.
Ask in the conversations: a conversation of its own, and the reader's rights elsewhere¶
Added¶
- A conversation with Ask (FW-247). New conversation offers Ask first, as a bot, to whoever
may ask it; the
withaskendpoint opens theirbotconversation named "Ask" (Ask::conversationFor()). Every message there is a question, answered in the conversation with the asker's rights. It is one analyst conversation that goes on from answer to answer, the same one/admin/Ailists, and each answer has an Open in Ask card. Ask is thinking… shows until the answer comes. It is listed with the direct conversations. Turn::rightsOf()andTurn::shared(): whose rights an answer is given with, and whether others read it.
Changed¶
@Askin any other conversation is answered with the rights of its least privileged reader (Ask::readerOf()): the lowest usertype among its members, or among everybody who may use the conversations in an open one. It carries none of the asker's memory and no CSV file, and the model is told which of the asker's tools it may not use there, so it can say so and suggest the conversation with Ask. The cost is still the asker's.AnalystAssistant::tools()builds the framework's tools forrightsOf().- A bot conversation's title is its name, not its members'.
- Ask is not a member of channels:
@Askalready answers in every one.
Documentation¶
- Conversations guide: References and commands, Ask.
- LLM guide: The application's figures, deciding by
rightsOf(). - Upgrade Guide:
@Askanswers with the least privileged reader's rights.
Ask's answers reach the conversations from any worker¶
Fixed¶
- An answer to
@Ask, or in the conversation with Ask, never reached the conversation where the queue worker was an application that boots no providers (FW-248). The answer was posted by a listenerConversationsServiceProvider::boot()registered, and that listener does not exist in such a worker.Asks::run()now posts every ask whoseextranames a conversation itself, once per ask (source = 'ask',externalid= the ask). The provider no longer registers a listener.
Documentation¶
- LLM guide: Asking in the background.
Ask's charts drawn in the conversations, its card in the reader's language¶
Fixed¶
- A chart in an answer of Ask's showed in the conversations as its JSON (FW-249).
Conversations\Markdownnow renders a```chartblock that is a chart as Ask's page does (data-pf-chart, its code inside). The page draws it withpf-chat.js, loaded besidepf-conversations.jsthe first time a message holds one, through the newwindow.PfChat.drawCharts(root), which draws each chart once. - The Open in Ask card was in English. The worker that writes it knows no reader, so the title is stored in English and the page translates the cards from Ask for the reader. Answers already posted are translated as well.
Documentation¶
- Conversations guide: References and commands, Ask.
Tables in the conversations; a chart's long category names cut to fit¶
Added¶
Conversations\Markdowndraws GFM tables (FW-250). A table has a header row, columns aligned by:---:and---:, and a.pf-conv-tablewrapper that scrolls sideways when the table is wider than the message. It is styled as Ask's tables in each theme'scomponents.css. Ask's answers, which hold tables often, now read as they do on Ask's page.
Fixed¶
- A bar chart's category names under the bars ran into one another when long, as in a message
30rem wide. Each name is now cut to the width its bar has, with
…, and the whole name is in its tooltip. This ispf-chat.js, so Ask's page draws its charts the same way.
Documentation¶
- Conversations guide: Messages, the text's marks.
Other people's Ask conversations: folded steps, drawn charts¶
Fixed¶
/admin/Ai/all?c=…opened every answer with its steps and tools unfolded, so the page was mostly the tools' JSON, and showed a chart as its JSON (FW-251). Inai/review.html.php, in each theme, the steps and tools are now folded as on Ask's page. The page runspf-chat.js(data-pf-chat, with no form, so it asks nothing), which draws the charts and puts a copy button over each table.
Documentation¶
- LLM guide: Other people's conversations, read only.
Help pages' headings are the levels they write¶
Fixed¶
Help::html()drew a help page's headings two levels lower:##became<h4>,###<h5>and####<h6>. The renderer,Chat\Markdown::toHtml(), shifts headings for a chat reply, which sits under its conversation's own<h1>, and a whole help page went through the same shift. A long page with clear sections then rendered as one block wherever the CSS stopped ath4.toHtml()now takes'headingShift'(2 by default, for replies), andHelp::html()passes 0. Reported by GlideDay.
What breaks in subclasses¶
- Nothing in code. CSS written for the shifted levels of help pages (
h3–h6) stylesh1–h4now; see the Upgrade Guide.
Documentation¶
- Admin Assistant guide: Links and pictures.
- Upgrade Guide: help pages' headings.
Conversations: history from another system, written through the API¶
Added¶
- An application that brings history in from another system writes it through
Messagesalone, with no table touched (FW-253, phase 1): Messages::find($source, $externalId)finds a message already brought in;post()takeseditedAtandauthorName(a writer with no account, shown by name as somebody who left);react()takes when the reaction was put and$quiet.- A file goes in as it always could:
Files::store(), thenfilesinpost(). The framework's own import is written on these calls now.
Documentation¶
- Conversations guide: Messages, history from another system.
Conversations: the application's own links at the foot of the list¶
Added¶
conversations.links: the application's own links at the foot of the conversations' list, on the page and in the dock, under Files and space (FW-253, phase 1). Each link has alabelalready translated, aurland anicon. It is shown to whoever passes itsabilityor itsvisiblecallable. The client receives them asprops.links.
Documentation¶
- Conversations guide: The page and the dock, the application's own links.
Conversations: bringing history in from another system is the application's¶
Removed¶
- The framework no longer brings in another chat's history (FW-253, phase 2). Gone:
- the
Conversations\Importnamespace and its queue taskconversations_import; /admin/ConversationImportsand its menu item, with the abilityadmin.chat.import(Access::IMPORT,Access::imports());- the actions of a person's connection to the other chat on
Controllers\Conversations, its page and the sidebar's link to it, and the client's prop for it; - the framework migration
create_conversation_imports_table, with its two tables in the personal-data registry. - An application that imports does it on the public API (see the entries above), with tables of its own.
SystemPost::post()takes its text as a message's Markdown, like every other message. The public static method that turned another chat's marks into Markdown is gone with it.
What breaks in subclasses¶
- A subclass of
Controllers\Conversationsthat calls or overrides one of the removed actions: delete the call or the override, as the base class no longer has the action.
Documentation¶
- Conversations guide: Bringing history in from another system, Posting from the application.
- Upgrade Guide: the import's removal, and
SystemPost's text.
Reactions: every emoji its own, on MySQL too¶
Fixed¶
- On MySQL and MariaDB a person's second, different emoji on a message was silently dropped
(FW-254).
conversation_reactions.emojiwasutf8mb4_unicode_ci, which compares every character outside the Basic Multilingual Plane as equal (😍 = 💝), so the unique key on message, person and emoji took the second for the first. The framework migrationmake_conversation_reactions_emoji_binarymakes the columnutf8mb4_binand keeps every row; PostgreSQL needed nothing. - A column's
charset()andcollation()were stored and never written: the schema grammar now writes them on MySQL and MariaDB, after the type.
Documentation¶
- Schema Builder guide: Column Modifiers, a column's collation and the one for emoji.
- Conversations guide: what a reaction is.
- Upgrade Guide:
collation()andcharset()reach the column.
Conversations: a person, as a client's card and profile show them¶
Added¶
People::profile($person, $reader)(FW-255): name, last sign-in and inactivity, local-time offset where the account names a time zone, the two people's direct conversation and whether the reader muted it, the conversations both are in, and the application's fields and links.conversations.profile: a callable given the person and the reader, answeringfieldsandlinks; a link that is not http(s) or a path of the site is dropped.Conversations::shared(),Files::sentBy();Conversations::dmBetween()is public;Messages::search()takes$fromand$mentioning, and with either needs no words.- The action
person/{id}, withshow=messages,mentionsorfiles.
Documentation¶
- Conversations guide: The page and the dock (a person, as a client shows them), Search, The HTTP API.