fix(manifest): validate against the declared schema; drop the dead Settings widget - #59
Conversation
…ttings widget
check:manifest reported 21 errors on a manifest that renders correctly and
passes its own declared schema with zero errors.
tests/validate-manifest.js hardcoded the v1 schema file. portaliq's manifest
declares v2 (`$schema` -> app-manifest-v2.schema.json), so every widget was
checked against v1's `$defs.widgetDef`, which sets additionalProperties:false
and predates the `content` / `icon` / `integrationId` keys that CnDetailPage
actually renders:
- widgetContentFor(item) returns def.content and feeds it to every catalog
widget (CnObjectListWidget's emptyText, the data widget's columns, the
stats-block entries)
- findWidget(item).icon is passed to <CnIcon :name> in the #title-icon slot
- resolveRegistryWidget(def.integrationId, surface) resolves the Files
integration on DocumentDetail
Verified in the browser on all four detail pages before changing anything:
every widget renders its content. PortalAccountDetail's session list shows
"No sessions issued yet" — that string exists only in content.emptyText; the
component's fallback is "No items". Renaming content -> props would have
broken working pages, so the manifest widgets are left untouched.
The validator now picks the schema variant from the manifest's own $schema,
matching what openconnector and scholiq already do. Positive control: pointing
$schema at v1 makes it select v1 and exit 1 with the original 21 errors.
Separately, page "Settings" carried a page-level widgets[] entry with
widgetKey "version-info". No widget is registered in src/registry.js
(registry.spec reports 0 custom widget entries), so it rendered a visible
"Widget unavailable / version-info" error box below the Save button. It
duplicated config.sections[0].widgets[0], which already renders the Version
Information card, so the entry is removed rather than registered. Its
"App Template" scaffolding placeholder, shown to users as
"Application Name: App Template", is corrected to "Portaliq".
Quality Report — ConductionNL/portaliq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 871/871 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-07 12:58 UTC
Download the full PDF report from the workflow artifacts.
One red gate, and it is pre-existing
Gate-63 is not caused by this changeThe gate is diff-triggered: it only runs when a manifest changed. This PR changes a manifest, so it ran — and found a violation that predates the PR. Running the gate's own script ( The violation is the page's Follow-up needed (separate PR)This is the failure mode the gate's own source comments describe for pipelinq: a single pre-existing violation blocks every manifest-touching PR in the repo, permanently. portaliq is now in that state. Resolving it means renaming or removing the in-app |
What this is
check:manifestreported 21 errors on a manifest that renders correctly and passes its own declared schema with zero errors. I looked at the rendered pages before changing anything, because the proposed fix (migratingcontentintoprops) would have broken working pages.The widgets render —
content/icon/integrationIdare liveThe premise was that
CnDetailPagereads onlyw.idand...(def?.props || {}), so thecontentblobs were inert. Thatpropsspread isgetIntegrationProps()— the integration-only path, two lines belowresolveRegistryWidget(def.integrationId, this.surface). The general path is different:widgetContentFor(item)returnsdef.contentand feeds it to every catalog widgetfindWidget(item).iconis passed to<CnIcon :name>in the#title-iconslotresolveRegistryWidget(def.integrationId, surface)resolves the Files integration on DocumentDetailAll four detail pages were checked in the browser. Every widget renders its content. The decisive one: PortalAccountDetail's session list shows "No sessions issued yet", and
That string exists exactly once in the whole repo — in
content.emptyText. Ifcontentwere inert the widget would say "No items".So the manifest widgets are left untouched.
iconis an MDI component name ("ShieldCheckOutline"), which is neithericonClass(a CSS class) noriconUrl— the proposed rename would have silently blanked every widget icon.The real cause: the validator picked the wrong schema
tests/validate-manifest.jshardcoded the v1 schema file. portaliq's manifest declares v2, so every widget was checked against v1's$defs.widgetDef—additionalProperties: false, and nocontent/icon/integrationId.The validator now selects the schema variant from the manifest's own
$schema, which is whatopenconnectorandscholiqalready do.Positive control — pointing
$schemaat v1 makes it select v1 and fail, so the pass is meaningful:$schemaapp-manifest-v2.schema.json(actual)app-manifest.schema.json(injected)The structural-lint fallback's page-type allowlist was also stale (missing
form,map,roadmap,search,wiki). It is synced to the schema enum — androadmapwas verified to actually render, not just to validate.A real user-facing bug, found by looking
The
Settingspage carried a page-levelwidgets[]entry withwidgetKey: "version-info". Nothing is registered insrc/registry.js(registry.specreports 0 custom widget entries), so it rendered a visible error box to users:Before —
Application Name: App Template, and an orange "Widget unavailable / version-info" box under Save.After —
Application Name: Portaliq, no error box, Version Information card intact, 0 console errors.It duplicated
config.sections[0].widgets[0], which already renders the Version Information card — so the entry is removed rather than registered (registering it would render the card twice). The"App Template"scaffolding placeholder is corrected to"Portaliq".Proven by an A/B on one instance: the bundle was patched in-flight in the browser, so the same deployed bytes produced both states. Clearing the cache brings the error box back, confirming the shared dev instance was never modified.
Checks
check:specs(json-strict + manifest-v2 + register + registry) — PASScheck:manifest— PASS, and demonstrated to still fail on a bad manifest.skip, nocontinue-on-error; no widget data deleted🤖 Generated with Claude Code