Roles dev guide: say "two", not "one" — the page contradicted itself
It opened with "the manifest is the single declaration… there is no second list
to keep in step", and then two paragraphs later told you to write two things.
A developer reading that would reasonably ask which it is.
It is TWO, and that is now the first thing on the page:
1. the NAME — a Cap:: constant
2. the DETAILS — the module's manifest entry
…with a diagram of the five things generated from them, and — the part that was
missing — WHY it cannot be one. The name has to be a real constant, a constant
has to exist before the code runs, and generating one needs a build step this
project deliberately doesn't have. And the constant isn't ceremony: it IS the
safety property. Cap::ASSETS_VCENTER misspelled is an immediate fatal error;
'assets.vcentre' misspelled is a silent permanent 403 the administrator never
sees. So the second list is the one worth having, and two is the floor.
Also clarifies that the residual block in settings_keys.php is a TO-DO list of
unconverted modules, not a duplicate — it empties as the roll-out proceeds.
The moving-parts table is demoted below this and now marks each row as
hand-written or derived, so it reads as reference rather than as the lede.
c84b425
Wiki: split the PHP floor page, refresh Roles & Permissions, restructure the sidebar
The PHP floor page had grown to 861 lines and was three documents in a trench
coat: a decision, a language tour, and a deep argument about permission keys.
Split into three, each standing alone:
Raising-the-PHP-Floor (307) the DECISION — where we are, who breaks, the
verdict, and the constants-to-enums bridge
PHP-8-Language-Features (261) the 101 tour of 8.0/8.1 with before/after from
real code, including the features that WOULDN'T
help us, said plainly
PHP-Enums-and-RBAC (353) the one feature the argument turns on — and the
reason a capability is a Cap:: constant and never
a string. Worth reading even if the floor never
moves, because it explains the permission system.
Section numbers and cross-references rewritten by meaning rather than by
find-and-replace, since the sections changed pages.
Roles & Permissions was stale: written before the manifest became the single
declaration and before the endpoint audit existed. Now says that capabilities are
derived from one file per module (and why the four-list version was the bug), and
carries a table of what D005 has actually found — the shared settings endpoint,
six Intune endpoints, all 49 RFP Builder endpoints, unauthenticated audit-log
forgery, and an LMS learner reaching the authoring settings. Every one found by
hand, by accident, which is the point.
Sidebar: the security pages are now grouped by the three layers they belong to
(module access → capabilities → administrator), rather than a flat list, and all
nine RBAC/PHP pages are linked. The two new PHP pages nest under the floor page.
19e7234
PHP floor page: note that the registry is now derived from manifests
The code samples in section 3.1 described rbacCapabilities() as current. They
aren't any more — capabilities are Cap:: constants and the registry derives from
the settings manifests. Kept the samples (they show the string-keyed shape the
enum argument is about, which is precisely what the constants replaced) but
flagged them so nobody reads them as today's code.
9202652
RBAC: the manifest is now the single declaration, and document D005
Roles — Developer Guide: the recipe changed. The capability registry, the module
list and the setting-key map are all DERIVED from <module>/settings/manifest.php
now, so adding a capability is a constant plus a manifest entry — there is no
capRegistry() to hand-edit and no settings_keys.php row to promote. Calls out the
one seam derivation doesn't cross (a Cap:: constant no manifest claims, which is
a capability nobody can hold — so any guard using it 403s everyone but admins,
permanently and invisibly to the admin who bypasses the check).
Adds a section on D005, the endpoint audit, and the discipline behind it: type
safety catches a MISSPELLED permission, nothing catches one nobody wrote. Lists
the five holes that were each found by hand, by accident. And the rule that keeps
the tool useful — if you add an auth mechanism, teach it, because its first run
gave five criticals of which four were false alarms, and a scanner that cries
wolf is ignored within a week.
Security: adds the auditing section so the tool is discoverable from the security
page, not just the developer guide.
da7eb1c
RBAC: bring the wiki up to date with per-tab capabilities
Both Roles pages predated the per-tab decision and described a system that no
longer exists: rbacCapabilities() (now capRegistry), bare-string guards (now
Cap:: constants), "one manage per module, split later" (we went per-tab), and a
rename that orphans grants and needs a data migration (there's an alias map now).
Neither mentioned the settings manifest, the shared-endpoint key ownership, the
sensitive badge, the umbrella, or the security fix.
Roles & Permissions — rewritten around the per-tab model: why a tab is the unit
(vCenter credentials sit next to a lookup list), the umbrella, sensitive badging,
and a full "how it's enforced" section covering all three mechanisms — panels not
rendered rather than hidden, hard guards on every write endpoint, and per-KEY
authorisation on the shared settings endpoint (including the #829 hole it closed).
Plus what is deliberately NOT guarded, and why gating a read breaks the module.
Roles — Developer Guide — rewritten as the extension guide: why capabilities are
constants (a mistyped string 403s silently and is invisible to admins, who bypass
the check — so the typo becomes a privilege escalation), adding/renaming/removing
a capability, and the full six-step recipe for converting a module, worked against
Asset Management. Now carries the traps that actually bit: a get_* endpoint that
isn't a read (get_vcenter performs the sync and Servers calls it), endpoints with
no guard at all, and the reminder that converting a module is an audit of it.
Plus the three-actor HTTP test matrix and a checklist.
Security — the Authorisation section was wrong, not just stale: it claimed the
System module "cannot be disabled" and is always accessible to active analysts,
which stopped being true when is_admin shipped. Rewritten as the three layers,
each with its gate and its default, and the note that hiding a button is never
the control.
Raising the PHP floor — made the constants-to-enums bridge concrete rather than
asserted. Shows the actual migration: call sites are a symbol rename an IDE can
do and the compiler verifies (a missed one FATALS; you cannot half-finish it),
against hunting 200 string literals where a miss keeps working and a typo 403s
in silence. The real work is confined to one file. Also states the honest
caveat: constants make the typo loud, an enum makes the wrong thing impossible.
7d62666
Blue sky: raising the PHP floor to 8.1
The case for dropping PHP 7.4/8.0 to unlock enums for the RBAC capability
system, plus a 101 on what PHP 8.0/8.1 actually added, with before/after
examples from real FreeITSM code.
Parked, and the page says why: class constants close the dangerous failure
mode (a typo in a capability key silently 403-ing, invisibly to the admin)
without leaving 7.4. What the enum adds on top is elegance, not safety, and
that doesn't justify breaking every 7.4 user's upgrade path. Documents the
constant-to-enum bridge so the swap stays cheap, and the triggers for
revisiting.
Linked from the Blue Sky hub and the sidebar.
c3a6df3
LMS authoring + AI + Roles: new deep-dive pages, refreshed LMS hub
- LMS.md: rewritten for the two course kinds (authored + SCORM), the
learner/manager split, correct table names, links to the new sub-pages.
- LMS-Authoring.md: native content model, editor, native player, server-side
grading (answer key never on the wire), the CMI-reuse trick, learner/manager
enforcement, SCORM upload safety.
- LMS-AI-Authoring.md: mechanics of the three AI helpers (outline / article→lesson /
lesson→quiz) — grounding, JSON contracts, the draft-never-writes safety model.
- Roles-and-Permissions.md: RBAC Layer 2 (settings permissions) — two layers,
deny-by-default, is_admin bypass, server-side enforcement, the LMS pilot.
- Roles-Developer-Guide.md: add/edit/remove a capability and wire a module's
settings in — registry, guards, the operational-vs-settings split, splitting
a capability later, gotchas.
- _Sidebar.md + Home.md wired up.
6f759b8
Time-Based Triggers: its own page, a dev recipe, and Pitfalls #9
The #801 write-up was buried mid-page in Workflows, so it never appeared in the
sidebar tree — you could only find it if you already knew it was there.
New page: Time-Based-Triggers.md — the four triggers and their crons, the
fire-once ledger, the fingerprint, the four starter recipes, cron setup, and how
to test it by hand without waiting for a scheduler.
Plus what was missing everywhere: HOW TO ADD A NEW ONE. Register the trigger,
write the detector, choose the entity_key and fingerprint (that choice IS the
job — everything else is boilerplate), register with the cron, ship a recipe.
With the traps nobody would guess: changing the fingerprint formula re-arms every
in-window record AT ONCE, and renaming a trigger silently orphans live workflows
(workflows.trigger_event is a plain string nothing validates — the workflow stays
active, looks healthy, and never fires again).
Pitfalls #9: the fire-once ledger that burned its emission on an audience of
nobody. The cron runs from install; by the time you BUILD a renewal workflow the
ledger already says "already fired" for every contract in-window, so switching it
on leaves it permanently silent for exactly the contracts you built it for — and
nothing is observable. Found by asking what happens on a FRESH INSTALL, which the
happy path never exercises.
Workflows.md keeps a summary + link. Sidebar and See-also updated.
0d633c1
Workflows: 24 recipes, filterable gallery
a17b600
Workflows: list the four time-based recipes in Starter templates
19ba8c3
Workflows: mark the SLA/contract trigger roadmap item done
The perl in the previous commit didn't land — bash ate the backticks.
83eadab
Workflows: time-based triggers, and the problem of not firing twice
Every other trigger hangs off a write path. 'The SLA is about to breach' is not
an event — nothing happened, time passed — so a cron goes looking.
The section leads with the actual hard part: finding a breached SLA is easy; NOT
telling you about it 300 times is the feature. A time-based condition stays
true, so naive detection re-fires forever. Covers the UNIQUE-key ledger (the
database is the arbiter, so overlapping crons can't double-fire), the fingerprint
(raise a priority and the SLA target shrinks — the new deadline must be allowed
to escalate again, or the workflow goes quiet exactly when the ticket gets more
urgent), and why nothing is recorded when no workflow is listening (a real bug
caught in testing: it would have made activating a workflow appear to do nothing).
Also updates the three-cases summary — 'time passed' is no longer 'not supported'
— and marks the roadmap item done.
426ad9a
Workflows/Webhooks: answer 'what runs when, and what must I schedule?'
Ed asked whether making a ticket P1 fires the webhook by itself, or whether a
scheduled task has to go looking for it. The facts were all in the wiki but
scattered across two pages and written from the implementer's point of view —
neither page answered the reader's actual question.
Worse, the Webhooks 101 said a webhook means 'no polling, no delay', which is
true of DETECTION and wrong about DELIVERY (which sits in a queue until the cron
worker sends it). Read literally, it implies nothing needs scheduling — the
exact wrong conclusion, and the reason Ed's delivery worker had never run.
New section at the top of Workflows.md lays out all three cases plainly:
1. Something happened -> instant, event-driven, NOTHING to schedule.
2. A webhook needs sending -> the delivery worker MUST be scheduled, or
messages queue forever. Send test bypasses the queue, so it can pass while
every real webhook silently piles up unsent.
3. Time passed (SLA breach, contract expiry) -> not events at all; needs a
scheduler we haven't built. Also why no starter template offers SLA
escalation.
Webhooks 101 corrected with the same warning up front.
d2141a7
Workflows: the execution log + the Watchtower card
Leads with WHY they have to exist: the engine deliberately swallows its own
errors so a broken workflow can't break the ticket save that triggered it —
which means a failing workflow is otherwise completely silent.
Covers the log's filters (and that they live in the URL, which is what makes the
Watchtower deep-link possible), the drill-down (conditions with the ACTUAL value
compared, so a skipped run explains itself; actions; queued webhooks; the
trigger payload snapshot), and that runs survive their workflow's deletion.
The card: failed / aborted / dead-lettered webhooks over 24h — the last of which
matters because the workflow SUCCEEDED, it queued the send, so a message that
never arrived shows up nowhere else. Names names, shows the error, excludes dry
runs. Stage 6 items marked done.
52e94b3
New page: Webhook Message Formats
A chat format is nothing but a JSON body template with a {{message}} slot, so
they're data now, not a PHP switch — add any platform without a release.
Covers: the idea; the five fields; ready-to-paste templates for Google Chat,
Mattermost, Rocket.Chat, ntfy, Telegram, PagerDuty and a Discord rich embed
(showing static fields and merge codes inside a template); the escaping
guarantee (decode -> substitute into values -> json_encode, never splice raw
text, with the injection test shown); the engine's hardcoded fallback; why the
built-ins are locked and Copy exists; and why the URL pattern and formatting
hint exist at all (both are bugs Ed hit today).
Webhooks.md section trimmed to a summary pointing here. Sidebar updated.
f962a3d
Webhooks: message formats are data — add your own platform
A chat preset is just a JSON body template with a {{message}} slot, so they're
rows now, not a PHP switch. Covers the four fields, why built-ins are locked,
the escaping guarantee (decode -> substitute into values -> json_encode, never
splice raw text), and the hardcoded fallback.
a46b4c2
Workflows: {{ticket.number}} — quote the reference, not the row id
New section: {{ticket.id}} is the database row id (83); {{ticket.number}} is the
reference a requester quotes (ETC-623-64409). Includes the exception that
matters — a Ticket ID action ARG must stay {{ticket.id}} because the engine
looks the record up by row id. The number goes in the words; the id stays in
the plumbing.
670366f
Workflows/Pitfalls: readable _name merge codes + the lying Send-test preview (pitfall #8)
Workflows.md: new 'Use the name, not the id' section — {{ticket.priority_name}}
renders Critical, not 4; twins derived from the same registry as the condition
dropdowns; never clobbers a host module's value.
Pitfalls: #8 — the Send-test sample invented fields the real payload never
carries, so a preview rendered 'High' and production rendered ''. Bug #1 reached
from the opposite direction. Takeaway: generate previews from the same
definition the real thing uses, never a hand-written mock that can drift.
93d8a6b
Workflows/Webhooks: templates, dry run, merge codes, encryption, retention + 2 new pages
Workflows.md: starter templates (with the name->id resolver design), dry run,
and a rewritten merge-codes section explaining why the picker is scoped to the
trigger (renderTemplate resolves unknown paths to an EMPTY STRING, so an
unavailable code fails silently). Refreshed "what's still ahead".
Webhooks.md: new "Data protection" section — URL + signing secret encrypted at
rest, URL redacted in the delivery log, the VARCHAR(2000) width requirement,
why the secret is NOT masked in the editor, and payload retention (default 7d)
vs row retention (30d) with the Replay trade-off stated. Plus a TLS
troubleshooting pointer.
New: HTTPS-Certificates-and-CA-Bundles.md — what certificate verification is,
what a CA bundle is in plain English, why Windows hits this, the fix (both
php.ini files), how to verify, and why "just disable SSL verification" is the
wrong answer.
New: Workflow-and-Webhook-Pitfalls.md — seven real bugs, their causes and
fixes. Five were INVISIBLE in production (nothing crashed, no test would have
failed). Meta-lesson: build the thing that makes the class of bug impossible
and it hands you the instances for free.
Sidebar: both new pages nested under Workflows.
360ddf8
Add Module Access developer guide (wiring new modules/pages/endpoints into Phase 3 enforcement); link from page + sidebar
d3e088d
Module Access Control: document the in-place Access-level toggle; fix team default (0) + drop unbuilt save-time warning claim
e08db74
Module Access Control: mark phases 2 & 3 shipped; document read-gating follow-up
6c3f85b
Module Access Control: add implementation-phases status table
d56d5ee
Module Access Control: correct team default (0, not all)
Teams default to granting NO modules (matching team company-access), not
all-access — under the default 'most' union mode a team defaulting to all would
silently hand every member every module. Clarified the strict-mode lock-out
case and the safe-upgrade wording accordingly.
54612b2
Add Module Access Control page (issue #30)
Documents the team-based module access design: mirrors company access
(individual + team grants + all-modules flag, one choke-point resolver), the
most/least-permissive policy with the strict-mode foot-gun called out,
server-side per-module enforcement, the summary/edit UI, the effective-access
tool, and the safe-upgrade grandfather. Linked under Security.
68294b9
Add Admin Access Control technical deep-dive (issue #34 / #781)
Developer-facing page on how the System module is restricted to admins: the
is_admin flag, the three helpers (analystIsAdmin / sessionIsAdmin self-heal /
requireAdminJson), the two enforcement layers (page gate + 41-endpoint API
gate), the deliberate scoping (what's NOT guarded and why, incl. db_verify
pre-login and the teams-vs-departments boundary), the one-time grandfather
back-fill, last-admin protection, and a contributor checklist. Linked under
Security in the sidebar.
e32fdeb
Add CSP hardening blue-sky page (issue #43)
Documents what strict-CSP support would take and why it's a large phased
refactor rather than a bug fix: ~1,300 inline handlers + 159 inline script
blocks, plus a measured risk assessment (defence-in-depth behind existing
output escaping; low-to-moderate urgency for a self-hosted internal app). Also
captures the working 'unsafe-inline' policy for today. Linked under Blue sky
thinking and cross-referenced from Email Rendering & Images.
fd91472
Add "Blue sky thinking" section for parked ideas
New landing page explaining it's a holding area for explored-but-uncommitted
ideas (risk / complexity-vs-reward / uncertain demand), with Remote Image
Archiving moved under it. Added a sidebar section and back-links.
7d3b7d9
Add Remote Image Archiving (parked idea) page
Records the idea of downloading remote https:// email images at import for
privacy/archival/offline, and — prominently — the SSRF danger that keeps it
parked (may never ship). Cross-linked from the Email Rendering & Images page.
8700007
Add Email Rendering & Images technical deep-dive page
New developer-facing page covering how inbound email is rendered: inline (cid)
vs remote (https) images, the get_attachment.php serving pipeline, the BASE_URL
sub-path 404 bug (#777), and the two CSS-isolation fixes — the scoped
box-sizing content-box reset (#778) and full Shadow DOM isolation (#779),
including why iframe was rejected. Linked from the sidebar under Tickets.
1df708e