Cyresora Manual
Cyresora — User Manual
A CRA and NIS2 incident-reporting register, with the GDPR and DORA reporting clocks on the same incident since 1.1.0: what you became aware of, when, which reporting clocks that started, what has actually been submitted, and what is still running. The API reports version 4.1.1 [src: app/controllers/api.php:17].
About this manual
Every statement here was written by reading Cyresora's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug.
This manual is not legal advice and does not verify either instrument. Articles, durations and dates below are reported as what the product implements — check them against the Official Journal and, for NIS2, against your own Member State's transposition, which the product deliberately does not model [src: app/src/Clock.php:38].
What Cyresora is
The clock engine is pure, offline, and never reads the system time — every function takes "now" as an argument, so tests pin real instants instead of hoping the suite does not run at midnight [src: app/src/Clock.php:3].
That is not a testing detail. It is why the deadlines in this manual could be reproduced exactly, by running the engine rather than describing it.
⭐ Four anchors, not two ladders
The obvious model — two ladders of 24 h / 72 h / final, both counted from the moment you found out — is wrong, and the source says it is wrong in the direction that produces a confident, late filing [src: app/src/Clock.php:9].
Read the instruments and there are four different anchors [src: app/src/Clock.php:11]:
| Regime | Stage | Counted from | Length |
|---|---|---|---|
| CRA — actively exploited vulnerability | early warning | becoming aware | 24 h |
| notification | becoming aware | 72 h | |
| final report | a corrective or mitigating measure being available | 14 days | |
| CRA — severe incident | early warning / notification | becoming aware | 24 h / 72 h |
| final report | submission of the incident notification | 1 month | |
| NIS2 — significant incident | early warning | becoming aware | 24 h |
| notification | becoming aware | 72 h | |
| intermediate report | a CSIRT asking for one | — | |
| final report | submission of the notification | 1 month | |
| final report after handling | the handling being completed | 1 month |
The regimes and the provision each rests on are declared in one place [src: app/src/Clock.php:72], and every stage carries the article it comes from and the instrument's own wording [src: app/src/Clock.php:93].
⭐ Three consequences the register has to carry
*A deadline can be not started yet.* The CRA's fourteen days does not begin until a corrective or mitigating measure exists. Showing a due date before then would be an invented date, so the engine returns pending and names the event that will start it [src: app/src/Clock.php:33]. In the ladder that looks like this — a Friday-evening discovery with no remedy yet, run through the engine:
early Early warning anchor=awareness due=2026-09-19 19:00:00 state=open weekend=YES
notify Vulnerability notification anchor=awareness due=2026-09-21 19:00:00 state=open weekend=no
final Final report anchor=remedy due=(none) state=pending weekend=no
The one-month clocks run from the actual submission, not from the deadline [src: app/src/Clock.php:36]. Notify at hour 40 and the final report is due a month after hour 40. Executed on a NIS2 incident notified at 23:30 on 5 May:
final Final report anchor=notification due=2026-06-05 23:30:00
the 72-hour deadline would have been 2026-05-07 08:00:00
a month from that deadline 2026-06-07 08:00:00 ← two days later than the truth
A product that anchored on the deadline rather than the filing would have shown a due date two days late. This one does not.
NIS2 is a Directive. What binds an entity is its Member State's transposition. The engine models the Directive's own stages and the register records the national authority; it does not pretend to know a national variation [src: app/src/Clock.php:38].
⭐ Two arithmetic hazards, both of them "wrong by a day" bugs
In a product whose entire value is the date, these are not tidiness [src: app/src/Clock.php:42].
Calendar months. PHP's own modify('+1 month') from 31 January returns 3 March — a deadline later than the law allows, silently. Every calendar-month deadline here clamps to the last day of the target month instead [src: app/src/Clock.php:45]. Both, side by side, from the running engine:
2026-01-31 09:00 PHP: 2026-03-03 09:00 Cyresora: 2026-02-28 09:00
2026-01-30 09:00 PHP: 2026-03-02 09:00 Cyresora: 2026-02-28 09:00
2026-03-31 12:00 PHP: 2026-05-01 12:00 Cyresora: 2026-04-30 12:00
Daylight saving. Twenty-four and seventy-two hours are absolute durations computed on UTC instants, where a clock change cannot touch them. "One month" is a calendar expression: it is computed in the business timezone and converted back, so it lands at the same wall-clock time — which is what a deadline in months means to the person who has to meet it [src: app/src/Clock.php:48]. Executed across both European changeovers, business timezone Europe/Bratislava:
one month from 2026-02-25 13:00 UTC -> 2026-03-25 13:00 UTC (14:00 local -> 14:00 local)
one month from 2026-09-25 13:00 UTC -> 2026-10-25 14:00 UTC (15:00 local -> 15:00 local)
24 h from 2026-10-24 13:00 UTC -> 2026-10-25 13:00 UTC (absolute, untouched)
72 h from 2026-10-24 13:00 UTC -> 2026-10-27 13:00 UTC (absolute, untouched)
The second month-deadline moves by an hour in UTC precisely because it did not move in local time.
⚠️ The clocks do not stop at the weekend
The source says this out loud because a buyer will assume otherwise: neither CRA Article 14 nor NIS2 Article 23 has a working-day carve-out, and a Friday evening discovery is due on Saturday evening [src: app/src/Clock.php:53].
The engine flags it rather than leaving you to notice — every stage carries a weekend flag computed in your business timezone [src: app/src/Clock.php:212], which is why the Friday example above shows weekend=YES against the 24-hour warning.
What a stage can say
Each stage reports one state [src: app/src/Clock.php:270]: pending when its anchor has not happened, open, due_soon inside the last six hours, overdue, submitted, or submitted_late when the filing came after the due instant [src: app/src/Clock.php:261]. A stage that exists only on request — NIS2's intermediate report — is not_requested until an authority asks [src: app/src/Clock.php:239].
Filing the same stage twice is refused, in the browser and through the API alike, with the same reasoning: "Filing it twice is a duplicate, not a second duty discharged" [src: app/controllers/api.php:161].
Users and roles
Four roles, not the usual three [src: app/controllers/app.php:21]. Executing the product's own permission function rather than reading the map:
audit.view viewer=Y member=Y responder=Y admin=Y
incident.edit viewer=n member=Y responder=Y admin=Y
notify.submit viewer=n member=n responder=Y admin=Y
contact.edit viewer=n member=n responder=Y admin=Y
settings.edit viewer=n member=n responder=n admin=Y
responder exists to separate two authorities that look alike. Editing an incident is ordinary work; recording that a notification was submitted to an authority is the act that stops a statutory clock, and only a responder or an admin can do it [src: app/controllers/app.php:26].
⚠️ A viewer can read the audit trail
The audit page and its CSV both require audit.view [src: app/controllers/v3.php:15], and the viewer role holds the wildcard *.view [src: app/controllers/app.php:24], which matches it — shown by the executed table above. If a read-only account is meant not to see who recorded which filing, that is not the behavior you have. This has been raised against the product.
Backups are gated properly, by contrast: both downloads require settings.edit [src: app/controllers/api.php:583], which no role below admin holds [src: app/controllers/api.php:589].
Webhooks fire from every path
Cyresora declares three webhook events [src: app/src/Webhook.php:20]. incident.opened fires from the browser [src: app/controllers/app.php:645] and the REST API [src: app/controllers/api.php:120]; notification.recorded fires from the browser [src: app/controllers/app.php:725], the REST API [src: app/controllers/api.php:167] and the agent tool [src: app/controllers/api.php:496]; deadline.alert fires from the alert watcher [src: app/controllers/app.php:375].
Deadline alerts
The warning window. "Warn me this many hours before a deadline" (1 to 72, default 6) decides when a running stage shows as due within hours [src: app/controllers/app.php:240]. Before 1.0.4 the setting was saved but the board always used 6.
Alerts to the rota. Switched on in Settings, alerts go out at your alert points (12, 4 and 1 hours before a deadline by default) and once when a deadline passes, but not for a stage that has been overdue for more than a day [src: app/src/Clock.php:317]. The notifiers on the rota for that regime and their deputies are emailed; with nobody on the rota, the organization's contact email is [src: app/controllers/app.php:322]. The email names the incident reference and title, the stage and its provision, the deadline and a link. A signed deadline.alert webhook fires at the same moments. Each point is latched, so the Clocks screen and the cron route alerts/run?t=<token> never send it twice [src: app/controllers/v3.php:269]. A failed send, a missing mail server or an empty rota is shown on the Clocks screen [src: app/controllers/app.php:452].
The API and agent access
Nine tools [src: app/controllers/api.php:313]: seven read — every running deadline worst-first, the incident list, one incident, what applies to a given situation, and since 1.1.0 a stage's report draft [src: app/controllers/api.php:366], the risk register and the document hub — and two write, opening an incident [src: app/controllers/api.php:425] and recording a notification [src: app/controllers/api.php:464].
The recording tool is gated on notify.submit [src: app/controllers/api.php:466] and refuses a stage that is not running on that incident, naming the ones that are [src: app/controllers/api.php:478].
The instructions given to an agent are the clearest short statement of what this product is for [src: app/controllers/api.php:302]:
"You cannot set a deadline. You record two facts — when the organization became aware, and when a notification was actually submitted — and the instrument decides the rest."
And the sentence that matters most [src: app/controllers/api.php:307]:
"Never record a notification as sent unless the human has told you it was sent. That record is what stops a statutory clock and what an authority will later be shown."
GDPR and DORA on the same incident (1.1.0)
One event is often more than a CRA or NIS2 incident. Two more regimes can run on the same incident, each with its own anchors [src: app/src/Clock.php:77]:
- GDPR, personal data breach (Regulation (EU) 2016/679, Article 33): the notification to the supervisory authority, 72 hours after becoming aware of the personal data breach [src: app/src/Clock.php:124]. That moment can be later than awareness of the incident (the export went to the wrong customer on Monday; that it held personal data was confirmed on Tuesday), so the incident form has its own field for it; left blank, the incident's awareness is used, the earlier and safer reading [src: app/controllers/app.php:212]. Telling the people affected (Article 34) has no hour count and no ladder stage; the stage note says so.
- DORA, major ICT-related incident (Regulation (EU) 2022/2554, Article 19; Delegated Regulation (EU) 2025/301, Article 5): the initial notification within 4 hours of classifying the incident as major, and no later than 24 hours from becoming aware when it was classified within those 24 hours; classified later, within 4 hours of the classification [src: app/src/Clock.php:133]. Until you record the classification the stage is "not started", as every clock whose event has not happened. The intermediate report runs 72 hours from the initial notification you actually submitted, the final report one month from the intermediate report on record.
Two things the DORA ladder does not do, and says so in its notes: it counts the final report from the intermediate report on record, so an updated intermediate report only gives you more time than shown; and it does not apply the move to 12:00 the next working day that Article 5(4) to (6) allows some financial entities for a deadline on a weekend or bank holiday. Both errors, if they are errors for you, err early.
Settings takes a GDPR supervisory authority and a DORA competent authority, pre-filled when you record a notification under those regimes [src: app/controllers/app.php:220]; the notification rota takes GDPR-only and DORA-only people. The API and the agent take personal_data_breach_aware_at_utc and classified_major_at_utc when opening an incident.
Report drafts (1.1.0)
Every stage not yet filed has Draft it beside its note. The draft is built from the incident record under headings that follow what the provision asks for, each with its cite: for a NIS2 early warning, whether the incident is suspected of being caused by unlawful or malicious acts and whether it could have a cross-border impact; for a GDPR notification, the four points of Article 33(3); for a CRA final report, the vulnerability, what is known of the actor and the update made available [src: app/src/Drafts.php:61]. What the record holds is filled in (the product and its version, the linked vulnerabilities and their remedies, the root cause, earlier submissions and their references); what it does not hold is marked [to complete: …] and listed above the text. A GDPR draft past its 72 hours asks for the reasons for the delay.
The draft is text for a person to check, complete and paste into the authority's own form or portal; headings paraphrase the provision and the official form decides. DORA drafts carry the core values and name the templates of Implementing Regulation (EU) 2025/302 as the place they go. When you have sent it, Record it as sent on the same page stores the text you edited as the notification's record [src: app/controllers/v11.php:38]. The same draft comes from GET /api/incidents/{id}/draft?regime=&stage= and the agent tool draft_report; neither records or files anything.
The risk register (1.1.0)
Risk register keeps each risk with likelihood and impact on your own 1–5 scale (the score is their product; 15 and above shows as high, 8 and above medium), the treatment (mitigate, accept, transfer, avoid) and plan, the residual likelihood and impact, the owner, a treat-by date and a review date [src: app/controllers/v11.php:91]. A risk is filed under one of the ten measures NIS2 Article 21(2) lists, (a) to (j) [src: app/controllers/v11.php:75], so the page shows which areas hold no open risk yet, beside a 5×5 grid of open risks. Reviews that are due and treatments past their date head the page [src: app/controllers/v11.php:118]. Every change is in the audit trail with the score and status before and after. Members and responders edit; viewers read. risks.csv (formula-safe), GET /api/risks and the agent tool list_risks give the same register.
"None recorded" for an area means only that: no open risk is filed under it here. Whether your measures are appropriate and proportionate is your organization's judgment.
The document hub (1.1.0)
Documents keeps your policies, procedures and plans (the incident response plan, the coordinated vulnerability disclosure policy, the continuity plan) with every version: the file, its SHA-256, the date it takes effect, who approved it, what changed [src: app/controllers/v11.php:270]. The file's content decides its type: PDF, Word (.docx), OpenDocument text (.odt), plain text or Markdown; an executable named .pdf is refused [src: app/controllers/v11.php:254]. The version in force is the latest whose date has come; a future version shows as upcoming. The review date runs from the version in force (every 12 months by default, or 6, 24, or none), and the list shows overdue and within-30-days reviews [src: app/controllers/v11.php:297]. Downloads are contained to the documents folder and re-checked. Removing a document removes its whole history, so only an admin can.
Assets from Microsoft Entra ID and other exports (1.1.0)
Import a list has a second importer for directory exports. In the Microsoft Entra admin center, Devices › All devices › Download devices writes a CSV whose columns include deviceId, displayName, operatingSystem, operatingSystemVersion, model, owner and upnName; the preset maps them [src: app/controllers/v11.php:465]. Each device becomes an internal system with its operating system and model, version and owner. The device id is kept with the asset, so importing a later export updates the same rows (name, version, description, owner) and never touches the CRA and NIS2 scope you marked [src: app/controllers/v11.php:478]. For an Intune, Microsoft 365 or any other export, choose "Another export" and type its column names; comma, semicolon and tab files are read. As every import: a dry run first, then the import, audited with the counts.
The import's dry run applies the same checks the import does, row by row, and writes nothing: a row the import would skip (an owner email that is not an address) is listed as skipped in the dry-run report, not counted ready.
Data requests: a person on the rota (4.1.1)
The people on Who notifies are usually your employees, sometimes an outside adviser. An administrator opens Data requests and picks one:
- Download their data writes one JSON file with their rota row and every deadline alert that went to their address.
- Erase this person replaces their name, email, phone and notes on the rota with
[removed]and takes their address out of the alert log; the others an alert went to stay [src: app/controllers/v3.php:731]. The role slot (role, regime, authority, deputy link) stays on Who notifies, so the rota shows a gap to fill rather than losing it. "Remove" on Who notifies still deletes the row, and leaves the alert log as it was.
Erasure is refused while they are on the alert rota for an open incident (a notifier for one of its regimes, or that notifier's deputy): a running statutory deadline must keep its notifier [src: app/controllers/v3.php:779]. Owners named on products, risks and policies, approvers of policy versions and whoever recorded a notification are your accountability record and are not touched. The audit trail records that an erase happened and how many rows it touched.
Kept: the audit trail's lines that this person was added to or removed from the rota, which carry their name, because the audit trail cannot be edited.
What Cyresora does not do
It does not read the system clock [src: app/src/Clock.php:3] — every deadline is computed from instants you supply.
It does not invent a due date for a clock that has not started [src: app/src/Clock.php:33].
It does not anchor a one-month report on the deadline [src: app/src/Clock.php:36] — it anchors on the filing.
It does not model your Member State's transposition of NIS2 [src: app/src/Clock.php:38].
It does not pause at weekends [src: app/src/Clock.php:53].
It does not submit anything to an authority. It records that you did.
It does not fill in the authority's form. A draft is text for a person to complete and send; the official template decides.
It does not judge your security measures. The risk register and the document hub record what you decided and approved.
Looking after your install (from 4.0)
Updates from inside Cyresora
Updates in the menu opens the Updates page (/updates) [src: app/views/updates.php]. Paste your license key once and press Save key: either key works, the one in your Lemon Squeezy receipt or the one on your order page (it starts with OWX-). Check for updates asks the store for the newest release your license covers; Cyresora never checks by itself.
When a new version is ready, Back up and update to that version backs up your data and the current version, installs the new one and checks it. If the update is interrupted, or the new version's database step or that check fails, the previous version is put back by itself. Cyresora is unavailable for about a minute while this runs. The update replaces Cyresora's files, including any you edited, except .htaccess, .user.ini and robots.txt: an edited copy of those is kept, with the new version's copy beside it as .new to compare and merge. The Health page lists any file that differs from the version installed, so you can see that before you update. A copy of the previous version (data/updates/code-<version>/) and a backup of your data from just before the update stay in data/updates/ [src: app/src/Updater.php]. If Cyresora shows an error page after an update anyway, that is your way back: copy the files from code-<version>/ over the app's files, then restore the backup on the backup page. Updating from the page needs PHP's sodium and Phar extensions; if one is missing, the Updates and Health pages say so and updates stay by hand. Where Cyresora is hosted for you, the host updates it and the page says so.
Reaching 4.0 from a 3.x version is the last update done by hand: replace the files as before (config.php and data/ stay, and the database migrates itself on the next page load). From 4.0 on, the Updates page does it.
Starting with sample data
The installer offers Start with sample data: incidents, risks and policies with their reporting clocks running to try things on. You can remove it in one click. [src: app/install/index.php:138]. It is ticked by default; untick it to start empty. The sample is added when you open the first page after the installer.
When you are done with it, press the remove button in the sample banner. It asks "Remove the sample incidents, risks, assets and policies? Your own records stay." Only the sample goes: your own records stay, and a sample record that one of your own records uses is kept and named in the report [src: app/src/SampleData.php].
Backups by themselves, and a copy somewhere else
On the Backups page (/restore), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:72]. The first visit at least 24 hours after the last backup starts the next one, after the page has loaded, so no cron job is needed. The newest 14 stay in data/backups/, with credentials left out of them (a restore keeps the live values for those). Back up now takes one straight away.
To keep a copy somewhere else as well, fill in an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3 or your own MinIO): the bucket address (an https endpoint), the region, the bucket name, an optional folder, and the access key ID and secret key. Leave the address empty to keep backups on this server only. The copy goes only to the bucket you set; nothing is sent to Ownware. Once a week Cyresora reads the newest backup back through the restore's own dry run and keeps the result for the Health page [src: app/src/BackupTargets.php].
On Nginx: keep data/ private
On Apache, the .htaccess that ships with Cyresora keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these three blocks to your server block, above the block that passes .php files to PHP (Nginx uses the first one that matches):
location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }
Then open /config.php in a browser: it should answer 403. Only the template files under views/ are closed, not the folder: the app's own saved-view addresses start with /views/ too, and they must reach it.
A complete server block for a new site (PHP-FPM 8.3; change the name, the folder and the socket to yours):
server {
listen 80;
server_name example.com;
root /var/www/cyresora/app;
index index.php;
location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }
location = /install/ { rewrite ^ /install/index.php last; }
location / { try_files $uri /index.php?$query_string; }
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
The Health page
Health in the menu (/health) shows, one line each, whether the install is looking after itself: the PHP version and extensions, disk space, whether data/ is writable, email, the age of the last backup, the offsite copy, the weekly check, whether an update is waiting, and any file that differs from the version installed. Each line says what to do when it is not fine. The page only reads: it never changes anything and never contacts anything [src: app/src/Health.php].
Connect it to other tools (Zapier, Make, n8n)
Cyresora sends a webhook the moment something happens and accepts calls to its API, so Zapier, Make and n8n can connect to it without a plugin. The steps, the events and example recipes are in API.md, in the section "Connect it to Zapier, Make or n8n"; the API page of Cyresora on the store shows the same file [src: app/API.md].
New in 4.1.2: sign-in limits
New in 4.1.2 (security update): Two-step codes are now limited (10 wrong per person or per address in 10 minutes), and wrong passwords count per account as well as per address. An installed app whose database does not answer keeps its web installer closed instead of showing the install form. On Ownware Cloud, opening the app from the console starts a fresh session.
Wrong passwords are counted per address and, since 4.1.2, per account too (the email typed, in any case, stored only as a hash). After 10 wrong passwords in 10 minutes the sign-in at /login answers "Too many attempts — wait a few minutes and try again." for that address, and for that account from any address. A right password clears both counts [src: app/src/LoginRate.php:40].
Wrong two-step codes (for people who turned two-step sign-in on under /security) are counted per person and per address. After 10 wrong codes in 10 minutes the code step answers "Too many wrong codes. Wait ten minutes, then sign in again. A recovery code works in place of the code." even for the right code, and the half-finished sign-in is dropped, so the password is asked again [src: app/src/LoginRate.php:91]. The right code clears that person's count; an address's count runs out ten minutes after its wrong codes. Before 4.1.2 the code step had no limit.
If you are locked out: wait ten minutes, then sign in again with your password and a current code, or one of your recovery codes (each works once). If you lost both the authenticator and the recovery codes, whoever runs the server can turn two-step sign-in off for you in Cyresora's database (by default the SQLite file in data/, or your MySQL database): UPDATE users SET totp_secret = '' WHERE email = 'you@example.com'; Then sign in with your password and turn it on again under /security.
On Ownware Cloud, opening Cyresora from the console starts a fresh session [src: app/index.php:50].
The web installer stays closed when the database does not answer: with config.php in place, /install now answers 503 "This app is already installed (config.php exists), but its database is not answering, so the installer stays closed." and writes nothing until the database answers again. Before 4.1.2 a database outage (or a missing SQLite file) showed the install form there, and whoever found it could set up a new administrator. To install again from scratch, remove config.php first [src: app/install/index.php:20].
New in 4.1.3: security update
New in 4.1.3 (security update): no other site can put Cyresora's pages in a frame; the /mcp address refuses requests from other sites' pages; company sign-in is stricter; and the audit CSV treats one more first character as text. Nothing else changed: no feature, screen, setting or database table, and your data, config.php and data/ are not touched.
No other site can frame these pages. Every page now answers Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so a page on another site cannot show Cyresora in a hidden frame and trick a signed-in person into pressing a button [src: app/index.php:3]. The installer answers frame-ancestors 'none' and X-Frame-Options: DENY [src: app/install/index.php:4]. If you had Cyresora framed inside another site of your own, that frame now stays empty; link to it instead.
/mcp refuses other sites. A request whose Origin header names another site gets 403 before anything is read. A desktop or server client sends no Origin and is served as before, and so is a page at Cyresora's own address [src: app/src/Mcp.php:272].
Single sign-on. The sign-in now uses PKCE (S256), so an intercepted sign-in code is of no use on its own; a sign-in that comes back to a different browser session is asked to start again [src: app/src/Oidc.php:228]. With Google as the provider, a company address needs a Google Workspace account of that same domain (the token's hd claim), and Google must report the address as verified, so a personal Google account made with a work address no longer gets in [src: app/src/Oidc.php:215]. Gmail addresses work as before, other providers are unchanged, and password sign-in is not affected.
The audit CSV. A cell that starts with a carriage return now opens in a spreadsheet as text, as cells starting with = + - @ or TAB already did [src: app/src/AuditLog.php:138].
Docker. The Dockerfile now installs PHP's zip extension, so a Docker install can make the backup with files [src: app/Dockerfile:6].
Also changed, with no effect on a running install: the corner card on a public demo offers only the purchase, and API.md no longer shows the config example of a product we retired.