Search: ⌘K content search and knowledge articles (#1072-#1073)
Discussion #53 is now down to attachments.
Full-Text-Search: status table updated, and §5.1 gains a note on where
the three original questions actually stand. Q1 done, Q3 substantially
done, Q2 (attachments) the only one outstanding - plus the fourth
question from the thread, whether search would reach Ctrl+K, answered
yes. The original assessment table is kept, because its "honest answer
today" column is what the discussion was replied to with.
Dev guide gains §9a on why articles are indexed by a DIRECT CALL from
KnowledgeService while tickets go through the dispatch seam. Tickets
needed the seam because three paths create them and share no code;
articles have exactly one writer. And the article events are the wrong
hook anyway - a new DRAFT fires nothing at all, because
knowledge.published is deliberately withheld so workflows do not
announce a page nobody can open, yet a draft still needs indexing.
Records the two traps that had been designed for and never exercised:
NULL tenant_id on an article means shared with EVERY company, the
opposite of a ticket (which is why searchCorpusArticleScope exists and
had never been called), and audience maps onto is_internal failing
CLOSED so a future portal search cannot leak by default.
Sections renumbered so 9/9a/9b/9c read in file order.
150e63b
Search: the index keeps itself current (#1070-#1071)
All three search pages said indexing was manual. It is not any more.
Searching-Inside-Tickets: the backfill is now genuinely one-off, and the
warning that new tickets stay invisible until you re-run it is replaced
by what actually happens. Adds the three cases where you WOULD run it
again - after correcting the full-text settings, on suspected drift, and
after restoring an old backup - because "never" is not true either.
"Attachment text" replaces it as the honest what-it-does-not-find entry.
Full-Text-Search: status block updated, and §3.5's open question is
marked answered. It asked whether to index history or only from
switch-on; the answer is both, and the two share one document builder.
Command-palette content search moves onto the not-built list, since the
discussion asked for it and it is genuinely still missing.
Search-Corpus-Developer-Guide gains §9 on the subscriber - why it
reindexes the WHOLE ticket rather than the changed row (ordering-immune,
self-healing, cheap), why the dispatch must come AFTER the commit, and
why the document construction moved out of the backfill: if the two
drifted, a result would depend on how a ticket happened to get indexed.
And §9a on the trap that will catch the next subscriber too:
ticket.created fired from ONE place, the analyst path, so it missed the
two channels most tickets arrive through. The mailbox file documents the
mistaken belief in a comment.
766e319
Inventory agent: make the API key location impossible to miss
Several people have been caught out by where the key comes from, and
there were two separate reasons for it.
FreeITSM has TWO unrelated things both called "API keys":
Software > Settings > API Keys 40 hex chars, no prefix, table
`apikeys` - the inventory agent
System > API starts fitsm_, table `api_keys` -
the REST API v1, Bearer auth
A key from the wrong page fails with "Invalid authorization key", which
reads like an expired or mistyped key rather than the wrong KIND of key,
so nobody thinks to go looking for a second keys page.
Inventory-Agent now leads that section with a callout, a table
contrasting the two, and the tell: if it starts fitsm_ it came from the
wrong page. Also explains why an asset script's key lives under
Software, so the location stops feeling arbitrary - the same endpoint
feeds the software inventory.
Troubleshooting split "Authorization key missing" from "Invalid
authorization key", which are different faults, and added the
"Unknown endpoint" row from the user who put /api/v1 in his -ApiUrl.
Software.md named the table `api_keys`. It is `apikeys`. That is
precisely the confusion this commit is about, so it is corrected with a
note rather than quietly.
a2f1ac9
Update for the Links-strip move (#1068)
Equipment and CMDB objects are pills in the Links bar now, not sections
below the thread. Both pages updated to match.
The dev guide gains section 9 on why: the strip already carried a
comment, written for the Jira work, saying a link belongs in the strip
rather than a panel of its own. Equipment qualifies by that definition
and the original card was the deviation.
Also records what the space problem actually was - a section cost a
bordered card whether or not it had contents, so an empty ticket paid
six times to say "nothing here yet" six times - and why the two are
still shown distinctly rather than merged, since a broken mouse will
never be a configuration item.
Plus the trap: the picker host must be a CHILD of .problem-strip,
because mobile.js relocates the strip into a phone sheet by selector
and a sibling would be left behind where the user cannot see it.
Notes the two loose ends left behind (dead .cmdb-* CSS, and a dead
SECTIONS entry in mobile.js) so neither looks intentional later.
23a12a0
Add "Linking equipment — Developer Guide" (discussion #57)
Completes the pair. The user page said what it does; this says why the
code looks the way it does.
Sections that exist because someone will otherwise undo them:
- Why ticket_assets has TWO creator columns where ticket_cmdb_objects
has one - analyst 7 and user 7 are different people.
- The three gates on the write path, and specifically that gate 3
(same-company) is the one that matters for the exact role which can
satisfy gates 1 and 2 together. It is also the one easiest to talk
yourself out of.
- Why the portal re-checks ownership when the dropdown is already
scoped, cross-referenced to the get_users.php leak in #54: a scoped
list is not a check.
- Why the location clause in the picker's WHERE is load-bearing rather
than a nicety, and that an empty result may be missing location data
rather than a broken query.
- The MERGE_MOVE_DEDUPE trap that produced #1064, written up as the
general rule: a wrong column name in a table-driven loop is a silent
no-op, not a mismatch.
Plus the forged-session recipe (no trailing newline, or PHP reports
what looks like an auth failure) and the control set, including that
the CASCADE has to be proven functionally because db_verify creates
FKs inside an empty catch.
Records that the neighbouring .cmdb-* CSS has no dark-mode handling at
all - left alone rather than changed as a side effect, but real.
Every file named in the table verified to exist; every wiki link
resolves.
cd73455
Add "Linking equipment to tickets" (discussion #57)
Covers both halves: attaching an asset to a ticket, and the Tickets tab
this creates on the asset.
Spells out three things a reader would otherwise have to discover:
- The picker searches LOCATION as well as hostname/model/serial/tag, and
why that matters. Nobody knows a meeting-room TV's hostname; they know
where it is. Includes the warning that this depends on assets actually
having locations set, which is missing data rather than a broken
search if it comes back empty.
- Requesters can only ever pick their OWN equipment, and shared kit is
therefore deliberately absent from the portal. Documented as a
decision with the two rejected alternatives and why each was worse,
rather than left looking like a gap.
- What happens to links on merge, delete and re-link, and that a ticket
and an asset must share a company.
Also flags that CMDB objects and assets are linked to tickets
separately and are not the same thing - the two sections sit next to
each other in the reading pane and will be confused otherwise.
Cross-linked from Assets, Tickets and the sidebar.
299fa2c
Add "The inventory agent" page, and separate the two certificate problems
The PowerShell inventory agent had no page of its own - only eight lines
under Assets covering the two endpoints and the API key. Nothing on
deployment, scheduled tasks, or certificates, which is where a user
reporting a failure this week actually got stuck.
Inventory-Agent.md covers running it by hand, the collect-only mode,
rolling it out as a GPO scheduled task, what it collects, where it goes,
and a troubleshooting table.
The certificate section is the bulk of it, because that is the common
failure. Notes that the fixes are ordered by safety, that pinning
deliberately ignores the name on the certificate (so XAMPP's default
localhost certificate stops being a problem), that a thumbprint is not a
secret, and that renewing a certificate silently breaks every pinned
machine.
Also flags on HTTPS-Certificates-and-CA-Bundles that it is the OUTBOUND
problem - FreeITSM not trusting Slack - and links across. A user
searching the wiki for "certificate" was landing there and reading advice
that cannot fix an inbound trust failure.
Thumbprint lookup is given per web server. The Cert:\LocalMachine\My
route is IIS-only; on Apache, XAMPP, WAMP or nginx the certificate is a
file and that store is empty.
Links to Inventory-Agent added from Assets, Software and the sidebar.
de07da0
Asset handover: new wiki pair for discussion #56
New pages (app commits 605811aa + 2aed483e):
- Asset-Handover (user)
- Asset-Handover-Developer-Guide
The dev guide records three decisions worth keeping:
1. The designer is BLOCKS, not a canvas, because the middle of the document
is a repeating region -- one row per asset, count unknown until render.
A WYSIWYG either cannot express that or makes the admin write a loop.
2. Escape FIRST, substitute SECOND. Merge values are user-supplied
(users.display_name arrives from the portal, from mail, from imports) and
this document is EMAILED, so getting the order backwards would put stored
markup into a mail client. Includes the test that proves it.
3. One renderer serves the printed page, the preview and the email. A preview
that is a separate approximation is a preview nobody can trust, and the
divergence surfaces on a document somebody has already signed.
Also records the users_assets stale-row trap (no foreign key; an INNER JOIN is
deliberate), why printing beats generating a PDF, and four honest gaps: no logo
in the emailed copy, one default template for the whole install, no record that
a handover happened, and 23 untranslated locales.
Every named file verified to exist; every link verified to resolve; the
factual claims (logo_path null, the 2000-char cap, the INNER JOIN) checked
against the source rather than recalled.
ec6bf40
Notifications: new wiki pair for discussion #55
New pages (app commits bbd49c50 + 75803509):
- Notifications (user)
- Notifications-Developer-Guide
The user page leads with what the bell does NOT do, because the four noise
rules are the feature -- a bell that fills faster than it can be read is dead
while still appearing to work.
The dev guide records two things worth keeping:
1. The bell is a SUBSCRIBER to WorkflowEngine::dispatch(), not a second
instrumentation layer. ~32 event types already fired from 48 call sites,
so adding a notification type must never mean editing call sites.
2. EVERY failure mode in this feature is silent. Suppressed, rejected,
thrown-and-swallowed and correctly-quiet all look like an empty table, so
every rule needs a POSITIVE control and not just a negative one. The four
bugs found during the build are written up for the same reason -- three of
them presented as "notifications just never arrive".
Also records three pre-existing broken ticket deep links (tickets/?id= in
escalate_ticket.php and engine.php, tickets/?ticket= in forms/approvals.php).
inbox.js reads only ?ticket_id=, so those return HTTP 200 and never open the
ticket. Verified present, not fixed.
Every named file verified to exist; every link verified to resolve.
3a593d7
Landing page: new wiki pair for discussion #63
New pages (app commit f1673644):
- Landing-Page (user)
- Landing-Page-Developer-Guide
The dev guide leads with the constraint that shaped the whole design: the
decision happens before authentication and before anything renders, so
user_preferences cannot hold it on its own (analyst_id is NOT NULL) and
localStorage cannot hold it at all (PHP cannot see it). The cookie is a cache
of the preference, not the setting.
Also records, for whoever touches this next:
- the stored value is a KEY and never a path, with the attack tests that
prove it, because this drives a redirect on the most-visited URL
- an endpoint writing several rows without a transaction must finish
validating before it starts writing (save_branding blanked the header
and footer on a rejected request)
- when a server-side upload rule is tightened, grep for the client-side
restatements of it (the accept attribute and the help text both still
recommended SVG after the server stopped taking it)
Cross-linked from Self-Service-Portal, Home and the sidebar. Every named file
verified to exist; every link verified to resolve.
d44d155
Morning check groups and routing: new wiki pair, plus corrections to the module page
New pages for discussion #64 (app commit 262d7af8):
- Morning-Check-Groups-and-Routing (user)
- Morning-Check-Groups-and-Routing-Developer-Guide
Both lead with the rule the design hangs on: routing is GUIDANCE, never
PERMISSION. No authorisation check anywhere consults AssignedAnalystID --
if it did, the round would stop on exactly the morning it matters most.
Corrections to Morning-Checks.md, which had gone stale:
- "Raise Ticket from a Check" said the button appears only on statuses
whose Requires notes flag is true. It is now on every check.
- Settings listed three tabs; there are four.
- Key Tables was missing morningChecks_Groups and _ResultLinks, and the
CreatedBy / ModifiedBy distinction.
- Added the Undo section.
Every file named in the dev guide verified to exist; every wiki link
verified to resolve.
9ad70c4
Document service uptime and history, as a pair
Plain-language page for whoever runs the status board, and a developer guide
covering the derivation, the phase-2 update log and the arithmetic.
The worked example Ed asked for is the centrepiece of the plain-language page:
a three-day power failure taking out four services, brought back at four
different times, with what you actually do at each point and what each service
then reports. Those figures are not illustrative — the scenario was built
through the real save endpoint as the test fixture, so the table is what the
code produced.
It also states the thing the feature depends on and which no amount of UI can
convey on its own: the history is only as good as the way incidents are
updated. Hence the contrast at the end between five saves giving four accurate
service histories, and one save at each end giving one blunt figure that is
wrong for three of the four.
The developer guide records the two compatibility rules that are easy to get
wrong later — returning null rather than an empty array for a pre-log incident,
and omitting the EXISTS clause entirely when the tables are absent — plus the
bug the worked example caught, where a restored service disappeared from its
own history because the query still keyed off the current links.
Linked from Service-Status and the sidebar.
85ee16b
Document the requester picker, as a pair
Plain-language page for someone raising a ticket on a colleague's behalf, and a
developer guide covering the request flow, why authorisation happens twice, and
the three bugs the feature fixed on the way through — the unscoped user list,
the name box that silently did nothing, and the requester who was created with
no company at all.
Two things recorded because they are the sort that get rediscovered the hard
way: appending a tenancy clause to an unbracketed OR list binds it to the last
branch only, and the `hidden` attribute loses to any author rule that sets
`display`, which rendered an empty chip with entirely correct markup.
Also corrects a stale line in Tickets.md. It described the reply modal's To/Cc
row as "matching the New Ticket modal's requester name / email row" — a row that
no longer exists. Rather than deleting the sentence, it now says what changed
and points at the new page, because the comparison is the sort of thing someone
will go looking for.
Linked from Tickets.md and the sidebar, beside the other ticket sub-pages.
0941c2a
Bring the response document onto the wiki with the rest
It was the only security write-up still living in the repository, at
docs/security-review-2026-08.md — which is why the link Ed sent the reporter had
/blob/ in it. There was never a round-one wiki page; the wiki's whole history
only ever held the round-two and round-three pages.
Two things in it had rotted since it was sent, and both are now fixed here and
flagged in place rather than silently corrected:
- it linked to security/findings-2026-08, a branch since merged and deleted
- all eight commit ids in its table were pre-rebase and are orphaned; the
table now carries the ids that exist on main, with a note that F5 sits out
of sequence because it reached main as a cherry-pick
The page opens by saying what it is — a record of what was said at the time,
not a live document — and points at the round pages for the current position,
because its own Outstanding section is now historical and mostly closed.
Linked from Security.md, the round-two developer guide and the sidebar.
439fa76
Correct the counts the added finding invalidated
Ten findings, not nine; 185 checks, not 183; Outstanding is §10, not §8. Both
openings now say why the tenth was nearly missed rather than quietly absorbing
it into the total, since that is the part worth remembering.
abbce5d
Correct the outstanding count, and add the guard that was missed
Ed asked whether the Security page's summary — "two significant items remain
open" — was accurate. It was not. Checking it turned up a third: a fail-open
access guard in the task service, the last of the round-two list's "S3's
cousins", now fixed in 980b0939.
Both round-three pages gain the finding, the plain-English one as item 8 and the
developer guide as 6b with the lock-wait reproduction. Security.md now lists the
two genuinely open items separately from the two unscheduled features, rather
than rolling everything into one sentence ending "and the rest" — which was the
phrasing that let a miscount hide.
de2479d
Document round three of the security work
Two new pages for the nine findings deferred when round two merged, following
the same pair as before: plain English for an operator, and a developer guide
that gives each finding its risk, a reproducible proof of concept, the
mitigation and why that mitigation actually closes it.
The existing round-two pages now say what has been superseded and what has not,
rather than leaving a reader to work out that their outstanding sections are
partly historical. Security.md carries both new links, and the sidebar lists
all four.
Care taken to keep the same framing Erlend asked for: the outstanding sections
lead, they name CSRF and lms/content as open in main today, and the round-three
guide records where a mitigation is conditional rather than absolute — the CSP
layer on the branding directory is skipped entirely without mod_headers, which
was measured on a stock WAMP rather than assumed.
98f9ef9
Mark the security work merged, and keep the open findings visible
Erlend signed the branch off on 2026-08-12 and it has fast-forwarded into
main at 86f8d257, so the "awaiting sign-off" and "not yet merged" banners on
both hardening pages were wrong.
His one substantive request was about framing rather than code: the deferred
items must not read as closed. S2 is four live cross-tenant bugs reachable in
main today, not a hardening backlog. Both pages now say that plainly, and the
Security page — which described none of this and was the page an evaluator
would actually land on — now carries the disclosure history and points at the
outstanding list.
Also corrects the default-admin bullet on Security: the published password is
still admin/freeitsm, but since S5 the account is seeded with
must_change_password so it cannot survive first sign-in, and Database
Verification flags older installs still using it.
9a1bee3
Security hardening 2026-08: add a troubleshooting section
The page explained what changed and why, which is not the same as being able to fix a
regression it caused. Five of these changes can now REFUSE something that previously
worked, and in every case the symptom names the wrong culprit -- S3 in particular turns
a database fault into what looks like a permissions problem.
Adds: a symptom-to-cause table for the five tightening changes, the exact log strings to
grep (all verified present in the code), the one genuinely new failure mode -- a secret
that will not decrypt now reads as EMPTY rather than as visible ciphertext, so "the token
is blank" means a key mismatch and not an unset setting -- and per-change revert notes.
Two of them need care rather than a plain revert: reverting S5 does not clear a
must_change_password already set, and reverting R3 restarts the media quarantining that
was the whole complaint. S3 should not be reverted at all; if it is denying, the database
fault it reports is the thing to fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
f700c75
Security hardening 2026-08: a user page and a developer guide
The second round of the August security work -- the nine items Erlend Volden raised
when he re-reviewed our fixes to his original report, plus four bugs in our own
first-round code.
Two pages, deliberately, because they have different readers:
Security-Hardening-2026-08 answers four questions per item in ordinary language --
what was wrong, how it could have been used against you, what changed, and why that
fixes it. Each item is labelled as a security flaw or as a feature we broke while
fixing one, since both happened and the difference matters.
Security-Hardening-2026-08-Developer-Guide is the same nine items with the code:
file table, the before/after shapes, why the isMultiTenant() short-circuit in S1 is
load-bearing, and what is deliberately still outstanding.
Both carry a "not yet merged" notice, because the branch is still awaiting the
reporter's sign-off and neither page describes current main.
Every file path and wiki link in both pages was checked to resolve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1109ad7
i18n Audit developer guide, including the script
53e98c9
War Room: split into a user guide and a developer guide
f16b9c7
War Room: Warbot, and why its hands and brain are separate
bd46fe5
War Room: why being a fallback is not a licence to make it thin
4983cd8
War Room: why a channel is a team, and why it depends on nothing
39b8ecf
Techniques: three notes from the settings tidy-ups (#1006)
- When you "normalise" a layout, state BOTH insets in one place. Leaving
the modal header to LAYER 3's plain rule aligned three pages of four:
inbox.css:2549 carries .modal-content:has(> .modal-body) >
.modal-header at (0,3,0), which beats it, and only one modal has a
.modal-body. Reads as a one-page quirk until you find the selector.
- Measure where the TEXT starts, not where the box starts. Comparing a
full-width header <div> to an inset <label> by rect.left measures the
padding, not the misalignment — it reported a 14px error on four pages
that were mostly fine.
- A shared class means a one-page fix is a five-page fix. .tab-content
is styled once in inbox.css at padding:30px — a sixth of a 360px
screen — so trimming it for one module improved all five.
d89cacb
Mobile: tickets settings (#1005) + five more techniques
Mobile: Tickets gains a settings section; overview status/table/layer
count updated.
Five things promoted into Techniques:
- Read a page's own CSS comments before opting it in. tickets/settings
states outright that <body> must not be flex (extension-injected nodes
became flex items and wrecked the layout) and builds its own shell.
LAYER 2 would have re-created that bug. No harness catches it -- a
headless run has no extensions.
- A page that renders one tab at a time is 1/N verified. Measuring it as
loaded reported clean; walking all 18 tabs found the real problems.
- Exclude what must not change, don't enumerate what must. The anti-zoom
rule listed six input types and missed a datetime-local.
- Stacking a grid to one column does nothing if its children span two --
the browser creates an implicit second column and the rule looks like
it failed. The tell is unequal tracks.
- Ask the question measurement can answer. "Contained but crushed" kept
slipping past overflow checks, so assert the actual property: nothing
may still be laid out in more than one column at phone width.
22f3d07
Mobile: Service Status round 2 (#1004) + two more techniques
Module page gains a Round 2 section (two screens, tap-anywhere cards,
pill alignment, the sticky action bar, the confirm) and a note that the
settings card feed from #1000 is gone. Calendar page's card-feed bullet
marked superseded, with the reasoning kept — it lost to consistency
across the set, not to being wrong.
Two things promoted into Techniques:
- position:sticky does NOT pin anything to the bottom of a short
container; it only stops an element scrolling away. Pair it with
margin-top:auto in a flex column. And the two fight a negative
margin-bottom — auto absorbs the free space the negative margin
creates, so they cancel and the bar lands its own margin short.
- Never mix getBoundingClientRect() with clientHeight on anything inside
a transformed container. Rects are scaled, layout properties are not;
mixing them produced a "bottom" beyond the viewport and sent a
diagnosis completely wrong. Fourth false alarm from animating modals.
6276664
Mobile: Service Status (#1003) + four more techniques
New page Mobile-Friendly-Service-Status, linked from the overview
(status blockquote, module table, honest assessment, layer count), the
sidebar, all four sibling module pages, Techniques and Service-Status.md.
Four things promoted into Techniques:
- An inline style.display set by the page's own JS beats your rule.
Service Status re-sets display:table on every render, so the card feed
needed !important or it reverts on the first refresh. Grep a module for
style.display before converting one of its tables.
- "It reflows" is not the same as "it works". The board was already an
auto-fill grid -- and a single column at 360px, i.e. a list. Ask what
the component is FOR; a status board is for scanning, so density beats
card size.
- Not every module needs a mobile.js branch. Four in a row had one; this
one shipped as CSS only.
- A CSS comment edit is a code edit. Closing a comment early orphans text
and the parser drops the rule that follows. Happened twice in one
session, and brace-balance checks pass happily both times.
26f0340
Mobile: Knowledge round 3 (#1002) + three widget-wrangling techniques
Module page gains a "Round 3" section: the editor footer reaching the
bottom edge, and full-screen text editing.
Three things promoted into Techniques:
- A JS-mounted widget injects its stylesheet at RUNTIME, so it beats
yours on source order whatever the specificity. TinyMCE's skin lands
after mobile.css; .tox-* rules need !important. The tell is a fix that
looks like it HALF worked -- hiding the upsell badge succeeded while
flex-wrap on the menubar did nothing, which read as "mostly fixed"
until it was measured. Measure which half.
- Reclaim a third-party widget's own chrome before adding your own. Four
wrapped menubar rows plus an upsell badge were ~130px of a 720px
screen; scrolling the menubar rather than hiding it returned 78px of
typing area with every menu still reachable.
- Build your own full screen, don't borrow the widget's. TinyMCE's
fullscreen plugin was one line away but had no reachable exit in this
init -- a phone user could get stuck. A fixed overlay with your own
Close plus a pushState back-button exit cannot trap anyone.
c1e2e44