Skip to content

History

Revisions

  • Absorb the README's deep-dive content ahead of the README slim-down The project README is being reduced to a landing page; every fact it held that the wiki lacked is merged here first (615 lines across 33 pages): the full email-threading & reply-flow design onto Tickets, directory layout + module-page pattern + file-locations reference onto Architecture, per-endpoint tables onto API Reference, MFA login flow + LDAP subsection + injection/audit notes onto Security, the icon library and editor internals onto Network Mapper, Branding + landing-page and help/search internals + debug-tool depth onto System, planned objects onto CMDB, custody/locations/warranty onto Assets, and more. Also corrects stale claims found during the audit: the Knowledge audience table still said customer-visible articles had no reader (the portal Knowledge section shipped), a pre-#903 Portal-Privacy ambiguity row, the Workflows trigger-wiring section (catalogue spans every module now), the superseded System module-access bullet, the Multi-Tenancy users page marked "Planned" for a shipped feature, and the locale count (21 languages incl. Ukrainian, not 20). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

    @edmozley edmozley committed Jul 21, 2026
    1a1e5a4
  • LDAP wiki: login-decision flowchart, and every fix from a code audit Adds the diagram that was missing: a mermaid flowchart of how the login decides between local, directory and single sign-on — the two moments the decision happens (leaving the first box, then pressing Sign in), all four outcomes including the OIDC-pinned rejection, the group gate, and which directories get tried for an unknown user. Audited both pages against the code. Corrected: FALSE - "db_verify only ever ADDS columns, never MODIFYs" — true of the $schema array, not of db_verify; there are seven probe-then-MODIFY precedents, two of them added last week. The page then contradicted itself by documenting those two. Replaced with the directional rule: relaxing is safe, tightening isn't. - A dead line reference (db_verify.php:2614 — the file is 2557 lines) and the wrong path for $schema (it lives in includes/db_verify_schema.php). - "covered by an explicit unit-style assertion" — no such test existed. Rather than soften the sentence, the test now exists (tests/ldap/), and the note says to go and look before writing that phrase again. - "an account with no email -> JIT must refuse cleanly" — it provisions them; that is the whole point of #872/#902. - "set default_modules so auto-created people aren't handed the keys" — they were handed the keys. Now fixed in code (#904) and documented, including that existing accounts are not retroactively restricted. INCOMPLETE - The two logins were described as identical. They differ in five ways; there is now a table. Includes the wrinkle that the analyst lookup is by username only, so an LDAP analyst typing their email takes the JIT branch. - The immutable-ID promise omitted the DN fallback, which has exactly the orphaning consequence the sentence says can't happen. - The empty-login guard, the retained-secret-on-protocol-switch behaviour, the LDAPS and phpldapadmin ports, NOCOMPLEXITY, and the OpenLDAP half of the seeding (missing it produces trap 3's misleading error). - w.noemail's NW-Sales membership, which is what makes the portal journey testable at all.

    @edmozley edmozley committed Jul 20, 2026
    2ba2475
  • LDAP wiki: portal directory sign-in, and staff with no mailbox LDAP-and-Active-Directory said a directory account with no email "cannot be auto-created" — false since #872 for analysts, and false for the portal since #902. Replaced with a section covering what actually happens for mailbox-less staff: username sign-in, company from the provider, name-not-address in the inbox, notes instead of replies, and the multi-company username limit. Developer guide gains a Portal directory sign-in section: why the candidate provider list is SCOPED (spraying a password across every client's DC, and AD lockout counters, are invisible at N=1), the three deliberate divergences in ldapResolveUser (NULL not '', claim-don't-refuse, company from provider), why both groups may use the portal, the schema table, and the two things that broke elsewhere — the privacy policy failing open on a blank address, and reply having no address validation at all. Known-gaps list updated: the portal item is done; the multi-company bare username limit takes its place.

    @edmozley edmozley committed Jul 19, 2026
    78f094c
  • Portal wiki: recording on reply, and the second write-it-once rule Screen Recordings now covers both entry points (raise and reply), the shared component behind them (screen-recorder.js + record-modal.php + $needsRecorder), the two deliberately opposite unclaimed-recording behaviours (cancel discards, send blocks), and ticket_recordings.email_id — which message a recording came with, NULL meaning the opening one. Developer guide gains 2e: the claim ownership guard as a second instance of the 2c argument, generalised to "if the two copies disagree, does anything break? if one merely permits more, write it once". Later sections re-lettered. Noted the analyst reading pane still ignores email_id — nothing hidden, but a reply's video shows on the original message too.

    @edmozley edmozley committed Jul 19, 2026
    19a6e08
  • Portal wiki: bring the four pages up to the redesigned portal The pages described the pre-redesign portal: a nav with New Ticket and Request Something in it, a page-per-ticket, an inline recording panel, and a guide called "Using the portal". Now documented: the four-destination nav and why raising a ticket is not in it, the dashboard's four bands including popular articles, the compose screen, the two-pane My Tickets, ticket.php as a redirect, the recording modal, and Help's nine sections. Developer guide gains the rule the popular-articles work produced — a visibility rule gets written once (portal_reader.php), with the two knowledge traps demoted to consequences of it — plus ORDER BY allow-listing and the three layout traps: someone else's stylesheet may own your prefix, full height is a flex chain not arithmetic, and specificity beats intent.

    @edmozley edmozley committed Jul 19, 2026
    0260c62
  • Self-service: comprehensive, up-to-date wiki with file tables Self-Service-Portal.md had drifted badly — no mention of the palette or dark mode, the requester's company, replies, attachments both ways, the Help Centre, the request catalogue, privacy or reopen-on-reply. Rewritten as the section hub, keeping the screen-recording and SSO detail that was still accurate and restoring the help.php and avatar-menu sections. New pages: - Portal-Request-Catalogue — offering forms to customers, what happens to a submission, the version/visibility rules, a worked example, and the limitations (no per-company catalogue, no uploads, no approvals) - Self-Service-Developer-Guide — the rules and traps, every one of which actually bit: the requester/analyst id-space split, server-derived scope, knowledge's INVERTED tenant rule, published vs archived, not-found over forbidden, hiding-a-link-is-not-enforcement, and the fail-closed-on-identity / fail-open-on-visibility asymmetry. Plus the nowdoc trap that killed a page's JS for two releases, and why rendered markup is not proof the JS ran. Every page now carries a colour-keyed table of the PHP/JS files involved and what each does, per the pattern set in Multi-Tenancy-Developer-Guide §1a. The tables were built by enumerating the actual repo, not from memory — all 21 api/self-service endpoints, the 10 pages, the shared chrome, and the shared logic outside the portal folder. Verified: every wiki link in these five pages resolves, and every repo file named in them exists on disk. Cross-linked from Forms and Knowledge (the two modules whose content the portal now surfaces) and added to the sidebar.

    @edmozley edmozley committed Jul 19, 2026
    3ce7980
  • Portal Help Centre: new page Covers publishing articles (the Help Centre starts empty by design, since every article defaults to analysts-only), the three visibility levels and what is never shown, ticket deflection, and a worked example on a multi-company system.

    @edmozley edmozley committed Jul 18, 2026
    ca14e04
  • Portal privacy: new page explaining the three options with worked examples A ticket carries correspondence that isn't the requester's — forwards to suppliers, copied-in colleagues, and their replies. The new Tickets -> Settings -> Privacy tab decides how much of that the self-service portal shows, and the rule has edge cases (chat channels exempt, ambiguity resolves to visible) that are hard to hold in your head, so this walks through six worked examples. Cross-linked from Self-Service-Portal, Tickets and the sidebar.

    @edmozley edmozley committed Jul 18, 2026
    d19930f
  • Database Verification: document the column drift guard (#884) Rewrites the section that said columns were unguarded — accurate when written yesterday, made false by building the guard. Now covers why columns are the harder case (both files hand-maintained, so drift runs in two directions and each direction breaks a DIFFERENT install), the #879 bug that prompted it, how the check works, and where $schema now lives. Emphasises that the comparison is deliberately forgiving — base type, length and nullability only, ignoring DEFAULT/case/whitespace — because a guard that cries wolf gets ignored. The noise-resistance test is called out as load-bearing rather than incidental. Records what is STILL unguarded: foreign keys, built from explicit FK groups with nothing comparing them to freeitsm.sql. Flagged as the next self-check. Also notes you can run the whole of Database Verification headlessly against a scratch database ($_SESSION['setup_access'], DB_NAME override) and that php -l proves nothing here, since a fatal in this endpoint is served as HTTP 200. Multi-Tenancy dev guide + CMDB case study: file maps updated for $schema's move to includes/db_verify_schema.php, and the "columns have no guard yet" line in the case study corrected — written an hour before the guard existed.

    @edmozley edmozley committed Jul 18, 2026
    7342983
  • Two new multi-tenancy dev pages, and make file involvement explicit Ed's feedback: the CMDB/Network Mapper work was substantial enough to deserve proper documentation, and dev guides should make it much clearer WHICH php/js files are involved. Fair - the guide named files in prose but had no scannable map, so it couldn't actually be worked from. NEW Multi-Tenancy-CMDB-Case-Study: the worked example for a module whose rows LINK to each other. Why a graph differs from a list (three link mechanisms, all separate holes); the no-shared-CIs decision and what it forced; the core lesson that a same-company invariant must bind ALL-ACCESS actors because a scope check cannot help someone who legitimately reaches both sides; a full colour-keyed file map; the Network Mapper boundary and why get_diagram.php is deliberately unfiltered; what stayed shared and why; the move-between-companies gap. NEW Multi-Tenancy-Test-Harness: the reusable verification kit, roughly 20 lines from module-agnostic. Leads with ALWAYS INCLUDE A POSITIVE CONTROL, because breaking that rule produced a green suite that proved nothing. Also the three tricks that make CLI testing work (DB_NAME defined first, session established before the endpoint's own session_start, php:// wrapper override for the empty php://input), the static tenant-count cache forcing separate processes, the fixture shape, and what to assert. Developer guide gains section 1a "The files you will touch" - a colour-keyed table of the ~10 places every module needs, each with what you do and whether it is skippable. Plus the point that a correctly-scoped module needs NO JavaScript changes at all (CMDB changed zero lines), and if you are editing JS to hide rows you are filtering in the wrong layer. The Reference section is now a "copy from these" table pointing at shipped examples per shape. Also fixed a pre-existing broken link (db-verify) and pointed two #882 references away from the user-facing Pitfalls page.

    @edmozley edmozley committed Jul 18, 2026
    ce20284
  • CMDB is now multi-company; record the invariant and two testing lessons Multi-Tenancy-Progress: CMDB marked done, with the two deferrals written down - moving a CI between companies (and the shape it would take), and the deliberate Network Mapper boundary where diagrams stay unfiltered. CMDB page: a Multi-company section - one CI, one company, no shared CIs, what that implies for parent/relationships/object_ref, what stays install-wide (classes, properties, relationship types, icons) and why, and the move-to- company gap. Developer guide: - new special case for a graph of inter-linking rows. The headline is that a same-company invariant must bind an ALL-ACCESS actor too - a scope check can't help when the actor legitimately reaches both sides - and that a link leaks even when the far row is unreadable. - ALWAYS INCLUDE A POSITIVE CONTROL. "Was it blocked?" assertions pass just as happily when the request was broken as when it was refused. php://input is empty under the CLI SAPI, so a whole write suite failed validation before reaching any tenancy check and every isolation assertion went green - nine checks proving nothing. The tell was the positive controls failing too. - drive the real endpoint rather than a copy of its SQL, and why: parameter ORDER (SELECT-subquery and JOIN...ON fragments bind before WHERE) is the bug class a reimplementation can never catch.

    @edmozley edmozley committed Jul 18, 2026
    82bf4d0
  • Document the connection shape, the NULL-semantics trap and fresh-install drift Multi-Tenancy-Developer-Guide: - section 1 now documents THREE table shapes, not two. Connections (mailboxes, messaging channels, web chat widgets) are a distinct shape where NULL means shared intake, not Default-owned, and the list is deliberately install-wide. Flags that using activeTenantFilter here would hide shared-intake channels and break per-sender routing. - notes the credentials exception to "caps guard writes not reads". - adds analystCanAccessChannel / analystCanAssignTenant to the toolbox. - verify section: N=1 and N=2 MUST run in separate processes because tenantCount() memoises in a static; plus the scratch-database fixture recipe, which doubles as a fresh-install test. - checklist: name what NULL means; assert the two schema files agree; validate a client-supplied company against the actor's access rather than existence. Database-Verification-Developer-Guide: new section on the gap the index drift guard doesn't cover — columns. Uses the asset_locations.tenant_id bug as the worked example and explains why it survived review (development only ever exercises the upgrade path, so fresh-install drift is invisible until someone downloads it). Suggests extending the self-check to columns. Multi-Tenancy-Progress: Knowledge and messaging/web chat marked done, with the open tenant_channel_senders UI gap and the webchat routing asymmetry recorded.

    @edmozley edmozley committed Jul 18, 2026
    73214be
  • DB Verify guide: document the index-list drift guard Add the "you can't forget to regenerate" section: dbVerifyIndexListSelfCheck() re-parses freeitsm.sql on every verify run and fails loudly when the generated list has drifted, with the same parser generating and auditing the list. This is the real answer to "how do we avoid this drift again" — enforcement, not discipline.

    @edmozley edmozley committed Jul 17, 2026
    7304314
  • Add Database Verification developer guide How the DB Verify page is built and maintained: the mutating Verify flow vs the read-only Describe flow, how tables are filed under modules (derived from name, how to handle multi-module tables), the index backfill and the duplicate-blocks-a-unique behaviour, and why we don't check against an exported "golden" database. Complements the existing Database Integrity page (FKs, delete rules, orphans).

    @edmozley edmozley committed Jul 17, 2026
    40b1959
  • Knowledge + Web chat: document article visibility (company + audience) Web-Chat.md was actively WRONG in three places — it carried a caveat saying the AI "draws from all published articles regardless of the widget's company" and listed per-company scoping as a future item. That shipped; the page hadn't. Knowledge.md gains a "Who can see an article" section (the two editor settings in plain terms, and the question this will generate: why the chat widget goes quiet on upgrade — every article defaults to Analysts only, deliberately), plus a developer note on the trap: tenant_id IS NULL means "shared with EVERY company" here, the OPPOSITE of tickets/assets, so activeTenantFilter() and apiKeyTenantFilter() must never be used for Knowledge — both would silently hide every shared article, with no error and green tests.

    @edmozley edmozley committed Jul 16, 2026
    feddc0f
  • Add "Setting up OpenLDAP & Samba AD in Docker" walkthrough A copy-paste guide from nothing to a working LDAP login, mirroring the Keycloak (Docker) page for SSO: run both directories, seed Samba AD with a realistic company (nested OUs, a no-email account, a disabled leaver, nested groups), grant the OpenLDAP ACL that everyone trips over, configure the provider, and test the group gate with four correct passwords that produce four different outcomes. Linked from the sidebar and cross-linked from both LDAP pages.

    @edmozley edmozley committed Jul 16, 2026
    5efc1be
  • Add LDAP & Active Directory pages (feature + developer guide) Covers the LDAP sign-in shipped for issue #47: the admin-facing guide (setup, group-based access, troubleshooting) and the developer deep-dive (architecture, schema, the two-step bind, the traps, the Docker test rigs). Both linked from the sidebar under Single Sign-On.

    @edmozley edmozley committed Jul 16, 2026
    9180ff2
  • Web chat dev guide: document the poller ↔ optimistic-echo reconciliation The Delivery section named polling but not its hazards. Rewrite it to explain the two independent bubble-writers (poller + optimistic echo), the lastId cursor, the send.php msg_id contract, and the sendInFlight / pollInFlight guards — the exact duplication traps hit during the part-3 build — plus the typing indicator. Add both to Gotchas along with the webchatAddMessage / lastInsertId ordering trap.

    @edmozley edmozley committed Jul 15, 2026
    059e84a
  • Web chat part 3: mark AI answers, office hours + escalation as live Runtime is wired (send/poll/config/escalate/widget.js). Update both the feature page and the developer guide: office hours, AI (assist/deflect) and escalation now live; only offline-email *delivery* remains. Rewrote the Developer Guide resume plan down to that single piece and noted the source_email_id / Database Verify gotcha.

    @edmozley edmozley committed Jul 15, 2026
    8ca45d3
  • Add Web chat channel wiki: feature page + developer guide Covers the embeddable website chat widget: end-to-end setup + embed steps, every setting, the public-key vs per-conversation-token security model, origin allowlist (+Referer fallback), rate limiting, the email direction (sends from the company mailbox, never spoofs the visitor), multi-company, the channel reuse (FreeitsmProvider + ingest), data model, public endpoints, the AI-answers/office-hours/email-when-away design, a roadmap of what's still to build, and a developer resume plan. No secrets/keys included.

    @edmozley edmozley committed Jul 15, 2026
    74648b8
  • Multi-Tenancy: locations are purely per-company (correct Settings/Progress/Dev Guide)

    @edmozley edmozley committed Jul 15, 2026
    7b423f9
  • Multi-Tenancy: add Developer Guide (make a module company-aware); assets in Progress + Settings

    @edmozley edmozley committed Jul 15, 2026
    4324fd2
  • Add Mobile: Techniques & Tricks page (CSS/JS deep-dive); cross-link + refresh overview

    @edmozley edmozley committed Jul 15, 2026
    c331f6e
  • Mobile Tickets: sticky day-heading polish (#855)

    @edmozley edmozley committed Jul 14, 2026
    ae6da9d
  • Mobile Tickets: audit history now a full-screen sheet (#855)

    @edmozley edmozley committed Jul 14, 2026
    d6313a4
  • Mobile Tickets: audit history day-grouped feed (#855)

    @edmozley edmozley committed Jul 14, 2026
    7c81c5e
  • PHP-Enums-and-RBAC: describe the code that exists, not the code that used to Ed's catch, and a fair one — the page was comparing enums against a string-keyed design that no longer exists. It quoted four functions that have been deleted (rbacCapabilities, rbacAllCapabilityKeys, rbacCapabilityExists, rbacCapabilityLabel), invented a capability that was never shipped (tickets.manage_settings), and presented all of it under "what we built last week". A wiki page should describe the actual code. Rewritten against the codebase as it stands: Cap:: constants, and a registry DERIVED from each module's settings manifest. Every symbol and every number on the page verified against the source — 76 capabilities, 76 constants, and the guard's parameter really is still `string`. The rewrite also forced an honest walk-back the old page never made. Its central claim was that enums would collapse four hand-maintained lists (constants, registry, tab layout, setting-key map) onto one type. THOSE LISTS WERE ALREADY COLLAPSED — without enums — by deriving the registry from the manifest. So the headline benefit is banked, and the metadata arguably lives somewhere better: next to the tab it belongs to, rather than on the type. What genuinely remains is one narrow thing: a guard whose signature could refuse a string outright. requireCapabilityJson(string $capability) still accepts one; the constant makes the right thing easy and a typo loud, but it cannot make the wrong thing impossible. That is a real gap, and it is now the ONLY one — which is exactly why the floor stayed at 7.4. Also corrected two claims on the floor page that had aged with it: it still called the enum "the only correctness argument on offer" (it isn't — the constants answered that), and its phase-2 plan still listed the deleted helpers among the things the swap would remove.

    @edmozley edmozley committed Jul 14, 2026
    664d740
  • RBAC wiki: verified against the code, and two stale claims corrected Checked every claim the Roles pages make against what the code actually does — not by reading the wiki back to myself, but by generating the truth from capabilities.php / the manifests and comparing: 16 modules ✓ 76 capabilities ✓ 143 guarded endpoints ✓ every module has a <module>.manage umbrella ✓ the umbrella satisfies every capability in its module ✓ 9 personal-preference tabs declare no capability ✓ an undefined key grants nothing ✓ module 'system' resolves to is_admin ✓ the registry is derived from the manifests ✓ every per-module capability count and every 'sensitive' badge ✓ Two things were stale, both now fixed: 1. The setting-key list in settings_keys.php was described as "a residual list of modules not yet converted, which shrinks to nothing". It doesn't: every module IS converted, and what remains is the System module's keys — permanently, by design, because System is deliberately administrator-only. Corrected in the wiki and in the code comment. 2. The "guard writes, never reads" rule appeared WITHOUT its credentials exception on both Roles & Permissions and Security. Only the dev guide carried it. Since that exception exists precisely because the rule was applied too comfortably — get_mailboxes was a "read", and it shipped the plaintext OAuth secret to every analyst — stating the rule without it is the dangerous half. Also documented two manifest features that had gone in late and never been written up: a tab may carry a plain 'label' instead of 'label_key' where a module isn't translated, and renderSettingsTabBar() takes the tab-switcher's function name (Problem Management's is pmsTab(), and its panels are id="tab-x" not "x-tab"). A converter that assumes the conventions renders a tab bar whose tabs open nothing. Plus the full field reference for a manifest tab, and the table of shared endpoints that need per-key authorisation — a recurring shape, not a one-off.

    @edmozley edmozley committed Jul 14, 2026
    8276a93
  • RBAC roll-out complete: all 16 modules, and the rules it taught us Roles & Permissions: the roll-out table is now the finished picture — 16 modules, 76 capabilities, 143 guarded endpoints, with the sensitive ones marked. Records why the System module is deliberately NOT converted: it is ~20 areas, every one of them administration by definition, with no operational half to separate out — splitting it would create permissions only administrators would ever hold, which is what is_admin already means. Developer Guide — the six rules the roll-out taught, promoted above the mechanics because they matter more: 1. A read that returns CREDENTIALS is not an operational read. get_mailboxes was a "read", and it shipped the plaintext OAuth client secret to every analyst. 2. A config change offered as a shortcut from an operational screen still needs the config permission (Kanban columns; CMDB property options). 3. Creating is not always administering (create a tag inline: everyday; delete one: administration). 4. An endpoint serving several tabs cannot have one guard — authorise per setting key. A recurring shape, not a one-off. 5. A feature reachable from several modules needs requireAnyModuleAccessJson(). 6. Converting a module is an AUDIT of it. Expect to find, not merely to move. Two new traps, both real: - CRLF. A guard-insertion script searching for "}\n" found nothing in the CRLF files, strpos returned false, false+2 = 2, and the guard was spliced into the middle of "<?php" in ten files — which STILL PASSED php -l, because short open tags made "<?" valid. The HTTP test caught it, the linter did not. - The audit can hide a hole as well as find one. D005 reported db_verify — the endpoint that creates every table in the database — as "signature authenticated", because it scanned for the word relay_secret and db_verify declares a relay_secret COLUMN. Match the call, not the word. And check your write-detection covers what you think: the same tool called CREATE TABLE a read.

    @edmozley edmozley committed Jul 14, 2026
    2627102
  • Roles dev guide: the guard that fatals, and why your test says it passed Add requireCapabilityJson() but forget to require rbac.php and the endpoint doesn't refuse anyone — it dies with "Call to undefined function". That is worse than no guard: it looks protected and is broken for everyone, admins included. And PHP serves a fatal as HTTP 200, so a status-code check reports a cheerful PASS on a completely broken endpoint. That is exactly how it slipped through on Change Management (#840): one endpoint requires its includes with __DIR__ rather than a relative path, so the automated insertion missed it, and the test agreed. Documents both defences: D005 now catches it statically and reports it as BROKEN above everything else, and the testing section now says in terms — check the response BODY, never the status code alone.

    @edmozley edmozley committed Jul 13, 2026
    441f68b