Skip to content

Commit d0b9760

Browse files
rubenvdlindeConduction Release Bot
andauthored
feat: migrate scholiq from Vue 2 to Vue 3 (@conduction/nextcloud-vue 2.1.0-vue3.13) (#249)
* test(e2e): make the target instance explicit — never fall back to the shared :8080 container The suite resolved its target three different ways, all defaulting to `http://localhost:8080` — the SHARED developer container, which bind-mounts real host checkouts: * playwright.config.ts baseURL * tests/e2e/global-setup.ts login + OR_BASE_URL for the seed * tests/e2e/seed-example-data.mjs and four specs in tests/e2e/spec-coverage/nextcloud-app.spec.ts built ABSOLUTE `http://localhost:8080/...` request URLs that ignored `baseURL` entirely — two of them WRITES (POST /api/settings, PUT /api/notification-preferences). Running `npm run test:e2e` without setting PW_BASE_URL therefore fired admin logins, a register import, an example-data seed and two config writes into somebody else's working environment, and reported the results as the branch under test. Centralise on tests/e2e/base-url.ts: strict (no localhost fallback — an unset target is a hard error) but accepting PLAYWRIGHT_BASE_URL, the shared CI quality workflow's BASE_URL, and this repo's historical PW_BASE_URL, so neither a local run nor a CI run silently retargets. * wip(scholiq): preserve in-flight Vue 3 migration after host reset Committed mid-flight so the work survives. NOT verified: the build/deploy/e2e run was still queued on the shared heavy-lock when the host was reset. See the branch's PR (if any) for what remains. * revert(docs): restore the 52 tutorial screenshots the e2e run rewrote `npm run test:e2e` reshoots and overwrites every PNG under docs/static/screenshots/tutorials/ as a side effect, because the `docs-capture` Playwright project was documented as opt-in but was not: a project listed in `projects[]` runs whenever no `--project` filter is given, and `test:e2e` gives none. The config is fixed on this branch (the project is now gated on SCHOLIQ_DOCS_CAPTURE=1). These 52 binary rewrites were incidental output from the measurement runs, not an intended docs update, so they are restored to their `development` content and do not belong in this PR. Also drops the untracked .scholiq-boot-smoke.mjs probe that was swept into the preservation commit — it is a local measurement tool, not app code. * style(php): clear the remaining 23 PHPCS errors so the gate can go green `PHP Quality (phpcs)` has been failing on `development` for a long time — 26 errors across 11 files. It is not a cosmetic red: the shared quality workflow gates `phpunit` on `needs.php-quality.result != 'failure'`, so scholiq's PHPUnit suite has NEVER executed on a pull request. An earlier commit on this branch fixed 3 of them in GradeFormulaEvaluator.php. That number came from reading the CI log through `tail -20`, which silently cut off every earlier file — the truncating filter decided the count. The full inventory is 26, and this commit clears the other 23: 8 lib/Listener/BsaProgressFlagHandler.php const + assignment alignment, @PARAM type padding 5 lib/Lifecycle/BsaDecisionGuard.php const + assignment alignment, doc-comment capital, 152-char line 2 lib/Listener/FraudCaseDecisionHandler.php assignment alignment, 152-char line 2 lib/Lifecycle/FraudCaseBlockGuard.php 163-char line, comment capital 1 lib/StudyProgress/BsaProgressEvaluator.php inline IF -> if/else 1 lib/Lifecycle/FraudCaseInvalidationGuard.php 159-char line 1 lib/Lifecycle/FraudCaseHearingGuard.php blank line before first function 1 lib/Lifecycle/ExemptionDecisionGuard.php blank line before first function 1 lib/Lifecycle/BsaWarningSigningGuard.php blank line before first function 1 lib/Lifecycle/FraudCaseDecisionGuard.php assignment alignment All behaviour-preserving: whitespace, comment capitalisation, long log strings split across two concatenated lines, one ternary expanded to if/else, and two long expressions extracted to a local variable. Every touched file passes `php -l` (positive-controlled against a deliberately malformed file). None of these files is touched by the Vue 3 migration; they are cleared here because the project standard is to fix pre-existing quality issues on contact rather than leave them. * style(php): use single-space assignment where alignment exceeds maxPadding The previous commit aligned `$verdict` with `$contestedGradeEntryId`, which is the wrong direction: Generic.Formatting.MultipleStatementAlignment falls back to requiring exactly ONE space once a group's longest variable would push the padding past maxPadding. phpcs said so plainly — "expected 1 space but found 15 spaces" — and the original code had the same error with 14. Aligning harder made it 15. This is the last of the 26 PHPCS errors that were failing on `development`. * fix(e2e): bound the networkidle wait in the docs-capture helper `go()` called `waitForLoadState('networkidle')` with no timeout. That inherits the test budget, so it can never REJECT — the `.catch()` next to it was dead code, and the helper blocked until the whole test timed out. That is the entire Vue 2 -> Vue 3 e2e delta. Measured on one instance, same page, same session, deploying each build in turn: Vue 2: 90 requests, 0 still in flight after 40 s -> 'UN grading' passes 2/2 (~57 s) Vue 3: 93 requests, 1 still in flight after 40 s -> 'UN grading' fails 3/3 (90 s), and still fails when given 300 s, so it HANGS rather than being slow The straggler is `GET /apps/openregister/api/schemas/grade-entry`, which 404s (3 of the register's 118 schemas do not import). @conduction/nextcloud-vue's `useObjectStore.fetchSchema` does: if (!response.ok) return null without consuming or cancelling the response body, so the browser keeps the request open indefinitely and `networkidle` becomes unreachable for the life of the page. Reported upstream — the real fix belongs in nc-vue, not here. Bounding the wait restores the helper's stated intent (its own comment says "idle never fires on some pages") and matches index-pages, detail-pages, shell and accessibility-axe-scan, which all already pass an explicit timeout. No assertion is changed, nothing is skipped: the test now PASSES on Vue 3 in 38 s. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
1 parent 03b554d commit d0b9760

52 files changed

Lines changed: 3299 additions & 2204 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/openspec-sync.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ on:
88

99
jobs:
1010
sync:
11-
uses: ConductionNL/.github/.github/workflows/openspec-sync.yml@feature/openspec-project-sync
11+
# ⚠️ This was pinned to `@feature/openspec-project-sync`, a branch that does
12+
# not exist in ConductionNL/.github (verified with `git ls-remote --heads`,
13+
# positive-controlled against `main`, which does resolve). The org is right,
14+
# so a `uses:`-grep for `Conduction/` finds nothing wrong — but Actions still
15+
# cannot resolve the ref, and the workflow has produced ZERO jobs on every
16+
# push since 2026-05-23. The tell is in `gh run list --json name`: runs from
17+
# 2026-05-18 and earlier report `OpenSpec Sync`; every run since reports the
18+
# raw path `.github/workflows/openspec-sync.yml`. Pin to the default branch,
19+
# as every other workflow in this repo does.
20+
uses: ConductionNL/.github/.github/workflows/openspec-sync.yml@main
1221
with:
1322
app-name: scholiq
1423
secrets:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ npm run build # production build
102102

103103
| Layer | Technology |
104104
|---|---|
105-
| Frontend | Vue 2.7, `@conduction/nextcloud-vue` CnAppRoot (Tier 4) |
105+
| Frontend | Vue 3.5, `@nextcloud/vue` 9, `@conduction/nextcloud-vue` CnAppRoot (Tier 4) |
106106
| Data model | `src/manifest.json` + `lib/Settings/scholiq_register.json` |
107107
| Build | Webpack 5, `@nextcloud/webpack-vue-config` |
108108
| Backend | PHP 8.1+, Nextcloud App Framework |

docs/Technical/specs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ All 6 Wave-2 compliance-audit wedge specs have been applied. Specs live in `open
1010

1111
**Status:** Applied
1212

13-
**Summary:** Foundation spec that ships the standards-compliant Nextcloud app shell before any other capability can land. Delivers: `appinfo/info.xml` with hard dependencies on OpenRegister and OpenConnector; `lib/AppInfo/Application.php` registering all services and listeners; the Vue 2 SPA entry point (`src/main.js`); the admin and user settings panels; `src/manifest.json` adopting CnAppRoot Tier 4 (ADR-024); `l10n/nl.js` and `l10n/en.js` i18n stubs. All downstream specs (`course-management`, `enrolment`, `certification`, `compliance-audit`, `dashboard`) depend on this foundation.
13+
**Summary:** Foundation spec that ships the standards-compliant Nextcloud app shell before any other capability can land. Delivers: `appinfo/info.xml` with hard dependencies on OpenRegister and OpenConnector; `lib/AppInfo/Application.php` registering all services and listeners; the Vue 3 SPA entry point (`src/main.js`); the admin and user settings panels; `src/manifest.json` adopting CnAppRoot Tier 4 (ADR-024); `l10n/nl.js` and `l10n/en.js` i18n stubs. All downstream specs (`course-management`, `enrolment`, `certification`, `compliance-audit`, `dashboard`) depend on this foundation.
1414

1515
---
1616

docs/features.json

Lines changed: 169 additions & 80 deletions
Large diffs are not rendered by default.

eslint.config.js

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,69 @@ const {
88
FlatCompat,
99
} = require('@eslint/eslintrc')
1010

11+
// The Vue-3 rule preset ships INSIDE @conduction/nextcloud-vue, so it can only
12+
// be enabled after the dependency is installed — deps first, then lint, then
13+
// code. `conductionVue3Fixes` is an ARRAY OF THREE configs, not one object, and
14+
// it registers no plugins, which is why it layers cleanly onto the @nextcloud
15+
// base. Spread it LAST so its overrides win.
16+
//
17+
// (CommonJS, so the extensionless subpath works. From ESM this must be
18+
// '@conduction/nextcloud-vue/eslint/index.js' — the package ships no `exports`
19+
// map, and the directory form throws ERR_UNSUPPORTED_DIR_IMPORT.)
20+
const { conductionVue3Fixes } = require('@conduction/nextcloud-vue/eslint')
21+
1122
const compat = new FlatCompat({
1223
baseDirectory: __dirname,
1324
recommendedConfig: js.configs.recommended,
1425
allConfig: js.configs.all,
1526
})
1627

17-
module.exports = defineConfig([{
18-
extends: compat.extends('@nextcloud'),
19-
20-
settings: {
21-
'import/resolver': {
22-
alias: {
23-
map: [
24-
['@', './src'],
25-
['@floating-ui/dom-actual', './node_modules/@floating-ui/dom'],
26-
['@conduction/nextcloud-vue', '../nextcloud-vue/src'],
27-
],
28-
extensions: ['.js', '.ts', '.vue', '.json', '.css'],
28+
module.exports = defineConfig([
29+
{
30+
extends: compat.extends('@nextcloud'),
31+
32+
settings: {
33+
'import/resolver': {
34+
alias: {
35+
map: [
36+
['@', './src'],
37+
['@floating-ui/dom-actual', './node_modules/@floating-ui/dom'],
38+
// Resolve the library from node_modules, NOT from the sibling
39+
// `../nextcloud-vue` checkout: that checkout sits on the Vue 2
40+
// (beta.*) line, so pointing the resolver at it made lint reason
41+
// about a different library than the one this app builds against.
42+
['@conduction/nextcloud-vue', './node_modules/@conduction/nextcloud-vue'],
43+
],
44+
extensions: ['.js', '.ts', '.vue', '.json', '.css'],
45+
},
2946
},
3047
},
31-
},
3248

33-
rules: {
34-
// Allow unused i18n functions (t, n) — imported for future translation wiring
35-
'no-unused-vars': ['error', { varsIgnorePattern: '^(t|n)$', argsIgnorePattern: '^_' }],
36-
'jsdoc/require-jsdoc': 'off',
37-
'vue/first-attribute-linebreak': 'off',
38-
'@typescript-eslint/no-explicit-any': 'off',
39-
'n/no-missing-import': 'off',
40-
'import/namespace': 'off', // disable namespace checking to avoid parser requirement
41-
'import/default': 'off', // disable default import checking to avoid parser requirement
42-
'import/no-named-as-default': 'off', // disable named-as-default checking to avoid parser requirement
43-
'import/no-named-as-default-member': 'off', // disable named-as-default-member checking to avoid parser requirement
49+
rules: {
50+
// Allow unused i18n functions (t, n) — imported for future translation wiring
51+
'no-unused-vars': ['error', { varsIgnorePattern: '^(t|n)$', argsIgnorePattern: '^_' }],
52+
'jsdoc/require-jsdoc': 'off',
53+
// `@spec` and `@e2e` are the Hydra traceability tags (gate-16 / gate-19).
54+
// They are not JSDoc-standard, so check-tag-names flagged every single
55+
// use: 256 of this repo's 274 pre-existing lint warnings were this one
56+
// rule complaining about a convention the quality gates REQUIRE.
57+
'jsdoc/check-tag-names': ['warn', { definedTags: ['spec', 'e2e'] }],
58+
'vue/first-attribute-linebreak': 'off',
59+
'@typescript-eslint/no-explicit-any': 'off',
60+
'n/no-missing-import': 'off',
61+
'import/namespace': 'off', // disable namespace checking to avoid parser requirement
62+
'import/default': 'off', // disable default import checking to avoid parser requirement
63+
'import/no-named-as-default': 'off', // disable named-as-default checking to avoid parser requirement
64+
'import/no-named-as-default-member': 'off', // disable named-as-default-member checking to avoid parser requirement
65+
},
4466
},
45-
}])
67+
68+
// MUST come last. The @nextcloud v8 base is a Vue 2 config: it activates
69+
// ZERO `vue/no-deprecated-*` rules (verified with `eslint --print-config`
70+
// on the pre-migration tree), so Vue-2-only idioms such as `beforeDestroy`
71+
// survive a clean lint run and become silent runtime no-ops under Vue 3.
72+
// It also arms two INVERTED rules — `vue/no-v-model-argument` and
73+
// `vue/no-v-for-template-key`, both at severity 2 — which reject valid
74+
// Vue 3 syntax. This preset fixes both directions.
75+
...conductionVue3Fixes,
76+
])

lib/Grading/GradeFormulaEvaluator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ private function compareGradedAt(array $a, array $b): int
386386
/**
387387
* Compute weighted average and breakdown from entries.
388388
*
389-
* exam-board-case-handling: `sourceKind: exemption` entries are excluded
389+
* Exam-board-case-handling: `sourceKind: exemption` entries are excluded
390390
* from the `$weightedSum`/`$totalWeight` accumulation (both overall and
391391
* per-period) — their `value` is always null and MUST NOT be cast to
392392
* `0.0` and summed with full weight (the pre-existing bug this fix
@@ -416,7 +416,7 @@ private function weightedAverage(array $entries, array $components): array
416416
foreach ($entries as $entry) {
417417
$cid = $entry['componentId'] ?? '';
418418

419-
// exam-board-case-handling: an exemption entry has no numeric value —
419+
// Exam-board-case-handling: an exemption entry has no numeric value —
420420
// it satisfies its component without contributing to the weighted sum.
421421
if (($entry['sourceKind'] ?? null) === 'exemption') {
422422
$componentBreakdown[$cid] = ['exempt' => true];
@@ -529,7 +529,7 @@ private function evaluatePassed(
529529
return false;
530530
}
531531
}
532-
}
532+
}//end if
533533

534534
return true;
535535

lib/Lifecycle/BsaDecisionGuard.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@
5252
class BsaDecisionGuard
5353
{
5454

55-
private const SCHOLIQ_REGISTER = 'scholiq';
55+
private const SCHOLIQ_REGISTER = 'scholiq';
5656
private const BSA_WARNING_SCHEMA = 'bsa-warning';
5757

5858
/**
59-
* decisionType values that trigger the warning + rationale requirement.
59+
* DecisionType values that trigger the warning + rationale requirement.
6060
*
6161
* @var string[]
6262
*/
@@ -114,11 +114,18 @@ public function check(array &$transitionContext): bool
114114
$isNegative = in_array($decisionType, self::NEGATIVE_DECISION_TYPES, true);
115115

116116
if ($isNegative === true) {
117-
$learnerId = $object['learnerId'] ?? '';
118-
$programmeId = $object['programmeId'] ?? '';
117+
$learnerId = $object['learnerId'] ?? '';
118+
$programmeId = $object['programmeId'] ?? '';
119119
$academicYear = $object['academicYear'] ?? '';
120120

121-
if ($this->hasIssuedWarning(learnerId: $learnerId, programmeId: $programmeId, academicYear: $academicYear, tenantId: $tenantId) === false) {
121+
$hasWarning = $this->hasIssuedWarning(
122+
learnerId: $learnerId,
123+
programmeId: $programmeId,
124+
academicYear: $academicYear,
125+
tenantId: $tenantId
126+
);
127+
128+
if ($hasWarning === false) {
122129
$this->logger->info(
123130
'BsaDecisionGuard: no issued BsaWarning found for learner {l}, programme {p}, year {y} — blocking negative decision.',
124131
['l' => $learnerId, 'p' => $programmeId, 'y' => $academicYear]

lib/Lifecycle/BsaWarningSigningGuard.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
*/
6464
class BsaWarningSigningGuard
6565
{
66-
6766
/**
6867
* Constructor.
6968
*

lib/Lifecycle/ExemptionDecisionGuard.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
*/
5252
class ExemptionDecisionGuard
5353
{
54-
5554
/**
5655
* Constructor.
5756
*

lib/Lifecycle/FraudCaseBlockGuard.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,14 @@ public function check(array &$transitionContext): bool
125125

126126
if ($lifecycle === 'decided' && $verdict === 'fraud-proven') {
127127
$this->logger->info(
128-
'[FraudCaseBlockGuard] GradeEntry {id} permanently blocked — linked FraudCase {caseId} decided fraud-proven; the only forward path is invalidate.',
128+
'[FraudCaseBlockGuard] GradeEntry {id} permanently blocked — linked FraudCase {caseId} '
129+
.'decided fraud-proven; the only forward path is invalidate.',
129130
['id' => $entryId, 'caseId' => $fraudCaseId]
130131
);
131132
return false;
132133
}
133134

134-
// decided/unfounded or dismissed — publication may proceed.
135+
// Decided/unfounded or dismissed — publication may proceed.
135136
return true;
136137

137138
}//end check()

0 commit comments

Comments
 (0)