# Phase 4.5 — Settings Refactor and Live-Site Fixes

Arc Post-Mortem Summary. Took the three threads that had no business waiting on the Phase 5 layout work: steadying the live sites, splitting the single Server Settings screen into Servers and a new User, Roles, Groups screen, and a run of small interface fixes. The live-site half ran from a worker pool saturated by bots through a mail server that had silently stopped delivering to a search of a 2019 index answering with a server error, and carried the certificate subsystem through to issuing for real. Serving seekquarry.com and frise.org from one instance took most of the arc's length and most of its surprises, the routing, the error pages, and the wiki page lists each having assumed a single domain. The interface thread closed on 2026-07-17 when the resource toolbar's remaining faults turned out to want the Phase 5 CSS foundation rather than more nudges, and moved there; what the arc kept teaching, in a bot filter that condemned the site's own infinite scroll and a picture whose name contained the word location, is that the costly bugs sat in strings being read for something they never were.

Arc started 2026-07-10, after Phase 4. Closed 2026-07-17. Sections are Roman numerals and steps within them are lettered; each step's title says what it does, followed by a sentence on what was proposed and, once worked, a few on what was done and anything learned. Marker legend: ✓ done, ▶ next, ? open decision, unmarked = planned.

## I. Fix live-site crashes and load

1. ✓ A. Stop the yioop.com out-of-memory crashes and CPU pinning. A crawl flood on one page's wiki history plus spam searches drove repeated out-of-memory crashes and a pinned core. Bounded the `PackedTableTools` dictionary cache to a byte budget with least-recently-used eviction, took the markdown and mediawiki parsers linear with `MarkUpScanner::peekLine`, clamped each index entry's `NUM_DOCS`, stopped a spaces-only list marker spinning `skipBlankLines`, and moved history-revision rendering to the browser. Every fix has tests, holds byte-for-byte against the confirmed head, and is live.
2. ✓ B. Stop the pollett.org mail server wedging on slow DNS. The single event loop stalled up to sixty seconds inside a blocking `dns_get_record` on inbound DKIM/SPF/DMARC and outbound mail-exchanger lookups, so one slow sender froze the daemon. Routed every lookup through a new `AsyncDnsResolver` that queries over a non-blocking socket, yields the loop, and is bounded by `MAIL_DNS_TIMEOUT`; a unit test covers name encode and decode, compression pointers, and hand-built replies.

## ✓ II. Split Server Settings into two activities

The one Server Settings screen mixed machine-level configuration with per-account policy; these steps split it and build out the new account-policy controls.

1. ✓ A. Move the settings field sets onto a Servers screen and a User, Roles, Groups screen. Partition the existing field sets between the two activities with no new fields, as a pure move that keeps every control and locale key working. Landed as a pure move.
2. ✓ B. Add a Role Group Limits field set with per-role storage and a read path. Give each role caps on how many groups it may own and how large those groups may grow, an account's cap being the most generous across its roles. A `ROLE_LIMITS` table holds one row per role (-1 means unlimited) with a version 117 migration, the screen edits every role's caps, and `getUserGroupLimits` resolves the maximum across an account's roles. Enforcement is step E.
3. ✓ C. Add a Moderation field set with an on/off toggle. Gather the moderation constants into one group and add a switch for whether moderation runs at all. The field set carries `MODERATION_FLAG_THRESHOLD` plus a new `USE_MODERATION`, and `flagGroupItem` only routes a flagged item to the moderation group when `USE_MODERATION` is on.
4. ✓ D. Retire GIT_MAX_REPO_SIZE for a resource-memory cap and add a Developer role. Fold the standalone git repository-size limit into the per-page resource-memory limit, and add a role that holds only the Feeds and Wikis activity. The git push check reads `MAX_PAGE_RESOURCE_MEMORY`, and a Developer role (version 118 migration and Createdb seed) holds Feeds and Wikis with no modifier, unlike the User role.
5. ✓ E. Enforce the role group limits at the points that create or grow a group. Check `getUserGroupLimits` at group create, member add, wiki-page add, resource upload, and post, and refuse past the cap.
   Done: `GroupModel` gains pure count methods (`countGroupsOwnedByUser`, `countGroupWikiPages`, `countGroupThreads`, `countThreadPosts`, joining the existing `countGroupUsers`); all enforcement lives in `SocialComponent` so the models stay independent of `RoleModel`. Two helpers, `overUserGroupLimit` and `overGroupLimit`, resolve the acting user's or the group owner's caps through `getUserGroupLimits` and refuse past a cap with `redirectWithMessage`. Group create is held to the acting user's `MAX_GROUPS_OWNED`; the wrappers `addGroupItemWithinLimits` and `addUserGroupWithinLimits` guard the feed and membership adds for `MAX_GROUP_THREADS`, `MAX_THREAD_POSTS`, and `MAX_GROUP_MEMBERS`; the new-wiki-page path is held to `MAX_GROUP_WIKI_PAGES`; and `handleResourceUploads` enforces `MAX_THREAD_RESOURCES` by file count for thread posts and `MAX_PAGE_RESOURCE_MEMORY` by folder bytes for pages. Seven refusal strings and a `GroupModelCountsTest` were added.
6. ✓ F. Rework the Profile.php constants onto the live accessor so settings changes skip a restart. A Profile.php constant is fixed at boot, so a setting read as `C\NAME` needs a restart to change; a `p($label, $value, $set)` in Config.php backs each setting with a per-process cache seeded from the boot constant and overridden whenever `updateProfile` rewrites Profile.php, so a read through `C\p('NAME')` sees the new value at once. Landed the accessor (keyed on `array_key_exists` so a stored zero holds, resolved against the configs namespace), the `updateProfile` write hook, and the `updateProfileFields` fallback, with a unit test. The settings reads, including the moderation flags, were then moved to `C\p` group by group and verified live.
7. ✓ G. Add per-role cost and charge-frequency billing. Charge a per-role cost in credits on a frequency (Never, Once, Monthly, Semi-Annual, Yearly). Done with the sellable-roles work: a role's `ROLE_LIMITS` row carries `ROLE_COST` and `CHARGE_FREQUENCY`, both editable in the roles admin panel and read by the account-page purchase and renewal paths.

Open decisions: ? relabel the current settings activity display name to “Servers”; ? whether Git Wiki Pages moves fully to the new activity or keeps a foot on the Servers screen.

## III. Multi-domain hosting and small interface fixes

1. ▶ A. Serve seekquarry.com and frise.com from one Yioop instance. Get multi-domain handling solid enough that one code base and index host answers for both domains, each with its own landing group and branding, rather than separate installs. Planned.
   Landing routing unified onto the static controller: a domain routed to a landing group in the Web Server field set used to serve that group's Main page through the group controller, while the Public-group landing and the “Use Wiki Public Main Page” option both went through the static controller. Both routed cases now take the static path. The router in `src/index.php` sends any positive routed group id to the static controller and passes the id on a `$_SERVER['ROUTE_GROUP_ID']` variable a browser cannot set, since it is not an `HTTP_` header, so only an admin-set `DOMAIN_ROUTE` chooses the group. `StaticController` gained a `staticGroupId()` resolver (the routed id when present and positive, else the world-readable Public group) and reads every wiki page, header, footer, alias, and resource from it in place of a hardcoded `PUBLIC_GROUP_ID`. Only the bare landing request is re-homed; other `c=static` requests still resolve to the Public group. A `StaticControllerTest` covers the resolver, and an end-to-end read confirmed a routed group's Main page is served while the default falls back to Public.
   Routed sub-pages and their self links: serving only the landing left the group's own links broken, since a link like `[[Bob]]` in a routed group's page is baked at save time as that group's read url on the group controller and, drawn under the static controller, became a `static/<id>?a=wiki…` url with no clean route (an error page). Two changes fix this. The router now resolves the request host to its landing group for every static request, not just the bare landing, so a `p/Bob` clean url on a routed domain reads that group's Bob page; the host comes from the admin-set `DOMAIN_ROUTE` table, so a request parameter cannot choose the group. And the static controller rewrites a routed group's baked self links to their `p/` form as it serves the page, body, header, and footer alike. The rewrite keys on the `page_name` query the read url ends with rather than on the exact base url, so it holds whether the page was saved with clean urls on or off. The rewrite covers a page's own links; cross-group `@@group@page@@` links and `[[pages]]` index links are left as they are, and a page missing in the routed group is not looked up in the Public group. End-to-end on the built-in server: the routed landing shows its Main with the Bob link drawn as `p/Bob`, a static request for that page on the routed host serves the routed group's copy, and the same request on an unrouted host still serves the Public group.
2. ✓ B. Give the Manage Crawl start-crawl [+] control a single-line accessible name. Its accessible name renders as the letters of “Add” stacked on three lines. Landed a `white-space:nowrap` guard on `.icon-glyph`, confirmed on the live browser.
3. ✓ C. Fix the Add Role form's vertically-stacked Name label. The “Name:” label rendered one letter per line. Removed a stray `min-width:100%` on the input cell, the only one in the views, which was collapsing the label column in Chrome; verified the fixed form renders on one line.

## IV. Other work

Tasks that surfaced while doing the above and stand on their own.

1. ✓ A. Restore wiki source, history, and page-list access. Several related regressions in wiki viewing. Historical revisions render their resources server-side, a `{{require-signin}}` left literal inside `<nowiki>` no longer fires, the whole-page sign-in warning is width-bounded, the Tools “Wiki Pages” link is hidden where the public group forbids its page list, and an empty `public_source` falls back to the group's page-source setting.
2. ✓ B. Move wiki-revision diff rendering to the browser. A diff built its longest-common-subsequence table server-side on every crawler visit. The two revisions now ride to the browser and `renderWikiDiff` in `help.js` computes the diff there, checked line for line against the server `diff()`; this also fixed a latent `array_slice(..., 0)` that collapsed any no-common-tail diff to coarse.
3. ✓ C. Instrument mail server down-events. A silent mail outage left no trace, since the heartbeat closure never captured the server object and threw every interval. Fixed that capture and added a shutdown handler that records a fatal with loop position, memory, and fiber and connection counts; catchable stop signals are logged, and the resolver logs a lookup no name server answers in time.
4. ✓ D. Build shared test tables from ProfileModel definitions. Seven model tests stood up shared tables with hand-copied CREATE TABLE statements that could drift. Each now calls `ProfileModel::initializeSql` and executes the matching `create_statements` entry, so the tests track schema changes; MAIL_SECRET and GIT_APP_CODE, which ProfileModel does not define, keep their local definitions.
5. ✓ E. Keep the admin signed in when the session cookie name or CSRF token field name changes. Renaming either on the Session Parameters panel signed the admin out: the browser kept a cookie under the old name while the server began expecting the new one, and a token-field rename dropped the token from the follow-up redirect. The `renameSessionCookie` migration and a token carry-forward now run on the panel's real save path, the `updatetypes` case of `security()`, just before `updateProfile`. An earlier copy of the migration sat in `updateProfileFields`, which no SESSION_NAME form reaches, so it never ran and is removed. Verified live against the secure server that both renames keep the session.
6. ✓ F. Let users buy roles and subscribe to sellable ones. A role is put on sale by giving its `ROLE_LIMITS` row a `ROLE_COST` above zero and a `CHARGE_FREQUENCY` other than "never" (both already editable in the roles admin panel). A new `USER_ROLE.EXPIRES` column (database version 120, default `FOREVER`) records when a bought grant lapses; the permission queries in `UserModel` now ignore a grant past its expiry, so a lapsed role stops conferring its activities. On the account page, when any role is for sale, a Roles row under the language setting lists the user's roles, gives each subscribed role an unsubscribe link, and offers a change-password-style Purchase form that debits the role's cost from the credit ledger and grants it with an expiry one billing period out (a "once" purchase never lapses). Viewing the account also runs a credits-forward pass that spends the balance to push each lapsed subscription's expiry forward, one period at a time, for as many periods as the balance covers. Reworking the roles admin limits screen to fetch only the selected role's row, with an unsaved-changes confirm on dropdown change, is the immediate follow-on.

## Other work accomplished

- Purchase New Roles now opens the purchase form. The link carried `edit_purchase=true` but not `edit`, and the account page draws the purchase form only in its edit view, so the click landed on the read-only account page with nothing to buy. `edit_purchase=true` now opens the edit view the way `edit_pass` does, and buying or unsubscribing returns to that view (the purchase form carries a hidden `edit_purchase`, the unsubscribe link carries `edit`, and both redirects copy the field forward). Verified by rendering the account page with a role put up for sale: the form and the Roles row appear.
- Role Group Limits keeps the role you were editing. The role drop-down was client-side only, so a save reloaded the panel on the first role, Admin. The drop-down now submits as `selected_role` and marks the chosen role selected, the save redirect copies `selected_role` forward, and the controller hands the view a `SELECTED_ROLE` it validates against the loaded roles. Verified by rendering the panel with Developer chosen: the drop-down comes back on Developer, not Admin.
- Unsubscribe is now a confirm on the role name. A subscribed role used to print its name followed by a separate "unsubscribe" link; the name itself is now the link, and clicking it raises a browser confirm dialog ("Unsubscribe from this role?") before following through to the unsubscribe action. Non-sellable roles stay plain text. Verified by rendering the account page for a user holding a role put up for sale: the name carries the confirm and the standalone link is gone.
- Roles label reads "Roles:" and tops the list. The label gained the colon its sibling rows carry, and the label cell takes the existing `align-top` class so it lines up with the first line of the roles list rather than floating to the vertical middle of the two-line cell. Confirmed the label cell has no competing vertical-align rule, so `align-top` applies.
- Unsubscribing a role now cancels at period end. A cancelled sellable role is kept until its paid period ends but stops renewing (a new WILL_RENEW flag on USER_ROLE, schema version 121 with its migration), and the account page shows the expiry date with a resubscribe link that only turns renewing back on and charges nothing. The account-page settle step removes a cancelled grant once it lapses, while a grant already lapsed, or a one-time permanent buy, goes at once. Confirmed by running the component methods: cancel keeps the role with the flag off, resubscribe restores it with no charge, a lapsed cancelled grant is removed while a lapsed renewing grant is pushed forward and charged, and the page renders the expiry and link; the same model is meant to carry to group subscriptions, which do not yet exist in the tree.
- Resubscribe is the expiry link, and the cell wraps. For a cancelled role the whole "expires DATE" text inside the parentheses is now the link, and clicking it asks "Would you like to resubscribe?" before resubscribing; the role name and the commas between roles stay plain text outside any link. The roles cell now sits in a container capped at the form field width so a long roles list wraps instead of widening the column and pushing the save button out of place. Confirmed by rendering both states: the link wraps only the expiry text with the resubscribe prompt, no comma falls inside a link, and the cell content sits in the width-capped container.
- pollett.org mail server no longer runs out of memory on an unterminated SMTP message. The daemon died with Allowed memory size exhausted during a socket read, memory having jumped from about 145 MB to a gigabyte in under a minute: each connection's received bytes accumulate in one buffer until a complete command or, in the DATA phase, the <CRLF>.<CRLF> marker is seen, and while the command phase already dropped a line passing 64 KiB, the DATA phase checked the message size only after the terminator arrived, so a body streamed and never terminated grew without bound and took every other connection down with it. consumeSmtpDataPhase now drops the connection once the buffered body passes MAX_MESSAGE_LEN with no terminator in sight, the way an over-long command line is already handled; a message that ends within the limit, or whose terminator lands in the same read that carries it past the limit, still gets the existing 552 reply. A MailSiteTest group covers the three outcomes: an unterminated over-limit body drops the connection, a short partial body keeps waiting, and a terminated over-limit body still receives 552.
- The wiki parser renders a page's block-class boxes, resource links, and stacked headings the way the old parser did. The recursive-descent WikiParser that replaced the old regex parser had dropped three constructs the frise.org front page is built from, so that page came out as literal markup, and three fixes restore the intent without the raw-html passthrough the old nowiki route allowed. A block-class directive now always emits a <div class="...">, in a block or inline, where plain class stays a span; a link splits on its first top-level bar only, so a bar nested inside a resource, a template, or an inner link is skipped and [[url| ((resource-nolink:file|alt))]] keeps its href while the three-field relationship|page|shown form still points at its middle field; and a heading may run over several lines, an opening = with no close gathering following lines until one ends in a =, with a blank line or the end of input leaving the block to ordinary parsing. A WikiMarkdownTest group covers all three, including the frise.org title stack, and the existing corpus and markdown suites still pass unchanged.
- A nowiki example that spans several lines now stays whole when it starts on a space-indented line. Writing sample wiki markup in the Wiki Syntax help page surfaced this: a single leading space marks a line as preformatted, the run was gathered one indented line at a time, and when a <nowiki> opened on an indented line but its </nowiki> sat past an unindented line, the run ended too soon, so the opener was shown as literal &lt;nowiki&gt; and the rest of the example parsed as ordinary text. The space-preformatted reader now does what the paragraph reader already did: while a nowiki span opened inside the run is still open it keeps taking lines as typed, including unindented ones, until the span closes, then returns to the leading-space rule. A WikiParserTest case covers the multi-line case, a single-line nowiki on an indented line is unchanged, and the parser, corpus, and markdown suites still pass.
- The Appearance activity now follows the domain in the address bar, so on a multi-domain instance the admin edits the look of the domain they are actually viewing. The Customize Domain picker used to default to the default scope and reload the same host when changed; it now defaults to the current host when that host is one of the configured route domains, and changing it moves the browser to the host serving the chosen scope. Because the two domains are different registrable names that cannot share a session cookie, the switch carries a single-use handoff token: the source host mints it with setSessionUser tied to the signed-in user, and the target host, in AdminController, redeems it the way a dropped session is rehydrated from a cookie, then deletes it so it cannot be replayed. The picker's script, delivered through the controller's script data so it runs after the page and its base helpers have loaded, marks the form dirty on any field change and confirms before switching; SystemComponentTest covers the scope-selection rule, while the live cross-host handoff needs a multi-domain instance to exercise end to end.
- The Appearance domain switch now builds its links the way the rest of the site does, and follows the site's own url scheme. The picker's switch link and the redirects behind it were assembled by hand as a query on the site root rather than through the shared controller-url builder every other link uses, so they pointed at a different path from the activity's own form; they now come from that builder. The switch also derived its url scheme from the site's base-url constant, which in a request is a host-relative path carrying no scheme at all, so the check always fell through to https and a plain-http site would have been sent to an https address that may not answer; the scheme now comes from the resolver that reports what the request actually arrived over. Checked against a running server with a routed domain configured: the switch link renders in the site's clean path form, the switch emits an absolute http address with its single-use handoff token on a plain-http site, and saving a colour for a routed domain stores it and returns to the Appearance form for that domain.
- Each domain a site serves now has its own themes, and the theme a domain is set to is the one it actually shows. Themes used to sit loose in the app css folder and be shared by every host, and a domain's chosen theme was never worn: the page layout read the theme name from the site-wide setting while only the domain overlay knew the domain's choice, so a routed domain dressed itself in the site's theme; each theme now belongs to a scope, is worn by the domain that chose it, and is kept in a folder of its own, app/css/<domain> for a routed domain and app/css/default otherwise, so two domains may each have a theme of the same name and each domain is offered only its own, and a version 122 step moves themes already in the css folder into the default folder while leaving a search.css override where it is. Adding or editing a theme while a domain was picked saved nothing at all, as the domain branch returned before the theme was written, and the editor showed the domain's theme name against the site theme's rules, so an edit would have saved them over the wrong theme; both are fixed, and the Appearance form now keeps its values apart from the fields dressing the page, so it shows the scope the admin picked rather than the look of the domain they are browsing. Checked against a running server with 127.0.0.1 configured as a route domain: adding a theme there writes it to that domain's folder, the domain's pages link and serve it, the form shows the domain's theme on the domain scope and the site's on the default scope, and rolling a work directory back a version moves its loose themes into the default folder.
- Deleting a theme works again, and the Appearance activity stops offering a theme that is not there. Moving themes into a folder per scope left the delete step looking for them where they used to be, in the css folder itself, so deleting any theme reported an error and removed nothing; delete now works in the scope's own folder, and deleting a domain's theme clears only that domain's record of it, leaving the domain's other settings and the site's own theme alone. The activity also offered an edit pencil for a theme the scope does not have: the theme named by the domain was laid over the form after the check that the named theme is one of those on offer, so a name left in a domain's record by the earlier save-does-nothing bug, when a theme was named but its file never written, still reached the form and opened an editor with no rules that could not delete; the check now comes after the domain has had its say, so a name with no theme behind it leaves the form offering no theme at all. The Adjust Search Page Components link is gone from the activity, as search page components are the same for every domain and the link sitting under a domain picker suggested otherwise; the setting is still on Page Options, where it reads as the site-wide choice it is.
- Switching the Appearance domain to another host no longer signs the admin out. The switch hands the target host a single-use token naming the signed-in user, and the target restored that user's session by reading the saved session blob a user carries; nothing writes that blob at sign-in, so it was always empty, the restore was skipped, and the token was spent anyway, leaving the admin at the sign-in page on the domain they had just switched to. The token is now redeemed by signing that user in under the target host's own session, the way signing in on that host does, which is all the rest of the request needed to treat them as signed in, and it is still spent the moment it is presented so it cannot be offered twice. Checked against a running server with a name server host and a routed domain: switching from the default scope to the domain lands signed in, adding a theme there then works rather than dropping to the sign-in page, and presenting the same token again from another browser gets the sign-in page.
- Signed-in sessions survive being dropped, so the Appearance domain switch no longer ends at the sign-in page. A session lives in the serving process's memory, and when it is not there, because the request reached a process without it or the server had restarted, the code meant to put it back read a saved copy of the session that is only written when a user changes their settings, so it was almost always empty and the visitor was sent to sign in; the same empty read is why the domain switch appeared to work and then dropped the admin on the next click. Restoring now uses the record of which user a session belongs to, which every sign-in writes, and puts the sign-in back under the session id the host is handing the browser rather than the one the browser arrived with and will never send again, along with the record of a recent token that a request without one needs to be given a fresh one; a domain switch also counts as consent to a session, so a domain the browser holds no cookie for yet has a session to sign the admin into. Checked against a running server with a name server host and a routed domain: after dropping the session, both a request carrying a token and a plain visit to the admin address stay signed in where both previously went to the sign-in page, and switching to the domain then adding a theme survives a dropped session and works from a browser with no cookie for that domain.
- The Search Toolbar upload moved from Appearance to the Page Options Search Time tab. A search toolbar is the XML file a browser reads to offer this site as one of the choices in its own search box, so it names the site once for a whole browser and has nothing to do with how a page looks; it was worse than misplaced, being one of the fields kept per domain, so each domain could hold its own and the last one saved is what a browser would have been handed. It is now a setting of the site alongside the other search page choices, and Appearance uploads images only, which took the file type test back to a plain check rather than one carrying an exception for the one file that was not an image; a domain row saved before this still holds a toolbar, so that field is now passed over when a domain's look is laid over a page, the way the landing page setting already was. Checked against a running server: the control is gone from Appearance and draws on Search Time, uploading an XML file puts it in the resources folder and names it in the profile, a file that is not XML is turned away, and a domain still holding an old toolbar gets the site's toolbar while keeping its own colors.
- The Search Toolbar control reads as part of the Search Time tab. It had been dropped in among the search page element checkboxes in a table of its own, so its label was set in a lighter face than everything around it and it sat under a heading it had nothing to do with. It now has its own Browser-based Search heading at the foot of the tab, after Search Results Grouping, with the control in the same kind of table and the label in the same cell as Minimum Results to Group directly above it, so the two labels come out at one size and weight. Rendered the tab headless to check it: the heading falls between Search Results Grouping and the end of the tab, and the Search Toolbar label matches the label above it.
- The Browser-based Search heading has a help button of its own. Every other heading on the Search Time tab carries one, so the new heading was the only place on the tab offering no explanation of what its control does. The button points at a Browser-based Search help page, whose source goes to Chris as a text file to paste through the wiki UI, since the file holding the help pages is generated rather than written by hand.
- The server names the request that makes it claim more memory. The memory sample says how much the server holds but not what asked for it, and since PHP hands memory back to the system rarely the figure only ever climbs, so reading it later says nothing about which request did the climbing. Reading the figure either side of each request turns that into a line naming the request the moment it happens, written only when the claim actually grows, which on a server that has been up a while is never, because by then requests are served out of memory already claimed. Checked on a running server: the first request wrote one line naming itself and reporting four megabytes, and the two requests after it wrote nothing.
- A video's width, height and running time are read once and kept, rather than by starting ffprobe on every view. These figures go into a video's page as the tags a link preview reads, and they only change when the video does, but each view of the page started an ffprobe process to work them out again; a crawler walking a site's videos made that a process per hit, which is what the Unable to fork warnings in the live log are. They now come from a note kept beside the video's thumbnails, holding the time the video was last changed so a replaced video is measured again; the test for whether the site has ffmpeg at all was also asking the wrong question, being true even when the setting is empty, which had every video view on an install without ffmpeg starting a shell to run an empty command. Measured against a real video: reading the figures took 80.7ms the first time and 0.1ms after that, and changing the video had them read again.
- Themes are one list the whole site shares, and a domain picks which of them it wears. Giving each domain its own folder of themes, as the earlier per-domain themes work did, meant a domain could not wear a theme another domain had made, and a wiki page's own choice of theme was worse off still: the page settings offered the themes of the default folder while the page was dressed from the folder named for the host it was served on, so on a routed domain a page named a theme that was not where it was looked for and went undressed. Every theme now lives in the one folder, and all that a domain, or a wiki page, holds is the name of the theme it wants; a new database version folds each domain's folder into the shared one, dropping a copy identical to one already there, and where a name is taken by different rules it brings the domain's theme over with the domain's name added to its own and points that domain's record at the new name, so a domain goes on looking as it did. Checked on a running server: a theme made for the site is offered while customizing a domain, where before that list was that domain's own and empty, and the domain's pages load it; the three cases of the fold were each exercised against a real database.
- The site has a name, and each domain may go by one of its own. A page's title and the label on its logo said Yioop because the words were written into the locale strings themselves, so a site serving several domains announced itself as Yioop on all of them and a page came out titled for two sites at once. Site Name is now an Appearance setting a domain may override like any colour, the title string takes the name rather than containing it, and a page that gives itself no title is titled for the site alone instead of for the site and a separator with nothing after it. Checked on a running server: the default host titles and labels itself Yioop while a domain named Frise does so as Frise, and the two locale strings that had held the names are deleted.
- The site also has a motto, and the two together title a page that has no title of its own. The words This Search Engine and This Site had been written into a locale string, which is a placeholder rather than a translation and named neither the site nor the domain being served; they are now the Site Motto and Site Name settings and the string takes them both. Because the profile writes an empty value for a setting it has never been given, and an empty value beats the default named in Config, a new site would have come out with no name at all: the defaults are put in where the profile is first written, and the Appearance form falls back to them the way it already did for the logos. Checked on a running server: the default host titles itself This Search Engine - Yioop while a domain given its own name and motto uses those.
- The theme dropdown lines up with the buttons beside it. The dropdown asked for a height of a fixed fraction of an inch while the buttons take theirs from the glyph they show, so the two boxes were sized by unrelated rules and only ever matched by luck; worse, the dropdown had no box-sizing, so the height written for it was silently not the height it got, the padding and border being added on top. The row is now laid out as a row and the dropdown takes whatever height the buttons come out as, which holds wherever the page is read rather than only where the glyph happens to measure what it does here.
- A page's resources can be downloaded as one zip. Anyone wanting a copy of what a page holds had to fetch the files one at a time and the folders beside them a level at a time, so the Actions menu over a page's resources now offers Resources ZIP, which packs the folder and everything below it. A folder larger than the limit named in Config is refused outright rather than sent short, since an archive quietly missing files is worse than none, and the limit is counted in powers of ten so that the size a reader is told is the size being enforced. The archive is built on disk and read back a block at a time through the streaming path rather than held whole the way the mail attachment zip does it, which at this size would have cost the always-on server a gigabyte it never gives back.
- The resources download opens as the page keeps them. It had packed only the resource folder, and packed it loose under a fixed name, so it unpacked as a heap into whatever folder it was opened in, carried the folder of earlier versions along with it, and gave every page the same file. The archive now opens as one folder holding a resources and a thumbs folder, named after the page with spaces written as underscores and anything else outside what a file name may hold url encoded, and the version record is left out wherever it appears and not counted against the size limit; the menu item reads Page Resources ZIP and follows Upload File. Checked by building real archives over a folder with a sub-folder and a version record: the layout and the names are as expected and none of the record is in them.
- Deleting a resource returns you to the folder you deleted it from. Where a reader is sent afterwards is worked out from the fields the request carried, and the delete link named the page and the folder but never said it was being used from the resource listing, so the reader was returned to the page's edit form instead and had to find their way back. The link now says so, as does the extract link beside it, which was built the same way and went the same place.
- A folder dropped onto a page's resources keeps its folders under a web server too. The folders a dropped file came from were read off the name the browser gave it, but PHP takes the folders off that name before a handler ever sees it, so under Apache every file landed in one heap while under the command line server, which does no such thinning, the folders survived. The path is now read from full_path, the field both leave whole and which the upload already required, so no name has to survive a parser that is entitled to rewrite it. The sub-path built from it has parent steps taken out where it is turned into a folder, as before.
- The resources download really is named after its page. It was meant to be, but the name was being read from a field the page record does not have, so it was always empty and every download fell back to the fixed name the fallback exists for, whatever page it came from. The name of the page being edited was in scope the whole time and is now what is used. Checked against a real page record: the field used before is absent from it, and the one used now gives an archive named for the page.
- The block- forms of id and style work again. The parser that replaced the old one knew block-class but not block-id or block-style, so pages written for the old parser had those directives show as text where a block was meant; they had been a way of giving a whole region an id to link to or a style to lay it out by. The three now sit in one list saying which attribute each sets, so writing class, id or style on its own still wraps text in a span that keeps flowing, while any of the block- forms wraps it in a div wherever it is written. Tested for each form, in the middle of a line as well as alone.
- A media list page can be served as a static HTML folder. Showing an index page only ever replaced the top folder, so a folder of generated documentation came out unstyled and its links dead: everything the index asked for by a path relative to itself was read as a folder to browse and answered with the wiki page instead of the file. The setting is now Static HTML Folder, with Directory Indexes and Index File beside it, and a path beneath such a page is answered with the file it names, out of that page's resources and nowhere else, a block at a time; a folder is answered with its index file, or with a 404 when directory indexes are off. Both the group url and the static p/ url do this through the same code, so a domain serves its docs the same way, and a page set up under the old setting names goes on working.
- Static HTML folders serve the way a web server serves. The first cut read the two settings as one, so directory indexes had to be on for a folder to reach its index file at all, unchecking Static HTML Folder still served as one, a folder asked for without its closing slash was not found rather than sent to the slash, and a step back up a path was dropped rather than taken. Index File now says what stands for a folder and accepts several names tried in order, while Directory Indexes only says whether a folder holding none of them shows the page's resource listing or is not found. The two rows are shown only when the page is a static HTML folder, since they say nothing otherwise.
- A signed-in reader is no longer sent away from a static HTML folder. A signed-in reader whose address carried no token was sent once to the same request with one put on it, which is harmless for a page but not for a folder of files: the address landed on is no longer the folder, so every stylesheet, script and link a file names relative to itself was looked for one level up, and the file arrived unstyled with dead links. That is why a docs folder read while signed out was fine and the same folder read while signed in was not. A read of a static HTML folder is now answered where it was asked for, while every other page is sent as before.
- Several index file names save as well as serve. Index File takes a comma separated list tried in order, and serving already read it that way, but saving cut the whole line back to a plain file name in one go: any list in which a name carried a slash kept only what came after the last one and quietly lost every name before it. Each name is now cut back on its own, as serving does, so what is saved is what was meant. Checked by serving a folder whose only index file is the last name in the list and one whose only index file is the middle name.
- A git repository page can be read at its static url too. Reading one was only ever set up on the group controller, so a repository in the Public group could be browsed at its group url but not at its p/ one, which showed the page's text rather than the repository. The static controller now sets a repository page up the same way the group controller does, through the same code, and the static view shows it with the same element the group view uses, so a routed domain reaches its repositories this way as well. The Index File setting is also now labelled Index Files and says that a comma separated list may be given.
- The commit and tag lists fill in as they are scrolled again. The call that sets that up was written into the list itself, but the file it lives in is loaded at the end of the layout, after the page's own content, so the call ran before the function existed and the reader saw only the first page of rows. It is now put where the layout writes it out after that file is loaded, which is the same place the page's other scripts go. This had been so on the group url as well as the static one; the static page is only where it was noticed.
- A repository carrying a ref that names another ref can be listed again. Every ref file was read as though it held an object's name, but one can instead hold the name of another ref, as the file recording which branch a remote's HEAD follows does; its contents were then handed on as an object name, nothing could be read under that name, and the listing a client is given died outright, which reaches the client as an empty reply and stops a clone or a push before it starts. Such a ref is now passed over, as it is absent from that listing anyway. Checked against a repository carrying one: the listing died before and lists the branch after.
- Adding a user gives them their role again. The row granting a new user the ordinary user role was written by giving two values in order, but a role grant has since gained two columns saying when it lapses and whether it renews, and a row given fewer values than the table has columns is refused outright, so every user added other than through the account pages ended up holding no role. The columns are now named, as they already are where the first accounts are seeded, which lets the two later ones take the defaults they were added with. This surfaced while importing an issue tracker, which adds a user per reporter.
- A file carrying an image's name but holding no image no longer warns. Reading such a file hands back false, but that was passed on to the turning and thumbnail steps below, which need an image to work on, and the site's own error handler reported the read rather than letting it answer, so importing an issue tracker's attachments filled the run with warnings and each such file was reported as having been given a thumbnail it never got. The read now answers for itself, as the exif read beside it already did, and a file that holds no image is left without a thumbnail and said to be. Checked against a file of plain text named .png, a truncated one, and a real image.
- No stray ampersand after the question mark. The routines that build a url leave it ready for the first thing in its query, and the bar put the signed-in reader's token there and wrote a separator before whatever came next; with nobody signed in there is no token, so what came next was itself the first thing and the separator it was given had nothing to separate, giving addresses reading question mark ampersand. The separator now belongs to the token, which is what needs one, so an address is well formed whether or not anyone is signed in. Checked signed out, where such addresses were two and are now none, and signed in, which is unchanged.
- An issue can be thrown away. There was no way to be rid of an issue once reported, which an import that brings in hundreds at once makes felt. A wastebin now sits on the line saying when an issue was reported and by whom, offered to whoever owns the group or may edit its pages and to nobody else, and it asks before it acts because there is no undoing it; an issue is kept as a page of its own, so throwing it away takes its record, history, discussion and uploads with it. Checked by deleting through the link as an editor, and by trying the same link signed out and with a stale token, neither of which removed anything.
- A routed domain lists its own wiki pages. A domain pointed at a group serves that group's pages, but the list of them is read through the group controller rather than the static one, and it alone was never told which group the domain serves; it asked the site's own public group instead, so every routed domain answered with that group's pages and that group's say over who may see them, however its own was set. The list is now asked of the group the domain serves, from the routing an admin set rather than from anything a request can name. Checked both ways: a routed domain whose group keeps its list to itself now refuses, and one whose group shares it shows its own pages and none of the site's.
- A group keeping its page list to itself says so properly. Refusing a stranger the list showed the page for saying a crawled copy of something could not be found, which has nothing to do with the ask and told the reader nothing; the reader is now sent where the site says a page is not there, so a routed domain says it in its own words on its own address. A domain may never have been given a page of its own for saying that, and answering with nothing at all would be worse than answering with the site's, so that one page falls back to the site's copy while a domain's ordinary pages stay its own. Checked with and without a page of its own, and the site's own list and pages are unchanged.
- Being refused says refused, not missing. A group keeping its list of pages to itself is not saying the list is not there but that it is none of a stranger's business, and the reader was told the former; the site now says the latter and sends the status that means it. The pages the site shows when it turns a request away, and the status sent with each, are named in one place, and a request for one may ask for the page that fits its reason rather than have everything read as not there. Checked that a refusal now answers 403, that a routed domain uses its own wording where it has been given some and the site's otherwise, and that the pages the site already had still answer as they did.
- The check for bots stops catching readers. Results past the first ten are fetched by the page as it is scrolled, and that fetch says what form it wants results in; the check that turns bots away read that alongside the protocol the request arrived on, and the two together condemned it, so scrolling any search over the older protocol gave the page saying there was nothing there. Neither says anything about who is asking, so the check now weighs the user agent alone: a named crawler, or a claim to be more browsers or systems at once than anything is. Its reasoning moved into a method that takes a user agent and answers, checked against the user agents real browsers and real crawlers send.
- Searching an index made before the current layout works again. Such an index is served by the older bundle, which never looked words up itself; its dictionary did, together with the newest shard when that had yet to be folded in. The current code asked the bundle directly, which for an older index is a method that is not there, so every search of one ended in a server error rather than results. The older way of looking a word up is restored for those indexes alone, newer ones being asked directly as before, and the two properties it keeps the newest shard in are now declared rather than made up as it goes.
- Arranging page resources works the way a hand expects. Dropping a resource onto a folder returned the reader to the page itself rather than to the folder they were arranging, because the url the drop was sent to never named the resource listing that the redirect works out its destination from. A resource row is draggable so it can be dropped onto a folder, and that took the pointer inside the row's name field as well, so a sweep across a name to select part of it dragged the row instead; the row now stops being draggable while the pointer is down in a field. The name column, being the one read and typed in, is now given the room, and the time a resource last changed is written to the minute rather than to the second with an offset from UTC, which had cost more than half that column's width.
- The editor's drawn icons are drawn rather than written. The three alignment icons were four underscores pushed into the shape of lines of text by twenty measurements taken off one font at one size, and the phone, whose buttons are smaller, got two of the four moved and the rest left where they were, so the icons came apart there. They are now four lines drawn in the colour of the text, sized as a fraction of whatever button asks for them, which needs no font to be present and no measuring of one; the digits beside the ordered-list icon are held the same way rather than at six pixels. The layout buttons for the resource listing were the colour of a link because the rule that dresses them never said otherwise, and are now black.
- A picture named for a location has not moved. A response whose code sets headers without saying what the status is gets one worked out for it, and the working out searched the whole block of headers for the word Location, taking whatever a header was carrying as though it were a header itself. A file's name travels in a header, so any resource whose name merely contained "location" went out announced as having permanently moved, with the picture itself inside the redirect, and browsers showed nothing; "refresh" in a name did the same by way of 302. Only a line that begins a header of that name counts now, both places this is worked out share the one method that does it, and a test names the picture that found this.
- The tests run without complaint. Three of the test scripts added during this arc kept what they were testing in properties they never declared, which PHP has been calling a mistake since 8.2 and says so aloud on 8.5, and one made a folder its own set up had already made, which said so on every version. The properties are declared and the folder is left to the set up that makes it. The checks run before a patch now read the whole of what the test run prints rather than the lines saying what passed, which is why these stood for as long as they did.
- The tests stop asking reflection for what it already gives. The test script for the status line told reflection to open a method and a property the class keeps to itself, which has been unnecessary since PHP 8.1 and is called a mistake as of 8.5, so the asking is gone and the test reads the same. The development process file gained a section listing which deprecations this environment's PHP 8.3 reports and which it passes over in silence, each one measured by running the pattern rather than read off a version note, since the silent ones reach Chris's 8.5 as a wall of notices. The rest of the source and tests carry none of the three silent patterns.
- A file is asked what it holds, and answers. Saying what kind of thing a file holds is meant to be settled by reading the file's own bytes, but the class that reads them was asked for by a name written in double quotes as "\finfo", where a backslash before an f is a form feed rather than a letter, so a class of that name was never found and the reading never once ran in the life of the code. Everything therefore fell back to a table of file endings that knew nothing of webp, which is why a screenshot uploaded to a page showed as a line of blue text rather than a picture, and why a resource went out typed as unknown. The name is spelt without the backslash, the reading is asked for the kind alone so that both ways of answering have the same shape, and webp, avif, svg, tif and tiff join the table.