Skip to content

Release: merge development into beta - #563

Open
github-actions[bot] wants to merge 112 commits into
betafrom
development
Open

Release: merge development into beta#563
github-actions[bot] wants to merge 112 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

remko48 and others added 21 commits August 27, 2026 15:36
Clicking a row now opens the secret in an NcAppSidebar over the vault
list instead of navigating to a separate page, so the list context
(folder, search, pagination) stays visible and intact behind it.

The detail page is gone: the id rides as an optional segment on the
list routes (/secrets/:id? and /folders/:folderId/:id?), because
CnPageRenderer keys its render on the page id and a separate page
remounted the list on every open. Deep links to #/secrets/<id> keep
resolving (list + open sidebar) and stay behind the vault-lock guard.
Route <-> sidebar mapping lives in src/utils/detailRoute.js.

The sidebar lays the detail out Proton Pass style: the action row
(labeled Edit opening the edit dialog, icon-only Share with ariaLabel,
and a "Secret actions" menu holding Move, Delete and Close - the
native X is hidden while the row renders; Esc still closes everywhere)
sits with the title in the header; fields render as grouped
icon/label/value boxes with a visible attachments box and a
last-modified/created metadata box (NcDateTime); the informational
tail folds into "More information" (sharing, requests, activity) and
owner-only "Advanced" (versions, rotation, honey) disclosures. Card
and identity secrets render their payloads as first-class rows (masked
number/CVV/PIN/BSN with eye + copy, revealed numbers regrouped in
blocks of four, identity split into Personal/Address/Contact
sections); the old CardDisplay/IdentityDisplay components are deleted
and the raw key row is hidden for composite types. The pane widens to
clamp(300px, 35vw, 560px) above the library's mobile breakpoint.
Decryption semantics are unchanged. Edit/Move refresh the list behind
the sidebar; Delete closes it.

The SecretDetail vitest specs are retargeted at the sidebar component;
the new route mapping has its own spec; e2e drives Move/Share through
the new action row and opens the More-information disclosure for the
activity assertion; the guards spec drops the removed page id.
New source strings from the sidebar restyle - "Details", "Versions",
"Secret actions", "More information", and the identity section
headings "Personal details" / "Address details" / "Contact details" -
translated into English plus all 36 required locales, with the .js
catalogs regenerated and the parity ratchet tightened to record the
progress.
Opening the folder the "..." stand-in node represents lit TWO rows in
the rail: the node itself (vue-router exact-active styling on its
link) and its parent, which received the deepest-visible-ancestor
highlight. The ancestor rule now returns nothing when the active
folder is the single hidden child the "..." stands for, so exactly one
row reads as selected. Navigation through the node is unchanged, and
the multi-hidden-children case keeps highlighting the parent row as
before.
Navigating to another folder used to keep the previous folder's rows
on screen until the new ones landed, then swap them mid-air -
CnIndexPage only shows its full loading spinner while it is loading
AND has nothing to render. A folderSwitching flag now blanks the list
for the duration of the navigation's fetch, so a switch reads as
spinner -> new folder's contents. The flag starts true because
root <-> folder navigations REMOUNT the view (the routes are different
manifest pages and CnPageRenderer keys its render on the page id), and
a fresh mount rendered the store's previous rows before mounted()
fetched anything - the folderId watcher only covers folder -> folder
moves. In-folder refreshes and searches keep the in-place behavior.
A sweep over every locale for values identical to their English source
found six bulk-copied strings: "Applications awaiting approval", "No
applications awaiting approval" (31 locales), "Your secrets, folders
and applications at a glance", "Vault overview", "Rotation due" (33
each) and "View all" (31). All now carry real translations phrased
with each locale's established terminology. Script-mismatched
loanwords are also fixed: Token and Status become Cyrillic in mk/sr,
and Link becomes Länk/Ligação/Colliaziun in sv/pt/rm. Values that
already held a genuine translation were never touched, and legitimate
loanwords (URL, Token in Latin-script locales, Dashboard, Widgets,
Layout) and the brand string "Keepiq {version}" stay as they are. The
.js catalogs are regenerated.
The secret types come from the server with English labels seeded by
SeedSecretTypes.php, and the frontend printed type.label raw - so the
type filter, the create and edit dialogs' type selectors and the
detail sidebar's type subline showed English in every language. A new
secretTypeLabel() helper (src/utils/secretTypes.js) routes the label
through the translator at all four sites, falling through unchanged
for custom types without a catalog entry - the dynamic-key pattern the
nav already uses for manifest menu labels. The ten system labels are
translated in English plus all 36 required locales (295 new entries;
seven keys were missing from every catalog), the .js catalogs are
regenerated and the parity ratchet is tightened accordingly.
… l10n commit

Commit 17988a7 accidentally carried a local development aid into
LockScreen.vue: a DEV-ONLY auto-unlock block that reads the master
password in plaintext from localStorage and unlocks the vault on page
load, plus its status banner and data field. It was guarded by a
NODE_ENV check (webpack strips it from production builds) and inert
without an explicit localStorage opt-in, but it defeats the lock
screen's security model and was never meant to be tracked.

The same commit also dropped two @SPEC tags from handlePasskeyUnlock;
they are restored. The file now matches its pre-17988a77 state exactly.
…p-row toolbar

The vault list adopts CnIndexPage's own selection: selectable with
rowClickToView (clicking opens the detail, checkboxes select),
selectedIds fed from the bulk store and @select synced back. The table
gets header select-all, row checkboxes and selected-row highlight;
cards get a checkbox each; the list keeps shift-range selection through
NcCheckboxRadioSwitch — the same Nextcloud checkbox the other views
render — with a capture-phase shift recorder. One selection is shared
across view switches, pruned to the visible rows on every list change
(folder navigation, page flips, filters, refresh), and the strip's bulk
buttons disable while the list is (re)loading so a stale selection from
the previous page cannot be moved or deleted.

Bulk actions live in the library's contextual selection strip alone
(live count announced via role="status" — WCAG 2.1 SC 4.1.3 — plus
Move, Share, Add to team folder and Delete with icons, and the strip's
own Clear; bulk-actions §3.1's required bar). The Actions menu keeps
only the Select-all checkbox — the sole whole-view select-all for the
list and card views. Selected list rows tint the item with its hover
token, never the checkbox gutter.

Folders left the collection: vaults/subfolders render as a captioned
strip (Vaults at root / Folders inside) above the collection in every
view mode (the library's #before-collection slot), so they no longer
masquerade as secrets in table/cards and survive pagination; the strip
owns the single divider. Breadcrumbs render below the actions bar,
above that strip, a step larger — the trail is the folder heading now
that the page title is visually hidden (the h1 stays for
accessibility).

The bar owns the top row: keepiq's doubled page padding is dropped and
the bar clears the floating nav toggle with a 30px !important margin —
a margin so its background starts after the toggle, !important because
design-system themes flatten the bar's box with their own !important
rules. Keepiq-scoped, as is the container-scale rounding; other apps
keep their heading and the library defaults. The secret-type filter is
a funnel button beside the search field carrying the sort radios too;
it flips to its filled glyph in the primary color whenever a type
filter is set or the sort deviates from the default name sort. New
l10n string "Filter and sort" in en + all 36 required locales.

e2e: clickOverflowAction opens the bar menu via a new openActionsMenu
helper; export-gdpr's four inline menu openers use it.
… display cap

Being in a folder deeper than the rail renders (e.g. test11 under the
5-level cap) highlighted the deepest visible ancestor (test5), which
read as the wrong folder being open. The active trail is computed once
(activeFolderTrail); when it exceeds the cap no ancestor row lights up
and the "…" node under the deepest visible branch renders active
instead (ellipsisHighlightId, passed into NavFolderTree's ellipsis
item) — exactly one row selected, and it is the row that stands for
the hidden chain. This subsumes the earlier single-hidden-child
special case.
ImageMagick's internal SVG renderer — used by Nextcloud theming to
generate the favicon — does not draw stroked paths, so the shackle
vanished from generated favicons. All five icons (app, app-dark,
app-store, pwa-icon, pwa-icon-maskable) redraw the shackle as a filled
outline of equivalent thickness, so stroke-less rasterizers render the
full lock.
CI's stylelint (declaration-empty-line-before) rejects a blank line
between a custom property and the following declaration — a formatting
habit that is valid in nextcloud-vue's stylelint config but not in
keepiq's. The full stylelint sweep is green again, and RESTYLE-PLAN's
per-stage verification list now names stylelint so the gate stops
being skippable locally.
gate-16: @SPEC tags on the seven changed methods the gate named —
onRowCheck/onSelectAll (bulk-actions multi-select requirement), the
selectedFolderId watcher (folder management), typeFilterOptions and
both secret dialogs' typeOptions (secret types), and the detail
sidebar's mounted() (read secret).

gate-40: the list checkbox's ariaLabel binding becomes aria-label —
identical Vue behaviour, and the form-label rule recognises the kebab
form.

gate-45: the sidebar's accordion-chevron transition gains the
prefers-reduced-motion fallback the rest of the file already has.

gate-46: five @SPEC tags pointed at anchors that never existed —
requirement-secret-sharing is requirement-share-a-secret
(user-sharing) and requirement-cached-vault-is-read-only is
requirement-offline-mode-is-strictly-read-only (offline cache).
Brings the released library features the vault page consumes, until now
only available through a locally built dist overlaid into node_modules:
the contextual selection strip (live role="status" count,
#selection-actions slot, clear-selection wiring), the #after-search and
#before-collection CnIndexPage slots, the role-grouped actions-bar
layout with its narrow-width container-query tiers, clickToView on
CnCardGrid/CnObjectCard, and the CnPageRenderer fix that stops lifted
manifest fields (title, icon) falling through as HTML attributes on
custom pages — the "Vault" tooltip that hovered over every custom page.

With the package live, the overlay workflow is retired for this line of
work: node_modules now matches the lockfile again.
…yword

CI's stylelint (declaration-property-value-keyword-no-deprecated, in
the newer toolchain the merged lockfile installs) rejects
`word-break: break-word` — a deprecated keyword that always aliased
overflow-wrap behaviour. The detail rows' value wrapping now uses
`overflow-wrap: anywhere`, which wraps long secret values identically.
The 0.2.6-beta.20260831102714 release bumped the version on beta. Without this,
development stays behind beta and the next development -> beta promotion
conflicts on the version file.

Version files resolve to development's side, which is the higher line,
so this never moves a version backwards.
)

Per-widget `valueColor` is applied as an INLINE style on the number, so it beats
the canonical card accent, and a hex literal opts the card out of NL Design
System theming — which kpi-card.css forbids outright, because the nldesign app
re-themes by overriding the Nextcloud tokens.

Two outcomes, no third:

  * a colour that merely restated the default accent is deleted — `#0082c9` IS
    `--color-primary-element` in the default theme, so the tile looks identical
    and now re-themes correctly;
  * a colour that carried meaning becomes `variant`, which is themed and drives
    the icon tint and the number together so the two cannot disagree.

`variant` on a stat/delta resolves through VARIANT_COLORS, which was moved onto
the `-text` tokens first (nextcloud-vue#888) — the plain fill tokens failed WCAG
AA at 1.08:1 as a foreground colour, and these conversions would have been the
first tiles to hit that path.

Edited as text rather than re-serialised, so the manifest keeps its hand-laid-out
formatting; every `valueColor` in this file was confirmed to sit on a KPI widget
first. Manifest schema validation passes.
Dependabot cannot propose composer updates for this app. Its updater fails
with:

  Your requirements could not be resolved to an installable set of packages.
    - edgedesign/phpqa[v1.27.0, ..., v1.27.2] require ext-xsl * ->
      it is missing from your system.

config.platform pins php 8.3 so composer resolves against a known PHP version,
but says nothing about extensions. edgedesign/phpqa requires ext-xsl and the
resolving environment does not have it, so the resolve fails before any bump
can be computed.

CI is unaffected, which is why this went unnoticed: composer install replays
the committed lock and never re-resolves, so the pipeline stays green while
dependabot -- which does re-resolve -- fails every time. The shared quality.yml
installs no xsl extension anywhere and never invokes phpqa.

Declaring ext-xsl beside the php pin makes resolution assume exactly what the
committed lockfile already assumes. Verified in a clean composer:2 container
without --ignore-platform-reqs: the same file fails without this line and
resolves (103 installs, lock written) with it.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…2.6-beta.20260831102714

chore(release): sync beta back into development
Levels this app with the fleet. 2.27.2 adds two fixes the earlier 2.27.0 pin does
not carry:

  * headerless is no longer chromeless — a flat KPI card in a borderless wrapper
    had no card, border or background at all;
  * a stat `variant` paints from the `-text` tokens rather than the fill tokens,
    which failed WCAG AA at 1.08:1 as a foreground colour. The KPI colour
    cleanup converted hardcoded values to `variant`, so this is what makes those
    conversions contrast-safe.

Lockfile only, and npm pruned nothing.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ 1292479

Check PHP Vue Security License Tests
lint ⏭️
phpcs ⏭️
phpmd ⏭️
psalm ⏭️
phpstan ⏭️
phpmetrics ⏭️
eslint ⏭️
stylelint ⏭️
build ⏭️
composer ⏭️ ⏭️
npm ⏭️ ⏭️
app:check-code ⏭️
info.xml ⏭️
REUSE ⏭️
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 11:46 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ 1042c3a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 111/111
npm ✅ 536/536
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 12:39 UTC

Download the full PDF report from the workflow artifacts.

…ection (#479)

* feat(vault): secret details in a right sidebar over the list

Clicking a row now opens the secret in an NcAppSidebar over the vault
list instead of navigating to a separate page, so the list context
(folder, search, pagination) stays visible and intact behind it.

The detail page is gone: the id rides as an optional segment on the
list routes (/secrets/:id? and /folders/:folderId/:id?), because
CnPageRenderer keys its render on the page id and a separate page
remounted the list on every open. Deep links to #/secrets/<id> keep
resolving (list + open sidebar) and stay behind the vault-lock guard.
Route <-> sidebar mapping lives in src/utils/detailRoute.js.

The sidebar lays the detail out Proton Pass style: the action row
(labeled Edit opening the edit dialog, icon-only Share with ariaLabel,
and a "Secret actions" menu holding Move, Delete and Close - the
native X is hidden while the row renders; Esc still closes everywhere)
sits with the title in the header; fields render as grouped
icon/label/value boxes with a visible attachments box and a
last-modified/created metadata box (NcDateTime); the informational
tail folds into "More information" (sharing, requests, activity) and
owner-only "Advanced" (versions, rotation, honey) disclosures. Card
and identity secrets render their payloads as first-class rows (masked
number/CVV/PIN/BSN with eye + copy, revealed numbers regrouped in
blocks of four, identity split into Personal/Address/Contact
sections); the old CardDisplay/IdentityDisplay components are deleted
and the raw key row is hidden for composite types. The pane widens to
clamp(300px, 35vw, 560px) above the library's mobile breakpoint.
Decryption semantics are unchanged. Edit/Move refresh the list behind
the sidebar; Delete closes it.

The SecretDetail vitest specs are retargeted at the sidebar component;
the new route mapping has its own spec; e2e drives Move/Share through
the new action row and opens the More-information disclosure for the
activity assertion; the guards spec drops the removed page id.

* l10n: detail-sidebar strings in every required locale

New source strings from the sidebar restyle - "Details", "Versions",
"Secret actions", "More information", and the identity section
headings "Personal details" / "Address details" / "Contact details" -
translated into English plus all 36 required locales, with the .js
catalogs regenerated and the parity ratchet tightened to record the
progress.

* fix(nav): highlight only the "..." node when its hidden folder is open

Opening the folder the "..." stand-in node represents lit TWO rows in
the rail: the node itself (vue-router exact-active styling on its
link) and its parent, which received the deepest-visible-ancestor
highlight. The ancestor rule now returns nothing when the active
folder is the single hidden child the "..." stands for, so exactly one
row reads as selected. Navigation through the node is unchanged, and
the multi-hidden-children case keeps highlighting the parent row as
before.

* feat(vault): show a loading state while switching folders

Navigating to another folder used to keep the previous folder's rows
on screen until the new ones landed, then swap them mid-air -
CnIndexPage only shows its full loading spinner while it is loading
AND has nothing to render. A folderSwitching flag now blanks the list
for the duration of the navigation's fetch, so a switch reads as
spinner -> new folder's contents. The flag starts true because
root <-> folder navigations REMOUNT the view (the routes are different
manifest pages and CnPageRenderer keys its render on the page id), and
a fresh mount rendered the store's previous rows before mounted()
fetched anything - the folderId watcher only covers folder -> folder
moves. In-folder refreshes and searches keep the in-place behavior.

* l10n: translate the English-copied strings across all locales

A sweep over every locale for values identical to their English source
found six bulk-copied strings: "Applications awaiting approval", "No
applications awaiting approval" (31 locales), "Your secrets, folders
and applications at a glance", "Vault overview", "Rotation due" (33
each) and "View all" (31). All now carry real translations phrased
with each locale's established terminology. Script-mismatched
loanwords are also fixed: Token and Status become Cyrillic in mk/sr,
and Link becomes Länk/Ligação/Colliaziun in sv/pt/rm. Values that
already held a genuine translation were never touched, and legitimate
loanwords (URL, Token in Latin-script locales, Dashboard, Widgets,
Layout) and the brand string "Keepiq {version}" stay as they are. The
.js catalogs are regenerated.

* fix(l10n): translate the secret-type labels everywhere they render

The secret types come from the server with English labels seeded by
SeedSecretTypes.php, and the frontend printed type.label raw - so the
type filter, the create and edit dialogs' type selectors and the
detail sidebar's type subline showed English in every language. A new
secretTypeLabel() helper (src/utils/secretTypes.js) routes the label
through the translator at all four sites, falling through unchanged
for custom types without a catalog entry - the dynamic-key pattern the
nav already uses for manifest menu labels. The ten system labels are
translated in English plus all 36 required locales (295 new entries;
seven keys were missing from every catalog), the .js catalogs are
regenerated and the parity ratchet is tightened accordingly.

* fix(security): remove the dev auto-unlock block that slipped into the l10n commit

Commit 17988a7 accidentally carried a local development aid into
LockScreen.vue: a DEV-ONLY auto-unlock block that reads the master
password in plaintext from localStorage and unlocks the vault on page
load, plus its status banner and data field. It was guarded by a
NODE_ENV check (webpack strips it from production builds) and inert
without an explicit localStorage opt-in, but it defeats the lock
screen's security model and was never meant to be tracked.

The same commit also dropped two @SPEC tags from handlePasskeyUnlock;
they are restored. The file now matches its pre-17988a77 state exactly.

* feat(vault): fleet selection with a contextual strip, vault strip, top-row toolbar

The vault list adopts CnIndexPage's own selection: selectable with
rowClickToView (clicking opens the detail, checkboxes select),
selectedIds fed from the bulk store and @select synced back. The table
gets header select-all, row checkboxes and selected-row highlight;
cards get a checkbox each; the list keeps shift-range selection through
NcCheckboxRadioSwitch — the same Nextcloud checkbox the other views
render — with a capture-phase shift recorder. One selection is shared
across view switches, pruned to the visible rows on every list change
(folder navigation, page flips, filters, refresh), and the strip's bulk
buttons disable while the list is (re)loading so a stale selection from
the previous page cannot be moved or deleted.

Bulk actions live in the library's contextual selection strip alone
(live count announced via role="status" — WCAG 2.1 SC 4.1.3 — plus
Move, Share, Add to team folder and Delete with icons, and the strip's
own Clear; bulk-actions §3.1's required bar). The Actions menu keeps
only the Select-all checkbox — the sole whole-view select-all for the
list and card views. Selected list rows tint the item with its hover
token, never the checkbox gutter.

Folders left the collection: vaults/subfolders render as a captioned
strip (Vaults at root / Folders inside) above the collection in every
view mode (the library's #before-collection slot), so they no longer
masquerade as secrets in table/cards and survive pagination; the strip
owns the single divider. Breadcrumbs render below the actions bar,
above that strip, a step larger — the trail is the folder heading now
that the page title is visually hidden (the h1 stays for
accessibility).

The bar owns the top row: keepiq's doubled page padding is dropped and
the bar clears the floating nav toggle with a 30px !important margin —
a margin so its background starts after the toggle, !important because
design-system themes flatten the bar's box with their own !important
rules. Keepiq-scoped, as is the container-scale rounding; other apps
keep their heading and the library defaults. The secret-type filter is
a funnel button beside the search field carrying the sort radios too;
it flips to its filled glyph in the primary color whenever a type
filter is set or the sort deviates from the default name sort. New
l10n string "Filter and sort" in en + all 36 required locales.

e2e: clickOverflowAction opens the bar menu via a new openActionsMenu
helper; export-gdpr's four inline menu openers use it.

* fix(nav): the "…" node carries the selection for any folder below the display cap

Being in a folder deeper than the rail renders (e.g. test11 under the
5-level cap) highlighted the deepest visible ancestor (test5), which
read as the wrong folder being open. The active trail is computed once
(activeFolderTrail); when it exceeds the cap no ancestor row lights up
and the "…" node under the deepest visible branch renders active
instead (ellipsisHighlightId, passed into NavFolderTree's ellipsis
item) — exactly one row selected, and it is the row that stands for
the hidden chain. This subsumes the earlier single-hidden-child
special case.

* fix(icons): draw the lock shackle as a filled outline in every app icon

ImageMagick's internal SVG renderer — used by Nextcloud theming to
generate the favicon — does not draw stroked paths, so the shackle
vanished from generated favicons. All five icons (app, app-dark,
app-store, pwa-icon, pwa-icon-maskable) redraw the shackle as a filled
outline of equivalent thickness, so stroke-less rasterizers render the
full lock.

* style(vault): no blank line after the crumbs custom property

CI's stylelint (declaration-empty-line-before) rejects a blank line
between a custom property and the following declaration — a formatting
habit that is valid in nextcloud-vue's stylelint config but not in
keepiq's. The full stylelint sweep is green again, and RESTYLE-PLAN's
per-stage verification list now names stylelint so the gate stops
being skippable locally.

* fix(quality): satisfy the four failing hydra gates

gate-16: @SPEC tags on the seven changed methods the gate named —
onRowCheck/onSelectAll (bulk-actions multi-select requirement), the
selectedFolderId watcher (folder management), typeFilterOptions and
both secret dialogs' typeOptions (secret types), and the detail
sidebar's mounted() (read secret).

gate-40: the list checkbox's ariaLabel binding becomes aria-label —
identical Vue behaviour, and the form-label rule recognises the kebab
form.

gate-45: the sidebar's accordion-chevron transition gains the
prefers-reduced-motion fallback the rest of the file already has.

gate-46: five @SPEC tags pointed at anchors that never existed —
requirement-secret-sharing is requirement-share-a-secret
(user-sharing) and requirement-cached-vault-is-read-only is
requirement-offline-mode-is-strictly-read-only (offline cache).

* chore(deps): bump @conduction/nextcloud-vue to 2.27.0

Brings the released library features the vault page consumes, until now
only available through a locally built dist overlaid into node_modules:
the contextual selection strip (live role="status" count,
#selection-actions slot, clear-selection wiring), the #after-search and
#before-collection CnIndexPage slots, the role-grouped actions-bar
layout with its narrow-width container-query tiers, clickToView on
CnCardGrid/CnObjectCard, and the CnPageRenderer fix that stops lifted
manifest fields (title, icon) falling through as HTML attributes on
custom pages — the "Vault" tooltip that hovered over every custom page.

With the package live, the overlay workflow is retired for this line of
work: node_modules now matches the lockfile again.

* style(sidebar): overflow-wrap instead of the deprecated word-break keyword

CI's stylelint (declaration-property-value-keyword-no-deprecated, in
the newer toolchain the merged lockfile installs) rejects
`word-break: break-word` — a deprecated keyword that always aliased
overflow-wrap behaviour. The detail rows' value wrapping now uses
`overflow-wrap: anywhere`, which wraps long secret values identically.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ de9a0ea

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 111/111
npm ✅ 536/536
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 13:51 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 3 commits August 31, 2026 15:58
…#571)

The folder-sharing spec drove the new sidebar's "Secret actions" menu via
getByTestId('secret-detail-move'), but data-testid falls through to
NcActionButton's ROOT element, which is the <li> wrapper rather than the
<button> that carries the click handler. Dispatching a native click on the
<li> is a silent no-op, so SecretMoveDialog never opened and the spec failed
on the downstream '.move-form' assertion instead of at the click.

The testid resolving fine is what made this read as a dialog bug: the click
step passed and only the next assertion timed out.

Descend to the button so the handler actually fires. The sibling testids in
this spec (secret-detail-share, secret-detail-more-info) sit on NcButton and
a native <summary>, whose roots ARE the interactive element, so they are
unaffected.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Four review conversations on the merged PR #479, all in
SecretDetailSidebar:

- isOwner reads the single canonical `ownerId` field; the
  owner_id/userId fallback chain was dead code (the Secret entity has
  serialized ownerId since its first version) and the pinning test now
  asserts fail-closed behaviour for legacy-style payloads instead.
- mounted() catches a failing fetchTypes() and surfaces the error
  instead of stranding the sidebar on an infinite spinner.
- refreshList() no longer swallows a failed list refresh silently; it
  shows an error toast so a stale list is signalled.
- remove() catches a refused delete (403/500/offline) and shows the
  reason inline instead of leaving the sidebar open without feedback.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ 15871e4

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 111/111
npm ✅ 536/536
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 14:14 UTC

Download the full PDF report from the workflow artifacts.

The secret detail sidebar swaps the route's :id segment without
remounting, so AttachmentPanel's mounted()-only fetch kept showing the
first opened secret's attachments on every secret opened after it.
Add a secretId watcher that resets the attachment store (no stale rows
while loading, none left behind on a failed fetch) and refetches,
mirroring the sidebar's own route-driven reload. Covered by a new
component regression test.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/keepiq @ f79dc1f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 111/111
npm ✅ 536/536
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 12:01 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 29 commits September 3, 2026 14:51
Every push to development fired Code Quality TWICE on the same commit: once
for the push event, once for the pull_request event of the permanently-open
"Release: merge development into beta", whose head_ref IS development.

The concurrency block is not the bug and must not be changed: it suffixes the
group by event name deliberately so the push run is NOT cancelled by the PR
run, because the push run is the only carrier of Coverage Baseline Check, SBOM
and Features Extract. The consequence is that both lanes run to completion, so
the duplicate has to be dropped at the job.

The PR run is the one to drop: its head sha IS development's, which the push
run already decided, and it runs strictly fewer jobs.

Not done by removing `beta` from pull_request.branches, though that would also
stop it: the release/v* and sync/main-to-beta PRs target beta too and
genuinely need their run. Only the head_ref:development PR is redundant. A
development -> main promotion PR is skipped by the same clause, for the same
reason and just as correctly.

Proven on openregister#3370 before rolling out: run 33757014196 (pull_request,
sha 4f3ef5e9) completed as `skipped` with ZERO jobs in 72s, while the push run
for the same sha ran the full suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SPA is moving its recipient links from hash URLs to paths under the
anonymous shell (/apps/keepiq/public/share/link/<token> and friends), so
a load or refresh of any such path must serve the shell without a
Nextcloud session. A publicShell catch-all route (/public/{path},
mirroring the AppHost dashboard#page / dashboard#catchAll split - a
distinct name, because Symfony silently replaces same-named routes)
serves the same #[PublicPage] template at every subpath. It sits in
$extra, so it precedes the authenticated /{path} fallback.
…links

The router runs createWebHistory, which never reads a URL fragment - yet
the recipient links in the wild are hash URLs
(/apps/keepiq/public#/share/link/<token>): vue-router saw the bare
/public path, the catch-all redirected to /, and the vault guard bounced
account-less recipients onto the lock screen, asking for a master
password they do not have.

Two changes, one per link population:

- routerBase() extends to /apps/keepiq/public on the anonymous shell,
  so the SAME manifest routes (/share/link/:token, /send/:token,
  /share/request/:token) resolve on both shells unchanged.
- Links already sent cannot be regenerated, so a bootstrap handoff
  (src/bootstrap/hash-route-handoff.js) rewrites any legacy #/ URL in
  place with replaceState before the router is created. A legacy
  fragment-mode send key (?k= INSIDE the old SPA fragment) moves to a
  new #k= fragment, never into the real query string, which the browser
  would send to the server on the next refresh.

isPublicSurface() now classifies by path prefix, keeping the hash
prefixes for reads that can happen before the handoff.
The builders of recipient-facing links still emitted the retired hash
form, which the createWebHistory router never reads - recipients got the
lock screen. All four now emit paths under the anonymous shell:

- LinkShareController (share links)
- ApplicationSecretRequestsController (application fill links, a fifth
  site the fix brief's list missed)
- fillLink.js (secret-request fill links)
- ephemeralSend.js (sends)

The ephemeral-send content key rides as a #k= fragment - still never
transmitted - and the access page reads it from the fragment with a
fallback to the legacy route query, so links from before this change
keep decrypting. The send page's card styling rides along with its
fragment-key reader. The stale hash-routing wording in the manifest
note is corrected.
… PWA manifest

Sweeping for the retired hash scheme turned up three more builders
outside the fix brief's list, all on the AUTHENTICATED shell: the
unified-search provider, the notification deep link, and the PWA
manifest's vault shortcut. Their /apps/keepiq/#/secrets/<id> links fell
through the catch-all to the dashboard - the deep link silently
swallowed. They now emit paths; the gated route lands on the lock screen
with the path as returnUrl and resumes there after unlock.
No spec ever opened a share, send, or fill link without a session - the
existing page-surface specs drive the same components on the
authenticated shell with the admin cookie jar, which is precisely why
dead recipient links could ship. This spec mints fixtures through the
authenticated API (the server stores only opaque blobs, so no real
crypto is needed) and opens the links in a browser context with an EMPTY
storage state: the path form, the RETIRED hash form (through the
bootstrap handoff), a legacy fragment-mode send - asserting the content
key stays out of the query string - and a fill link.
…d-password message

Every string an account-less recipient sees on the share access and
ephemeral-send pages now has a Dutch translation (18 keys; the fill page
was already covered). The nl parity ratchet is lowered 382 -> 362 so the
paid-off debt cannot silently return.

Also adds "Invalid password. Please try again.", translated into all 36
locales, for the link-share access page to adopt: that page compares
nothing against a second entry, so the existing "The password does not
match" wording - correct on the lock screen, where it compares the two
typed setup passwords - is wrong there.
…ecret as copyable fields

The recipient card sat pinned in the top-left corner: core's guest
layout makes #content.app-public a flex row, so #keepiq-app shrank to
the card's width and the card's `margin: auto` had no free space to
distribute. The app mount now fills the flex row (app.css) and the
public shell centres its card on both axes with flex + margin:auto,
which stays scrollable when the card outgrows the viewport. All three
recipient views share the same card treatment.

The revealed link share was a bare <dl> that printed a card secret as
its raw JSON composite. It now renders labelled field rows with a copy
button per row (CopyButton, clipboard auto-clear) and masked sensitive
values behind an eye toggle - card/identity composites are parsed via
parsePayload() into Card number / Expiry / CVV / PIN / Cardholder rows
(number, CVV, PIN and BSN masked), additional fields render as masked
rows, and a plain key stays one masked "Secret value" row. The
usage-cap notice moved to the top of the card, under the heading, where
a whole-card status banner belongs.

The terminal not-found/expired state is a recognisable error page
(broken-link icon, "Link not found or expired" heading, translated
explanation) instead of the raw English server message, and a failed
decrypt now says "Invalid password. Please try again." The password
form moved to house-style controls.
The freshly created link and password - which cannot be retrieved again
- render in a highlighted panel so they read as the thing to take away
from the dialog. Sections gained real padding and separators, rows
breathe, the password is proper monospace at a readable size, and the
disabled user-sharing affordance is visually set apart from the working
controls. No copy changes; the folder-sharing e2e selectors are
unchanged.
#611)

* feat(nav): give keepiq a Reports page, from the reports it already had

ADR-114 Decision 3 puts Reports in every app. Keepiq needed nothing invented:
it already had TWO reports and they were two footer menu entries.

  PasswordHealth   component HealthReportView, and its own note calls it a
                   "health report"
  PersonalActivity the session user's own audit-trail feed

My first fleet audit called keepiq an app with no reports. That was a keyword
regex over page ids and titles, and neither "Password health" nor "My
activity" contains a report word — the COMPONENT name does. The query decided
the answer.

WHAT MOVES. A type:"reports" page cards both, and the two menu entries are
DELETED. ADR-112 Decision 2: a report is a card or an entry, never both.

Deleted rather than listed in menu-layout's `removals`: that key retires an
entry that still EXISTS, and gate-53 reports a removal naming an absent entry
as a stale removal. I had it the wrong way round first and the gate said so.

The PAGES stay routable under the same route names, which is what keeps deep
links, the dashboard widget's viewAllRoute and the older specs working
(ADR-044 Decision 5). An e2e test asserts that rather than trusting it.

⚠️ The walkthrough's `go-personalactivity` stop targeted the retired nav
entry. An OPTIONAL step whose target is absent is SKIPPED SILENTLY — no error,
no change to the step count — so it now points at Reports.

Footer renumbered to the canonical 90/95/100 from 80/85/86/87.
ChartBoxOutline registered in src/icons.js: an unregistered name renders no
glyph at all.

NO STORE, and that is deliberate. Keepiq's OpenRegister register declares zero
schemas — secrets live in its own tables — so `installable` could name nothing
and an install would have nothing to write. ADR-080 Decision 4 refuses the word
Store on a surface that cannot honour it. The e2e asserts the absence, so the
day keepiq grows one, something notices.

Six new strings, translated into all 36 locales the parity ratchet requires;
"Reports" harvested from decidiq's catalogues for the 24 locales that had it.

Verified: gate-22, gate-53, gate-60, gate-70, gate-102, gate-104 and gate-107
all clean, the parity ratchet reports no regression, and all five new e2e tests
collect. eslint could not run here (keepiq's node_modules is not installed);
CI lints it.

* fix(router): gate the Reports route, and format the chrome spec

Two CI failures on this branch, both correct catches.

ROUTER GUARD. tests/router/guards.spec.js pins every page id in
src/manifest.json against an explicit PROTECTED/PUBLIC classification, so a page
added to the manifest without being classified fails the drift check instead of
silently inheriting whichever default the guard applies. Adding Reports made it
17 routes against 16 classified. That test is doing exactly what its own comment
says it exists for.

Reports is classified PROTECTED, not public. It is the page PasswordHealth and
PersonalActivity now live on as cards, and both read vault state: Password
health analyses the UNLOCKED vault in memory, and My activity is the caller's
own audit trail. A Reports page reachable while the vault is locked would be a
way around the lock even if each card then refused.

PRETTIER. tests/e2e/app-chrome.spec.ts was not formatted. Run through prettier;
`prettier --check` over the whole tree is clean.

The guard suite passes 71/71. The full vitest run reports 647 tests passed and
zero test failures; 16 test FILES failed to collect, which is this sandbox
rather than the branch — I had symlinked another app's node_modules to get
prettier and vitest to run at all, and removed it after. CI installs the app's
own dependencies, so read those 16 there rather than from me.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Found by RUNNING the spec against a throwaway Nextcloud rather than
collecting it. All 5 keepiq specs now pass; before this, all 5 failed.

Keepiq redirects every route to /lock until a vault exists, and the app
navigation is present in the DOM but NOT visible behind the lock screen —
so the failure reads "cn-nav hidden" and names nothing useful. The
suite's global-setup logs in and suppresses the walkthrough but does not
create a vault; its own comment says "every spec here unlocks", and this
is the first spec that needs the app UI.

⚠️ A FULL PAGE LOAD RE-LOCKS THE VAULT, so unlocking once in beforeEach
is not enough: every page.goto lands on /lock again. Navigation now goes
through a gotoApp() helper that unlocks after each load, which is the
only reason the routability test passes.

Also carries the fleet-wide chrome-spec fixes: the cn-nav-* testids are
on the <li> wrapper rather than the clickable <a>, and the first-run
setup wizard modal intercepts pointer events on a clean install.
fix(e2e): make the chrome specs pass in a real browser
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.9 to 2.2.10.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [web-token/jwt-library](https://github.com/web-token/jwt-library) from 4.1.7 to 4.2.1.
- [Commits](web-token/jwt-library@4.1.7...4.2.1)

---
updated-dependencies:
- dependency-name: web-token/jwt-library
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ialogs

fix(applications): render the register and private-key dialogs as real overlays
…vault

fix(dashboard): stop the quick-action tiles from re-locking the vault
…trength-badge

fix(secrets): complete the cards and table views and move vault context to Proton-style indicators
Four KeepiqNotifierTest tests failed across all six CI combos, and the
URLs were not why: commit 33bf6c7 already moved every expectation to the
path form. That same commit round-tripped the file through a CP1252
reader, turning all eight em dashes into "a-euro-quote" (c3a2 e282ac
e2809d where e2 8094 belongs). Four of the eight sit inside asserted
message strings, so exactly four tests failed - the other four
occurrences are in comments and were silent.

Restores U+2014 in all eight places. Nothing else about the file changes;
development still carries the correct bytes, so the diff against it is
now empty for these lines.
Hydra gate-16 failed on src/views/EphemeralSendAccess.vue::fragmentKey -
the computed was added with an inline comment but no docblock, so the
changed-method scan found no @SPEC line. Adds the docblock with the
anonymous-recipient anchor the neighbouring mounted() hook already uses.
isPublicSurface() classified any path merely CONTAINING
'/apps/keepiq/public' as a recipient surface, so
/apps/keepiq/publications/..., /apps/keepiq/publicfoo and
/apps/keepiq/secrets/public all answered true. It is not the security
gate - access is authorised server-side by scoped token, and the
locked-vault gate is the name-based isPublicRoute() - but a classifier
that is wrong for a whole family of paths invites being mistaken for one,
and its docblock read as if it were authoritative.

Splits the app-relative remainder off the pathname first, then compares
prefixes against that. The app segment is still LOCATED rather than
anchored, so sub-directory installs (/nextcloud/index.php/apps/keepiq/..)
keep resolving; what it now requires is that the remainder begin at a "/"
boundary, which also rules out another app whose id merely starts with
this one. The docblock says plainly what the function is not.

Specs cover the former false positives, the neighbouring-app id, and the
webroot install the boundary check must not cost.
publicShell#pageCatchAll ('path' => '.+') matches every GET below
/public/ and hands it to a #[PublicPage] controller, so a session-gated
route mistakenly placed there is shadowed: the anonymous shell renders,
the intended handler never runs, and the author sees a working page
rather than a 404. The shell exposes no data, so there is nothing to
exploit today - the hazard is that a routing slip becomes a silent auth
bypass instead of an error.

A comment cannot enforce that, so this adds both: the warning on the
route entry, and a contract test that reads the route table as source
text and asserts every /public route resolves to a method actually
declaring #[PublicPage] (and #[NoCSRFRequired], which a session-less
recipient needs). Injecting dashboard#summary at /public/oops fails it
with the route named. A negative control keeps the check honest.

Not done instead: rejecting requests that carry a valid Nextcloud
session. A logged-in user opening a recipient link is ordinary - the
sharer checking their own link, or a colleague with an account but no
vault suite - and that reject would break it. The rationale is recorded
in the test.
The coverage ratchet blocks this branch on PublicShellController, and the
reason only became visible once PHPUnit itself stopped failing first. The
guard scopes to changed PHP files and keeps head-only method buckets on the
head side, so pageCatchAll() — two statements, added by this branch, with no
test class anywhere for the file — entered the denominator uncovered and pulled
the scoped ratio under the merge base. The other five changed lib/ files are
comment and string edits inside methods that were already covered, so nothing
else moved.

Covering the file rather than the method: page() was untested too, and a shell
at 0% is what let a new public method land uncovered without anything looking
wrong. The assertions pin what would actually strand a recipient — RENDER_AS_BASE
rather than the session-expecting user layout, the wasm-unsafe-eval directive
the Argon2id derivation needs to run at all, and the identity of the two methods
across the three live link shapes, since page() answers /public and pageCatchAll()
answers everything below it and a divergence would break only one half with a
correct-looking route table.
…ng-2

fix(routing): make public share, send and fill links reach the recipient
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants