feat(ui): member pickers, a real request form, and vault creation from the rail - #667
Merged
Conversation
Opening any vault flooded the console with one Vue warning per crumb per
render:
[Vue warn]: Invalid prop: type check failed for prop "name".
Expected String with value "undefined", got Undefined
NcBreadcrumb declares `name` as a required String, and the trail's first
crumb is icon-only -- `{ icon: 'Home', to: ... }`, the shape CnBreadcrumbs'
own docs advertise for a Home crumb -- so `crumb.label` reached it as
undefined. The trail is rendered only inside a folder, which is why the
root page was quiet and every vault was not.
The crumb now carries `label: t('keepiq', 'Vaults')`. Nothing renders it:
NcBreadcrumb prints `name` only for crumbs without an icon slot, so the
crumb looks exactly as before. The word matches the nav item pointing at
the same route, and the key is already translated across l10n/, so this
adds no string.
CnBreadcrumbs stops forwarding undefined on its own side too, but keepiq
builds the published @conduction/nextcloud-vue dist, so that fix reaches
this app only on the next release -- the label is what silences it now.
Not addressed here: the Home crumb's button has no accessible name at all.
NcBreadcrumb sets aria-label only when its `icon` prop is used, not the
`#icon` slot CnBreadcrumbs renders through.
With the page title hidden in keepiq, the actions bar is the page's TOP element: `.cn-index-page` gives up its remaining 4px of top padding, and the bar's top corners go square so the bar meets the page edge as a seam instead of showing the page background in two notches. The bottom corners keep the container-large radius, so the shorthand is split into logical longhands rather than losing the token and its fallbacks for older server generations.
The "Applications awaiting approval" widget listed the pending registrations
and then left an admin with nowhere to go: its configured "View all" footer
is dead. `limit` never reaches the widget on @conduction/nextcloud-vue
2.36.3 -- the object-table host adapter destructures it out to fold it into
`source`, and an endpointSource table has no `source` -- so every row
renders, the footer's total-greater-than-shown condition can never hold, and
`viewAllRoute` is unreachable no matter how many applications are waiting.
`rowRoute: "ApplicationRegister"` gives the card its navigation back: any
row opens /applications, the page where the queue is handled. A route NAME,
so the widget router-pushes it -- the vault master key is memory-only, and a
full page load would re-lock the vault and land on /lock, which is the trap
this manifest's tile note already documents for linkType "app".
Deliberately NOT ApplicationDetail (/applications/:id, which does exist):
the queue's job is to get an admin to the page that handles the whole queue,
not to one application. The cost is one dev-only console line per click --
the widget's rowRoute always pushes `params: {id}` and vue-router 4.6.4
discards a param this path has no segment for (measured: it resolves to
/applications and warns "Discarded invalid param(s) id"). Production builds
strip the warning.
The widget's `_note` is corrected with it. It claimed "No rowRoute:
applications have no detail page", and ApplicationDetail has existed for
some time; the note now records why the row still goes to the index, and
what makes the View-all footer dead rather than merely unconfigured.
The upstream adapter fix is committed in nextcloud-vue, so the footer starts
appearing at a fourth pending application once keepiq bumps past 2.36.3.
Until then rowRoute carries this on its own.
Reported from testing, while clearing the development seed: deleting several
secrets ran fine and then ended on a screen that was part result, part
command -- with a live "Delete 0 secrets" button on it.
Everything in the dialog counted off the SELECTION, and the host reloads the
list when the run completes (SecretList.onBulkDone), after which the
reconciled selection is empty. So a finished run left a warning about
deleting 0 secrets, a destructive button offering to delete 0 secrets, and
the per-item report, all at once.
The dialog now has two phases and the phase decides the chrome. While it
asks, nothing changes. Once the run has finished the warning and the typed
confirmation are gone, the destructive button is REMOVED rather than
disabled, Close becomes the primary action, and the title turns into an
outcome: "Deleted {ok} of {total} secrets", counted off the report so it
stays true when the selection behind it is empty and when part of the run
failed or was cancelled.
The phase is an instance flag, not a read of the store. The report outlives
the dialog -- it is cleared with the selection, not on close -- so deriving
"finished" from it would have opened a fresh dialog straight into a result
state. The same flag now gates BulkRunPanel, which fixes a second, quieter
version of the same confusion: reopening the dialog showed the PREVIOUS
run's report table before anything had been asked for.
A failed run keeps its report and its Retry button; only the affordances
that would act on the emptied selection go away.
l10n: one new key, translated into all 36 required locales rather than left
to fall back to English, and the .js catalogues regenerated from the .json
(npm run l10n:build). Each translation follows the phrasing that locale
already uses -- the noun from its own "Delete {count} secrets", the "X of Y"
construction from its "Page {page} of {total}" / "Used {used} of {limit}".
The three sibling bulk dialogs (move / share / team folder) share the
pattern and are fixed in the commit after this one.
The same defect the delete dialog was just fixed for, in the three dialogs
beside it. Everything counted off the SELECTION, and the host reloads the
list when a run completes, which empties the reconciled selection -- so a
finished run left a title counting 0 secrets, a destination picker, and a
live primary button.
Quieter than delete only because their run buttons carry static labels
("Move", "Share", "Add to team folder") rather than a count. Worse in one
respect: pressing the button again ran the runner over an empty id list,
which RESET the report to empty -- the record of what had just happened was
one stray click away from being wiped.
Each now has the two phases delete has. While it asks, nothing changes. Once
the run has finished the input is gone, the run button is REMOVED rather than
disabled, Close becomes primary, and the title turns into an outcome counted
off the report: "Moved/Shared {ok} of {total} secrets", "Added {ok} of
{total} secrets to the team folder". The same instance flag gates
BulkRunPanel, so reopening a dialog no longer shows the previous run's table
before anything has been asked for.
Two details that are not copy-paste from delete:
• The team-folder run is two steps -- the chunked move, then the
membership fan-out -- so its switch waits for `fanOut.running` as well.
Reporting while members were still receiving copies would have been a
new version of the same lie.
• Share resolves the recipient's certificate BEFORE the runner starts and
returns early when the recipient has no active suite. The flag is set
only after that resolves: such a dialog never ran, so it must keep
asking with the reason on screen.
Tests: tests/dialogs/BulkPhaseDialogs.spec.js runs the asking phase, the
switch to reporting, and the fresh-open case over all three dialogs
(describe.each), plus the refused-recipient path for share. Verified they
fail without the change.
l10n: three new keys, translated into all 36 required locales and the .js
catalogues regenerated, each following the verb and "X of Y" construction
that locale already uses.
The extension does not exist yet, so the section promised a feature no one can install. Commented out rather than deleted: markup, the steps style rule and securitySettingsUrl stay in place so restoring it is a matter of uncommenting. While in there, two fixes to the section itself. The description dropped its implementation detail — that the extension decrypts locally and the server ships only encrypted blobs — since a user wants to know their secrets are safe, not how. And the steps list gained a logical inline padding, because the server CSS zeroes list padding, which left the markers hanging outside the settings section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every one of these was reachable from the same dialog, so they land
together.
The form. Name and folder were four bare inline elements in a div with no
layout, so both labels and both controls ran onto one line and read as a
single sentence. They are NcTextField and NcSelect now, stacked in one
labelled column. "No folder" became the picker's placeholder rather than
an option, because NcSelect filters an empty-string model value out of
its selection and an option valued '' renders as though nothing were
picked. The placeholder explanation moved to a note at the top: as the
last child of the name/folder block it read as a footnote to the folder
picker rather than the premise of the dialog, and it no longer claims
something happened "now", which implied opening the dialog had already
created something.
Required fields. A blank dialog offered an enabled "Create request". The
name was only checked past the button, in a pane below the fold, and the
field selection was not checked at all — so the POST went out and the
endpoint answered 400 "requestedFields cannot be empty". The button is
gated on both now, with the reason shown next to the missing input.
The checkboxes. The server's input styling applies to checkboxes too, so
each hand-rolled row came out a clickable-area tall with padding and
margins on top of that, and the label sat on the text baseline instead of
level with the box. NcCheckboxRadioSwitch, as everywhere else in the app.
The bordered fieldset stays: the group holds the list AND the
add-a-field row, and the border is what says where it starts and stops.
Deleting a custom field. Unticking one only declines to ask for it, and
the name stayed in the list by design, which left a typo with no way out
of the dialog but cancelling it. Names typed here now carry a delete
button; the built-ins and the secret's own members do not, since they
exist whether or not this request asks for them.
The fill link. fillLinkFor passed `{ absolute: true }` to generateUrl,
which reads like it says so, but @nextcloud/router has no such option and
silently ignored it — the requester was handed a bare path to paste into
a mail. The origin is prepended explicitly, matching the absolute form
the machine API already returns for the same token.
New source strings carry all 36 locale translations, derived from the
long-form entries already in each file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening "Ask someone for a credential" from inside a vault or folder already says where the credential belongs, so the picker started empty and made the requester name the place they were standing in — and a mis-set destination is only noticed once the filled secret turns up somewhere else. SecretList passes the browsed folder in and the dialog starts on it, including after a close: reopening from the same place must offer the same destination again, not reset to unfiled. Unfiled stays available, and stays meaningful — a null folderId is the list's whole-vault query, so a request filed nowhere is still one the requester can find. Terminology, while here: "folder" is the word for the picker at every level (team decision) — the first level under the root is a vault and everything below it is a folder — so the labels stay as they are and the rail keeps its "Vaults" caption for the level that is one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were only reachable from the secret list, which is the wrong place to look for them: the rail is where vaults live. A vault's "⋮" menu gains "New folder", first in the menu because it is the only entry that adds something rather than editing or removing the vault it was opened on. Nested folder rows deliberately get nothing — they have no menu at all — so the rail cannot offer a folder as a parent, matching FolderCreateDialog's rule that a folder is only ever created inside a vault. "New vault" is a plain row at the end of the vault group rather than a caption action: a menu you have to open first is what made this look missing. The Vaults caption is no longer gated on the tree being non-empty, which was the worse half of the gap — a user with no vaults yet saw no caption, no tree, and no way to create the first one. Both entry points open the same FolderCreateDialog and differ only in the parentId they pass, which is what fixes the level at open time. No new translation keys: "New vault" and "New folder" are the dialog's own titles, already in every locale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Typing a user id by hand is how you add a member who cannot receive one: holding an ACTIVE encryption suite is what makes someone shareable at all (no suite, no public key to encrypt a copy for), and nothing in the field said so. A typo produced a membership whose copies the reconcile pass then reported as permanently missing. encryptionSuite.fetchSuiteOwners() reads the shareable users off GET /suites: active suites, owner type user, distinct, minus the current user, who is not a candidate for their own team folder. The dialog offers a picker when that list has anyone in it and keeps the free-text field when it does not, so nobody is left with a dropdown that cannot be opened. Which means it is EMPTY today, on purpose: the endpoint answers with the caller's own suites only (EncryptionSuiteController::index passes the session user to getSuitesByOwner), so every row is the current user and they are dropped. The day that endpoint can report other owners, the same code returns them and the picker appears with no further change. That is deliberately expressed as "who is in the response" rather than a capability flag or a query parameter: nothing to agree with the backend up front, and nothing to remove afterwards. Groups are still free text. The suites table knows users; a group directory would have to come from somewhere else (the sharee API, or the provisioning API for an admin), which is a decision of its own rather than a detail of this one. Also: switching the member type now clears the id. Carrying it across offered to add "bob" as a group — accepted by the field, refused by the server. No new translation keys. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Groups are the server's data, not keepiq's — a team folder stores a gid and who is in it is the server's business — so the candidates come from the provisioning API: GET /ocs/v2.php/cloud/groups, which any logged-in user may call (GroupsController::getGroups is NoAdminRequired) and which answers a plain list of gids. So the group half of the member picker works today, unlike the user half: a user must hold an active encryption suite before a secret can be encrypted to them, and that list still waits on /suites learning to answer for more than the caller. A group carries no key of its own — its members are resolved and key-checked when the fan-out runs — so there is nothing to filter it by. The endpoint pages, so the picker searches rather than pretending one fetch is the whole directory: typing refetches on a 300ms debounce, which is also the answer to "why is my group not in the list" on an instance with more groups than one page. Users need none of that; their candidates are a local list the picker already filters itself. Two OCS details that each fail as "no groups" rather than as an error, and so are pinned by tests: the OCS-APIRequest header (without it the route answers 401 whatever the session says) and format=json (without it, XML), plus reading the list out of the ocs.data envelope rather than off the response body. No new translation keys. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NcSelect ships min-width: 260px, so two of them could not share a 600px dialog with a button: the pickers took roughly half the width each and the Add button wrapped onto its own line underneath. The row sizes them itself now — 10rem for the type, the rest to the member field, natural width for the button — since neither picker holds anything long, just "User"/"Group" and an id. 10rem rather than less because NcSelect passes inputLabel to its search field instead of an external label, so the floating "Member type" has to fit inside the control. The row still wraps: under ~512px the dialog goes full width and three across does not fit. What is gone is the wrap on a desktop dialog. While lining them up: NcSelect carries its own bottom margin, so on a bottom-aligned row its control box sits one grid baseline above the row edge. The button and the plain-input fallback take the same offset, which is what actually makes the three bottoms agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uses the batch lookup from #656: POST /api/v1/shares/recipient-certificates answers, for ids the caller names, which of them hold an active suite and can therefore receive an encrypted copy. It PROBES rather than enumerates, deliberately — a certificate is a public key and safe to hand out, but "who has a keepiq vault" is a membership fact gated by no sharing permission — so the candidate ids come from Nextcloud's own sharee search, which is already permission-filtered, and the probe narrows them. That is two requests per search term, not one, and no list endpoint is involved. Which retires the placeholder this replaces: encryptionSuite's fetchSuiteOwners() read owner ids off GET /suites and waited for that endpoint to widen its scope. It never will, and now need not. A user who cannot receive secrets is LISTED AND UNSELECTABLE rather than filtered out, using the reason the endpoint returns per recipient: "alice is missing" is a bug report, "alice has no encryption suite" is an answer. Reusing that existing string, so no new translation keys. An unknown user and one without a suite report identically by design, so nothing here treats a reason as proof an account exists. Both member kinds now search the server per keystroke-burst on one debounce, and both produce the same option shape, so the picker has a single template: groups are always selectable (they hold no key of their own — members are resolved and key-checked when the fan-out runs), users only with an active suite behind them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A candidate ruled out by `selectable` came out as a near-white block with dim grey text, and the not-allowed cursor appeared only on the row's padding. Both come from the teleport. NcSelect appends its menu to <body> (vue-select's appendToBody, defaulted true), so the menu sits outside the control and loses every theme variable NcSelect declares on it. @nextcloud/vue re-declares the menu's own set on the teleported `.nc-select__dropdown` — background, text, option padding, active and keyboard-focus colours — but not the disabled ones, so the row fell back to vue-select's `:root` defaults: #f8f8f8 on rgba(60,60,60,.5). Themed in assets/app.css because the teleport puts the menu beyond the reach of any component's scoped styles, and the gap is the same for every NcSelect in the app that rules options out. Transparent background, grey text, unchanged everything else: unavailable is said by the colour and the cursor, not by a block of background. The option content also inherits the cursor now — over the text the topmost box is that span, which is why only the padding changed the pointer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A user with no active suite cannot be a member — there is no public key to encrypt their copies to — so the picker no longer lists them at all. Listing them greyed out was the previous take; it put an unpickable row in front of every choice to explain a case most requesters never hit. Which takes several things with it, all of them only there to render that row: the option objects (both kinds are plain ids again, so the picker needs no reduce and no option template), the shareability flag in the store's state, and the global disabled-option theming in assets/app.css, since nothing in the app rules select options out any more. The endpoint's per-recipient `reason` stays unread on purpose. It says no_active_suite both for a user without a suite and for one that does not exist — deliberately, so it cannot be used as a user-existence oracle — so it can tell a caller nothing beyond "not this one". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding the last candidate swapped the control: with nothing left to offer, the picker was replaced by a free-text field. A control that turns into a different control underneath you is worse than an empty list, and NcSelect already says "No results" for that state. The free-text form is gone rather than hidden, because there was nothing left for it to do: an id typed by hand is either already in the list or cannot be a member at all — a user with no active suite has no public key to encrypt their copies to, and a typo produced exactly the membership whose copies the reconcile pass then reports as permanently missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prettier's own output, no assertion touched: two single-quoted test names holding an apostrophe become double-quoted, and one expectation wraps. They arrived unformatted with 9f5b9ea and fail the format gate for the whole branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Quality Report — ConductionNL/keepiq @
|
| 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 | ✅ | ✅ 543/543 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ✅ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-08 14:17 UTC
Download the full PDF report from the workflow artifacts.
Contributor
Quality Report — ConductionNL/keepiq @
|
| 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 | ✅ | ✅ 543/543 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ✅ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-08 14:24 UTC
Download the full PDF report from the workflow artifacts.
gate-16 (spec-coverage) named 12 changed methods with no @SPEC tag, and gate-46 (spec-anchor-existence) one tag pointing at an anchor that does not exist. Reproduced locally against origin/development with the gate package from ConductionNL/.github; all 79 applicable gates pass now. The missing tags, by what they turned out to be: - The four bulk dialogs' `finished` and `title`. Both are the report half of the two-phase dialog — `title` counts its outcome off the report rather than the emptied selection — so both cite the per-item report requirement. - TeamFolderDialog's `onShareFolder`, which had no docblock at all: it is the share step every membership then hangs off, so it cites the share-a-folder requirement. - `memberIdLabel`, `candidatesLoading`, the `newMemberType` watcher and `beforeUnmount` are excluded with a reason each, naming the method that does carry the specified behaviour. A label, a spinner flag, input hygiene the server re-checks, and a cleared timer. The bad anchor was `#requirement-bulk-delete` in the delete dialog's spec, which the live bulk-actions spec has never had — its requirements are named by mechanism (the four operations, chunked execution with a per-item report), not per operation. Pointed at the two it actually tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Quality Report — ConductionNL/keepiq @
|
| 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 | ✅ | ✅ 543/543 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ✅ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-08 14:34 UTC
Download the full PDF report from the workflow artifacts.
remko48
requested changes
Sep 9, 2026
- the picker offers display names, not raw ids. `searchSharees` carries the
`label` the sharee search returns, `searchShareableRecipients` joins the
probe's id-only answer back onto it, and `fetchGroups` maps a gid to
`{ id, label: gid }` so both directories share one option shape. The
NcSelect gets `label` + `reduce`; on an LDAP or SSO instance the old list
of GUIDs was unusable for the thing the picker is for
- a failed lookup is no longer indistinguishable from "nobody matches":
both stores record `candidatesError` — kept apart from share.js's `error`,
which belongs to the share list — and the picker renders it as its own
helper line. NcSelect's `#no-options` slot would have meant
re-implementing its translated "No results" in keepiq's domain, and only
shows while the dropdown is open
- out-of-order responses cannot win any more: a monotonic seq per store
means a superseded search neither writes its list nor clears a flag the
newer one owns, while still returning its own answer to its own caller
- `onCandidateSearch` ignores the empty term vue-select re-emits when an
option is picked, which used to refetch page 1 under the user ~300 ms
after every member added
- the member field's comment no longer claims the server agrees with it.
The narrowing to the sharee-vouched, suite-holding set IS the point and is
deliberately narrower than assertMemberAddable, which asks only that the
user exists and is not the owner
- group.js's header pointed at `encryptionSuite.fetchSuiteOwners`, removed
in 211ccd3; it now names share.searchShareableRecipients
Not from the review: the edit tripped gate-16 (spec-coverage), which flags
changed methods carrying no @SPEC tag — onUpdateOpen, onRunFanOut, onUnshare
and onGradeChange had none. Tagged, onUpdateOpen excluded with a reason,
every anchor checked against the actual headings.
One new string, translated into all 36 locales; l10n:build re-run.
Contributor
Quality Report — ConductionNL/keepiq @
|
| 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 | ✅ | ✅ 543/543 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ✅ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-09 11:40 UTC
Download the full PDF report from the workflow artifacts.
remko48
approved these changes
Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Frontend work on three dialogs and the left rail, plus a handful of smaller fixes carried on the same branch.
Team folder sharing
Ask someone for a credential
generateUrlsilently ignored the{ absolute: true }it was being passed, so it was a bare path.Vault rail
Also