Changelog¶
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[1.15.0]¶
Added¶
- Model names can come from the environment. Every model setting —
provider.<name>.modelfor all seven providers, andtts.model— now accepts%env(NAME)%, like the API keys already did. A project can keep model names out of its versionedsettings.phpand move to another model, or away from a deprecated one, by changing an environment variable instead of committing and deploying. Backward compatible: literal model names are returned unchanged. - Unlike an API key, a model placeholder whose variable is empty or missing
resolves to the provider's default model, not to an empty string. A missing
key correctly marks a provider as unavailable; an empty model name would be
sent to the provider and fail every request, so one forgotten ENV entry would
switch off every AI feature. API keys keep resolving to
''as before.
Changed¶
- A model field saved empty in the backend now falls back to the provider default as well. Previously only a missing entry did; an empty one went out to the provider as an empty model name, and every request failed.
[1.14.1]¶
Fixed¶
- The scheduled audit ignored page time control.
AuditTask::findCandidatePages()callsremoveAll()on the query restrictions — legitimately, because the report table'sdeletedfilter has to live in the JOIN condition — and then re-added the page restrictions by hand. The QueryBuilder'sDefaultRestrictionContainercarries four (deleted, hidden, starttime, endtime); only the first two were restored. A page whose end time had passed therefore stayed a candidate, answered 404 like any hidden page, and took one slot in every run. Unlike a page under maintenance it never becomes auditable again on its own, so it returned week after week. Reported by an installation that recognised its own expired registration page in the new deferral table. Only the Scheduler task was affected:AuditCommandandAnalyzePagesCommanddo not callremoveAll()and get the time restrictions from the default container. - A page with a future start time is excluded for the same reason and joins the rotation by itself the moment it goes live. Auditing a page deliberately before publication remains possible through the signed preview token — that is a manual, explicit act rather than the business of the nightly rotation.
[1.14.0]¶
Fixed¶
- Translated page records were audited under the wrong URL and could never
succeed. The candidate queries filtered by doktype, deleted and hidden, but
not by
sys_language_uid, so translation rows were picked as targets in their own right. A page is audited by building the target language's URL for the default-language UID; taking a translation row instead produces the translated slug without the language prefix —/about-us/rather than/en/about-us/— a URL that does not exist. Every such page answered 404. On a bilingual site this was roughly half of all candidates: on the instance where it was found, 52 of 53 deferral entries after a run were translation rows. Before 1.13.0, when the pause did not yet survive a run, they could fill the batch entirely and stop the rotation. The same gap existed in four places and is closed in all of them:AuditTask::findCandidatePages(),AuditCommand::findCandidatePages(), and both candidate paths ofAnalyzePagesCommand(findAllNormalPages()andcollectChildren()).LighthouseCommandalready did this correctly — its comment states the reasoning — and served as the model.
Upgrade notes¶
- Nothing to configure, but check your language coverage. Translated records leave the rotation by themselves after the update; leftover deferral entries from earlier runs expire on their own. If you relied on those rows to have any audit of your translated pages, note that they never produced a valid result — they only ever recorded a 404. To audit a second language, set up one task per language, each with its own Language setting. They share the deferral table.
- A page with no translation in the target language answers 404 under a
strictfallback and is set aside for a week as a result, which is correct and harmless; with a fallback configured it is audited as rendered.
[1.13.1]¶
Changed¶
- A deferral row now says where its pause came from. The
reasoncolumn carries the origin —Retry-After: 1800s,no Retry-After, default,clean 404/410, or the position in the failure escalation. Reported from an installation verifying 1.13.0: their maintenance page sendsRetry-After: 3600and the built-in fallback is also one hour, so the measured timestamp was identical either way. They could only prove the header was read at all by temporarily setting it to 1800 — one experiment too many for a question the stored row should answer itself.
Documentation¶
- Installation: how to replace the extension directory when deploying by rsync or
tar instead of
composer installon the target. Removing the old directory before copying the new one leaves a window in which the extension is present but incomplete; a request landing in it reports a class-not-found error for a class that does exist. Diagnosed in a production installation and contributed by the site that hit it — it applies to any TYPO3 extension, not only this one.
[1.13.0]¶
Fixed¶
- The audit's cooldown for unreachable pages never took effect.
AuditTaskkept the cooldown in two properties on the task object, and their docblocks stated it was "persisted with the task between runs" — it was not, and could not be. The scheduler saves a task before it executes (Scheduler::executeTask()), andSchedulerTaskRepository::updateExecution()narrows the written fields to nextexecution/disable/execution_details; nothing writes a task's parameters back afterwards. Every cooldown was therefore forgotten the moment the run ended. The consequence was severe: an unreachable page writes no report, so itslast_checkedstays 0 andORDER BY last_checked ASCpicks it first again on the very next run. Enough such pages fill every batch and the rotation stops entirely — reported from an installation with 27 sites where 34 unreachable pages at a limit of 25 reduced the output from 144 audit reports per hour to one or two, for 16 hours, while the task reported red on every run. The state now lives in the new tabletx_ntai_audit_deferral, one row per page and language, which survives a run and is shared by all audit tasks instead of each keeping its own copy. - HTTP 503 and 429 were counted as technical failures. A site in maintenance mode answers 503 on purpose, and a rate limiter answers 429 on purpose; both say "come back later". They are now treated like a clean 404/410 — a result, not a failure — so a batch made up of them no longer trips the "every page failed" rule and turns the task red. A site under maintenance now steps out of the rotation by itself and returns when it is back, with no configuration.
Retry-Afteris honoured when the server sends one, so the pause matches what the server actually asked for instead of a guess. Both header forms are accepted (a delay in seconds and an HTTP date); a value in the past, a malformed value or an absurd one cannot park a page (capped at a week).
Added¶
- Table
tx_ntai_audit_deferralrecords why a page was set aside and until when, so "why is this page not being audited?" is answerable:SELECT page_uid, status_code, FROM_UNIXTIME(deferred_until), reason FROM tx_ntai_audit_deferral;Runextension:setuponce after updating so the table is created. - Documentation section "Pages the audit cannot reach" in
Documentation/Configuration/Index.rst(the tree that ships with the package).
Note¶
- A per-task exclusion list for sites in maintenance was considered and deliberately not built: with 503 handled correctly, such a site removes itself from the rotation for exactly as long as it says it needs, which is better than a second list that has to be maintained by hand and removed again afterwards.
[1.12.3]¶
Documentation¶
- The Scheduler-arguments section now ships with the extension. 1.12.2 added
it to
docs/(MkDocs) only — anddocs/carriesexport-ignore, so it never reaches an installed instance. Anyone searchingvendor/netthinks/nt-ai/during a fault diagnosis, which is the first place to look, did not find it. It is now inDocumentation/Configuration/Index.rst, which is part of the distributed package. Reported from outside, after the section could only be located viagit diffagainst the public repository. - The section also states the consequence that surprised that reporter: a run in
which nothing could be measured exits non-zero, so
nt_ai:lighthouseon a local DDEV instance always ends with exit code 1 — correct, because PSI cannot reach a local URL, but it makes the command unsuitable as a local smoke test. (This is not new behaviour; the rule predates 1.12.2.) Documentation/ChangeLog/Index.rststopped at 1.2.0 while the extension was at 1.12.x, which reads as an abandoned project. It now points to the shippedCHANGELOG.mdas the authoritative source and marks its own entries as historical.- Fixed a typo in the Lighthouse section ("keepin" → "keeping").
[1.12.2]¶
Fixed¶
- An empty optional
pageIdfrom a Scheduler task no longer becomes page 0. A task created in the backend stores every argument it was configured with, so an unsetpageIdarrives as""rather thannull. Bothnt_ai:analyze-pagesandnt_ai:lighthousecast that straight toint, and(int)'' === 0: instead of measuring every page, they measured exactly one page that does not exist. The same case was already handled for thestrategyoption; the argument now gets the same treatment, and apageIdof0is rejected with a clear error rather than one unreachable-page warning per language. nt_ai:analyze-pagesno longer reports success for a run that scored nothing. A page that cannot be reached counts as a result, not an error — by design, so that record folders and clean 404s do not turn a healthy nightly run red. But when a run scored nothing, had nothing already up to date, and every single candidate was unreachable, that is not a quiet day: it is a task pointed at a page id that cannot work. Combined with the bug above, such a task could run green for weeks without ever writing a score. That case now warns, explains the likely cause, and exits non-zero. A mixed run is unchanged: as long as anything was scored or was already up to date, unreachable pages alone never turn the task red.
Documentation¶
- Configuration: a section on optional arguments in Scheduler tasks — why an
empty
pageIdis stored at all, that it now means "all pages", and that tasks created from a script should passarguments: [].
[1.12.1]¶
Fixed¶
- The page module panel and the audit module no longer disagree about a translated page. The panel shows the report for the language currently being edited, but its "Open in module" link passed only the page id. The module fell back to the default language and therefore presented a different report — different score, different error counts — for what looked like the same page. A translation is audited under its own URL, so the two reports legitimately differ; the link now carries the language so both views stay on the same one. The same applies to the Lighthouse link.
[1.12.0]¶
Added¶
AiService::complete()— run your own prompts through the front door. Every other method onAiServicebuilds its system prompt throughPromptBuilder, which is right for the tasks nt_ai defines and leaves no way in for an extension that has a prompt of its own and has measured what that wording does. The only route left was to reach pastAiServiceto the provider locator — and everything that does so skipsTokenUsageService, so the calls never appear in the dashboard while the money is being spent.complete($systemPrompt, $userPrompt, $providerId, $context, $task)takes both prompts verbatim, pins the call to a named provider, and records the usage like any other call.$contextand$taskare free labels and show up as the corresponding columns.
Found in t3radar, which had exactly this problem: a deterministic provider chain built on the locator from 1.4.0, and a token dashboard that stayed empty.
[1.11.0]¶
Added¶
- Per-site filter in the accessibility audit backend module. On a multi-site installation (e.g. many school websites in one instance) the Site overview and the Lighthouse tab listed every page of every site — a global list that is useless for the editors of a single site. A "Website" dropdown next to the tabs now scopes both tabs (page table, "most frequent issues" aggregation, totals and the Lighthouse table) to the pages of the chosen site; "Alle Websites" keeps the previous cross-site view. The choice is remembered per backend user and carried across tabs, and defaults on first use to the site of the page currently selected in the tree. The dropdown only appears when more than one site is configured, so single-site installations are unchanged. The page-detail and declaration tabs are inherently single-page and are not affected.
Changed¶
WidgetSiteScopegained a publicoptions()method (site identifier => label list) shared by the dashboard widgets and the new module filter; it is now registered as a service so the audit module can reuse it.
Fixed¶
link.distinguishableno longer fires a global, unsuppressable error on every page. The rule scans the stylesheet text fora { text-decoration: none }— the audit is HTTP + static DOM and cannot measure the rendered link, so a single theme-wide declaration flagged an Error (−10 points) on every page even when content links were perfectly distinguishable, burying actionable findings. Now: the finding is suppressed when the same stylesheet re-adds a non-colour indicator in a content-scoped selector (e.g..ce-bodytext a { text-decoration: underline }— exactly the correct fix); it is a Notice (not an Error) reported once per page and worded as a theme-wide CSS hint, not a per-link defect; and the snippet now shows the offendingselector { … }declaration instead of the first 300 characters of the (minified) stylesheet. The per-anchor inlinetext-decoration:nonecheck is unchanged in intent (now a Warning).- Individual audit rules can finally be disabled per page tree via TSconfig. The
disabledRulesmechanism existed but no caller populated it, so the documented TSconfig switch did nothing.AuditService::auditPage()now readsmod.tx_ntai.audit.disabledRules(comma-separated rule IDs) and merges it with any programmatic list — effective for the command, scheduler task, backend module and on-save inline panel. Example:mod.tx_ntai.audit.disabledRules = link.distinguishable.
Changed¶
link.distinguishableis now listed in the built-in rules table, and the docs state its limitation plainly: it is a CSS-text heuristic, not a rendered-link measurement. A true per-link check (link colour vs. surrounding text,:hover/:focusper G183) needs a headless browser and is tracked as a separate feature.
[1.10.1]¶
Fixed¶
nt_ai:analyze-pagesno longer reports the scheduled task red when a page is merely a result rather than a failure. The command exited with code 1 as soon as one page was counted an "error", which turned the scheduler task red on every run — for two cases that are not technical failures and that the audit task already treats as results since 1.8.6: a page without a site (the central record folders for forms, footer and navigation content aredoktype = 1but reachable through no site) and a clean HTTP 404/410 (a news/events detail URL with no record). Pages without a site are now skipped up front via theSiteFinder, and a clean 404/410 is counted as "unreachable", not "error". The exit code is 1 only on genuine failures (timeout, provider error, write error), so a healthy instance stays green. Neither case consumes a--limitslot any more, so a small--limitreaches real pages instead of being used up by these two page types (0 analyzed). The run summary now readsN analyzed, N unchanged, N unreachable, N error(s).
Changed¶
- The content-change detection is fronted by a small
PageScoreServiceInterface, so the analyze-pages command depends on the score-service behaviour rather than the final class. No functional change; it keeps the command unit-testable (the exit-code contract above is now covered by unit tests).
[1.10.0]¶
Added¶
nt_ai:lighthouse --changed-onlymeasures only pages whose content changed since their last measurement. A full Lighthouse run measures every page on every invocation, so a scheduled run's wall-clock grows with the page tree. With the flag a page/language/strategy is skipped when its latesttx_ntai_lighthouse_reportentry is newer than the last change to the page or its content elements — the sameMAX(pages.tstamp, MAX(tt_content.tstamp))comparison asanalyze-pages, but per strategy (abothrun never skips mobile just because desktop is recent). No schema change. Note: Lighthouse scores also drift without content edits (server, assets, third parties), so a periodic full pass is still worthwhile.nt_ai:lighthouse --limit N(-l) caps the number of pages measured per run (0 = no limit); skipped, unchanged pages do not count.
Changed¶
- The content-change detection behind
--changed-onlyis now a sharedPageChangeDetectorservice used by bothnt_ai:analyze-pagesandnt_ai:lighthouse, instead of being duplicated in the commands. No behavioural change toanalyze-pages.
[1.9.0]¶
Added¶
nt_ai:analyze-pages --changed-onlyre-scores only pages whose content actually changed. Every run previously re-analysed every page and re-sent its text to the AI provider, so a monthly schedule repeated the full cost even though only a fraction of pages change (and it was the one command that sends page text externally, which is unwelcome whenaudit.aiRules = 0). With the flag a page/language is skipped when its latesttx_ntai_page_scoreentry is newer than the last change to the page or its content elements — the comparison usespages.tstampandMAX(tt_content.tstamp), so an edit to a single content element is not missed. No schema change. A scheduled daily or weekly run now costs nothing on quiet days and picks up changes promptly instead of waiting for the next full inventory run.nt_ai:analyze-pages --limit N(-l) caps the number of pages analysed per run (0 = no limit). Skipped, unchanged pages do not count against the cap, so a catch-up run after a large editorial change processes at most N changed pages instead of everything in one pass — mirroring the per-run cap of the audit task.
[1.8.6]¶
Fixed¶
- Pages that permanently return HTTP 404 no longer block the scheduled audit or
mark it as failed. A page answering a clean 404/410 (e.g. a news/events detail
URL with no record) was counted as a technical failure: it wrote no report, kept
last_checked = 0, was re-selected first on every run, and a batch made up entirely of such pages tripped the "every page failed" rule and turned the task red on a healthy instance. A 404/410 is now treated as a result — excluded from the "every page failed" rule and deferred for a week — while genuine technical failures get an escalating cooldown (6 h → 24 h → 3 d → 7 d) instead of the previous flat six hours. Pages removed from the search index (no_index, EXT:seo) are skipped entirely. nt_ai:pdf-auditno longer dies silently on one file and blocks the rest. A memory-heavy PDF exhausted the CLImemory_limitinsidesmalot/pdfparser, a fatal that try/catch cannot catch: the run died with exit 255 and no output, and because the file never got a report it stayed first in the queue forever. A shutdown handler now records the in-progress file as "not auditable" (score 0) and writes a note to stderr, so the next run skips it and the crash is visible; files that throw are marked the same way. A new--max-size-mb Noption skips PDFs larger than N MB (also recorded) before parsing, for hosts with a tightmemory_limit. The memory requirement is now documented.
[1.8.5]¶
Added¶
- The PDF-accessibility dashboard widget can be scoped to a single folder. In a multi-site install most PDFs are never linked from a page, so the site scope of the other widgets does not fit them; files are organised by folder instead. The widget now has an "Ordner" setting (default "Alle Ordner") that filters the figures to a top-level folder of the file mount, and — like the site-scoped widgets — shows the selected folder as a badge (also in the empty state). Filtering uses the file's live storage path, so it stays correct even after a document is moved.
nt_ai:pdf-audit --folder="1:/GaD/"restricts a run to one folder, so the check can be split into per-folder scheduler tasks staggered over time instead of auditing every PDF in a single run.
[1.8.4]¶
Changed¶
- The site-scoped dashboard widgets now show which site they are scoped to. The Audit-Score, Page-Score and Lighthouse widgets carry a small pill naming the selected website ("Alle Websites" by default), so their figures are never read out of context — shown in the empty state too, before any data exists. The scope is still changed via the widget's settings (gear icon); this only makes the current choice visible.
[1.8.3]¶
Fixed¶
- The scheduled audit no longer stalls on pages that belong to no site. A page
that fails to audit — most commonly a shared record container (form, footer or
menu content) that lives outside every site — wrote no report, so its
last_checkedstayed 0 and it was re-selected first on every run. Once enough accumulated they occupied every slot and no new reports were produced at all, while the task still reported success.AuditTasknow (a) skips pages that belong to no site when choosing candidates, and (b) records each failed attempt and defers that page for six hours, so any failure cause (missing site, timeout, redirect) rotates out of the queue instead of blocking it. The deferral map is persisted with the task between runs.
Added¶
- Dashboard audit widgets can be scoped to a single site. In a multi-site
installation an average across every site tells a single site's editors nothing.
The "Audit-Score", "Seiten-Score" and "Lighthouse" widgets gained a "Website"
setting (default: all sites) that restricts their figures to the pages of one
site, resolved via
SiteFinder. (The token widgets stay instance-wide by nature; the PDF widget is file- rather than page-scoped.)
Changed¶
- The token-usage widget explains the "enabled but empty" state. When tracking
is on but nothing has been recorded this month — the common state right after
setup — the widget now says so instead of rendering a blank view that is
indistinguishable from a fault. The disabled message additionally names the exact
setting key (
tokenTracking.enabled).
[1.8.2]¶
Fixed¶
- Alt-text length capping no longer produces sentence fragments that look complete. When a generated alt text exceeded the character limit, the fallback cut at a word boundary (or, failing that, mid-word) and appended a full stop — so a truncated fragment read as a finished sentence, misleading both editors and the read-aloud feature ("… vor dem Haupteingang der Schule und hält."). The cap now (a) uses the last complete sentence that fits, with no minimum-length gate, so a short leading sentence is kept instead of discarded; (b) never breaks mid-word; and (c) marks a genuinely truncated text with an ellipsis, never a period. Before truncating at all, an overrun now triggers one retry with a stricter length instruction, and that length limit is stated in the output language (a German limit is followed far more reliably than the previous English-only line) — so German, compound-heavy descriptions come back as a whole sentence within the limit rather than being cut.
- The audit names the original file, not TYPO3's processed preview. Image
findings showed the rendered
csm_…_<hash>.jpgname, which an editor cannot find in the file list. Findings now display the original file name (resolved viasys_file_processedfile, with a decoration-stripping fallback). - Generating an alt text or description for a file outside the editor's file mount now returns a clear message instead of the raw "Access to table sys_file_metadata … was denied by a ModifyRecordEditUserAccessEvent listener" exception: „Diese Datei liegt außerhalb Ihrer Ordner. Bitte wenden Sie sich an die Administration."
[1.8.1]¶
Fixed¶
- The audit tabs no longer drop the selected page, so the declaration can be
generated again. The "Site-Übersicht", "Lighthouse" and "Erklärung" tab
links were built without the current page id, while the module reads the site
to work on from exactly that
id. Switching tabs therefore lost the tree selection: "Erklärung" showed "Bitte zuerst eine Seite … auswählen" and its generate button stayed hidden — even for administrators — and "Seitendetail" fell back to "(Seite im Baum wählen)". The three cross-tab links now carry the current page id, so the selection survives a tab switch and the declaration resolves its site. - The "Erklärung" tab is hidden from users who may not use it. 1.7.0 secured
the generate/publish actions and hid the workbench, but the tab itself stayed
visible and clickable, so an editor could land on a page where nothing could be
done. The tab is now omitted unless the user is an administrator or
declaration.editorsMayPublishis enabled — the same permission check that already guards the actions. Operators who hid the tab with custom CSS can drop that workaround.
[1.8.0]¶
Fixed¶
- The declaration's PDF statistics are now scoped to the site. After 1.7.0 made the page statistics site-specific, the PDF document counts (and the synthetic "Dokumente (PDF)" barrier) still aggregated every document in the installation, so one site's declaration reported documents belonging to other sites. Documents are now attributed to a site by reference — a PDF counts when a non-deleted file reference to it lives on a page or record of that site ("documents published on this site"); a PDF linked from no site is claimed by no declaration instead of installation-wide. Applies only when a site context is known; single-site installs are unchanged.
Added¶
ntai.declaration.pdfPathPrefixsite setting (and the globaldeclaration.pdfPathPrefixfallback) to scope PDF documents by folder instead of by reference — e.g.1:/documents/schule-a/. When set it is authoritative (deterministic and auditable, fits a per-site folder layout and also captures PDFs linked only from RTE text); when empty, reference-based attribution applies. Added to thenetthinks/nt-ai-declarationset.
[1.7.0]¶
Makes the accessibility declaration site-aware and safe for multi-site installations (reported on an install with 26 school sites sharing one page tree), and stops it from asserting features or measures that are not actually in place.
Security¶
- Generating and publishing the declaration is now administrator-only by
default. The declaration is a legally binding statement of the operator, but
the two write actions (
generateDeclarationAction,publishDeclarationAction) had no permission check beyond module access — any editor group that could see the audit module could publish one. TYPO3 has no per-module-tab permission, so operators previously had to choose between giving editors the whole module or none of it. Both actions are now gated onisAdmin(); the newdeclaration.editorsMayPublishswitch (default off) re-opens them to editors where wanted. Editors still see the tab and the facts.
Fixed¶
- The declaration target page is now resolved within the calling site.
DeclarationPublisher::resolveTargetPageUid()searched the whole page tree for a/barrierefreiheitslug and returned the lowest UID — in a multi-site install that is an arbitrary (possibly orphaned) page belonging to no site or to the wrong school. The page is now taken from the site of the page selected in the module tree: its per-sitentai.declaration.targetPageUidsetting first, then a/barrierefreiheitpage that actually belongs to that site. The globaldeclaration.targetPageUidis no longer used when a site is known (it is a single value shared by all sites and would collide across them). - Organisation, feedback contact and enforcement body are now per site.
DeclarationDataServiceread all four fields from the global extension configuration, so a feedback link for one school pointed at another. They are now read from the site'sntai.declaration.*settings with the global value as fallback, and the page statistics (total/clean/warning/error pages, known barriers) are scoped to the calling site instead of aggregating the whole installation. Per BITV 2.0 / Directive (EU) 2016/2102 the feedback mechanism must lead to the responsible body. - The "regular automated audit" measure is only claimed when it is true.
The measure was appended unconditionally; the declaration asserted a recurring
automated check even where the
AuditTaskscheduler task is switched off (e.g. during a migration). It is now added only when a non-disabledAuditTaskexists in the scheduler, mirroring the existing nt-lingua check.
Added¶
- New site set
netthinks/nt-ai-declarationcarrying the per-site declaration settings (ntai.declaration.targetPageUid,.organisation,.feedbackContact,.enforcementBody,.enforcementUrl). It ships no frontend TypoScript, so a site can manage its declaration data without enabling the frontend widget. Include it per site to edit these in the Sites module; the values also work when set directly inconfig.yaml. declaration.editorsMayPublishextension setting (see Security).
Changed¶
- The assistant, read-aloud and "simple language" measures are no longer
pre-filled.
declaration.measuresnow defaults to empty; the accessibility assistant and the read-aloud (TTS) measure are instead auto-derived from whether thenetthinks/nt-aifrontend set is actually active for the site (via itsntai.a11yWidget.enabled/ntai.tts.enabledsettings). Previously the factory default claimed a widget, a read-aloud feature, "sufficient colour contrast" and "alt texts" even on sites that shipped none of them. On installs created before this change the old string is already stored insettings.phpand should be cleared once.
[1.6.0]¶
Fixed¶
- The scheduler audit now rotates across the entire page tree.
AuditTasksorted the candidate pages byuidascending, without taking into account when a page was last checked — every run checked the same pages with the lowestuid, and the rest of the tree was never reached (reported with 1,216 pages andlimit = 25: the same 25 pages every night, 1,191 never checked). The candidates are now sorted by their last report date — never-checked pages first — via a LEFT JOIN ontx_ntai_audit_report. A fixedlimitthus covers the complete inventory across multiple runs. The report table'sdeletedfilter is placed in the JOIN condition so that never-checked pages (NULL) are not dropped; the page restrictions (deleted/hidden) are set explicitly. audit.aiRulesnow actually disables the AI audit rules. The setting had a getter (ConfigurationService::areAiRulesEnabled()) that was never evaluated anywhere — the five AI rules ran as soon as any provider key was configured, even withaudit.aiRules = 0.AuditServicenow checks the toggle as well, so operators can keep page text away from the AI provider while the alt-text feature remains usable at the push of a button. Important for the data protection documentation ("which data goes when where").- The custom FormEngine elements evaluate the base render hooks.
ntAiAltText,ntAiImageDescriptionandntAiTextnow callrenderFieldInformation(),renderFieldControl()andrenderFieldWizard()— in addition to the field's TCAdescription(which is already rendered viarenderLabel()). Adescriptionset on the field — e.g. a data protection notice right at the AI button — is now displayed instead of being silently discarded.
Documentation¶
- Installation: The backend modules "AI Tools" and "Accessibility Audit" are
registered with
access = userand are therefore invisible to editor groups until they are granted via be_groups → "Module" (groupMods) — added to the installation guide.
[1.5.0]¶
Added¶
- Self-hosted read-aloud (text-to-speech). A ReadSpeaker-style read-aloud feature, admin-switchable between two engines: the browser's Web Speech API (local, free) or cloud TTS (premium voices, generated server-side and cached — the API key stays on the server). It reads
#maincontentblock by block with highlighting, a player bar and keyboard control, and is clearly labelled as an addition to — not a replacement for — screen readers. Placeable inside the accessibility widget and freely in templates ([data-nt-tts]/ theReadAloudFluid partial). Ships astts.js,TtsServiceandTtsMiddleware; configured via the site settings (engine / rate / scope) and the nt-ai extension configuration (cloud provider / key / voice). - Alt text and image description directly on the image in the content element. The
AI buttons previously only appeared on
sys_file_metadata(the file list). An image in a content element is asys_file_referencewith its ownalternative/descriptionfields — these are now wired up as well, so that the text can be generated where the image sits, without the detour via the media module. A newFormAiFileResolverresolves both contexts in one path (metadata viafile, reference viauid_local), and the request's HMAC additionally binds the table.
Changed¶
- Image description in the language of the record. The image-description controller previously always generated in the default language; now a description on an English page comes back in English — as the alt text already did.
[1.4.0]¶
Changed¶
searchFieldsremoved from the TCA oftx_ntai_audit_report. TYPO3 14 no longer evaluates the entry in thectrlsection and reports a deprecation at bootstrap; which fields are searchable is now declared assearchableon the respective column. As with the point below, this affects every test suite that runs withfailOnDeprecation.ext_emconf.phpremoved, metadata moved intocomposer.json. TYPO3 14.3 discardsext_emconf.phpand expects insteadextra."typo3/cms".versionandextra."typo3/cms".Package.providesPackages. Without both, the PackageManager reports a deprecation as soon as the extension is installed in a TYPO3 14.3 project — which turns every test suite withfailOnDeprecationred. The version is deliberately placed in theextrablock and not at the top level, because the package is delivered via Git tags.
Added¶
- Public API for addressing a specific provider.
Netthinks\NtAi\Provider\AiProviderLocatorInterface(@api,public: true) returns, viaget('openai'), a provider that stays permanently bound to this identifier. Previously, the public interface always resolved the configured default provider — correct for the normal case, but it made it impossible to build a fallback chain: an external extension that wants to try OpenAI first and Anthropic on failure could not name the second provider and would have had to access the internals (ProviderRegistry). In addition,getAvailableProviders()returns the registered and configured providers, so that a chain can skip unconfigured providers. The returned objects implement the sameAiProviderInterface— calling code remains unchanged.
Changed¶
ProviderAdapterno longer maps requests and responses itself, but delegates to the locator. Behavior unchanged: the default provider is still resolved anew on every call, so that configuration changes take effect without rebuilding the container.
Fixed¶
- Developer documentation named the wrong interface for custom providers.
docs/developer.de.mdanddocs/developer.en.mdreferred to the publicNetthinks\NtAi\Provider\AiProviderInterface, but showed the methods of the internalNetthinks\NtAi\Service\Provider\AiProviderInterfaceandAiResultinstead ofAiResponseas the return type. Anyone who followed the example got a class that could not be tagged as a provider. The RST version was already correct.
[1.3.6]¶
Fixed¶
- Accessibility window protruded at the top on flat screens. The
height of the panel was bound to the window height with
min(80vh, 640px), not to the space actually available above the button. On an iPad in landscape orientation (695 px visible height), the heading and close button were outside the image. The height now calculates with the configured edge margin, the button size and a little breathing room;dvhadditionally accounts for the retracting and extending bars of mobile browsers. Measured at 1024x695, 1024x600, 768x1024 and 390x664: the panel stays completely within the image.
[1.3.5]¶
Fixed¶
- Score-alert emails failed under TYPO3 v14:
AlertServicesent viaMailMessage::send(). This method was removed in TYPO3 v14 —MailMessageinherits from Symfony'sEmail, which does not havesend(). As soon as a threshold was actually breached, the send would have aborted with a fatal error. The send now runs viaMailerInterface; the system sender is automatically added as before.
[1.3.4]¶
Changed¶
- Deliver the a11y/TTS init script CSP-nonce-capable: The inline script that
sets the
data-nt-*configuration early on the<html>tag (including dark mode before the first paint) is no longer output as a raw<script>viapage.headerData, but viapage.jsInline(PageRenderer). As a result, with an active Content Security Policy, it automatically receives the request nonce (script-src 'nonce-…') and is no longer blocked. It still runs synchronously in the<head>(no dark-mode flash). A prerequisite for switching to an enforcing frontend CSP in the sitepackage.
[1.3.3]¶
Fixed¶
- "Erklärung" tab also visible in the page detail: The tab previously appeared only in the site overview and in Lighthouse, not in the page detail view. It is now displayed (like the Lighthouse tab) in all audit views.
[1.3.2]¶
Changed¶
- Accessibility declaration without internal tool names: The draft spoke of "automated audit by nt-ai" — meaningless to readers. It is now phrased neutrally ("self-assessment using automated accessibility testing tools as well as supplementary manual review"); the measures texts also no longer name any product names (nt-ai/nt-lingua). The prompt additionally instructs the AI not to name any software products.
[1.3.1]¶
Fixed¶
- Lighthouse scheduler still reported an error: The command returned
FAILUREas soon as ONE page failed — but without a PSI API key, a run across all pages inevitably hits Google's rate limit (429), so that individual pages fail and the scheduler marked the whole task as failed. A monitoring run is now only considered a failure in the case of a total failure (not a single page successful); partial failures are warnings. Additionally, a hint aboutlighthouse.apiKeywhen errors occur without a key. - Accessibility declaration: The notice box in the module ran across multiple
columns because the flex container arranged each inline element (icon,
<strong>) as its own column. The text now sits in its own block — the notice again flows normally on a single line around the icon.
[1.3.0]¶
Added¶
- AI alt texts for PDF documents: PDFs can now also receive an alt text
(relevant when a PDF is embedded as a preview image with a link to the document).
Since a PDF does not go to the vision AI, its text content is
extracted (
PdfTextExtractor) and summarized by the text AI into a concise alt text. - Works at the same "Alt-Text generieren" button in the file metadata form (detects PDF automatically) as well as at the generic AJAX endpoint.
- New CLI switch
nt_ai:generate-alt-texts --include-pdf(opt-in; without it, it stays with images, so that existing cron runs remain unchanged). - Scanned PDFs without a text layer are skipped with a clear message.
Changed¶
- Extension configuration made clearer: The seven individual provider
tabs (Anthropic, OpenAI, Gemini, DeepSeek, Mistral, Groq, Ollama) are combined into
one "KI-Anbieter" tab (labels with provider prefix),
Rate-Limitunder "Allgemein", the "Barrierefreiheitserklärung" tab shortened to "Erklärung" — 16 tabs → 9, so that the tab bar fits into the window. Only categories/labels changed, no keys — existing values remain.
Fixed¶
- Accessibility declaration: "Leichte Sprache" (easy-to-read language) is no longer listed across the board as a measure. The simplification language is now configurable (default: only "Einfache Sprache"), so that "Leichte Sprache" only appears when actually enabled. Multilingualism (nt-lingua) is still added automatically.
[1.2.0]¶
Added¶
- Accessibility declaration generator (new "Erklärung" tab in the audit module): generates, from the existing audit data, the draft of a declaration on accessibility (based on the BITV template text / BFSG § 14, EU 2016/2102) and publishes it after editorial approval on the accessibility page.
DeclarationDataService: aggregates the conformity status, affected WCAG barriers (identical grouping as the BFSG overall report), PDF results, measures (config + automatically detected nt-lingua/nt-ai audit) and the audit date.DeclarationGenerator: AI draft de/en, only from the supplied facts, missing mandatory information as "[bitte ergänzen]".DeclarationPublisher: publication via DataHandler (versioned, access-checked). On the first time, the existing declaration element is adopted and updated (no duplicate); the managed record is remembered per page/language insys_registry.- New extension settings tab "Barrierefreiheitserklärung" (
declaration.*): target page, organization, feedback contact, enforcement body, measures. - AJAX routes
nt_ai_declaration_generate/nt_ai_declaration_publish.
[1.0.34]¶
Fixed¶
- Lighthouse scheduler task failed ("Task failed to execute successfully",
task type
nt_ai:lighthouse): The scheduler passes all options — an unset strategy arrived as an empty string and went unchecked into the PSI request (strategy=""). Empty/invalid values now fall back to the configured default (lighthouse.strategy).
[1.0.33]¶
Added¶
- PDF accessibility phase 3a: Dashboard widget for PDF results and a dedicated "Dokumente" section in the BFSG overall report (BFSG also covers documents).
[1.0.32]¶
Changed¶
- Docs: PDF accessibility fully documented (user manual + configuration).
[1.0.31]¶
Fixed¶
- Upload gate warning disappeared after 5 seconds. TYPO3 hides notifications
without an explicit client duration for all severities except
errorafter 5 s — anderrorseverity is not an option, because it flips the upload response tohasErrors=trueand the stored file would be incorrectly marked as "failed". A newpdf-upload-notice.jsis loaded once into the backend main frame viaAfterBackendPageRenderEvent(all notifications render there, no matter which module iframe they come from) and sets the duration to 0 for the gate messages — they stay put until the editor actively closes them. Only active when the gate is switched on.
[1.0.30]¶
Changed¶
- Upload gate: default severity is now
warning(pdf.uploadGateSeverity) — the gate thereby also takes effect by default on warnings (e.g. missing document title), not only on errors.
[1.0.29]¶
Added¶
PDF accessibility phase 2: upload gate
- New
PdfUploadGateListener(PSR-14): checks PDFs at backend upload —pdf.uploadGate=off(default) /warn(file is saved, FlashMessage with findings) /block(upload is rejected with a message including specific findings; the temp file is already analyzed inBeforeFileAddedEvent, no FAL entry is created). Thresholdpdf.uploadGateSeverity(error/warning),pdf.uploadGateAdminBypass(admins receive only the warning instead of the block). Frontend form uploads are exempt. - The block message uses
ResultException/ResultMessage— the only way in whichExtendedFileUtilityrenders a custom message text in the upload dialog (a RuntimeException is swallowed into the generic "write permissions" message). - Every backend PDF upload automatically persists a report
(
triggered_by=upload) — the module stays current without manual runs.
[1.0.28]¶
Fixed¶
- PDF module: "Befunde"/"Prüfen" loaded the backend nested in the content frame.
The links were relative
?detail=…URLs without a module route/token — TYPO3 redirected to the backend root. All module links are now generated via the backend router (UriBuilder::buildUriFromRoute). - German module label:
de.locallang_pdf_mod.xlfadded — the module is now named "PDF-Barrierefreiheit" in the German backend instead of "PDF Accessibility".
[1.0.27]¶
Added¶
PDF accessibility (phase 1: inventory check)
- New subsystem
Pdf/:PdfAnalyzerextracts the accessibility-relevant facts in a single parse pass (smalot/pdfparser, pure PHP — runs without Java/binaries even on shared hosting); 9 rules check them against Matterhorn/PDF-UA: tagged PDF, scanned pages without a text layer, document language, document title (+ DisplayDocTitle), figure alt texts, encryption, font embedding, bookmarks, PDF/UA marking. Reuses the audit DTOs (Finding/Severity/Report) → consistent scores and severities. - Backend module "PDF Accessibility" (Media): all FAL PDFs with traffic-light status/score, findings list per file with recommended action and WCAG/PDF-UA reference, single and batch check ("new & changed").
- CLI/Scheduler
nt_ai:pdf-audit(--limit,--force,--fail-on=error|warningfor CI). New tabletx_ntai_pdf_report(one report per file, upsert); unchanged files (same SHA1) are skipped — scheduled runs are practically free. - Planned (phase 2): upload gate (warn/block analogous to the Publishing Quality Gate); phase 3: AI rules, veraPDF connection, dashboard widget, BFSG section.
[1.0.26]¶
Added¶
- Site-wide keyboard guards (WCAG 2.4.1 / 2.4.7) in
a11y-widget.css: skip links with.visually-hidden-focusableare made visible on keyboard focus (many themes don't provide the resolving:focusrule — the link stayed invisible at 1×1 px for sighted keyboard users), and a:focus-visiblesafety net enforces a visible focus outline, even when the theme suppressesoutlineglobally (keyboard only; mouse focus stays theme-styled). Both guards with a dark-mode variant.
[1.0.25]¶
Fixed¶
- Accessibility widget and TTS player marked as
translate="no". The UI has its own i18n; DOM translators (nt_lingua) previously collected and translated their labels along with it (appeared e.g. as "Deficienza di colore" in the cache). Containers now carrytranslate="no"/data-nt-notranslate.
[1.0.24]¶
Added¶
- Concurrent completions (
ConcurrentAiProviderInterface). New optional public capability:completeMany()performs several independent text completions simultaneously (curl_multi inAbstractHttpProvider, implemented for the OpenAI provider). Consumers (e.g. nt_lingua) checkinstanceofand otherwise fall back to sequentialcomplete()calls. Speeds up large batches split into chunks considerably (N-times serial → ~one roundtrip).
[1.0.23]¶
Added¶
- Setting
ntai.a11yWidget.scaleTarget(CSS selector, default#maincontent): determines which element font enlargement and color filters act on. This way a customer theme with a differing content wrapper can set the target itself, without fixed elements (sticky header, back-to-top, widget) being scaled/filtered along with it.
Fixed¶
- Color-deficiency filter now takes effect. The SVG
url()color-matrix filter was on<html>, where Blink/WebKit silently ignore it → no visible effect (contrast/saturation as function filters did take effect). Text size (zoom) and color filters are now applied via JS as an inline style on a configurable content element (instead of on<html>). - Color-deficiency modes are now correction instead of simulation. The matrices (red/green/blue color deficiency) daltonize — they separate confusable colors, instead of simulating the visual impairment; matching the setting text.
[1.0.22]¶
Fixed¶
- Accessibility overlay & TTS player dark in manual dark mode. The
widget chrome dark colors previously only applied under OS dark
(
@media prefers-color-scheme: dark). A newhtml[data-nt-theme="dark"]block ina11y-widget.cssmirrors them, so that the overlay and player also appear dark when dark mode is activated via the widget (including!importantagainst the hover hardening). - No white flash on page change in dark mode. The set's inline config script
now reads
localStorage.ntA11yPrefsbefore the first paint and setsdata-nt-theme="dark"immediately — no more FOUC.
[1.0.21]¶
Added¶
Reusable site set netthinks/nt-ai
- nt-ai now ships its own site set (
Configuration/Sets/NtAiFrontend) with: settings definitions (categories accessibility / read-aloud, prefixntai.*), asset includes (a11y-widget.js/.css,tts.js) and the inline config script (data-nt-*on the<html>). Customer systems now include only one dependencynetthinks/nt-aiin their site configuration — no more manual maintenance ofsettings.definitions.yaml. Site-specific theming (dark-theme colors, tweaks, read-aloud button in the template) stays in the respective sitepackage. - Setting prefix migrated from
netthinks.*tontai.*;TtsMiddlewarereadsntai.tts.*accordingly.
[1.0.20]¶
Added¶
Read-aloud: player control + click-and-read
- Player bar extended: back/forward (paragraph navigation) as well as controls for
volume and speed (cloud live via
playbackRate/volume, Web Speech from the next paragraph or on release). Complements pause/stop. - Click and read as a widget mode: activate and click on a paragraph
to read aloud from there (
window.ntTts.setClickMode).
Fixed¶
- Seek fix: forward/backward seeking no longer read two paragraphs in parallel — the
AbortErrorwhen aborting cloud playback no longer triggers a Web Speech fallback.
[1.0.19]¶
Added¶
- TTS pronunciation replacements. A new site setting to replace terms before
speech output (format
Begriff=Aussprache, several separated by|), e.g.TYPO3=Typo drei. Only the speech output changes, the visible page text stays.
[1.0.18]¶
Added¶
- "Vorlesen" icon in the navbar (next to the language selection) as a freely placeable trigger of the read-aloud feature.
Fixed¶
- Widget hardening against theme bleed: generic
button:hover/a:hoverrules of the theme (white text,::aftereffects, letter spacing) made the icon/options unreadable on hover and caused line breaks. Colors/metrics are now enforced via!important(hover = dark, never white), decorative pseudo-elements neutralized.
[1.0.17]¶
Added¶
Accessibility widget (frontend, stage 1)
- New frontend personalization widget (deliberately not an accessibility overlay):
text size, text spacing (WCAG 1.4.12), contrast, dark mode, reduce
animations, highlight links, more readable font, reading aid (ruler),
hide images. Settings stored locally in
localStorage, no tracking. - No DOM/ARIA rewriting: the preferences act via
data-nt-*attributes on<html>and pure CSS feature layers. The widget itself is fully keyboard/ AT operable (aria-expanded/aria-pressed,Esc, focus return, no focus trap) and respects system settings (prefers-reduced-motion/-contrast/-color-scheme). - Build-free delivery:
a11y-widget.css+a11y-widget.js(classic script). Additionally contains a site-wideprefers-reduced-motionguard for the theme scroll animations (.animate-box) and focus visibility in Windows contrast mode (forced-colors). - Backend-configurable via the site settings (accessibility):
widget on/off, position (4 corners), label on/off. Default: subtle
symbol at the bottom right (does not cover the cookie button). Config reaches the
frontend as
data-nt-a11y-*attributes on the<html>tag (inline script in the<head>; the sitepackage set delivers assets + attributes). - Font-size scaling made more robust via an
!importantCSS rule perdata-nt-fontscale(instead of inline style), index logic corrected (level 0 = neutral). - Widget chrome more subtle (icon launcher, text optional) and in
px/eminstead ofrem, so that it does not grow along with the font scaling itself. - Symbol size and edge margins (horizontal/vertical) configurable via the site settings
(
--nt-a11y-size/-offset-x/-offset-y). - Native dark mode instead of an invert filter: the widget dark mode now switches
html[data-nt-theme="dark"]and thus a real dark theme (nt-dark-theme.cssin the sitepackage; the brand orange remains an accent). First version — fine-tuning of individual sections follows. prefers-contrast: moreis now taken into account site-wide (links underlined, stronger focus/border rendering);forced-colorssupport extended.- Font size via page zoom instead of root
font-size(the theme sizes px-based, so the scaling was ineffective) —zoomon<body>, reflow-friendly. - Position setting as a dropdown (site settings
enum+labels.xlf/de.labels.xlfwith German labels) instead of free text. - Zoom now acts on
#maincontentinstead of<body>— otherwise fixed elements (launcher, back-to-top) were shifted and hero headings (home page/subpages) were enlarged along with it. - The launcher sits freely above the bottom corner (spacing to back-to-top/cookie badge).
- "Hide images" leaves the logo, navigation, hero and footer untouched.
- "Reset everything" additionally as a symbol (↺) at the top of the panel next to the close X.
- Explanatory tooltip on the launcher (hover/focus) with a short description + shortcut hint — important when only the symbol (without a label) is displayed.
- One-time first-visit hint: the tooltip appears automatically on the first visit
and is remembered (
localStorage), no longer on subsequent pages. - Keyboard shortcut Alt + 1 opens/closes the panel; all features are operable by keyboard
(real buttons, focus moves into the panel,
Esccloses with focus return). - New features (EyeAble parity): blue-light filter (warm overlay, intensity control),
color-deficiency filter (red/green/blue color deficiency via SVG color matrix), color saturation/
grayscale (control), larger mouse cursor, mute sound (including later
loaded media), focus highlighting. Contrast gets an intensity control.
Filters (contrast/saturation/color deficiency) are composed as one
--nt-filterchain on<html>; blue light as a separate overlay. - Explanatory text per menu item: on hover/focus, a short description appears at the side of the panel.
Read-aloud (text-to-speech) — ReadSpeaker replacement
- Self-hosted read-aloud feature, hybrid and admin-switchable: Web Speech API
(browser-native voices, free, local) or cloud TTS (premium voices, currently
OpenAI). Reads
#maincontentblock by block with highlighting of the spoken paragraph, a player bar (pause/stop), fully keyboard-operable. - Usable in the widget ("Webseite vorlesen") and freely placeable in the template via
[data-nt-tts]buttons or the Fluid partialReadAloud(e.g. next to the click path). - Cloud audio is cached server-side (
TtsService+ntai_ttscache), the key stays on the server; the client automatically falls back to Web Speech on cloud errors. FE endpoint/nt-ai/ttsviaTtsMiddleware. Config: site settings (engine/rate/scope) + nt-ai ext config (cloud provider/voice/model). - Fix: pause in the player was unreliable (
speechSynthesis.pause()browser-dependent) → now cancel + restart of the current paragraph on "Continue", with a guard against a race. - Fix: oval pause/stop buttons (theme
buttonstyles) hardened. - Cloud TTS in the token/cost dashboard: every real cloud generation (cache miss)
is recorded (
TokenUsageService::trackTts, context "tts", character count in the input field). The pricing table extended by TTS (USD per 1M characters:tts-115,tts-1-hd30,gpt-4o-mini-tts12). The "Token usage" widget shows the row "Vorlesen (TTS, Zeichen)" including estimated costs; cache hits cost nothing. - Cloud TTS endpoint secured: responds only with site engine
cloud, rejects cross-site requests (Sec-Fetch-Site), rate limit per IP (tts.rateLimit, default 30/min., HTTP 429 withRetry-After),X-Robots-Tag: noindex. Cache hits do not count towards the limit. - Documentation comprehensively updated: all widget features (blue light/color deficiency/saturation/ cursor/sound/focus, controls, explanatory texts), symbol selection, read-aloud feature including protection & costs (user-guide + configuration, de/en).
Launcher symbol selectable
- Eight vector-rebuilt accessibility symbols (person in a double circle,
person in a circle, person with arms, person on a line, person in a filled circle,
wheelchair, active wheelchair, hand with person) — selectable in the site settings
(
data-nt-a11y-icon), all single-color (currentColor).
[1.0.16]¶
Added¶
Lighthouse in the page layout
- The AI assistant panel in the page layout module shows a new column
"Lighthouse" with the four measurement rings of the page's most recently saved
measurement (read-only, link into the module). New endpoint
LighthouseAjaxController::latestAction(routent_ai_lighthouse_latest).
Changed¶
- Page-score tips in the backend language. The improvement suggestions of the
AI content-quality score are now generated in the UI language of the backend user
instead of in the language of the checked page (a German backend delivers
German tips even for English pages).
PageScoreService::analyzePage()receives an optionalsuggestionLanguageCode;ScoreAnalysisAjaxControllerpasses the backend language.
[1.0.15]¶
Added¶
- Overall report (cross-page): a new printable BFSG report across all
audited pages — aggregated conformity verdict, key figures including
average score, page overview (worst first) and the most frequent
WCAG barriers with occurrences + affected pages. Button "Gesamtbericht (BFSG)"
in the site overview (
BfsgReporter::renderOverview, routent_ai_audit_report_overview); considers only accessible pages.
[1.0.14]¶
Added¶
- Print-optimized CSS for both BFSG reports:
print-color-adjust: exactpreserves the severity/verdict colors in the PDF, screenshot and snippets are limited, clean page breaks. A dedicated CSP header so that the "Print" button works despite the backend CSP.
[1.0.13]¶
Fixed¶
- BFSG report: findings appeared without text. The reporter iterated the
top-level keys of the
findingscolumn instead of the actual findings list under thefindingskey (the column storesReport::toArray()). Now the message, severity, WCAG group, recommendation and snippet are displayed correctly. - BFSG report: "Print / save as PDF" not working. The backend CSP blocked the inline script; the report response now sets a dedicated CSP header (which the CSP middleware respects).
- Report button showed a broken icon (
actions-file-reportdoes not exist) →actions-file-pdf.
[1.0.12]¶
Added¶
BFSG report & screenshots
- New printable accessibility report (BFSG / EN 301 549 / WCAG 2.2):
findings grouped by WCAG criterion, conformity statement, score +
Lighthouse a11y, screenshot evidence and disclaimer. Button "Bericht (BFSG)" in the
audit module (
BfsgReporter, routent_ai_audit_report), as standalone HTML for printing/saving as PDF. - Lighthouse: the screenshot rendered by PageSpeed Insights
(
fullPageScreenshot, otherwisefinal-screenshot) is now saved (new columntx_ntai_lighthouse_report.screenshot) and displayed as visual evidence in the report.
[1.0.11]¶
Added¶
Publishing Quality Gate
- New
QualityGateHook(DataHandlerprocessDatamapClass): warns about or blocks publishing a page while its last accessibility audit still has findings at or above the configured severity.blockmode vetoeshidden=0in the pre-hook and forces the page to stay unpublished;warnmode shows a flash message after save. - Extension configuration (Audit tab):
audit.qualityGate(off/warn/block),audit.qualityGateSeverity(error/warning),audit.qualityGateScope(onPublish/onSave),audit.qualityGateAdminBypass. Uses the latest stored report — keepaudit.autoOnSaveon so it stays current (fix → save → re-audit → publish passes). nt_ai:audit --fail-on=error|warning— non-zero exit code for CI/CD quality gates.
Audit not-yet-published pages (preview fetch)
- The audit can now render and check pages that are not publicly reachable yet —
future start date, hidden, or expired — so issues are caught before go-live
(which is the whole point of the quality gate).
PageFetcherappends a short-lived, HMAC-signed preview token (AuditPreviewToken); a new frontend middleware (AuditPreviewMiddleware) verifies it and relaxes time/visibility for that single render only. Signed with the site encryption key, so only this installation can mint a valid token; the preview render is not cached. - Module audits and on-save audits use it automatically; CLI:
nt_ai:audit --preview. - The audit module now reports the real reason when a page can't be reached (e.g. "not published yet / hidden") instead of a generic "Audit fehlgeschlagen".
[1.0.2] - 2026-07-09¶
Changed¶
- SEO AI: language instruction in the prompt reinforced — more consistent target language for generated SEO fields.
[1.0.1] - 2026-07-09¶
Added¶
SEO editing
- SEO field assistant in the SEO panel of the Page Layout module. Generate
→ preview → apply buttons for the SEO title, meta description and
focus keyphrase, each derived from the rendered page content and saved to
the matching
pagescolumn via DataHandler. - Inline "KI" buttons in page properties on
pages.seo_title,pages.descriptionandpages.tx_ntai_focus_keyphrase(in addition to the page teaser).AiTextElementnow renders both single-line inputs and textareas; supported fields and prompts live in the sharedPageAiFieldProvider. - Inline image-description button on
sys_file_metadata.description(vision-based caption, complementary to the alt-text button). - Inline teaser button on
pages.abstractthat generates a teaser from the rendered page content.
[1.0.0] - 2026-07-09¶
First public Git release (Composer). Bundles the previously internally developed features (see internal history below) plus the following new subsystems.
Added¶
Score history and sparkline
tx_ntai_page_scoreis now append-only (addedtstampcolumn, removeddeletefromPageScoreRepository::save()). Every analysis run creates a new row so historical data is preserved.PageScoreRepository::getScoreHistory(pageUid, languageUid, limit)— returns the last N runs in chronological order withcrdateandscoresper run.PageScoreRepository::getAverageScores()andgetScoreDistribution()fixed to consider only the latest row per (pid, sys_language_uid) pair (PHP deduplication on uid DESC); with multiple history rows per page the averages were otherwise counted multiple times.PageScoreRepository::countAnalyzedPages()now counts distinct(pid, sys_language_uid)combinations viaGROUP BYinstead of total rows.ScoreAnalysisAjaxController::latestAction()returns ahistoryarray (last 8 runs).inline-audit.js—renderScorePanel()shows a row of coloured sparkline dots (green/amber/red by average score) when ≥ 2 historical runs are available.
Lighthouse monitoring (Google PageSpeed Insights)
- New DB table
tx_ntai_lighthouse_report— stores Lighthouse category scores (Performance, Accessibility, Best Practices, SEO, 0–100) and Core Web Vitals (LCP, CLS × 1 000, INP, FCP, TTFB) per page + language + strategy (mobile/desktop). - New
Lighthouse/LighthouseService— calls the PSI v5 API, parses the JSON response, saves toLighthouseRepository. Requires the analysed URL to be publicly reachable. - New
Lighthouse/LighthouseRepository— save, findLatest, findHistory, getAverageScores, countAnalyzedPages, findAllLatest. - CLI command
nt_ai:lighthouse [pageId] [recursion] [--strategy=mobile|desktop|both]— schedulable, mirrorsnt_ai:analyze-pagesin structure. - New
Dashboard/Widget/LighthouseWidget— four gauge rings (Performance, Accessibility, Best Practices, SEO) showing averages across analysed pages; shows strategy label and page count. - New extension settings tab Lighthouse:
lighthouse.apiKey,lighthouse.strategy. - Dashboard widget
nt_ai_lighthouse(large,dashboard.widget.nt_ai_lighthouse). ConfigurationService::getLighthouseApiKey(),getLighthouseStrategy().
Email alerts (score threshold monitoring)
- New
Service/AlertService— compares latest AI content scores and latest Lighthouse scores against configurable thresholds; sends one summary HTML email per run listing all violating pages. No per-page spam; one mail covers all violations. - CLI command
nt_ai:alert-check— schedulable; designed to run daily after the nightlynt_ai:analyze-pagesandnt_ai:lighthouseruns. - New extension settings tab Alerting:
alerting.enabled,alerting.recipient(comma- separated),alerting.thresholdGeo/Performance/Semantics/Keywords/Accessibilityfor AI scores andalerting.thresholdLighthousePerformance/Accessibility/BestPractices/Seofor Lighthouse. Zero = disabled for that category. ConfigurationService::isAlertingEnabled(),getAlertRecipients(),getScoreThresholds(),getLighthouseThresholds().
AI Content Quality Scoring (Seiten-Score)
- New subsystem
Score/withPageScoreService,PageScoreRepository,ScoreRangeenum. - DB table
tx_ntai_page_score— append-only history (see Score history above). Stores JSON scores for five categories and JSON suggestions list. - Five score categories analysed per page via LLM (rendered HTML as input):
geo(GEO/SEO),performance(page structure),semantics(HTML5/ARIA),keywords(focus keyword density),accessibility(WCAG signals). Scale: 0–100 each. - Focus keyword read from
pages.tx_ntai_focus_keyphrase— the existing SEO field is re-used so no extra editor input is required. - Score thresholds: High ≥ 75 (green), Medium ≥ 50 (amber), Low < 50 (red).
- Suggestions are LLM-written, language-aware (language of the analysed page), max 120 chars each, max 2 per category, only when score < 80.
- AJAX routes
nt_ai_score_analyze(POST) andnt_ai_score_latest(GET) for the inline panel. - CLI command
nt_ai:analyze-pages [pageId] [recursion]— bulk analysis, supports all configured site languages per page; registered as schedulable Scheduler command.
Page Layout module — Seiten-Score panel (3rd inline column)
PageLayoutAuditListenernow renders a third column "Seiten-Score" beside the Barrierefreiheits-Audit and SEO panels.- Displays five SVG gauge rings (52×52 px, colour-coded) with the scores and the improvement suggestions list below.
- "Neu analysieren" button triggers analysis on demand and refreshes the panel.
- Language resolved from the module URL (
?languages[0]=…) for correct per-language results when editors switch language view.
Dashboard — Score widgets
- Five
DoughnutChartWidgetinstances (one per category): display the average score across all analysed pages as a gauge ring — e.g.85 / 100filled 85 % in the category colour. Legend hidden; score is shown in the chart label. - New
PageScoreOverviewWidget(large widget): renders five mini rings with their average scores plus the total count of analysed pages, all in one widget. PageScoreRepository::getAverageScores()andcountAnalyzedPages()power both.
Token usage — Cost estimation
- New
Configuration/ModelPricing.php— built-in USD price table for 35+ models across all supported providers (OpenAI, Anthropic, Gemini, DeepSeek, Mistral, Groq). Keys are model-name prefixes; longest prefix wins, so versioned names likegpt-4o-mini-2024-07-18resolve correctly. - New
CostEstimationService— calculates(input_tokens / 1M × input_price) + (output_tokens / 1M × output_price)in USD; converts to EUR on request; formats as~$0.19or~0,18 €. - Three new extension settings:
tokenTracking.currency—USDorEUR(default:USD).tokenTracking.eurRate— conversion rate (default:0.92).tokenTracking.customPricing— JSON object to override or extend the built-in price table (e.g. private/fine-tuned models).TokenUsageWidgetshows a blue "ca. Kosten" box below the token total. When a model is not in the pricing table a warning with the model name is shown.TokenUsagePerUserWidgetgains a "ca. Kosten" column (per-user × per-model aggregation with separate input/output token queries).
New AI providers (4 added)
- Google Gemini —
GeminiProvider(gemini-2.0-flashdefault). Vision supported. - DeepSeek —
DeepSeekProvider(deepseek-chatdefault). No vision. - Mistral AI —
MistralProvider(mistral-small-latestdefault). Vision onpixtral-large-latest. - Groq —
GroqProvider(llama-3.3-70b-versatiledefault). No vision.
All four providers are configurable via separate Extension Configuration tabs.
New audit rules (15 added since v1.1.0)
Deterministic rules:
| Rule | WCAG | Severity |
|---|---|---|
IframeTitleRule |
4.1.2 | Error |
ButtonTextRule |
2.4.4 | Warning |
DuplicateIdRule |
4.1.1 | Error |
DocumentLandmarkRule |
1.3.1 / 2.4.1 | Warning |
TableHeaderRule |
1.3.1 | Warning |
FocusOutlineRule |
2.4.7 | Warning |
AriaHiddenFocusRule |
4.1.2 | Error |
VideoCaptionsRule |
1.2.2 | Error |
MetaRefreshRule |
2.2.1 | Error |
ReadabilitySentenceLengthRule |
3.1.5 AAA | Notice |
ReadabilityParagraphLengthRule |
3.1.5 AAA | Notice |
ReadabilitySubheadingRule |
3.1.5 AAA | Notice |
SeoTitleLengthRule |
2.4.2 (SEO) | Warning |
SeoMetaDescriptionLengthRule |
SEO | Warning |
SeoKeyphraseRule |
SEO | Error/Warning/Notice |
AI-powered rules:
| Rule | What AI checks |
|---|---|
AiAltTextQualityRule |
Quality of existing alt texts (generic, filename-based, nonsensical) — up to 20 images |
AiHeadingQualityRule |
Descriptiveness of headings — up to 15 headings |
AiMetaDescriptionRule |
Accuracy and clickability of meta description |
Total: 26 audit rules (9 deterministic + 3 AI in v1.1.0 → 23 deterministic + 5 AI now counted as two batches; 3 AI rules in this release).
Fixed¶
- Upload error message suppressed correctly.
GenerateAltTextOnUploadListenerwrapped only the AI call (generateSafe) but left DB operations (language overlayinsert/update) andgetForLocalProcessing()uncovered. Any exception from these paths surfaced as "Upload fehlgeschlagen" in the backend even though the file was stored successfully. The entire processing logic is now wrapped in a top-leveltry/catchin__invoke(); errors are logged and the upload always completes. - Dashboard score widgets showed ~100% for one colour.
ScoreDistributionDataProviderreturned page-count distributions (how many pages fall into each quality tier). With only a few analysed pages all falling in one tier, the ring appeared 100 % one colour. Changed to return the average score as a gauge —data = [avgScore, 100 − avgScore]with colour matching the score value.
Removed¶
- "AI Tools" backend module (
web_ntai) and its playground UI. The AI services are now integrated directly into the editing forms and the SEO panel where they are actually used. The genericnt_ai_generateandnt_ai_summarizeAJAX endpoints remain as the shared backend API; the unusednt_ai_translateendpoint was removed.
Internal pre-release history (May 2026, scheme
1.0.0→1.2.0) — before the public Git releasev1.0.0(July 2026), which delivered the above feature set bundled together. Retained for traceability.
[1.2.0] - 2026-05-24 (intern)¶
Added¶
- TYPO3 v14 LTS compatibility. Tested against TYPO3 v14.0 (released April 2026). CI matrix now covers v12.4, v13.4 and v14.0 against PHP 8.1-8.4.
Changed¶
- Backend module parent auto-detected. In v14, the toplevel module
webwas renamed tocontent(Core feature #107628). The extension detects the running TYPO3 major version and picks the correct parent automatically — no manual change required when upgrading. - Composer constraints widened:
typo3/cms-core: ^14.0,phpunit/phpunit: ^11.0 || ^12.0,typo3/testing-framework: ^10.0.
Fixed¶
- Auto-audit-on-save now actually triggers on regular saves.
Previously the listener was bound to
AfterRecordPublishedEvent, which only fires when records are published from a workspace. The feature never worked in workspace-less installations. The listener now usesAfterDatabaseOperationsEventwith status filtering, so it fires on every backend page save. It also resolvesNEW...placeholder UIDs viaDataHandler::substNEWwithIDsso newly created pages get audited correctly.
[1.1.0] - 2026-05-24 (intern)¶
Added¶
- Accessibility audit module. New backend module "Web -> Accessibility Audit" with score (0-100), severity-grouped findings, WCAG references, AI-generated fix suggestions, CSV export, and history.
- 9 built-in WCAG rules: image alt, heading hierarchy, link text, lang attribute, form labels, page title, inline contrast, AI link suggestions, AI readability check.
- Three audit triggers: manual button, auto-on-save event listener
(with cooldown),
nt_ai:auditCLI command (CI-friendly via--min-score). - Configurable alt-text generation: style (concise/descriptive/detailed), max length, tone, brand context, custom multi-line instructions, "no image-of prefix" enforcement, detected-text transcription toggle.
- Per-call alt-text overrides via
$optionOverridesargument onAiService::generateAltText(). - Defensive post-processing strips quote-wrapping, "Image of"/"Bild von" prefixes, and enforces the length cap on AI output.
- TYPO3-standard documentation in
Documentation/(reST/Sphinx) with introduction, installation, configuration reference, user manual, developer guide, known problems, changelog.
Changed¶
AiService::generateAltText()now accepts a fourth argument$optionOverrides(BC-compatible default[]).- README rewritten for the expanded feature set.
Tests¶
- 56 unit tests, 144 assertions, all green.
- New tests for
AltTextOptions,ContrastCalculator,ImageAltRule,LinkTextRule,Report.
[1.0.0] - 2026-05-23 (intern)¶
Added¶
- Initial release.
- AI providers: Anthropic Claude, OpenAI, Ollama.
- High-level
AiServiceAPI withgenerate,translate,summarize,generateAltText,describeImage. - Backend module "AI Tools" with playground UI.
- AJAX endpoints for backend JavaScript integration.
- CLI command
nt_ai:generate-alt-textsfor batch alt-text generation. - TYPO3 12.4 and 13.4 compatibility.
- Unit tests for
ConfigurationService,PromptBuilder,ProviderRegistry. - GitHub Actions CI for PHP 8.1-8.4 across TYPO3 12/13.
- GPL-2.0-or-later.