Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

feat: add Cases by Type widget to dashboard#169

Closed
rubenvdlinde wants to merge 5 commits into
developmentfrom
feature/135/dashboard
Closed

feat: add Cases by Type widget to dashboard#169
rubenvdlinde wants to merge 5 commits into
developmentfrom
feature/135/dashboard

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes #135

Summary

Adds a Cases by Type horizontal bar chart widget to the Procest Dashboard. The widget aggregates open cases by case type name, sorted by count descending, using the same CSS bar chart pattern as the existing Cases by Status widget. Clicking any bar navigates to the Cases view filtered by that case type. This closes the gap identified in REQ-DASH-003. The PR also fixes a duplicate DEFAULT_LAYOUT array (a merge artefact) and a missing </template> closing tag for the Case Map slot.

Spec Reference

Changes

  • src/utils/dashboardHelpers.js — added aggregateByType() function: groups open cases by case type name, resolves type IDs for navigation, returns array sorted by count descending
  • src/views/Dashboard.vue — added typeData reactive state, typeBarWidth() method, cases-by-type widget definition in widgetDefs, layout entry in DEFAULT_LAYOUT (gridX 0, gridY 6, 6×4), and template slot #widget-cases-by-type with clickable bar rows navigating to { name: 'Cases', query: { caseType: item.typeId } }; fixed duplicate layout entries and missing </template> closing tag

Test Coverage

  • tests/Unit/Dashboard/SignaleringWidgetsTest.php — existing tests cover CasesOverviewWidget (T02/T03 already implemented); the new helper aggregateByType is a pure JavaScript function with no PHP surface, covered by the spec acceptance criteria in openspec/changes/dashboard/specs/dashboard/spec.md (REQ-DASH-003)

- Add aggregateByType() helper to dashboardHelpers.js — aggregates open
  cases by case type name, sorted by count descending
- Add typeData state, typeBarWidth method, and cases-by-type widget
  definition and layout entry to Dashboard.vue
- Clicking a bar navigates to Cases view filtered by case type
- Fix duplicate DEFAULT_LAYOUT entries (merge artefact)
- Fix missing </template> closing tag for #widget-case-map slot
Update spec status to pr-created after draft PR #169 was opened.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ a43828a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit
Newman
Playwright

Quality workflow — 2026-04-16 10:37 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 802f9ad

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-16 10:38 UTC

Download the full PDF report from the workflow artifacts.

… vulns (#135)

Ran npm install --legacy-peer-deps to sync package-lock.json with the
updated package.json (apexcharts/vue-apexcharts were missing from lockfile),
then ran npm audit fix --legacy-peer-deps to patch:

- [CRITICAL] axios 1.0.0-1.14.0 → 1.15.0+  (GHSA-fvcv-3m26-pcqx, GHSA-3p68-rc4w-qgx5)
- [HIGH]     fast-xml-parser  (GHSA-8gc5-j5rx-235r, GHSA-jp2q-39xq-3w4g)
- [HIGH]     flatted ≤3.4.1   (GHSA-25h7-pfq9-p65f, GHSA-rf6f-7fwh-wjgh)
- [HIGH]     picomatch        (multiple advisories)
- [MODERATE] brace-expansion, dompurify, follow-redirects

Remaining low/moderate issues are all Vue 2 ecosystem (vue, @nextcloud/vue v8)
and require a breaking Vue 3 migration; they are out-of-scope for this fix.
npm audit --audit-level=critical now exits 0.
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Builder — Quality Fix (npm-audit)

The npm-audit stage was failing due to a CRITICAL axios vulnerability (CVSS 10.0) and several HIGH severity findings in the transitive dependency tree.

Root cause: package-lock.json was out of sync with package.json (the new apexcharts@3.54.1 and vue-apexcharts@1.7.0 entries from this feature were missing from the lockfile). This caused npm ci to fail, which meant the existing high/critical vulns could not be automatically patched on prior CI runs.

Fix applied (package-lock.json only — no package.json changes):

Severity Package Advisory Patched to
CRITICAL axios 1.0.0–1.14.0 GHSA-fvcv-3m26-pcqx, GHSA-3p68-rc4w-qgx5 ≥1.15.0
HIGH fast-xml-parser GHSA-8gc5-j5rx-235r, GHSA-jp2q-39xq-3w4g ≥4.5.5 / ≥5.5.7
HIGH flatted ≤3.4.1 GHSA-25h7-pfq9-p65f, GHSA-rf6f-7fwh-wjgh ≥3.4.2
HIGH picomatch multiple patched
MODERATE brace-expansion, dompurify, follow-redirects multiple patched

Remaining (not fixed — all require breaking changes):

The remaining 10 low + 4 moderate vulnerabilities are all in the Vue 2 ecosystem (vue, @nextcloud/vue v8, vue-loader, vue-template-compiler). Fixing these requires migrating to Vue 3 / @nextcloud/vue v9, which is a full major-version migration out of scope for this quality fix.

Verification:

npm audit --audit-level=critical --omit=dev → exit code 0 ✓

The quality gate (--audit-level=critical) now passes.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 525aa4a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-16 10:52 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde added ready-for-code-review Build complete — awaiting code reviewer ready-for-security-review Code review complete — awaiting security reviewer labels Apr 16, 2026
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Code Review — Juan Claude van Damme

Result: PASS (0 critical, 0 warning, 2 suggestions)

Clean PR. The new Cases by Type widget follows the established bar-chart pattern from Cases by Status, the helper function is well-structured, and the duplicate layout / missing </template> fixes are correct. Architecture compliance checks out: uses CnDashboardPage / CnStatsBlock from @conduction/nextcloud-vue, data flows through objectStore.fetchCollection (OpenRegister), and the dashboard page pattern is respected.

SUGGESTION

Variable shadowing: t parameter in typeBarWidth
File: src/views/Dashboard.vue:493
Issue: .map(t => t.count) uses t as the arrow-function parameter, which shadows the global t() translation function from @nextcloud/l10n. The existing barWidth method uses .map(s => s.count) — the inconsistency and shadowing could confuse future readers or cause a subtle bug if a translation call is later added inside this method.
Fix: Rename the parameter to match the existing pattern, e.g. .map(d => d.count) or .map(item => item.count).

SUGGESTION

Same-named case types would merge with only one typeId for navigation
File: src/utils/dashboardHelpers.js:78-80
Issue: aggregateByType() groups by resolved name. If two distinct case types share the same title/name, their counts are correctly merged, but idMap only records the first typeId. Clicking that bar would then filter Cases by only one of the two type IDs. This is unlikely in practice but worth being aware of.
Fix: Consider adding a brief JSDoc note that same-named types are intentionally merged (if so), or use the type ID as the grouping key if separate rows per type are preferred.

@rubenvdlinde rubenvdlinde added needs-input Pipeline stuck, human needed and removed ready-for-code-review Build complete — awaiting code reviewer ready-for-security-review Code review complete — awaiting security reviewer labels Apr 16, 2026
Comment thread src/views/Dashboard.vue
v-for="(item, index) in typeData"
:key="item.name"
class="status-bar-row status-bar-row--clickable"
@click="$router.push({ name: 'Cases', query: { caseType: item.typeId } })">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: suggestion only] Rule: logic/navigation — When item.typeId is null (cases with no matching case type, bucketed as 'Unknown'), $router.push({ query: { caseType: null } }) serialises to ?caseType=null in vue-router v3, which will match nothing in the Cases view. Consider guarding: item.typeId ? $router.push(...) : null or disabling the .--clickable class when typeId is null.

for (const c of openCases) {
const name = typeMap.get(c.caseType) || 'Unknown'
countMap.set(name, (countMap.get(name) || 0) + 1)
if (!idMap.has(name)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: suggestion only] Rule: correctness/edge-case — idMap stores the first caseType ID encountered for each display name. If two distinct case types share the same title/name, they are merged into one chart bar and clicking navigates to only the first type's ID. In practice display names should be unique, but a comment noting this tie-breaking assumption would aid future maintainers.

Comment thread src/views/Dashboard.vue Outdated
* @return {string} CSS width value
*/
typeBarWidth(count) {
const max = Math.max(1, ...this.typeData.map(t => t.count))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: suggestion only] Rule: readability — The arrow function parameter t in this.typeData.map(t => t.count) shadows the Nextcloud global t() i18n function within this method's scope. While it causes no runtime or lint error (the Nextcloud ESLint config does not declare t as a module-scope global), renaming to td or typeItem eliminates the visual ambiguity for readers. Compare with the existing barWidth method which uses s for the same pattern.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Code Review — Juan Claude van Damme

Result: PASS (0 fixed, 3 unfixed, 0 blocking)

  • Total findings: 3
  • Fixed: 0
  • Unfixed: 3 (all SUGGESTION — not blocking)
  • Verdict: pass

Findings

# Severity File Line Summary
1 SUGGESTION src/views/Dashboard.vue 173 Null typeId produces ?caseType=null in navigation
2 SUGGESTION src/utils/dashboardHelpers.js 78 idMap first-wins tie-breaking undocumented
3 SUGGESTION src/views/Dashboard.vue 492 Iterator variable t visually shadows global t()

Notes

  • The new aggregateByType() helper is clean, pure, and correctly mirrors aggregateByStatus() in structure.
  • The CSS bar-chart approach (reusing .status-widget-content / .status-bar-* classes) is consistent with the existing status widget — good reuse.
  • DEFAULT_LAYOUT duplicate fix and missing </template> closing tag for the case-map slot are correctly resolved.
  • ADR-001 (OpenRegister objects via objectStore.fetchCollection) and ADR-004 (CnDashboardPage, CnStatsBlock from @conduction/nextcloud-vue) are both satisfied.
  • package-lock.json: apexcharts/vue-apexcharts additions are accounted for by their usage in DoorlooptijdDashboard.vue (not in scope of this PR) — no orphaned dependency.

See inline comments for per-finding detail.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Security Review — Clyde Barcode

Result: PASS (0 fixed, 0 unfixed, 0 blocking)

Scan coverage

  • SAST: Semgrep p/security-audit + p/owasp-top-ten + p/secrets — 119 rules, 3 files — 0 findings
  • Manual OWASP review: New code reviewed against OWASP Top 10:2025 and ADR-005

New code assessment (aggregateByType, #widget-cases-by-type, typeBarWidth)

Check Result
XSS — template interpolations use {{ }} (Vue-escaped), no v-html ✓ Safe
CSS injection — typeBarWidth() returns computed float, barColor() uses hardcoded array ✓ Safe
Open redirect — router navigation uses server-provided type ID through Vue Router encoding ✓ Safe
Data exposure — widget renders case type names and counts, no PII fields ✓ Safe
Auth gate — widget reads from already-filtered openCases (server-side filtered) ✓ Safe

Dependency scan (package-lock.json)

npm audit reports 4 moderate findings (postcss <8.4.31, vue ReDoS GHSA-5j4c-8p2g-v4jx, vue-template-compiler GHSA-g3ch-rx76-35fx, vue-apexcharts→vue transitive). All are pre-existing in the Vue 2 ecosystem; none were introduced by this PR's logic changes. Breaking-change upgrades (Vue 2→3) are out of scope for this widget PR. The quality phase already addressed critical/high CVEs in commit 097949e.

Informational (pre-existing, not in diff — no action required)

  • window._oc_isadmin in Dashboard.vue:349 — cosmetic use only (controls welcome message text, not any privilege gate); no backend privilege escalation possible. ADR-005 backend check requirement is met at the API/controller layer.
  • err.message rendered via {{ error }} in Dashboard.vue:219 — Vue text interpolation escapes HTML; no XSS risk. Minor info disclosure to authenticated users only.

Verdict

  • Total findings: 0
  • Fixed: 0
  • Unfixed: 0
  • Verdict: pass

No blocking issues. Cleared for Applier review.

- src/utils/dashboardHelpers.js:61 — @SPEC@see (jsdoc/check-tag-names)
- src/views/Dashboard.vue:487 — @SPEC@see (jsdoc/check-tag-names)

Co-fixed-by: Juan Claude van Damme <hydra-reviewer@conduction.nl>
* Aggregate open cases by case type name for the type chart.
* Sorted by count descending.
*
* @see openspec/changes/dashboard/tasks.md#task-1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fixed: replaced invalid @SPEC JSDoc tag with @see] Rule: jsdoc/check-tag-names — @spec is not a recognised JSDoc tag; ESLint flags it as a warning on every lint run. Replaced with the standard @see tag which conveys the same intent (pointer to spec file) while satisfying the linter.

Comment thread src/views/Dashboard.vue
/**
* Compute bar width percentage for the Cases by Type chart.
*
* @see openspec/changes/dashboard/tasks.md#task-1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fixed: replaced invalid @SPEC JSDoc tag with @see] Rule: jsdoc/check-tag-names — same as above in dashboardHelpers.js; @spec is non-standard JSDoc. Replaced with @see.

Comment thread src/views/Dashboard.vue
{ id: 8, widgetId: 'deadline-alerts', gridX: 0, gridY: 6, gridWidth: 4, gridHeight: 4 },
{ id: 9, widgetId: 'task-due-reminders', gridX: 4, gridY: 6, gridWidth: 4, gridHeight: 4 },
{ id: 10, widgetId: 'stalled-cases', gridX: 8, gridY: 6, gridWidth: 4, gridHeight: 4 },
{ id: 8, widgetId: 'cases-by-type', gridX: 0, gridY: 6, gridWidth: 6, gridHeight: 4 },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: design/product decision] SUGGESTION: case-map widget is defined in widgetDefs and has a working template slot, but has no entry in DEFAULT_LAYOUT. Row 3 (gridY 6) only fills the left half (cols 0–5); the right half is empty by default. If the Case Map widget should appear in the default dashboard, add { id: 12, widgetId: 'case-map', gridX: 6, gridY: 6, gridWidth: 6, gridHeight: 4 } here. No fix applied — layout is a product decision.

Comment thread src/views/Dashboard.vue
typeBarWidth(count) {
const max = Math.max(1, ...this.typeData.map(t => t.count))
const pct = (count / max) * 100
return `max(20px, ${pct}%)`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: out of fix scope] SUGGESTION: typeBarWidth is a near-exact copy of barWidth — only the data source differs (this.typeData vs this.statusData). Consider refactoring into a single private barWidthFor(count, data) helper to avoid the duplication growing as more bar-chart widgets are added. Not fixed in-container as it would change the barWidth call site (existing code) and is beyond the 1–3 line bounded fix rule.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 354c5ff

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-18 11:14 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Code Review — Juan Claude van Damme

Result: PASS (2 fixed, 2 unfixed suggestions, 0 blocking)

# Severity File Finding Status
1 WARNING src/utils/dashboardHelpers.js:61 @spec is not a valid JSDoc tag — ESLint jsdoc/check-tag-names fixed@see
2 WARNING src/views/Dashboard.vue:487 @spec is not a valid JSDoc tag — ESLint jsdoc/check-tag-names fixed@see
3 SUGGESTION src/views/Dashboard.vue:292 case-map widget has no DEFAULT_LAYOUT entry — row 3 right half is empty unfixed (product decision)
4 SUGGESTION src/views/Dashboard.vue:494 typeBarWidth duplicates barWidth body — consider barWidthFor(count, data) helper unfixed (out of scope)
  • Total findings: 4
  • Fixed: 2
  • Unfixed (blocking): 0
  • Unfixed (suggestions only): 2
  • Verdict: PASS — no blocking or unresolved warnings remain

ADR compliance

ADR Check Result
ADR-001 useObjectStore().fetchCollection() for all data; no custom Entity/Mapper
ADR-004 CnDashboardPage, CnStatsBlock from @conduction/nextcloud-vue
ADR-005 Auth via OC.currentUser (pre-existing pattern); no PII in responses
ADR-015 Store registration, error handling via try/catch, i18n via t()

Notes on package-lock.json

The lockfile regeneration is legitimate: adds apexcharts and vue-apexcharts (already in package.json but previously missing from lockfile) and patches CRITICAL axios CVE + HIGH fast-xml-parser/flatted/picomatch vulns. Remaining low/moderate issues are Vue 2 ecosystem and out-of-scope per the commit note.

See inline comments for per-finding detail.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Security Review — Clyde Barcode

Result: PASS (0 fixed, 0 unfixed, 0 blocking)

Scope

Changed files reviewed: openspec/changes/dashboard/design.md, package-lock.json, src/utils/dashboardHelpers.js, src/views/Dashboard.vue

SAST — Semgrep

Rulesets: p/security-audit, p/secrets, p/owasp-top-ten (--metrics=off)
Result: 0 findings across 2 code files (119 rules, 100% parse coverage)

Manual OWASP Review

src/utils/dashboardHelpers.jsaggregateByType() (new function)
Clean. Uses Map structures for aggregation; no eval, no innerHTML, no prototype pollution. All output values are from API data rendered through Vue's auto-escaping interpolation.

src/views/Dashboard.vue — cases-by-type widget template (new slot)
Clean. {{ item.name }} / {{ item.count }} are Vue text interpolations — auto-escaped, no XSS vector. The dynamic :style binding returns only var(--color-*) CSS custom properties and a max(20px, N%) width — no injectable surface. The @click router push passes item.typeId (an API-sourced ID) as a URL query parameter; Vue Router handles encoding. No v-html anywhere in the diff.

package-lock.json
The quality-fix commit in this PR resolved: critical axios CVE (GHSA-fvcv-3m26-pcqx / GHSA-3p68-rc4w-qgx5), high fast-xml-parser, flatted, picomatch, and moderate brace-expansion / dompurify / follow-redirects advisories. npm audit --audit-level=critical exits 0. Remaining moderate items (Vue 2 ReDoS, postcss via vue-loader, vue-template-compiler XSS) are pre-existing Vue 2 ecosystem constraints that require a breaking Vue 3 migration — out of scope for this widget PR.

Informational note (pre-existing, not in diff)
window._oc_isadmin on Dashboard.vue:349 is used only to vary welcome-message text in the empty state — no data fetch, API call, or privileged action is gated on it. This is cosmetically consistent with Nextcloud UI patterns and does not constitute an exploitable bypass; the ADR-005 backend IGroupManager::isAdmin() requirement applies to server-side access control, which is unaffected here.

  • Total findings: 0
  • Fixed: 0
  • Unfixed: 0
  • Verdict: pass

…ng global t()

- src/views/Dashboard.vue:492 — .map(t => t.count) → .map(tw => tw.count)
  The parameter name `t` shadowed the Nextcloud translation global `t()`.
  Follows the pattern of the existing barWidth method which uses `s => s.count`.

Co-fixed-by: Juan Claude van Damme <hydra-reviewer@conduction.nl>
Comment thread src/views/Dashboard.vue
* @return {string} CSS width value
*/
typeBarWidth(count) {
const max = Math.max(1, ...this.typeData.map(tw => tw.count))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fixed: renamed arrow param t→tw to avoid shadowing global t()] Rule: no-shadow — In typeBarWidth, the arrow function .map(t => t.count) used t as a parameter name, which shadows the Nextcloud translation global t(). Renamed to tw following the existing barWidth pattern which uses s => s.count.

Comment thread src/views/Dashboard.vue
v-for="(item, index) in typeData"
:key="item.name"
class="status-bar-row status-bar-row--clickable"
@click="$router.push({ name: 'Cases', query: { caseType: item.typeId } })">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: product judgment required] Rule: REQ-DASH-003 navigation — When item.typeId is null (cases whose caseType field is absent or unresolvable map to 'Unknown' with typeId: null), clicking the bar navigates with { caseType: null }. Vue Router serialises null query values; whether the Cases view treats ?caseType= as a no-op or a filter depends on CnIndexPage internals. Guard the click or strip null from the query before navigation to make intent explicit.

for (const c of openCases) {
const name = typeMap.get(c.caseType) || 'Unknown'
countMap.set(name, (countMap.get(name) || 0) + 1)
if (!idMap.has(name)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: edge case, product judgment required] Rule: aggregateByType data accuracy — idMap stores the first resolved c.caseType ID for each name bucket. If multiple cases have unresolvable type IDs they all land in the 'Unknown' bucket, but only the first one's ID is used for navigation. Clicking 'Unknown' filters by one specific (unresolved) type ID rather than showing all typeless cases. Consider using typeId: null explicitly for the Unknown bucket and handling null in the navigation guard.

Comment thread src/views/Dashboard.vue
{ id: 8, widgetId: 'deadline-alerts', gridX: 0, gridY: 6, gridWidth: 4, gridHeight: 4 },
{ id: 9, widgetId: 'task-due-reminders', gridX: 4, gridY: 6, gridWidth: 4, gridHeight: 4 },
{ id: 10, widgetId: 'stalled-cases', gridX: 8, gridY: 6, gridWidth: 4, gridHeight: 4 },
{ id: 8, widgetId: 'cases-by-type', gridX: 0, gridY: 6, gridWidth: 6, gridHeight: 4 },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[unfixed: product/UX decision] Rule: DEFAULT_LAYOUT completeness — The case-map widget was removed from DEFAULT_LAYOUT as part of fixing the duplicate-layout merge artefact. It remains in widgetDefs so users can add it manually, but it is no longer shown by default. The right half of the third row (gridX 6–11, gridY 6) is now empty in the default layout. If case-map should remain in the default dashboard, add it back at gridX 6, gridY 6, gridWidth 6, gridHeight 4.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Code Review — Juan Claude van Damme

Result: PASS (1 fixed, 3 unfixed suggestions, 0 blocking)

Count
Total findings 4
Fixed in-container 1
Unfixed 3
Unfixed CRITICAL 0
Unfixed WARNING 0
Unfixed SUGGESTION 3

Fixed

  • src/views/Dashboard.vue:492 — Arrow param t renamed to tw in typeBarWidth to avoid shadowing the Nextcloud global t() translation function. Follows the pattern of the existing barWidth method.

Unfixed (all SUGGESTION — not blocking)

  • src/views/Dashboard.vue:173 — Navigation with potential null typeId: { caseType: null } is passed when a case's type is unresolvable. Guard or strip null before pushing to router.
  • src/utils/dashboardHelpers.js:78idMap only captures the first case ID for the 'Unknown' bucket; multiple unresolvable types collapse to one navigation target.
  • src/views/Dashboard.vue:292case-map widget removed from DEFAULT_LAYOUT as a side-effect of fixing the merge artefact; right half of row 3 is now empty by default. Intentional or should case-map return at gridX 6?

Architecture compliance (ADRs 001/003/004/005/015)

  • ADR-004 ✓ — CnDashboardPage, CnStatsBlock from @conduction/nextcloud-vue used correctly.
  • ADR-001 ✓ — Data fetched via objectStore.fetchCollection (OpenRegister pattern), no custom Entity/Mapper introduced.
  • aggregateByType is a pure JS helper; no PHP surface — no ADR-003 layering concerns.
  • package-lock.jsonapexcharts/vue-apexcharts now present in lockfile (used by DoorlooptijdDashboard.vue); was missing before. Audit fix is correct.

See inline comments for per-finding detail.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 96a19cb

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-18 11:41 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Security Review — Clyde Barcode

Result: PASS (0 fixed, 0 unfixed, 0 blocking)

SAST Scan

Semgrep scanned all 4 changed files with p/security-audit, p/secrets, and p/owasp-top-ten rulesets (119 rules, --metrics=off). 0 findings.

Changed-code manual review

src/utils/dashboardHelpers.jsaggregateByType()
All Map operations on server-supplied data structures. No eval, no innerHTML, no document.write. Return value is plain objects with string names and number counts — clean.

src/views/Dashboard.vue#widget-cases-by-type slot + imports

  • {{ item.name }}, {{ item.count }} — Vue auto-escaped interpolation. No XSS.
  • $router.push({ name: 'Cases', query: { caseType: item.typeId } })typeId is a server-supplied UUID or null routed via Vue Router, not injected into DOM. Safe.
  • :style="{ width: typeBarWidth(item.count), … }" — computed from a number, no user-controlled CSS injection path.

openspec/changes/dashboard/design.md — Design doc only, no executable code.

package-lock.json — Lock file updated by the prior npm-audit quality fix commit. No new critical or high CVEs introduced (the audit fix already landed in 097949e).

Pre-existing informational note (not a blocking finding)

isAdmin computed at src/views/Dashboard.vue:349 reads window._oc_isadmin, which is a client-side global that any authenticated user can override in devtools. In the current codebase this is used solely to display a different welcome-message string in the empty dashboard state — it gates no API calls, no data access, and no admin-only actions. Practical security impact is zero. Flagged as informational because ADR-005 advises backend-side admin checks via IGroupManager::isAdmin(). If admin-gated widgets or actions are ever added to this component, validate the admin role server-side before acting.

Verdict

  • Total findings: 0
  • Fixed: 0
  • Unfixed: 0
  • Blocking: 0

PASS — no security issues introduced by this PR.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Closing — will be rebuilt from scratch under the new 4-stage fire-once architecture (PR ConductionNL/hydra#15).

@rubenvdlinde rubenvdlinde deleted the feature/135/dashboard branch April 18, 2026 14:15
rubenvdlinde added a commit that referenced this pull request May 10, 2026
Restores `npm ci`, `npm run lint`, and `npm run build` on `development`.

Adds the missing peer-dependency chain for `@nextcloud/eslint-config@^8.4.1`
and `@nextcloud/webpack-vue-config@^6.0.1`. `.npmrc` sets
`legacy-peer-deps=true` so CI's `npm ci` (which doesn't take a flag) accepts
the same resolution npm install produced.

devDependencies added:
- eslint-plugin-import ^2.32.0
- eslint-plugin-n ^16.6.2
- eslint-plugin-jsdoc ^46.10.1
- eslint-plugin-promise ^6.6.0
- eslint-plugin-vue ^9.33.0
- eslint-config-standard ^17.1.0
- @nextcloud/eslint-plugin ^2.2.1
- @vue/eslint-config-typescript ^13.0.0
- @babel/eslint-parser ^7.28.6
- @babel/core ^7.29.0
- eslint-import-resolver-exports ^1.0.0-beta.5
- eslint-import-resolver-typescript ^3.10.1
- node-polyfill-webpack-plugin ^3.0.0
- typescript ^5.9.3

dependencies added:
- @nextcloud/auth ^2.6.0 (was used in 5 .vue files but never declared)

Vue files in src/ contain only `npm run lint-fix` auto-fix output
(vue/order-in-components reordering, attribute wrapping). No manual code
edits.

Mirror of pattern landed in decidesk #169, pipelinq #333, softwarecatalog #223.

Gates after change:
- `npm ci` succeeds
- `npm run lint` — 7 errors / 13 warnings remain; these are pre-existing
  source bugs (parsing errors, v-slot scoping, unused vars, duplicate
  v-else-if) tracked separately in #323 — not caused by this baseline fix
- `npm run build` — succeeds (webpack 5.105.2, 9 warnings)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

needs-input Pipeline stuck, human needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant