Skip to content

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's mailtemplates says 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, entity user) 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_tables creates person_events only; audience.event_log_days is gone.
  • audience:prune-events prunes the rule run log and segment stays that ended more than audience.segment_history_days ago (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, changelog feature).

What changes for an application

  • Windowed conditions ("N times in the last M days") and A/B goals need the changelog feature; without it they are refused or shown as not counted. A person_event_log made 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 (ability admin.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() encodes details when it spools the row. A drain in a process where the changelog provider never booted — an application with changelog_events and 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%, which sprintf read as a placeholder and printed a literal %s for 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() and Filter::lines(): what a sample shows under each name.
  • Segments::uses() and Rules::linksUsingSegment(): what uses a segment, with links.
  • data-pf-combobox (pf-audience.js) and data-pf-filter-rows (pf-utils.js) for any form.
  • Icon names 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, tagged personal) and audience_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 stream transport: an event is one entry in event_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 past events.log_days (30) that every group has read.
  • Migration create_event_log_tables (queue feature). event_log and event_deliveries are declared personal data.
  • QueuedEvents::payload() and QueuedEvents::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 queue tool answers the same figures under streams.
  • StreamConsumer::status() and StreamConsumer::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_log stays the outbox, written in the caller's transaction; a relay appends what is committed, at once outside a transaction and on every pass of events:consume, so nothing is lost between the commit and Redis. Consumer groups with XREADGROUP/XACK; what was read and never acknowledged is claimed again once it has waited; dead entries go to event_deliveries as on the database (FW-235 P3).
  • A Redis whose maxmemory-policy is allkeys-* is refused for the queue, asked through CONFIG GET or, 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:consume daemon 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 conversations feature: 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 by source and externalid (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), and admin.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.activity on each other member's own channel with whether it mentions them, and presence-chat for who of the team is connected (Pramnos\Conversations\Realtime). The feature's provider adds the channel rules; typing is a whispered client-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 under var/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 setting conversations_max_file_mb or conversations.max_file_mb; a thumbnail for an image; sent only to a reader of the conversation; usage() and prune() for the space, a removed file keeping its place in its message. Messages::post() takes files (FW-236, phase 1).

Documentation

  • Conversations guide: Files.

Added

  • Messages::search($user, $terms, ?$conversationId): every word, each at its start, through the full-text index (MySQL/MariaDB FULLTEXT, PostgreSQL to_tsvector('simple')), by LIKE when a word is shorter than three letters; only in what the searcher reads, never a deleted message. Conversations::readable() and Conversations::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 and admin.chat, every write a POST with the session's token (FW-236, phase 1).

Documentation

  • Conversations guide: The HTTP API.

Fixed

  • /admin/Chat refuses a write without the session's token with 403 and {"error": …} instead of a redirect to the site, whatever the request's Accept and X-Requested-With say; create answers the new conversation as well as its id.

Realtime: presence members and whispers in pfRealtime

Added

  • window.pfRealtime hears 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 with channel(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 by pf-conversations.js, built from Svelte sources in resources/js/conversations/ by npm run build:conversations and 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, behind admin.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, conversation titles and a conversation's members; Conversations\Client gives the client its start (props(), strings()); People::search().
  • A system message is signed by its card's from, else its source (jira → Jira).

Changed

  • The screen is Controllers\Conversations at /admin/Conversations; /admin/Chat reaches it still (Controllers\Chat extends it), except in an application with the AI chat page, whose site controller is also Chat. A message's markdown classes are pf-conv-* (they were pf-chat-*, the AI chat page's prefix).
  • Cards side by side in .pf-grid-2/.pf-grid-3 are 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 --js adds the scripts an enabled feature's pages load where they are missing — pf-conversations.js for conversations — 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 — #name a system channel made on first use, an account id (or @id:N, or a name the application resolves with resolvePeople()) 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, color by name or hue); username signs 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:id as 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) — /name in 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 client refTypes and commands.

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

  • @Ask in 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 by www/beacon.php without 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 takes settings_file from app/beacon.php, else app/config/settings.php or app/settings.php, or the application's own db_check — so a primary with its settings elsewhere is not reported down (FW-238).
  • beacon.php requires app/beacon.bootstrap.php first when it is there, for the constants an application's autoloaded files need, and defines DS; 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.js checked and subscribed navigator.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 no push handler, leaving the server a dead second endpoint. It now uses the registration at the site's address, and ready only 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} and Conversations::star(); conversation_members.starred (migration 2026_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 now pf-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, and sent; Saved for later — 🔖 on a message (save/{id}, saved, table conversation_saved, migration 2026_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 @all or @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; people takes inactive; a conversation's members and the sidebar's conversations carry inactive.

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 read is 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_key still told every page there was a beacon; the page asked for its emergency key and Conversations/emergencykey failed 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), and emergencykey answers 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 has admin.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 an ai.conversation_viewed row 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: AiController gains all(), readsAll() and reader().

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/all no 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 to Ai?c=. The page and its ⚙ entry are now Other people's Ask conversations. Review::list() takes an except filter and Review::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 withask endpoint opens their bot conversation 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/Ai lists, 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() and Turn::shared(): whose rights an answer is given with, and whether others read it.

Changed

  • @Ask in 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 for rightsOf().
  • A bot conversation's title is its name, not its members'.
  • Ask is not a member of channels: @Ask already answers in every one.

Documentation

  • Conversations guide: References and commands, Ask.
  • LLM guide: The application's figures, deciding by rightsOf().
  • Upgrade Guide: @Ask answers 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 listener ConversationsServiceProvider::boot() registered, and that listener does not exist in such a worker. Asks::run() now posts every ask whose extra names 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\Markdown now renders a ```chart block that is a chart as Ask's page does (data-pf-chart, its code inside). The page draws it with pf-chat.js, loaded beside pf-conversations.js the first time a message holds one, through the new window.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\Markdown draws GFM tables (FW-250). A table has a header row, columns aligned by :---: and ---:, and a .pf-conv-table wrapper that scrolls sideways when the table is wider than the message. It is styled as Ask's tables in each theme's components.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 is pf-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). In ai/review.html.php, in each theme, the steps and tools are now folded as on Ask's page. The page runs pf-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 at h4. toHtml() now takes 'headingShift' (2 by default, for replies), and Help::html() passes 0. Reported by GlideDay.

What breaks in subclasses

  • Nothing in code. CSS written for the shifted levels of help pages (h3–h6) styles h1–h4 now; 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 Messages alone, with no table touched (FW-253, phase 1):
  • Messages::find($source, $externalId) finds a message already brought in;
  • post() takes editedAt and authorName (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(), then files in post(). The framework's own import is written on these calls now.

Documentation

  • Conversations guide: Messages, history from another system.

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 a label already translated, a url and an icon. It is shown to whoever passes its ability or its visible callable. The client receives them as props.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\Import namespace and its queue task conversations_import;
  • /admin/ConversationImports and its menu item, with the ability admin.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\Conversations that 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.emoji was utf8mb4_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 migration make_conversation_reactions_emoji_binary makes the column utf8mb4_bin and keeps every row; PostgreSQL needed nothing.
  • A column's charset() and collation() 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() and charset() 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, answering fields and links; 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 $from and $mentioning, and with either needs no words.
  • The action person/{id}, with show = messages, mentions or files.

Documentation

  • Conversations guide: The page and the dock (a person, as a client shows them), Search, The HTTP API.