From 2489e5f69d34b9e6ff0ac07bbeda1cb700489ef2 Mon Sep 17 00:00:00 2001 From: Clupai8o0 Date: Sun, 27 Sep 2026 20:04:20 +1000 Subject: [PATCH 1/2] chore: T2 2026 platform, CI and dependency updates Brings the T2 2026 platform work from ontrack-features-t2-2026 11.0.x (reviewed and merged work) onto thoth-tech 11.0.x. Co-authored-by: maplefoxgit Co-authored-by: Maple Fox Co-authored-by: Maple 'Ryan' Fox Co-authored-by: lgr2002 Co-authored-by: Eloise Ridder-Strickland --- .github/dependabot.yml | 4 + .github/workflows/deployment-institution.yml | 15 +- .github/workflows/deployment.yml | 16 +- .github/workflows/lint.yml | 4 +- .github/workflows/nodejs-ci.yml | 13 +- .github/workflows/test.yml | 18 +- .github/workflows/validate-nginx.yml | 34 + .nvmrc | 2 +- Gruntfile.js | 756 ------- MIGRATION-GUIDE.md | 224 -- README.md | 171 +- build.config.js | 97 - docs/course-flow-rebase.md | 96 + docs/course-flow/README.md | 51 + docs/course-flow/browser-check.mjs | 228 ++ docs/course-flow/http-check.mjs | 185 ++ docs/course-flow/validation.md | 40 + docs/css-style-guide.md | 81 + docs/desktop-pwa-validation.md | 118 + docs/desktop-pwa.md | 150 ++ docs/index.md | 26 + docs/migration/analytics.md | 76 + docs/migration/legacy-audit.md | 104 + docs/migration/recovery-register.md | 98 + docs/migration/ui-catalogue.md | 93 + docs/student-onboarding/README.md | 96 + docs/student-onboarding/threat-model.md | 58 + docs/student-onboarding/validation.md | 125 ++ docs/task-sheet-widget-feedback.md | 190 ++ env.config.js | 31 - karma/karma-unit.tpl.js | 60 - migration_progress.sh | 14 - module.prefix | 1 - module.suffix | 1 - nginx.conf | 59 +- package-lock.json | 2120 +++++++++--------- package.json | 48 +- scripts/test-nginx.py | 121 + scripts/verify-pwa.js | 124 + setup.sh | 82 - src/environments/environment.prod.spec.ts | 12 + src/environments/environment.prod.ts | 22 +- src/vitest-setup.ts | 9 + 43 files changed, 3412 insertions(+), 2461 deletions(-) create mode 100644 .github/workflows/validate-nginx.yml delete mode 100644 Gruntfile.js delete mode 100644 MIGRATION-GUIDE.md delete mode 100644 build.config.js create mode 100644 docs/course-flow-rebase.md create mode 100644 docs/course-flow/README.md create mode 100644 docs/course-flow/browser-check.mjs create mode 100644 docs/course-flow/http-check.mjs create mode 100644 docs/course-flow/validation.md create mode 100644 docs/css-style-guide.md create mode 100644 docs/desktop-pwa-validation.md create mode 100644 docs/desktop-pwa.md create mode 100644 docs/index.md create mode 100644 docs/migration/analytics.md create mode 100644 docs/migration/legacy-audit.md create mode 100644 docs/migration/recovery-register.md create mode 100644 docs/migration/ui-catalogue.md create mode 100644 docs/student-onboarding/README.md create mode 100644 docs/student-onboarding/threat-model.md create mode 100644 docs/student-onboarding/validation.md create mode 100644 docs/task-sheet-widget-feedback.md delete mode 100644 env.config.js delete mode 100644 karma/karma-unit.tpl.js delete mode 100755 migration_progress.sh delete mode 100644 module.prefix delete mode 100644 module.suffix create mode 100644 scripts/test-nginx.py create mode 100644 scripts/verify-pwa.js delete mode 100755 setup.sh create mode 100644 src/environments/environment.prod.spec.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8abca405fb..5c8d9a0ceb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,7 @@ updates: directory: "/" # Location of package manifests schedule: interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/deployment-institution.yml b/.github/workflows/deployment-institution.yml index d9f6075048..d326026b90 100644 --- a/.github/workflows/deployment-institution.yml +++ b/.github/workflows/deployment-institution.yml @@ -4,18 +4,21 @@ on: tags: - 'v*' workflow_dispatch: +permissions: + contents: read jobs: docker-web-server: + if: github.repository_owner == 'doubtfire-lms' environment: deployment-secrets runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Check custom institution deployment secrets id: custom_config env: @@ -30,14 +33,14 @@ jobs: echo "Skipping custom institution Docker publish because one or more institution secrets are not set." fi - name: Login to DockerHub - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 if: github.event_name != 'pull_request' && steps.custom_config.outputs.enabled == 'true' with: username: ${{ secrets.INSTITUTION_DOCKERHUB_USERNAME }} password: ${{ secrets.INSTITUTION_DOCKERHUB_TOKEN }} - name: Setup meta for custom institution web server id: docker_meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 if: steps.custom_config.outputs.enabled == 'true' with: images: ${{ secrets.INSTITUTION_DOCKERHUB_USERNAME }}/${{ secrets.INSTITUTION }} @@ -49,7 +52,7 @@ jobs: type=semver,pattern=prod-{{major}} - name: Build and push custom web server id: docker_build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 if: steps.custom_config.outputs.enabled == 'true' with: file: deploy.Dockerfile @@ -57,6 +60,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} labels: ${{ steps.docker_meta.outputs.labels }} + sbom: true + provenance: mode=max build-args: | SENTRY_DSN=${{ secrets.SENTRY_DSN }} SENTRY_ORG=${{ secrets.SENTRY_ORG }} diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 6c986fcff7..507dfb6c2a 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -41,7 +41,7 @@ jobs: # context: . # push: ${{ github.event_name != 'pull_request' }} # tags: ${{ steps.docker_meta.outputs.tags }} - # labels: ${{ steps.meta.outputs.labels }} + # labels: ${{ steps.docker_meta.outputs.labels }} # - name: Image digest # run: echo ${{ steps.docker_build.outputs.digest }} docker-web-server: @@ -50,20 +50,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Login to DockerHub - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup meta for web server id: docker_meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: lmsdoubtfire/doubtfire-web tags: | @@ -74,12 +74,14 @@ jobs: type=semver,pattern=prod-{{major}} - name: Build and push web server id: docker_build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: file: deploy.Dockerfile context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + labels: ${{ steps.docker_meta.outputs.labels }} + sbom: true + provenance: mode=max - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0bb56ce5ad..053acc04a6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,9 +13,9 @@ jobs: node-version: [22] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: ${{ matrix.node-version }} - run: npm ci diff --git a/.github/workflows/nodejs-ci.yml b/.github/workflows/nodejs-ci.yml index df3f516514..318561c9dc 100644 --- a/.github/workflows/nodejs-ci.yml +++ b/.github/workflows/nodejs-ci.yml @@ -15,11 +15,16 @@ jobs: matrix: node-version: [22] steps: - - uses: actions/checkout@v6 - - uses: browser-actions/setup-chrome@v2 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + submodules: recursive + - uses: browser-actions/setup-chrome@48ad923757ca74d66703209fe939badbdf80f2f4 # v2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: ${{ matrix.node-version }} - run: npm ci - - run: npm run build --if-present + - run: npm run verify:deployment-config + - run: npm run verify:pwa + - run: npm run build -- --configuration production + - run: npm run verify:pwa:build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0254469205..8ef1fa6685 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,11 +9,23 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Use Node.js 22 - uses: actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 22 cache: npm - run: npm ci - - run: npm run test:ci + - name: Accessibility regression tests + run: npm run test:a11y + - name: Check theme first paint, compiled surfaces and contrast + run: npx vitest run scripts/theme/theme-surfaces.spec.ts + - run: npm run test:coverage + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: web-coverage + path: coverage/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/validate-nginx.yml b/.github/workflows/validate-nginx.yml new file mode 100644 index 0000000000..67d04a3d0d --- /dev/null +++ b/.github/workflows/validate-nginx.yml @@ -0,0 +1,34 @@ +name: Validate production Nginx + +on: + pull_request: + paths: + - nginx.conf + - deploy.Dockerfile + - src/manifest.webmanifest + - scripts/test-nginx.py + - .github/workflows/validate-nginx.yml + push: + branches: ["11.0.x"] + paths: + - nginx.conf + - deploy.Dockerfile + - src/manifest.webmanifest + - scripts/test-nginx.py + - .github/workflows/validate-nginx.yml + +permissions: + contents: read + +concurrency: + group: nginx-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + nginx: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Exercise static responses from the production configuration + run: python3 -B scripts/test-nginx.py diff --git a/.nvmrc b/.nvmrc index 6276cf12fb..6bb900519d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.14.2 +v22.22.3 diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index d05e0b8b1a..0000000000 --- a/Gruntfile.js +++ /dev/null @@ -1,756 +0,0 @@ -module.exports = function (grunt) { - /** - * Load required Grunt tasks. These are installed based on the versions listed - * in `package.json` when you do `npm install` in this directory. - */ - grunt.loadNpmTasks('grunt-contrib-clean'); - grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-watch'); - - grunt.loadNpmTasks('grunt-contrib-coffee'); - grunt.loadNpmTasks('grunt-sass'); - grunt.loadNpmTasks('grunt-contrib-connect'); - grunt.loadNpmTasks('grunt-bump'); - grunt.loadNpmTasks('grunt-coffeelint'); - grunt.loadNpmTasks('grunt-karma'); - grunt.loadNpmTasks('grunt-ng-annotate'); - grunt.loadNpmTasks('grunt-html2js'); - grunt.loadNpmTasks('grunt-env'); - grunt.loadNpmTasks('grunt-preprocess'); - grunt.loadNpmTasks('grunt-sass-globbing'); - grunt.loadNpmTasks('grunt-postcss'); - grunt.loadNpmTasks('grunt-newer'); - - var sass = require('sass'); - /** - * Load in our build configuration file. - */ - var userConfig = require('./build.config.js'); - - /** - * Load in our environment configuration file. - */ - var envConfig = require('./env.config.js'); - - /** - * This is the configuration object Grunt uses to give each plugin its - * instructions. - */ - var taskConfig = { - /** - * We read in our `package.json` file so we can access the package name and - * version. It's already there, so we don't repeat ourselves here. - */ - pkg: grunt.file.readJSON("package.json"), - - /** - * The banner is the comment that is placed at the top of our compiled - * source files. It is first processed as a Grunt template, where the `<%=` - * pairs are evaluated based on this very configuration object. - */ - meta: { - banner: - '/**\n' + - ' * <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + - ' * <%= pkg.homepage %>\n' + - ' *\n' + - ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + - // ' * Licensed <%= pkg.licenses.type %> <<%= pkg.licenses.url %>>\n' + - ' */\n' - }, - - /** - * Creates a changelog on a new version. - */ - changelog: { - options: { - dest: 'CHANGELOG.md', - template: 'changelog.tpl' - } - }, - - /** - * Increments the version number, etc. - */ - bump: { - options: { - files: [ - "package.json" - ], - commit: false, - commitMessage: 'chore(release): v%VERSION%', - commitFiles: [ - "package.json" - ], - createTag: false, - tagName: 'v%VERSION%', - tagMessage: 'Version %VERSION%', - push: false, - pushTo: 'origin' - } - }, - - /** - * The directories to delete when `grunt clean` is executed. - */ - clean: { - build: [ - '<%= build_dir %>/*', - '<%= compile_dir %>/*' - ], - styles: [ - '<%= temp_scss_file %>' - ] - }, - - /** - * The `copy` task just copies files from A to B. We use it here to copy - * our project assets (images, fonts, etc.) and javascripts into - * `build_dir`, and then to copy the assets to `compile_dir`. - */ - copy: { - build_assets: { - files: [ - { - src: ['**'], - dest: '<%= build_dir %>/assets/', - cwd: 'src/assets', - expand: true, - follow: true - } - ] - }, - font_bootstrap: { - files: [ - { - src: ['**'], - dest: '<%= build_dir %>/assets/fonts/bootstrap/', - cwd: 'node_modules/bootstrap-sass/assets/fonts/bootstrap', - expand: true - } - ] - }, - font_fontawesome: { - files: [ - { - src: ['**'], - dest: '<%= build_dir %>/assets/fonts/font-awesome/', - cwd: 'node_modules/font-awesome/fonts', - expand: true - } - ] - }, - build_app_js: { - files: [ - { - src: ['<%= app_files.js %>'], - dest: '<%= build_dir %>/', - cwd: '.', - expand: true - } - ] - }, - build_vendor_js: { - files: [ - { - src: ['<%= vendor_files.compile.js %>'], - dest: '<%= build_dir %>/', - cwd: '.', - expand: true - } - ] - }, - build_vendor_jsmap: { - files: [ - { - src: ['<%= vendor_files.compile.jsmap %>'], - dest: '<%= build_dir %>', - cwd: '.', - expand: true - } - ] - }, - copy_vendor_js: { - files: [ - { - src: ['<%= vendor_files.copy.js %>'], - dest: '<%= build_dir %>/assets', - cwd: '.', - expand: true - } - ] - }, - copy_vendor_jsmap: { - files: [ - { - src: ['<%= vendor_files.copy.jsmap %>'], - dest: '<%= build_dir %>/assets', - cwd: '.', - expand: true - } - ] - }, - copy_vendor_css: { - files: [ - { - src: ['<%= vendor_files.copy.css %>'], - dest: '<%= build_dir %>/assets', - cwd: '.', - expand: true - } - ] - }, - compile_assets: { - files: [ - { - src: ['**'], - dest: '<%= compile_dir %>/assets', - cwd: '<%= build_dir %>/assets', - expand: true - } - ] - }, - to_api: { - files: [ - { - src: ['**'], - dest: '<%= api_dir %>/public', - cwd: '<%= compile_dir %>', - expand: true - } - ] - } - }, - - /** - * `grunt concat` concatenates multiple source files into a single file. - */ - concat: { - /** - * The `compile_js` target is the concatenation of our application source - * code and all specified vendor source code into a single file. - */ - compile_js: { - options: { - banner: '<%= meta.banner %>' - }, - src: [ - '<%= vendor_files.compile.js %>', - 'module.prefix', - '<%= build_dir %>/src/**/*.js', - '<%= html2js.app.dest %>', - 'module.suffix' - ], - dest: '<%= compile_dir %>/assets/<%= pkg.name %>.js' - } - }, - - /** - * `grunt coffee` compiles the CoffeeScript sources. To work well with the - * rest of the build, we have a separate compilation task for sources and - * specs so they can go to different places. For example, we need the - * sources to live with the rest of the copied JavaScript so we can include - * it in the final build, but we don't want to include our specs there. - */ - coffee: { - source: { - options: { - bare: true - }, - expand: true, - cwd: '.', - src: ['<%= app_files.coffee %>'], - dest: '<%= build_dir %>', - ext: '.js' - } - }, - - /** - * Runs global imports on SASS directories. This will generate our - * main.scss for us which will be compiled by the `sass` task - */ - sass_globbing: { - source: { - files: { - '<%= temp_scss_file %>': [ - 'src/styles/vendor-config/**/*.scss', - '<%= vendor_files.compile.scss %>', - '<%= app_files.scss %>' - ] - }, - options: { - useSingleQuotes: false - } - } - }, - - /** - * Autoprefixes to handle browser vendor prefixes - */ - postcss: { - options: { - map: true, - processors: [ - require('autoprefixer')({ browsers: ['last 2 versions'] }) - ] - }, - source: { - src: '<%= build_dir %>/assets/<%= pkg.name %>.css', - dest: '<%= build_dir %>/assets/<%= pkg.name %>.css' - } - }, - - /** - * `sass` compiles the SASS into the build path's CSS file - */ - sass: { - source: { - options: { - implementation: sass, - lineNumbers: true, - sourcemap: 'none' - }, - files: [{ - cwd: '.', - src: ['<%= temp_scss_file %>'], - dest: '<%= build_dir %>/assets/<%= pkg.name %>.css' - }] - } - }, - - /** - * `ng-annotate` annotates the sources before minifying. That is, it allows us - * to code without the array syntax. - */ - ngAnnotate: { - compile: { - files: [ - { - expand: true, - src: ['<%= build_dir %>/src/**/*.js', - '<%= app_files.js %>'], - dest: '.', - } - ] - } - }, - - /** - * `jshint` defines the rules of our linter as well as which files we - * should check. This file, all javascript sources, and all our unit tests - * are linted based on the policies listed in `options`. But we can also - * specify exclusionary patterns by prefixing them with an exclamation - * point (!); this is useful when code comes from a third party but is - * nonetheless inside `src/`. - */ - jshint: { - src: [ - '<%= app_files.js %>' - ], - gruntfile: [ - 'Gruntfile.js' - ], - options: { - curly: true, - immed: true, - newcap: true, - noarg: true, - sub: true, - boss: true, - eqnull: true, - esversion: 6 - }, - // No test or globals provided - comment out to silence warnings - // test: [ - // '<%= app_files.jsunit %>' - // ], - // globals: {} - }, - - /** - * `coffeelint` does the same as `jshint`, but for CoffeeScript. - */ - coffeelint: { - src: { - files: { - src: ['<%= app_files.coffee %>'] - } - }, - test: { - files: { - src: ['<%= app_files.coffeeunit %>'] - } - }, - options: { - max_line_length: { - level: 'ignore' - } - } - }, - - /** - * HTML2JS is a Grunt plugin that takes all of your template files and - * places them into JavaScript files as strings that are added to - * AngularJS's template cache. This means that the templates too become - * part of the initial payload as one JavaScript file. Neat! - */ - html2js: { - /** - * These are the templates from `src/app`. - */ - app: { - options: { - base: 'src/app' - }, - src: ['<%= app_files.atpl %>'], - dest: '<%= build_dir %>/templates-app.js' - } - }, - - /** - * The Karma configurations. - */ - karma: { - options: { - configFile: '<%= build_dir %>/karma-unit.js' - }, - unit: { - port: 9019, - runnerPort: 9101, - background: true - }, - continuous: { - singleRun: true - } - }, - - /** - * The `index` task compiles the `index.html` file as a Grunt template. CSS - * and JS files co-exist here but they get split apart later. - */ - index: { - /** - * During development, we don't want to have wait for compilation, - * concatenation, minification, etc. So to avoid these steps, we simply - * add all script files directly to the `` of `index.html`. The - * `src` property contains the list of included files. - */ - build: { - dir: '<%= build_dir %>', - src: [ - '<%= vendor_files.compile.js %>', - '<%= build_dir %>/src/**/*.js', - '<%= build_dir %>/assets/node_modules/**/*.css', - '<%= build_dir %>/assets/node_modules/**/*.js', - '<%= html2js.app.dest %>', - '<%= postcss.source.dest %>' - ] - }, - - /** - * When it is time to have a completely compiled application, we can - * alter the above to include only a single JavaScript and a single CSS - * file. Now we're back! - */ - compile: { - dir: '<%= compile_dir %>', - src: [ - '<%= compile_dir %>/assets/**/*.css', - '<%= compile_dir %>/assets/**/*.js', - '<%= concat.compile_js.dest %>' - ] - } - }, - - /** - * This task compiles the karma template so that changes to its file array - * don't have to be managed manually. - */ - karmaconfig: { - unit: { - dir: '<%= build_dir %>', - src: [ - '<%= vendor_files.compile.js %>', - '<%= html2js.app.dest %>', - 'node_modules/angular-mocks/angular-mocks.js' - ] - } - }, - - /** - * And for rapid development, we have a watch set up that checks to see if - * any of the files listed below change, and then to execute the listed - * tasks when they do. This just saves us from having to type "grunt" into - * the command-line every time we want to see what we're working on; we can - * instead just leave "grunt watch" running in a background terminal. Set it - * and forget it, as Ron Popeil used to tell us. - * - * But we don't need the same thing to happen for all the files. - */ - delta: { - /** - * Disable livereload for delta; let the BrowserSync task handle this - * for us - */ - options: { - livereload: false, - interval: 4000 - }, - - /** - * When the Gruntfile changes, we just want to lint it. In fact, when - * your Gruntfile changes, it will automatically be reloaded! We also - * should rebuild in case we changed any vendor files - */ - gruntfile: { - files: 'Gruntfile.js', - tasks: ['jshint:gruntfile', 'build'] - }, - - /** - * When our JavaScript source files change, we want to run lint them and - * run our unit tests. - */ - jssrc: { - files: [ - '<%= app_files.js %>' - ], - tasks: ['jshint:src', 'copy:build_app_js'] - }, - - /** - * When our CoffeeScript source files change, we want to run lint them and - * run our unit tests. - */ - coffeesrc: { - files: [ - '<%= app_files.coffee %>' - ], - tasks: ['newer:coffeelint:src', 'newer:coffee:source', 'copy:build_app_js'] //'karma:unit:run', - }, - - /** - * When assets are changed, copy them. Note that this will *not* copy new - * files, so this is probably not very useful. - */ - assets: { - files: [ - 'src/assets/**/*' - ], - tasks: ['copy:build_assets'] - }, - - /** - * When index.html changes, we need to compile it. - */ - html: { - files: ['<%= app_files.html %>'], - tasks: ['newer:index:build'] - }, - - /** - * When our templates change, we only rewrite the template cache. - */ - tpls: { - files: [ - '<%= app_files.atpl %>', - ], - tasks: ['newer:html2js'] - }, - - /** - * When the CSS files change, we need to compile and minify them. - */ - styles: { - files: ['src/**/*.scss'], - tasks: ['styles'] - }, - - /** - * When a JavaScript unit test file changes, we only want to lint it and - * run the unit tests. We don't want to do any live reloading. - */ - jsunit: { - files: [ - '<%= app_files.jsunit %>' - ], - tasks: ['jshint:test', 'karma:unit:run'] - }, - - /** - * When a CoffeeScript unit test file changes, we only want to lint it and - * run the unit tests. We don't want to do any live reloading. - */ - coffeeunit: { - files: [ - '<%= app_files.coffeeunit %>' - ], - tasks: ['coffeelint:test', 'karma:unit:run'] - } - }, // end delta - - /** - * Use the browserSync task to reload our web browser on change - */ - // browserSync: { - // dev: { - // bsFiles: { - // // Watch build directory - // src: userConfig.build_dir + '/**/*.*' - // }, - // options: { - // watchTask: true, - // server: '<%= build_dir %>', - // port: 8000 - // } - // } - // } - }; // end task config - - grunt.initConfig(grunt.util._.extend(taskConfig, userConfig, envConfig)); - - /** - * Allow override for development env using NODE_ENV - */ - gruntEnv = "env:" + (process.env.NODE_ENV || 'development'); - - /** - * In order to make it safe to just compile or copy *only* what was changed, - * we need to ensure we are starting from a clean, fresh build. So we rename - * the `watch` task to `delta` (that's why the configuration var above is - * `delta`) and then add a new task called `watch` that does a clean build - * before watching for changes. - */ - grunt.renameTask('watch', 'delta'); - grunt.registerTask('watch', ['build', 'delta']); - - /** - * Grunt watchsvr no longer exists - */ - grunt.registerTask('watchsvr', function () { - grunt.log.warn('`grunt watchsvr` has been deprecated. Just use `grunt` instead'); - }); - - /** - * The default task is to build and compile. - */ - grunt.registerTask('deploy', ['production', 'copy:to_api']); - grunt.registerTask('production', ['env:production', 'build', 'compile']); - grunt.registerTask('development', ['watch']); - grunt.registerTask('default', ['development']); - - /** - * Style tasks in one grunt task - */ - grunt.registerTask('styles', [ - 'sass_globbing', - 'sass', - 'postcss', - 'clean:styles' - ]); - - /** - * The `build` task gets your app ready to run for development and testing. - */ - grunt.registerTask('build', [ - gruntEnv, - 'clean', - 'html2js', - 'jshint', - 'coffeelint', - 'coffee', - 'styles', - 'ngAnnotate', - 'copy:build_assets', - 'copy:font_bootstrap', - 'copy:font_fontawesome', - 'copy:build_app_js', - 'copy:build_vendor_js', - 'copy:build_vendor_jsmap', - 'copy:copy_vendor_js', - 'copy:copy_vendor_jsmap', - 'copy:copy_vendor_css', - 'index:build', - 'preprocess' - ]); - - /** - * The `compile` task gets your app ready for deployment by concatenating and - * minifying your code. - */ - grunt.registerTask('compile', [ - 'styles', - 'copy:compile_assets', - 'ngAnnotate', - 'concat', - 'index:compile' - ]); - - /** - * A utility function to get all app JavaScript sources. - */ - function filterForJS(files) { - return files.filter(function (file) { - return file.match(/\.js$/); - }); - } - - /** - * A utility function to get all app CSS sources. - */ - function filterForCSS(files) { - return files.filter(function (file) { - return file.match(/\.css$/); - }); - } - - /** - * The index.html template includes the stylesheet and javascript sources - * based on dynamic names calculated in this Gruntfile. This task assembles - * the list into variables for the template to use and then runs the - * compilation. - */ - grunt.registerMultiTask('index', 'Process index.html template', function () { - var dirRE = new RegExp('^(' + grunt.config('build_dir') + '|' + grunt.config('compile_dir') + ')\/', 'g'); - var jsFiles = filterForJS(this.filesSrc).map(function (file) { - return file.replace(dirRE, ''); - }); - var cssFiles = filterForCSS(this.filesSrc).map(function (file) { - return file.replace(dirRE, ''); - }); - - grunt.file.copy('src/index.html', this.data.dir + '/index.html', { - process: function (contents, path) { - return grunt.template.process(contents, { - data: { - scripts: jsFiles, - styles: cssFiles, - version: grunt.config('pkg.version') - } - }); - } - }); - }); - - /** - * In order to avoid having to specify manually the files needed for karma to - * run, we use grunt to manage the list for us. The `karma/*` files are - * compiled as grunt templates for use by Karma. Yay! - */ - grunt.registerMultiTask('karmaconfig', 'Process karma config templates', function () { - var jsFiles = filterForJS(this.filesSrc); - - grunt.file.copy('karma/karma-unit.tpl.js', grunt.config('build_dir') + '/karma-unit.js', { - process: function (contents, path) { - return grunt.template.process(contents, { - data: { - scripts: jsFiles - } - }); - } - }); - }); - -}; diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md deleted file mode 100644 index 3c3800c24f..0000000000 --- a/MIGRATION-GUIDE.md +++ /dev/null @@ -1,224 +0,0 @@ -![Doubtfire Logo](src/assets/icons/android-chrome-192x192.png) - -# Doubtfire Web - Angular.js to Angular migration guide - -This guide will provide the basic steps you can follow in order to successfully migrate a Doubtfire component from CoffeeScript and Angular.js to TypeScript and Angular. - -Here we will demonstrate what to do using the changes associated with the Task Description card. - -## Step 0 - Come up with a plan - -Before you really get started you need to identify a suitable component to migrate, and come up with a plan for the components new styling. - -### Identifying a suitable component to migrate - -When picking a component make sure your component **DOES NOT** have any other Angular.js components nested within it. Always start at the bottom and work our way up to the "bigger" components. - -Look for components that should be easy to migrate. It will be easier to migrate things that just present data, but in general remember you already have a working component. So it should mostly be a matter of mapping the code across, and changing the styling to switch from bootstrap to material design. - -When in doubt... ask. We are happy to make suggestions for components that should be beneficial to migrate. - -We picked the Task Description Card for this guide as it was relatively straightforward in terms of functionality, and it does not have any other Doubtfire components nested within it. - -### Plan your components new styling - -Explore the [Angular Material](https://material.angular.io) details. Look for similar [components](https://material.angular.io/components/categories) to the bootstrap components being used in the component you are migrating. Where there is a simple mapping you can probably just proceed, but if you want to make bigger changes (which is good) then please mock something up and discuss with the team first. We do not want you wasting effort if your ideas are not in line with what we will accept. - -For the Task Description Card there is a matching [card component](https://material.angular.io/components/card/overview) in Angular Material with a suitable [example](https://material.angular.io/components/card/examples) we can build from. We can do a first pass and then make any styling fixes if needed at the end. - -## Step 1 - Create a branch - -Ok, this should be a given but it is always going to be important to isolate the changes for these migrations. So start by checking out a new branch for this change. - -For the task description card this was done using the following command in the Terminal: - -```bash -git checkout -b migrate/task-description-card -``` - -## Step 2 - Create replacement files - -Create a typescript, scss, and html file to replace the coffeescript, scss, and html files from the angular.js project. - -For the Task Description Card we had the files: - -- task-description-card.coffee -- task-description-card.tpl.html -- task-description-card.scss - -In the same folder we can start by creating the following files: - -- task-description-card.component.ts -- task-description-card.component.html -- task-description-card.component.scss - -Notice the naming convention. When migrating a component we use the format _name_.**component**._extension_. - -Add the start of the typescript using something based on the following: - -```typescript -import {Component, Inject, Input} from '@angular/core'; - -@Component({ - selector: 'task-description-card', - templateUrl: 'task-description-card.component.html', - styleUrls: ['task-description-card.component.scss'], -}) -export class TaskDescriptionCardComponent { - constructor() {} -} -``` - -We cant see any of these changes yet, but it is a good clean start so lets commit this before we move on. - -```bash -git add task-description-card.component.ts -git add task-description-card.component.html -git add task-description-card.component.scss -# or "git add ." if you have a clean repository and only these files are changed. -git commit -m "NEW: Create initial files for migration of task-description-card" -``` - -Then we should make sure to push this back to GitHub so that others can see our progress. As this is a new branch you will need set the upstream branch, but if you forget the `git push` will remind you anyway. - -```bash -git push --set-upstream origin migrate/task-description-card -``` - -You should see a suggestion to create a pull request when you add this branch. This is also a good idea. At this stage you can create a [Draft Pull Request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) which will get updated as you push your changes. Use this to get a quick screenshot of the component in action currently. Then you can include this as a "from" image. - -See the resulting [commit](933df7b673b5d57dc162fb42f79c511444f5fbe3) and [pull request](https://github.com/doubtfire-lms/doubtfire-web/pull/321). Its great that you can go back and see how things evolved. - -## Step 3 - Unlink old component and add in new - -We want to make sure we can see our progress as quickly as possible. So lets start by replacing the old component with the new one. - -There are a few files we need to update to achieve this. - -- Remove link to component from the angular module. - - Open the component's CoffeeScript file and make a note of the name of the module. - - ```coffeescript - angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-description-card', []) - ``` - - The name is `doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-description-card` - - - Search for where this is referenced. For the task-description-card this was in file `src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee`... hence the suggestion to search :satisfied:. - - Remove the reference. This will mean that the component is no longer loaded by angular.js. - -- Setup the new component in **doubtfire-angular.module.ts** - - Import like this: - ```ts - import {TaskDescriptionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component'; - ``` - - Then add the component name to the list of **declarations**. Now the component will be available in Angular. -- Remove the old and downgrade the new in **doubtfire-angularjs.module.ts** - - Remove the line importing the old javascript file: - ```typescript - import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.js'; - ``` - - Import the new component... _it is the same code as importing to the angular module_. - - Downgrade to make the new component available to Angular.js - ```typescript - DoubtfireAngularJSModule.directive( - 'taskDescriptionCard', - downgradeComponent({component: TaskDescriptionCardComponent}), - ); - ``` -- Update attributes on the new component usage. - - Search for all of the places where the component was already used (i.e. search for the component HTML tag). - - Update the property binding style to use the Angular form which is `[property]="value"`. - - For example: - - ```html - - ``` - - Needs to change to: - - ```html - - ``` - -- Add matching inputs into your components typescript declaration. These use the syntax `@Input() name: type;`. For the task description card we use: - - ```ts - export class TaskDescriptionCardComponent { - @Input() task: any; - @Input() taskDef: any; - @Input() unit: any; - ... - } - ``` - -Now we can compile to see if this has all worked... :crossed_fingers:. When you change the config in this way I generally find you need to kill any old build processes and start them again. So we can compile now using: - -```bash -npm start -``` - -Open your local copy of Doubtfire and navigate somewhere that you can see the lack of the old component. As we have no HTML yet, there should be nothing in its place but we are mostly checking that we have connected this all up correctly. - -See the resulting [commit](81563fa3bc04ba4c1afc40a682dbce0703dfc10b) for all of these changes. - -Once things are working make sure to commit and push your changes. This will update your draft pull request as well. - -Now we are on to the "real" work. - -## Step 4 - Migrate the HTML, CoffeeScript, and SCSS - -This is where things are going to depend on what you are migrating. Hopefully you have already got a plan for this... - -For the task description card I used the [mat-card](https://material.angular.io/components/card/examples) as the main component I needed to migrate to. I did this with the following steps: - -1. Copy in the current bootstrap HTML -2. Identify update to wrap in the material card component (save and test) -3. Update the header (save and test) -4. Update the footer to actions (save and test) -5. Scan for other bootstrap tags and replace. - -As I needed it I copied across the CoffeeScript code into the TypeScript component. This included switching the hyperlinks to use buttons, and including the addition of the file saver to make it easy to save the downloaded task sheets and resources as a click action rather than opening a new page with the links. - -At the end I made sure to check that **all functionality** from the old component had a matching set of code in the new component. - -Here are a few things to watch out for when doing this migration: - -- Switching `ng-show` to `[hidden]` remember to change the boolean expression. -- Font awesome icons needs to change to Material Design icons using [mat-icon](https://material.angular.io/components/icon/overview) - check out the [icon list]. -- Make sure to add [aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-hidden_attribute) and [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute) to ensure icons and images are screen reader friendly. -- Make sure to run `ng lint` at the end of this process to make sure your TypeScript code is looking good! - -### A note on porting the CSS of components - -In Doubtfire's AngularJS codebase, the components use bootstrap classes such as `card-sm` or `card-danger`. The eventual goal of the rewrite is to remove this Bootstrap dependancy as we are moving to Angular Material. As such, when the new components are being created, these styles need to be replaced with Angular Material equivalents, or a similar component design needs to be written which can replace the old one. - -These bootstrap components are usually bassed into the `class` of the components, for example: - -- `class="card-heading"`, `class="card-body"` would need to be recreated with: https://material.angular.io/components/card/overview -- `class="text-muted"` would be recreated with: https://material.angular.io/guide/typography - -Once things are all working... you can delete the old CoffeeScript, HTML and SCSS files. These are no longer needed... so they can go! - -Check the [final commit](9c8a62c1d70e8f950f8c72b3a7a48c0d7274f670) with all of these changes. - -## Step 5 - Create the Pull Request - -Now it is time to update the Pull Request. Please ensure the pull request contains two screenshots for each use-case of the component, showing the old component (before) and the new component (after). The component can also be demonstrated in a video which can be directly uploaded to the Pull Request (please do not link to another hosting platform). Once you have added this, make sure to note any changes that the team should check. You want to make sure that you dont break things when then is merged in. If you think everything is ready then switch from a draft to full PR... As long as things are tidy, and you have clear screenshots to show that everything is working, you can expect things to be merged quickly or for you to get some instructions on what to change. - -You can check out how this [pull request](https://github.com/doubtfire-lms/doubtfire-web/pull/321) went... :crossed_fingers: - -## Conclusion - -Hopefully this has provided some useful steps that will mean you can quickly and efficiently migrate the Doubtfire components. We are all really looking forward to switching to the new Angular approach, and trying to keep things more up to date going forward. - -Many thanks for your contributions on behalf of the Doubtfire team! diff --git a/README.md b/README.md index b050cbf4a6..9fde95fce2 100644 --- a/README.md +++ b/README.md @@ -2,110 +2,165 @@ OnTrack logo

-# OnTrack Web [![CI](https://img.shields.io/github/workflow/status/doubtfire-lms/doubtfire-web/Node.js%20CI?label=CI&logo=GitHub)](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml) +# OnTrack Web [![CI](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml/badge.svg)](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml) A modern, lightweight learning management system. ## Table of Contents -- [Doubtfire Web ![CI](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml)](#doubtfire-web-) - - [Table of Contents](#table-of-contents) - - [Getting Started](#getting-started) - - [Deployment](#deployment) - - [Resources](#resources) - - [Contributing](#contributing) - - [License](#license) +- [Getting Started](#getting-started) +- [Checks](#checks) +- [Production build and handover](#production-build-and-handover) +- [Demo and live feature boundaries](#demo-and-live-feature-boundaries) +- [Resources](#resources) +- [Contributing](#contributing) +- [License](#license) -## Getting Started +## Desktop and mobile app -If you will be using [Docker](https://github.com/doubtfire-lms/doubtfire-api/#getting-started-via-docker), follow the instructions there. +OnTrack can be installed as a desktop PWA from a supported browser. See the +[desktop app guide](docs/desktop-pwa.md) for installation, release checks and the +browser acceptance checklist. Desktop and mobile use the same web build and API. -Before you get started, make sure you have the [Doubtfire API](https://github.com/doubtfire-lms/doubtfire-api) up and running. You will need to do this before continuing. +## Getting Started -First, clone the web repository, and change to the root directory: +OnTrack Web requires Node.js 22.22.3 or newer. The exact handover version is in +`.nvmrc`. Clone recursively because the JPlag report viewer is a production +asset supplied by a git submodule: ```sh -git clone https://github.com/doubtfire-lms/doubtfire-web.git -cd ./doubtfire-web +git clone --recurse-submodules https://github.com/doubtfire-lms/doubtfire-web.git +cd doubtfire-web +git submodule update --init --recursive +nvm install +nvm use +npm ci ``` -You can automate the installation process by running the automated setup script: +Run the [Doubtfire API](https://github.com/doubtfire-lms/doubtfire-api) on +`http://localhost:3000`, then start the Angular development server: ```sh -./setup.sh +npm start ``` -Or, you can continue following the below steps to manually install `doubtfire-web`. +Open . `proxy.conf.json` forwards `/api` to the local API. +It also forwards `/lti/api` to the optional local LTI service on port 3001; LTI +requests will fail if that service is not running. -Install [Node.js](http://nodejs.org/) either by [downloading it](http://nodejs.org/download/) and installing it manually, or via [Homebrew](http://brew.sh) on OS X: +The legacy root `docker-compose.yml` also defaults to local database +authentication. Optional AAF development must use a dedicated non-production +registration supplied through an ignored `.env` file copied from +`.env.example`. Any AAF secret ever committed to Git must be treated as +compromised and rotated by its identity owner. -```sh -brew install node -``` +## Checks -_or_ by using `apt-get` on Linux: +Run the same checks required for a release before handing the branch over: ```sh -curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - -sudo apt-get install nodejs +npm run verify:deployment-config +npm run lint +npm run typecheck +npm run test:ci +npm run deploy ``` -Install Ruby [SASS](http://sass-lang.com): +`npm run test:ci` runs the unit suite with Vitest through Angular's test builder. +The test target is configured in `angular.json`; shared setup lives in `src/vitest-setup.ts`. +`npm run test:coverage` writes reports to `coverage/`; Test CI publishes the `web-coverage` artifact. -```sh -gem install sass -``` +## Production build and handover -If `gem` fails, you should read the Doubtfire API README to install ruby. If you are _not_ using `rbenv`, e.g., using Docker instead, you may need to prepend `sudo` to the above commands to have root write access. +The production application deliberately uses the browser origin for both API +contracts: -If using `rbenv`, rehash to ensure each of the gems are on your `PATH`: +- `/api` must be reverse-proxied to Doubtfire API. +- `/lti/api` must be reverse-proxied to the LTI service when LTI is enabled. -```sh -rbenv rehash -``` +`DF_API_URL` is not a runtime switch for the Angular production build. Hosting +the API on another origin requires an explicit web configuration and matching +CORS policy. Production must use HTTPS for service workers, installation, and +Web Push. -Install all node dependencies using `npm`, as well as [grunt-cli](http://gruntjs.com/using-the-cli) globally: +Build the static production bundle directly with: ```sh -npm install +npm ci +git submodule update --init --recursive +npm run deploy ``` -**Note:** You may need to install `grunt-cli` globally in Linux using `sudo`. - -Lastly, to compile and run a watch server and web server, use `npm start`: +Or build the production Nginx image, which is the release artifact used by the +deployment repository: ```sh -npm start +docker build -f deploy.Dockerfile -t doubtfire-web:handover . +docker run --rm -p 8080:80 doubtfire-web:handover ``` -This will automatically run the angular 1 `grunt watch`, and the angular 7 `ng serve`. +The root `Dockerfile` is the bind-mounted development image: it contains the +source tree, npm, and development dependencies so that local Compose workflows +can rebuild on startup. Do not publish or assess it as the production runtime. +Release vulnerability scans must build `deploy.Dockerfile` and scan its final +Nginx image digest; build-stage and development-image findings should be tracked +separately. -You can then navigate to the Doubtfire web interface at **http://localhost:8000**. +The Dockerfile pins its Node and Nginx base images by multi-architecture digest +and performs the build as the unprivileged `node` user. Refresh those digests +deliberately as part of a reviewed dependency update; do not replace them with +floating tags for a release. -## Deployment +Sentry is optional. Direct production builds leave it disabled. The image build +can substitute `SENTRY_DSN`, `SENTRY_RELEASE`, and `SENTRY_DIST`; source-map +upload additionally requires the documented Sentry build arguments and a +BuildKit `sentry_auth_token` secret. -To compile the front-end, ensure `doubtfire-api` is placed as a sibling directory to `doubtfire-web`, then run: +Before publishing, verify the generated service-worker files and JPlag assets: ```sh -cd /path/to/repos -ls -doubtfire-api doubtfire-web -cd ./doubtfire-api -grunt deploy +test -f dist/browser/index.html +test -f dist/browser/ngsw.json +test -f dist/browser/ngsw-worker.js +test -f dist/browser/manifest.webmanifest +test -n "$(find dist/browser/JPlag -mindepth 1 -maxdepth 1 -print -quit)" ``` -You may prefix this command with the following environment variables: +Against the running image, confirm application routes fall back to `index.html`, +each named control-file URL returns 404 rather than HTML if its build output is +missing, and these files return `Cache-Control: no-store, no-cache` when present: + +- `/index.html` +- `/ngsw.json` +- `/ngsw-worker.js` +- `/safety-worker.js` +- `/worker-basic.min.js` +- `/manifest.webmanifest` + +The external proxy or CDN must preserve that cache policy. Pin the tested image +digest in `doubtfire-deploy`, then smoke-test sign-in, API routing, service-worker +registration/update, and any enabled LTI or Web Push integration. -- `DF_API_URL` - the URL of the API (e.g., `https://doubtfire.com/api`). This will default to `window.location.host` if not set and dynamically generate a URL. -- `DF_EXTERNAL_NAME` - a new name that removes references to the _Doubtfire_ name should you so want to not use such its original name (😢). +## Demo and live feature boundaries + +Demo source, fixtures, controls, and evidence remain in the repository for local +demonstrations. Production builds set `production: true` and +`enableDemoTools: false`; `/demo-controls` is unavailable and genuine API data +passes through unchanged. Do not change that flag from deployment tooling. + +Task-level peer progress is live when the matching API feature and privacy +configuration are enabled. The unit summary and burndown peer-median displays +are explicitly demo-only fixtures. The push-notification demo preview is visual +only; real Web Push requires API VAPID configuration, HTTPS, a running worker, +and browser permission. ## Resources -Doubtfire Web is an [Angular](https://angular.dev) application built using [Material UI]https://material.angular.dev). It uses many Open Source libraries, which you can read up on: +See the [documentation index](docs/index.md) for setup, feature guides and contributor handovers. -- [Lodash](http://lodash.com/docs) -- [Moment.js](http://momentjs.com) -- [NVD3 Charts](http://krispo.github.io/angular-nvd3/#/) +Doubtfire Web is an [Angular](https://angular.dev) application using +[Angular Material](https://material.angular.dev/). Production is served as a +static progressive web application by Nginx. ## Contributing @@ -114,3 +169,9 @@ Refer to [CONTRIBUTING.md](CONTRIBUTING.md) ## License Licensed under GNU Affero General Public License (AGPL) v3 + +## Frontend maintenance + +- [CSS style guide](docs/css-style-guide.md) and [remaining layout catalogue](docs/migration/ui-catalogue.md) +- [Legacy source audit](docs/migration/legacy-audit.md) and [Unit Statistics charts](docs/migration/analytics.md) +- [Migration recovery register](docs/migration/recovery-register.md) and [upstream PR triage](docs/migration/open-pr-triage.md) diff --git a/build.config.js b/build.config.js deleted file mode 100644 index ffb10f91a4..0000000000 --- a/build.config.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * This file/module contains all configuration for the build process. - */ -module.exports = { - /** - * The `build_dir` folder is where our projects are compiled during - * development and the `compile_dir` folder is where our app resides once it's - * completely built. - */ - build_dir: 'build', - compile_dir: 'bin', - api_dir: '../doubtfire-api', - temp_scss_file: 'tmp.scss', - - /** - * This controls which files need preprocessing. This allows us to have - * compile-time changes. For now this is just the environment but in the future - * it might be more complex. Even though it's called preprocessing, I feel like - * it might be a good idea to try and keep it on the build directory to ensure it's - * as unobtrusive as possible, and not confuse which files to compile/overwrite - * actual source code. - */ - preprocess: { - api: { - src: ['build/src/app/api/api-url.js', 'build/src/app/config/external-name/external-name.js'], - options: { - inline: true, - }, - }, - }, - - /** - * This is a collection of file patterns that refer to our app code (the - * stuff in `src/`). These file paths are used in the configuration of - * build tasks. `js` is all project javascript, sass tests. - * `atpl` contains our app's code. `html` is just our - * main HTML file, `sass` is our main stylesheet, and `unit` contains our - * app's unit tests. - */ - app_files: { - js: ['src/**/*.js', '!src/**/*.spec.js', '!src/assets/**/*.js'], - jsunit: ['src/**/*.spec.js'], - - coffee: ['src/**/*.coffee', '!src/**/*.spec.coffee', '!src/**/*.old.coffee'], - coffeeunit: ['src/**/*.spec.coffee'], - - atpl: ['src/app/**/*.tpl.html'], - - html: ['src/index.html'], - - scss: [ - // Do not modify the order - 'src/styles/mixins/**/*.scss', - 'src/styles/common/**/*.scss', - 'src/styles/modules/**/*.scss', - 'src/app/**/*.scss', - '!src/app/**/*.component.scss', - ], - }, - - /** - * This is the same as `app_files`, except it contains patterns that - * reference vendor code (`vendor/`) that we need to place into the build - * process somewhere. While the `app_files` property ensures all - * standardized files are collected for compilation, it is the user's job - * to ensure non-standardized (i.e. vendor-related) files are handled - * appropriately in `vendor_files`. - * - * The `vendor_files.compile` holds files that will compile JavaScript - * or SCSS files into compile vendor files into `doubtfire.{js,css}`. - * These files are to be automatically concatenated and minified with - * our project source files. - * - * Anything insider `vendor_files.copy` will copy files into the `assets/` - * directory under the same directory structure (i.e., under `vendor/.../`) - */ - vendor_files: { - compile: { - js: [], - jsmap: [], - scss: [ - 'node_modules/bootstrap-sass/**/_bootstrap.scss', - 'node_modules/font-awesome/**/font-awesome.scss', - ], - }, - copy: { - js: [], - jsmap: [], - css: [ - 'node_modules/nvd3/build/nv.d3.css', - 'node_modules/angular-xeditable/dist/css/xeditable.css', - 'node_modules/codemirror/lib/codemirror.css', - 'node_modules/codemirror/theme/xq-light.css', - ], - }, - }, -}; diff --git a/docs/course-flow-rebase.md b/docs/course-flow-rebase.md new file mode 100644 index 0000000000..bfdd22dc6f --- /dev/null +++ b/docs/course-flow-rebase.md @@ -0,0 +1,96 @@ +# Course Flow planner + +Course Flow lets a signed-in user select a versioned course catalog, arrange its +units into study periods, and save named private plans. Reopening a plan restores +its unit positions and empty periods. Course Flow uses `/api/courseflow`, provided +by the companion API change; it does not request the administrative teaching-unit +catalog or use teaching-unit IDs. + +## Using the planner + +Open **Course Flow** from the account menu. Choose a course/version and start a +plan, or open one of your saved plans. Name the plan and place units using the +labelled unit, period and position controls. Drag and drop is also available. +Units can be moved, swapped, or removed; removing a period makes its units +available to place again. Save explicitly before leaving. Unsaved edits trigger +confirmation when navigating away or closing the tab. + +Plans may be saved while incomplete. Planning checks identify missing required +units, the configured number of electives, prerequisites that are missing or +scheduled too late, and units scheduled outside their configured trimesters. +Passing these checks is **not a certification of degree eligibility**. + +Each save replaces the plan atomically. If another session has changed the plan, +the API rejects the outdated save instead of overwriting newer data. The page +keeps local edits available so the user can resolve the conflict. Deleting +also checks the saved version. All map requests are scoped to the signed-in +owner, including requests made by administrators. + +## Catalog and API contract + +Catalogs come from an administrative JSON import in the companion API repository. +No real institutional curriculum is supplied by this PR. The sample catalog is +explicitly labelled demonstration data and is not automatically seeded. + +Each course has a code, name, immutable version, elective count and unit list. +Each unit has a course-specific code, name, required/elective classification, +prerequisite codes and offered trimesters. All listed prerequisites must be +scheduled in an earlier study period. New curricula require a new catalog +version; existing saved plans continue to use the version they selected. + +This model supports required units, a fixed elective count, conjunctions of +prerequisites, and trimester availability. Credit-point rules, specialisations, +alternative prerequisite expressions, co-requisites and credit exemptions are +not represented. Only import catalogs whose rules fit the supported model. + +| Request | Purpose | +| ------------------------------------------------ | ------------------------------------------- | +| `GET /api/courseflow/courses` | Catalogs available to signed-in users | +| `GET /api/courseflow/courses/:id` | One versioned catalog | +| `GET /api/courseflow/maps` | Current user's saved plans | +| `GET /api/courseflow/maps/:id` | One owned plan | +| `POST /api/courseflow/maps` | Create a private plan | +| `PUT /api/courseflow/maps/:id` | Replace an owned plan with version checking | +| `DELETE /api/courseflow/maps/:id?lock_version=N` | Delete the current version | + +Create/update bodies contain `course_id`, `name`, `periods` and `slots`; updates +also require `lock_version`. A period is `{year, trimester}`. A slot is +`{unit_code, year, trimester, position}`. Trimesters are 1–3, positions 1–4, +years 2000–2200. A plan contains 1–60 unique periods and up to 240 slots. Unit +codes and positions cannot repeat. Every unit must belong to the chosen catalog +and every slot to a declared period. Client-supplied ownership fields are rejected. +Other users' map IDs return 404, invalid plans return 422, and stale versions +return 409. Responses include `issues` and `complete` for the configured checks. + +## Integration and verification + +Deploy the companion API migration before this web change, then import the +institution's approved catalog using the documented `courseflow:import[path]` +task. An empty catalog produces an explicit empty state. Backend failures are +shown as failures; they are not converted into successful empty responses. + +The component tests render the actual template. Service tests check HTTP +methods and payloads; planning checks have separate rule tests. Run: + +```sh +npm run test:ci -- --include='src/app/courseflow/**/*.spec.ts' --include='src/app/api/services/course-flow.service.spec.ts' +npm run lint +npm run build +``` + +The [HTTP integration check](course-flow/http-check.mjs) uses actual authentication +and persistence against a disposable local API with the sample catalog imported. +It exercises ordinary-student access, owner isolation, atomic invalid-save +rejection, empty-period roundtrips, planning issues and optimistic concurrency. +It must not run against production data. The [validation record](course-flow/validation.md) +includes browser evidence and the exact source revisions. + +## Original rebase history + +The branch retains the 16 original non-merge commits and their authors from +upstream [`new/course-flow` at `e8fb9b983d10cda40c2ce005727387106e04ca40`](https://github.com/doubtfire-lms/doubtfire-web/tree/e8fb9b983d10cda40c2ce005727387106e04ca40). +It was initially rebased onto the fork's `11.0.x` at +`a35f2826ddff8a816175490c108f1794779c3254`, then updated against the current +integration branch. The completed planner replaces the old uncalled CRUD +scaffolding and its ambiguous ID contract. Angular Router, the application's +authentication interceptors and the existing signed-in role guard are retained. diff --git a/docs/course-flow/README.md b/docs/course-flow/README.md new file mode 100644 index 0000000000..719dd602bb --- /dev/null +++ b/docs/course-flow/README.md @@ -0,0 +1,51 @@ +# Course Flow integration checks + +See the [recorded validation](validation.md) for results and screenshots. + +These scripts use the real application and API. They require a **disposable, +local** environment with separate test/development databases, the companion +API migration, and its explicitly fictional `DEMO-CF / QA-2026` catalog. +They refuse remote hosts. No network responses are mocked. + +Start from the repositories' local setup instructions and a seeded development +database. Import the API's `docs/courseflow/sample-catalog.json` using +`bundle exec rake 'courseflow:import[docs/courseflow/sample-catalog.json]'`. +The fixtures use the seed accounts `student_1`, `student_2`, and `aadmin` with +their local development password `password`. Complete first-time setup for the +two students. In the disposable development API container this can be prepared +with: + +```sh +bundle exec rails runner 'abort unless Rails.env.development?; User.where(username: %w[student_1 student_2]).update_all(has_run_first_time_setup: true)' +``` + +Run the API on `127.0.0.1:4320` and Angular on `127.0.0.1:4321`, proxying `/api` +to the API. Alternative local URLs can be supplied through the variables below. +The scripts create named QA plans. Successful checks delete them; a failed +browser run can leave its named plan for inspection in this disposable database. + +```sh +COURSEFLOW_API_URL=http://127.0.0.1:4320 \ + COURSEFLOW_HTTP_RESULT=/tmp/courseflow-http-result.json \ + node docs/course-flow/http-check.mjs + +COURSEFLOW_WEB_URL=http://127.0.0.1:4321 \ + COURSEFLOW_BROWSER_OUTPUT=/tmp/courseflow-browser-check \ + node docs/course-flow/browser-check.mjs +``` + +The browser script needs Playwright and locally installed Google Chrome. It uses +the `playwright` module if available, or an absolute `PLAYWRIGHT_MODULE` path to +an installed Playwright `index.mjs`. This keeps browser tooling optional rather +than adding it to the production dependencies. + +The HTTP check verifies student catalog access, private owner scoping (including +administrator isolation), create/save/reload/delete, explicit empty periods, +malformed placement rejection, atomicity, stale writes/deletes, and configured +planning issues. The browser check covers pointer drag, keyboard placement, +move/swap/remove, reload, canceled dirty navigation, two-session conflict and +save-copy recovery, another student's denied deep link, and a 390px viewport. +Screenshots and a token-free JSON result are written to the chosen output path. + +These checks use sample rules, not an institutional curriculum. They are not a +manual screen-reader review, native Safari check, or official degree audit. diff --git a/docs/course-flow/browser-check.mjs b/docs/course-flow/browser-check.mjs new file mode 100644 index 0000000000..ba44b97544 --- /dev/null +++ b/docs/course-flow/browser-check.mjs @@ -0,0 +1,228 @@ +// Real browser + real API. Use the isolated local environment described in README.md. +import assert from 'node:assert/strict'; +import {mkdir, writeFile} from 'node:fs/promises'; + +const {chromium} = await import(process.env.PLAYWRIGHT_MODULE || 'playwright'); +const base = process.env.COURSEFLOW_WEB_URL || 'http://127.0.0.1:4321'; +assert( + ['127.0.0.1', 'localhost'].includes(new URL(base).hostname), + 'Use a disposable local server', +); +const out = process.env.COURSEFLOW_BROWSER_OUTPUT || '/tmp/courseflow-browser-check'; +await mkdir(out, {recursive: true}); +const browser = await chromium.launch({channel: 'chrome', headless: true}); +const context = await browser.newContext({viewport: {width: 1440, height: 1000}}); +const page = await context.newPage(); +page.setDefaultTimeout(30000); +const checks = []; +const runtimeErrors = []; +page.on('pageerror', (error) => runtimeErrors.push(error.message)); +const year = new Date().getFullYear(); +const planName = `Browser QA ${Date.now()}`; +let mapUrl; +let result = 'failed'; +function passed(name) { + checks.push(name); + console.log(`PASS ${name}`); +} +async function login(target, username) { + await target.goto(`${base}/sign_in`); + await target.getByRole('textbox', {name: 'Username', exact: true}).fill(username); + await target.locator('input[name="password"]').fill('password'); + await target.getByRole('button', {name: 'Sign In', exact: true}).click(); + await target.getByRole('button', {name: 'Open account menu', exact: true}).waitFor(); +} +async function ready(target) { + await target.getByRole('heading', {name: 'Course Flow', exact: true}).waitFor(); + await target.getByLabel('Course and version', {exact: true}).waitFor(); +} +async function place(code, trimester, position, targetYear = year) { + await page.getByLabel('Unit', {exact: true}).selectOption(code); + await page + .getByLabel('Destination', {exact: true}) + .selectOption(`${targetYear}-${trimester}-${position}`); + const button = page.getByRole('button', {name: 'Place or move unit', exact: true}); + await button.focus(); + await page.keyboard.press('Enter'); + await page + .locator(`#slot-${targetYear}-${trimester}-${position}`) + .getByRole('heading', {name: code, exact: true}) + .waitFor(); +} +async function addPeriod(targetYear, trimester) { + await page.getByLabel('Year', {exact: true}).fill(String(targetYear)); + await page.getByLabel('Trimester', {exact: true}).selectOption({label: String(trimester)}); + await page.getByRole('button', {name: 'Add period', exact: true}).click(); + await page.locator(`#slot-${targetYear}-${trimester}-1`).waitFor(); +} +async function save(target) { + const response = target.waitForResponse( + (response) => + response.url().includes('/api/courseflow/maps') && + ['POST', 'PUT'].includes(response.request().method()), + ); + await target.getByRole('button', {name: 'Save plan', exact: true}).click(); + const saved = await response; + assert(saved.ok(), `Save returned ${saved.status()}`); + await target.getByText('Saved to your account', {exact: true}).waitFor(); + return saved.json(); +} +try { + await login(page, 'student_1'); + await page.goto(`${base}/coursemap`); + await ready(page); + const option = await page + .locator('#course-select option') + .filter({hasText: 'DEMO-CF'}) + .getAttribute('value'); + assert(option, 'Import DEMO-CF sample catalog first'); + await page.getByLabel('Course and version', {exact: true}).selectOption(option); + await page.getByLabel('Plan name', {exact: true}).fill(planName); + const dragSource = page.locator('#required-units .unit-card').filter({ + has: page.getByRole('heading', {name: 'DEMO101', exact: true}), + }); + const dragTarget = page.locator(`#slot-${year}-1-1`); + await dragSource.scrollIntoViewIfNeeded(); + await dragTarget.scrollIntoViewIfNeeded(); + const sourceBox = await dragSource.boundingBox(); + const targetBox = await dragTarget.boundingBox(); + assert(sourceBox && targetBox); + await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + 20); + await page.mouse.down(); + await page.mouse.move(sourceBox.x + sourceBox.width / 2 + 12, sourceBox.y + 32, {steps: 5}); + await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { + steps: 20, + }); + await page.mouse.up(); + await dragTarget.getByRole('heading', {name: 'DEMO101', exact: true}).waitFor(); + await page.getByRole('button', {name: 'Remove DEMO101 from plan', exact: true}).click(); + passed('actual CDK pointer drag places a unit and removal returns it to the catalog'); + await addPeriod(year, 2); + await addPeriod(year, 3); + await addPeriod(year + 1, 1); + await place('DEMO101', 1, 1); + await place('DEMO102', 2, 2); + await place('DEMO201', 2, 3); + await page + .getByText('Your plan satisfies the configured planning rules.', {exact: true}) + .waitFor(); + passed('signed-in student chooses a catalog and builds a valid plan using keyboard controls'); + + await place('DEMO201', 2, 2); + await page + .locator(`#slot-${year}-2-3`) + .getByRole('heading', {name: 'DEMO102', exact: true}) + .waitFor(); + await place('DEMO201', 2, 3); + await page.getByLabel('Unit', {exact: true}).selectOption('DEMO201'); + await page.getByRole('button', {name: 'Remove selected unit', exact: true}).focus(); + await page.keyboard.press('Enter'); + await page + .locator('#elective-units') + .getByRole('heading', {name: 'DEMO201', exact: true}) + .waitFor(); + await place('DEMO201', 2, 3); + passed('keyboard swap, move and remove preserve the units'); + + const original = await save(page); + mapUrl = `${base}/coursemap/${original.id}`; + await page.waitForURL(mapUrl); + await page.reload(); + await ready(page); + await page.getByLabel('Plan name', {exact: true}).waitFor(); + assert.equal(await page.getByLabel('Plan name', {exact: true}).inputValue(), planName); + for (const [code, trimester, position] of [ + ['DEMO101', 1, 1], + ['DEMO102', 2, 2], + ['DEMO201', 2, 3], + ]) { + await page + .locator(`#slot-${year}-${trimester}-${position}`) + .getByRole('heading', {name: code, exact: true}) + .waitFor(); + } + assert.equal(await page.locator(`#slot-${year + 1}-1-1 .empty-slot`).count(), 1); + passed('real save and full reload restore unit positions and empty periods'); + + await page.getByLabel('Plan name', {exact: true}).fill(`${planName} local edit`); + page.once('dialog', (dialog) => dialog.dismiss()); + await page.getByRole('button', {name: 'New plan', exact: true}).click(); + assert.equal( + await page.getByLabel('Plan name', {exact: true}).inputValue(), + `${planName} local edit`, + ); + assert.equal(page.url(), mapUrl); + passed('canceling dirty navigation preserves the current draft'); + + const otherSession = await browser.newContext({viewport: {width: 1440, height: 1000}}); + const second = await otherSession.newPage(); + await login(second, 'student_1'); + await second.goto(mapUrl); + await ready(second); + await second.getByLabel('Plan name', {exact: true}).fill(`${planName} other session`); + await save(second); + const conflict = page.waitForResponse( + (response) => + response.request().method() === 'PUT' && + response.url().endsWith(`/api/courseflow/maps/${original.id}`), + ); + await page.getByRole('button', {name: 'Save plan', exact: true}).click(); + assert.equal((await conflict).status(), 409); + await page.getByRole('button', {name: 'Save a copy', exact: true}).waitFor(); + assert.equal( + await page.getByLabel('Plan name', {exact: true}).inputValue(), + `${planName} local edit`, + ); + const copyResponse = page.waitForResponse( + (response) => + response.request().method() === 'POST' && response.url().endsWith('/api/courseflow/maps'), + ); + await page.getByRole('button', {name: 'Save a copy', exact: true}).click(); + const copy = await (await copyResponse).json(); + assert.notEqual(copy.id, original.id); + await page.waitForURL(`${base}/coursemap/${copy.id}`); + await page.getByText('Saved to your account', {exact: true}).waitFor(); + passed( + 'competing session save produces a conflict and preserves local edits through Save a copy', + ); + + await page.screenshot({path: `${out}/desktop.png`, fullPage: true}); + await page.setViewportSize({width: 390, height: 844}); + await page.screenshot({path: `${out}/mobile.png`, fullPage: true}); + assert( + await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1), + 'No horizontal document overflow at 390px', + ); + passed('planner fits a 390px viewport'); + + const foreignContext = await browser.newContext(); + const foreign = await foreignContext.newPage(); + await login(foreign, 'student_2'); + await foreign.goto(mapUrl); + await foreign + .getByText('This course plan is unavailable or does not belong to you.', {exact: true}) + .waitFor(); + assert.equal(await foreign.getByLabel('Plan name', {exact: true}).count(), 0); + await foreignContext.close(); + passed('another student sees an ownership error with no private plan content'); + + page.once('dialog', (dialog) => dialog.accept()); + await page.getByRole('button', {name: 'Delete saved plan', exact: true}).click(); + await page.waitForURL(`${base}/coursemap`); + second.once('dialog', (dialog) => dialog.accept()); + await second.getByRole('button', {name: 'Delete saved plan', exact: true}).click(); + await second.waitForURL(`${base}/coursemap`); + await otherSession.close(); + assert.deepEqual(runtimeErrors, [], 'No uncaught page errors'); + passed('owner deletes saved plans without uncaught browser errors'); + result = 'passed'; +} catch (error) { + await page.screenshot({path: `${out}/failure.png`, fullPage: true}).catch(() => {}); + throw error; +} finally { + await writeFile( + `${out}/result.json`, + JSON.stringify({result, base, checks, runtimeErrors}, null, 2), + ); + await browser.close(); +} diff --git a/docs/course-flow/http-check.mjs b/docs/course-flow/http-check.mjs new file mode 100644 index 0000000000..c21e0e482f --- /dev/null +++ b/docs/course-flow/http-check.mjs @@ -0,0 +1,185 @@ +// Real HTTP integration check. Run only against a disposable, seeded local API. +// Import the API's docs/courseflow/sample-catalog.json first. No mocked services. +import assert from 'node:assert/strict'; +import {writeFile} from 'node:fs/promises'; + +const base = process.env.COURSEFLOW_API_URL || 'http://127.0.0.1:4320'; +assert(['127.0.0.1', 'localhost'].includes(new URL(base).hostname), 'Use a disposable local API'); +const checks = []; +const created = []; +let student; +async function request(path, user, method = 'GET', body) { + const response = await fetch(`${base}/api${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + ...(user ? {Username: user.username, 'Auth-Token': user.token} : {}), + }, + ...(body === undefined ? {} : {body: JSON.stringify(body)}), + }); + const text = await response.text(); + return {status: response.status, data: text ? JSON.parse(text) : null}; +} +async function login(username) { + const response = await request('/auth', null, 'POST', {username, password: 'password'}); + assert.equal(response.status, 201, `Login ${username}`); + assert(response.data.auth_token); + return {username, token: response.data.auth_token}; +} +function passed(name) { + checks.push(name); + console.log(`PASS ${name}`); +} +try { + const anonymous = await request('/courseflow/courses'); + assert(anonymous.status >= 400 && anonymous.status < 500); + passed('anonymous catalog access denied'); + student = await login('student_1'); + const other = await login('student_2'); + const admin = await login('aadmin'); + const catalog = await request('/courseflow/courses', student); + assert.equal(catalog.status, 200); + const course = catalog.data.find( + (entry) => entry.code === 'DEMO-CF' && entry.version === 'QA-2026', + ); + assert(course, 'Import the explicitly labelled DEMO-CF QA-2026 sample catalog'); + passed('ordinary student can retrieve course catalog'); + const periods = [ + {year: 2026, trimester: 1}, + {year: 2026, trimester: 2}, + {year: 2026, trimester: 3}, + {year: 2027, trimester: 1}, + ]; + const draft = {course_id: course.id, name: `HTTP QA ${Date.now()}`, periods, slots: []}; + let response = await request('/courseflow/maps', student, 'POST', draft); + assert.equal(response.status, 201); + let map = response.data; + created.push(map.id); + assert.equal(map.complete, false); + assert(map.issues.some((issue) => issue.code === 'missing_required')); + assert(map.issues.some((issue) => issue.code === 'elective_count')); + assert.deepEqual(map.periods, periods); + passed('student creates an incomplete plan with explicit empty periods'); + for (const user of [other, admin]) { + assert.equal((await request(`/courseflow/maps/${map.id}`, user)).status, 404); + assert.equal( + (await request(`/courseflow/maps/${map.id}`, user, 'PUT', {...draft, lock_version: 0})) + .status, + 404, + ); + assert.equal( + (await request(`/courseflow/maps/${map.id}?lock_version=0`, user, 'DELETE')).status, + 404, + ); + const listed = await request('/courseflow/maps', user); + assert(!listed.data.some((entry) => entry.id === map.id)); + } + passed('other student and administrator cannot read, edit, delete, or list this private plan'); + assert.equal( + (await request('/courseflow/maps', student, 'POST', {...draft, user_id: 1})).status, + 422, + ); + passed('client-supplied ownership rejected'); + const slots = [ + {unit_code: 'DEMO101', year: 2026, trimester: 1, position: 1}, + {unit_code: 'DEMO102', year: 2026, trimester: 2, position: 2}, + {unit_code: 'DEMO201', year: 2026, trimester: 2, position: 3}, + ]; + const full = {...draft, slots, lock_version: map.lock_version}; + response = await request(`/courseflow/maps/${map.id}`, student, 'PUT', full); + assert.equal(response.status, 200); + map = response.data; + assert.equal(map.complete, true); + assert.deepEqual(map.issues, []); + assert.equal(map.lock_version, full.lock_version + 1); + const reloaded = await request(`/courseflow/maps/${map.id}`, student); + assert.deepEqual(reloaded.data.slots, slots); + assert.deepEqual(reloaded.data.periods, periods); + passed( + 'whole-plan save and HTTP reload preserve positions, empty periods, and configured rule results', + ); + assert.equal( + (await request(`/courseflow/maps/${map.id}`, student, 'PUT', {...full, slots: []})).status, + 409, + ); + assert.equal( + ( + await request( + `/courseflow/maps/${map.id}?lock_version=${full.lock_version}`, + student, + 'DELETE', + ) + ).status, + 409, + ); + passed('stale updates and deletes rejected'); + for (const badSlots of [ + [slots[0], {...slots[1], trimester: 1, position: 1}], + [slots[0], {...slots[0], position: 2}], + [{...slots[0], unit_code: 'UNKNOWN'}], + [{...slots[0], year: '2026'}], + [{...slots[0], trimester: 4}], + [{...slots[0], position: 0}], + [{...slots[0], year: 2028}], + ]) { + assert.equal( + ( + await request(`/courseflow/maps/${map.id}`, student, 'PUT', { + ...full, + lock_version: map.lock_version, + slots: badSlots, + }) + ).status, + 422, + ); + } + const preserved = await request(`/courseflow/maps/${map.id}`, student); + assert.deepEqual(preserved.data.slots, slots); + assert.equal(preserved.data.lock_version, map.lock_version); + passed('invalid placements reject atomically without changing stored data or version'); + response = await request(`/courseflow/maps/${map.id}`, student, 'PUT', { + ...full, + lock_version: map.lock_version, + slots: [ + {...slots[0], trimester: 2}, + slots[1], + {...slots[2], unit_code: 'DEMO202', trimester: 1}, + ], + }); + assert.equal(response.status, 200); + map = response.data; + assert.equal(map.complete, false); + assert(map.issues.some((issue) => issue.code === 'prerequisite')); + assert(map.issues.some((issue) => issue.code === 'unavailable_trimester')); + passed('prerequisite and offering violations are saved as explicit planning issues'); + assert.equal( + ( + await request( + `/courseflow/maps/${map.id}?lock_version=${map.lock_version}`, + student, + 'DELETE', + ) + ).status, + 204, + ); + assert.equal((await request(`/courseflow/maps/${map.id}`, student)).status, 404); + passed('owner deletes the current plan'); +} finally { + if (student) { + for (const id of created) { + const latest = await request(`/courseflow/maps/${id}`, student); + if (latest.status === 200) + await request( + `/courseflow/maps/${id}?lock_version=${latest.data.lock_version}`, + student, + 'DELETE', + ); + } + } + if (process.env.COURSEFLOW_HTTP_RESULT) { + await writeFile( + process.env.COURSEFLOW_HTTP_RESULT, + JSON.stringify({base, checks, completed: checks.length === 10}, null, 2), + ); + } +} diff --git a/docs/course-flow/validation.md b/docs/course-flow/validation.md new file mode 100644 index 0000000000..a8338d13e4 --- /dev/null +++ b/docs/course-flow/validation.md @@ -0,0 +1,40 @@ +# Course Flow validation — 22 September 2026 + +Validated with the completed web planner and companion [API PR #176](https://github.com/ontrack-features-t2-2026/doubtfire-api/pull/176). + +- Web implementation: `c44f8ecc08920b16b4f43c7a06f01d64183edb7f`, including current `11.0.x` at `283b493367681d1abab23e5fe7c633ba3a57e5c2`. +- API implementation: `129cb8f40a6c8d60b3e3cef2805fabb0285ef656`, based on `449945f33230140ce6c8da7eb3a15784844548f6`. +- Deploy `11.0.x` inspected at `2e513e190483d43112b09573a79c5bcf64bd220b`; unchanged. Validation ran in a separate local runtime, not a production deployment. +- Runtime: Node 22.23.2, Ruby 3.4.10, MariaDB 12.3, local Google Chrome through Playwright. Test and browser databases were separate and contained only generated seed data plus the explicitly fictional sample catalog. + +| Check | Result | +| -------------------------------------- | ---------------------------------------------------------- | +| Full web `npm run test:ci` | 164 files, 1,288 tests passed | +| Full web `npm run lint` | Passed, zero warnings | +| Web `npm run build` | Passed, including lazy Course Flow bundle | +| API Course Flow model/API tests | 24 tests, 206 assertions passed | +| Existing API authentication regression | 24 tests, 162 assertions passed using CI LTI configuration | +| Changed API Ruby lint | 8 files, no offenses | +| API eager loading | `zeitwerk:check` passed | +| Real HTTP integration | 10 checks passed | +| Real Chrome browser integration | 9 checks passed; no uncaught page errors | +| Whitespace | Both repositories passed `git diff --check` | + +The live browser test covers actual CDK pointer dragging, keyboard placement, +move/swap/remove, saving and full reload with empty periods preserved, cancellation +of dirty navigation, two-session save conflict and save-copy recovery, another +student's denied deep link, deletion, and a 390px viewport without document +overflow. The service tests run through the real authentication/error interceptors +and cover preserved 404/409 errors during normal requests and token-refresh retries. + +Read the [browser result](evidence/result.json), [HTTP result](evidence/http-result.json), +and [reproduction instructions](README.md). Screenshots show fictional test plans: + +- [Desktop](evidence/desktop.png) +- [390px layout](evidence/mobile.png) + +The checks validate supported configured planning rules. They do not supply or +verify an official institutional curriculum, certify degree eligibility, or +claim a manual screen-reader/native Safari review. Neither repository was merged +or deployed as part of this work. Apply the companion API migration and import +approved catalog data before releasing the frontend. diff --git a/docs/css-style-guide.md b/docs/css-style-guide.md new file mode 100644 index 0000000000..3154e7f2c6 --- /dev/null +++ b/docs/css-style-guide.md @@ -0,0 +1,81 @@ +# CSS style guide (MG-05) + +Use this guide for new components and incremental layout migration. It describes +repository conventions verified against `11.0.x` at `d16f6201c` on 20 September 2026. +The layout status below was rechecked on 21 September 2026 against `11.0.x` +at `283b493367681d1abab23e5fe7c633ba3a57e5c2`. On that date, the requester confirmed +that they knew of no external CSS guide. This records the requester's knowledge; +it does not claim a conversation or approval from Brian/Maple. +The [theme contract](theme/THEME-CONTRACT.md) governs colour and theme behaviour. + +## Choose the existing layer + +1. **Angular Material** supplies interactive components: buttons, form fields, + dialogs, menus, cards. For example, `common/edit-profile-form` uses Material + fields and `units/states/analytics/unit-analytics-route.component.html` uses cards + and buttons. Keep their keyboard and accessible-name behaviour. +2. **Tailwind utilities** express layout, spacing and responsive dimensions. Use + existing patterns such as `flex flex-col gap-2.5` in the analytics route. Read + `tailwind.config.js`: `important: true` makes generated utilities important, so + they can override normal Material/component declarations of the same property. + Do not fight that policy with local `!important` escalation. The global `[hidden]` + rule deliberately preserves Angular visibility bindings over display utilities. +3. **Shared SCSS** lives under `src/styles/`. Reuse + `common/variables.scss`, `common/typeface.scss`, `common/doubtfire-panel.scss`, + `common/hero-sidebar-layout.scss`, `common/task-status-colors.scss`, and existing + `mixins/scrollable.scss`, `mixins/task-list.scss` and + `mixins/task-status-colors-generator.scss` before inventing another helper. +4. **Component SCSS** handles local structures the preceding layers do not cover, + such as chart dimensions and tables in `visualisations/unit-analytics-chart.scss`. + Keep selectors scoped; avoid reaching into unrelated components or Material internals. + +`src/theme.scss` is the live Material theme (also listed in `angular.json`). +`src/styles.scss` loads it with `@use 'theme' as *`; `styles/m3-theme.scss` remains +commented out. The shared light/dark `--ot-*` tokens are loaded from +`src/styles/tokens/`. Follow those tokens for new theme-sensitive surfaces; the +existing two Tailwind custom colours are `formatif-blue` (`#3939ff`) and +`formatif-blue-lighter` (`#e7e7ff`), used by `common/f-chip/chip.component.html`. +They are the only custom Tailwind colours, not the only colours/tokens in the app. +The `dark:` variant follows `[data-ot-theme="dark"]`, not just the OS preference. + +## Rules enforced in CI + +The source of truth is `eslint.config.js`; Tailwind reads +`src/tailwind-intellisense.css`, which loads `tailwind.config.js`. +The following warnings apply to `**/*.component.html`. `npm run lint` runs +`ng lint --max-warnings 0`, so warnings fail the gate in `.github/workflows/lint.yml`. +Run the fixer to get the precise class order for the installed Tailwind version. + +| Rule | Invalid example | Correction | +| -------------------------------- | --------------- | ------------------------------------------------------------------------------------ | +| `classnames-order` | `p-4 flex` | `flex p-4` | +| `enforces-shorthand` | `px-4 py-4` | `p-4` | +| `no-contradicting-classname` | `flex block` | Pick the required display, e.g. `flex`. | +| `no-unnecessary-arbitrary-value` | `p-[16px]` | `p-4` when the active spacing scale produces the same size. Check the rendered size. | + +`no-custom-classname` is deliberately **off**, allowing shared/component SCSS +classes alongside utilities. `.tpl.html` is globally ignored because these are +legacy templates, not a pattern for new components. See the +[remaining inventory](migration/legacy-audit.md). +[CONTRIBUTING.md](../CONTRIBUTING.md) covers ESLint, Prettier and the npm commands. +The former `MIGRATION-GUIDE.md` was deleted on 15 September; do not follow old +copies that instruct AngularJS downgrades or Bootstrap conversion. + +## Layout migration is complete + +Use Tailwind for new layout. Commit `0caa10617` completed the Flex Layout teardown +on 20 September 2026: `ng-flex-layout`, `FlexLayoutModule` and their remaining +template directives are absent from the current application. The +[catalogue](migration/ui-catalogue.md) retains the dated two-template inventory as +historical evidence; it is no longer an outstanding conversion list. Do not +reintroduce the removed dependency when bringing changes forward from older branches. + +When porting older code, for `fxLayoutAlign` the first value controls the main axis and the second the +cross axis: a row with `center start` becomes `justify-center items-start`. +`fxFlex` requires inspection of its numeric/basis value. `fxShow`/`fxHide` can need +responsive utilities plus application state; `MediaObserver` has no direct CSS +replacement. Check mobile layouts, focus order and hidden elements after each change. + +Bootstrap is absent from dependencies and must not be reintroduced for migrated +components. Old `btn`, `panel`, grid and input-group names in the catalogue are +leftovers to replace after checking local SCSS, not evidence of a supported framework. diff --git a/docs/desktop-pwa-validation.md b/docs/desktop-pwa-validation.md new file mode 100644 index 0000000000..6234a4f0b2 --- /dev/null +++ b/docs/desktop-pwa-validation.md @@ -0,0 +1,118 @@ +# Desktop PWA validation — 21 September 2026 + +Implementation base: web `283b49336` on `11.0.x`; companion deploy base `2e513e1`. +Test environment: macOS, Node 22.23.2, Angular 22.0.3, Chrome 153.0.8010.48. + +## Automated checks + +- 60 focused Angular tests passed across installation service/dialog/button, + offline status, header, sign-in and update service (seven spec files). +- Production Angular build and application type checking passed. The build emits + existing large component-style, CommonJS and dependency/minification warnings; + this feature does not resolve those unrelated warnings. +- Targeted ESLint, Prettier and `git diff --check` passed. +- `npm run verify:deployment-config`, `npm run verify:pwa` and + `npm run verify:pwa:build` passed against the final production output. +- Companion deploy verifier: 15 HTTP regression tests passed; the CLI passed + against these production assets served on loopback. + +## Real Chromium smoke test with synthetic API responses + +A disposable persistent Chrome profile loaded the actual production output from +an isolated loopback HTTP server. Only public branding/auth-method responses were +stubbed; authentication returned a signed-out result. No real account or live +institutional server was used. This automated run did not install an OS app; +the separate interactive checks below did. + +Passed: + +1. Sign-in install entry, browser help dialog, Escape and return of focus. +2. Chrome's manifest parser and installability checks returned no errors; the + real Angular service worker controlled the page. +3. Public-asset verifier accepted the served production output. +4. Offline status appeared; after all prefetch assets finished caching, a reload + came from the service worker and rendered the offline app shell. +5. Reconnect cleared the warning without forcing navigation. +6. `/api/d2l/callback` navigation returned network JSON instead of app HTML. +7. Installation help remained within a 390px viewport, retaining mobile guidance. +8. No uncaught page errors occurred. + +The cold offline shell displays the existing unavailable state because sign-in +and public settings need the API; this is expected, not offline authentication. +The test waits for all prefetch cache entries and `Driver state: NORMAL`; a +controller alone is insufficient. Chrome's network simulation uses both request +blocking and `Network.overrideNetworkState` so `navigator.onLine` remains false +across document reloads. The earlier controller-only and incomplete emulation +attempts were corrected in the test harness, without changing production code. + +[Machine-readable browser result](evidence/desktop-pwa-20260921/browser-results.json) + +![Desktop installation help](evidence/desktop-pwa-20260921/desktop-install.png) + +![Offline connection status](evidence/desktop-pwa-20260921/desktop-offline.png) + +![390px viewport installation help](evidence/desktop-pwa-20260921/mobile-install.png) + +## Interactive macOS installation + +Using the actual production build at an isolated loopback origin with signed-out +API fixtures, native UI checks passed on macOS 27.0 (26A428): + +| Browser | Observed result | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Chrome 153.0.8010.48 | Browser installation created an OS application with the manifest name and `/index.html` start URL. The app launched in a standalone window, showed **OnTrack app help**, and relaunched after closing. Uninstall removed the application. A second pass through the foreground in-page Install button opened Chrome's native prompt, allowed cancellation and retry, and reinstalled with the same application ID. The temporary app was then uninstalled again. | +| Safari 27.0 | The help dialog offered manual instructions without a Chromium-only Install button. **File → Add to Dock** selected `/index.html`; the temporary app, named **OnTrack Desktop QA**, opened in its own window, showed the installed help state, and relaunched successfully. The app was quit and moved to Bin through Finder; removal was verified. | + +These checks used normal local browser profiles and native installation +confirmations. Temporary applications were removed afterwards; unrelated browser +data was not cleared. They establish local installation behavior, not a result +for an institutional HTTPS origin or authenticated Safari session. + +[Native installation record](evidence/desktop-pwa-20260921/native-install-results.json) + +## Database authentication against an isolated API + +A separate Rails/MariaDB stack with synthetic demo fixtures served real API +responses to the production web build through a loopback proxy. A disposable +Chrome profile passed nine authentication checks: protected-route redirection, +incorrect-password rejection, successful sign-in returning to `/notifications`, +loading seeded notification records, authenticated reload through the refresh +cookie, logout, rejection of the revoked access token, inability to refresh the +logged-out session, and denial of a protected route after logout. There were no +uncaught page errors. Credentials and tokens are excluded from the evidence. + +The [authentication evidence and reproduction guide](evidence/desktop-pwa-20260921/authenticated-checks/README.md) +record the API source/runtime and fixture scope. This exercised database +authentication in a normal browser, not institutional SSO or an authenticated +installed Safari app. The application's existing external editor/font assets +were allowed to load; these checks do not establish offline authentication. + +## Real worker update and recovery + +The [portable regression harness](evidence/desktop-pwa-20260921/update-checks/README.md) +passed eight checks in a disposable Chrome profile using copies of the production +build, synthetic release markers and signed-out API fixtures. The actual worker +downloaded release B and the application's Reload notice appeared. The existing +document and a synthetic unsaved textarea value survived for ten seconds until +the user action; Reload then loaded B successfully. + +Restoring the identical original A manifest left both existing and newly opened +clients on cached B. Publishing the same A asset bytes with a fresh Angular +manifest timestamp produced a distinct recovery version. The real Reload action +returned to A, preserving app identity and scope, with worker state `NORMAL`. +The recovered shell also reloaded offline and recovered on reconnect. There were +no uncaught page errors. This is a coherent synthetic-release regression, not a +Docker image publication or production rollback rehearsal. + +[Machine-readable update/recovery results](evidence/desktop-pwa-20260921/update-checks/results.json) + +## Not yet exercised + +Windows and Linux OS installation; Edge and Firefox installation; institutional +SSO; authenticated Safari sessions; course submission/download workflows; live +Web Push; and real Android/iOS installation regression. The currently documented +hosting address was not reachable during this session, so no public HTTPS +deployment or production acceptance is claimed. Complete the applicable +[staging acceptance checklist](desktop-pwa.md#required-staging-acceptance) on the +intended origin before production rollout. Local results do not certify browser +and platform combinations that were not exercised. diff --git a/docs/desktop-pwa.md b/docs/desktop-pwa.md new file mode 100644 index 0000000000..310812df0e --- /dev/null +++ b/docs/desktop-pwa.md @@ -0,0 +1,150 @@ +# OnTrack desktop app + +OnTrack uses the same installable PWA on desktop and mobile. It opens in an app +window, has an operating-system launcher icon, and receives the existing web +releases. No separate executable, app store, desktop API, or database migration +is needed. Deploy the production web build to the existing HTTPS origin. + +## Install and use + +Open your institution's OnTrack address. Select **Install OnTrack** below the +sign-in form or in the account menu. When the browser offers installation, the +dialog's Install button opens its confirmation. Otherwise, the dialog provides +manual browser instructions. Opening or closing help never installs the app. + +| Desktop browser | Installation | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Chrome on Windows, macOS or Linux | Address-bar install icon, or menu → Cast, save and share → Install page as app | +| Edge on Windows or macOS | Address-bar app icon, or menu → Apps → Install this site as an app; availability on other platforms varies | +| Safari on macOS Sonoma 14 or newer | File → Add to Dock (or Share → Add to Dock) | +| Firefox on Windows | Supported versions offer web apps through the address bar; Firefox 143+, or 150+ for the Microsoft Store build | +| Other browsers/platforms | Use the website normally, or open the same address in a browser above | + +Browser menu wording and enterprise policies can vary. A missing in-page Install +button does not prove installation is impossible: Safari and Firefox do not use +the Chromium `beforeinstallprompt` flow. A tab also cannot reliably discover an +installation made in another browser/profile. The app recognises standalone +windows and the current browser's `appinstalled` event; it does not store a +permanent installed flag that becomes stale after uninstall. + +Launch OnTrack from Start, the Dock/Applications, or your desktop environment's +app launcher. You may need to sign in again, especially in Safari's separate web +app storage. Existing account roles and access rules apply. Where the browser and +OS support manifest shortcuts, right-click the app icon for Home, Notifications +and Unit Hub. Pinning, shortcut placement, and opening external links are managed +by the browser/OS. Remove the app through the browser's app management or OS; +uninstalling the app does not delete your OnTrack account. + +Mobile users retain Android install and iPhone/iPad Share → Add to Home Screen +instructions in the same dialog. + +## Connection, updates and notifications + +Installation caches application code, not a complete offline learning system. +Sign-in, current unit/task data, submissions, downloads and server actions need a +working connection. The status message appears when the browser reports offline; +it does not certify that the API is reachable when the browser reports online. +Submissions are not queued for automatic upload. A first visit requires a +connection, and browsers may evict the cache. Reconnect and reload if the cached +shell cannot finish loading. + +The existing update service checks after application stability and every four +hours. A downloaded release offers **Reload**; save/submit any work before using +it. Failed background checks while offline are handled and later polling +continues. Angular's worker also checks on navigation. There is no forced reload +when the connection returns. + +For recovery, rebuild the previous source as a new release with a freshly +generated Angular service-worker manifest. Restoring an identical older image +can leave clients on the newer cached release because Angular ignores manifest +versions it already knows. Publish the complete recovery build, preserve app +identity, and verify the Reload notice actually returns an existing client to +the intended version. The companion deployment runbook covers this procedure. + +Push notifications reuse the API's current Web Push setup and the user's opt-in +settings. Installing does not grant notification permission or configure VAPID +keys. Delivery depends on browser/OS settings and background-process policies; +the app does not promise delivery when the browser is completely shut down. + +## Implementation and release + +- `src/manifest.webmanifest` keeps `start_url` and sets the explicit `id` to + `/index.html`, matching the previous implicit identity. Keep these stable to + prevent duplicate mobile or desktop apps. Scope remains `/`; subpath hosting + would require a separate routing, asset and service-worker design. +- The existing 192px, 512px and maskable icons are reused. All shortcuts stay on + the same origin and contain no account identifiers or credentials. +- `PwaInstallService` is constructed by the root NgModule to catch early browser + events. It stores each prompt in memory, consumes it once on user action, and + handles cancellation, errors, installed windows and teardown. Previous snackbar + dismissal storage no longer prevents a deliberate installation attempt. +- The standalone install button and dialog serve sign-in and account-menu entry + points. `PwaConnectionStatusComponent` supplies the shared offline message. +- `ngsw-config.json` excludes `/api` and `/api/**` from navigation fallback, keeping + API callbacks and downloads on the network. Existing API caching remains + disabled. Existing authentication, logout and callback sanitisation are reused. +- Production Nginx already serves manifests with the correct MIME type and + revalidates service-worker control files. Missing control files return 404, + while Angular routes receive the app shell. + +From the web repository, using its supported Node version: + +```sh +npm ci +git submodule update --init --recursive +npm run verify:deployment-config +npm run verify:pwa +npm run test:ci -- --include='src/app/common/pwa/**/*.spec.ts' --include='src/app/common/pwa-connection/**/*.spec.ts' --include='src/app/common/header/header.component.spec.ts' --include='src/app/sessions/service-worker-updater/*.spec.ts' --include='src/app/sessions/states/sign-in/*.spec.ts' +npm run typecheck +npm run build -- --configuration production +npm run verify:pwa:build +``` + +The Node CI workflow verifies the production output, icon dimensions, cached-file +hashes and generated navigation rules. `dist/browser` is the deployable output. +Do not edit files after generating `ngsw.json`: its hashes must match the files +served. Use the existing `deploy.Dockerfile` and deployment release process. +The companion `doubtfire-deploy/DESKTOP-PWA.md` describes the public +HTTPS asset verifier, rollout and rollback checks. + +The [validation record](desktop-pwa-validation.md) identifies completed local +checks and the browser/OS combinations still requiring acceptance. + +## Required staging acceptance + +Automated source/build checks and simulated install events do not prove OS +installation, SSO, notification delivery or every browser implementation. Record +the release SHA, browser/OS version and result for each intended platform: + +1. Use a fresh normal profile on the staging HTTPS origin; verify the public PWA + assets, then install through the OnTrack dialog or browser menu. Confirm app + name/icon and a standalone launch from the OS launcher. +2. Repeat with an existing mobile installation to check identity preservation. + Dismiss an install prompt, reopen help, and use the browser menu if it does not + reissue an event. In an installed window, check the installed/help state. +3. Sign in with the institutional database/SSO flow. Open a protected deep link + while signed out, then verify sign-in returns to it. Verify logout and another + account do not expose the first account's data. SSO popups/redirects may be + handled differently by installed browsers and require this real test. +4. Open Home, Unit Hub and Notifications; resize and test keyboard access, + Escape/focus return from help, dark theme, uploads and downloads. Test available + OS shortcuts. Check mobile layout for regressions. +5. After a successful online load, wait for the worker to finish caching its + prefetch assets (`/ngsw/state` should report `NORMAL`). A registered worker + alone does not mean the cache is ready. Then go offline. Verify the + status message and failure of network actions without a success/queued claim. + Reconnect and confirm the message clears without interrupting current work. +6. With notification permission and server push configured, send a test event to + a synthetic account. Check delivery and that clicking it opens the protected + destination. Repeat with notifications denied; installation must still work. +7. Deploy a second build, wait for/check an update, and accept Reload after saving. + Confirm the new version and no duplicate app. Test the documented rollback + while preserving the origin, app identity and matching service-worker hashes. + +## Browser references + +- [Chrome install behaviour](https://web.dev/learn/pwa/installation/) +- [Stable manifest identity](https://developer.chrome.com/docs/capabilities/pwa-manifest-id) +- [Safari web apps on Mac](https://support.apple.com/en-us/104996) +- [Firefox web apps on Windows](https://support.mozilla.org/en-US/kb/web-apps-firefox-windows) +- [Edge PWA documentation](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps/) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..f23e9c6351 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +# OnTrack Web documentation + +Start with the [repository setup and checks](../README.md) and +[contributor conventions](../CONTRIBUTING.md). + +## Student features + +- [Calendar how-to](CAL-DOC01-calendar-how-to.md) +- [Cross-project dashboard guide and handover](cpd-user-guide-handover.md) +- [Peer progress help text](ppi-help-text-pack.md) +- [Unit announcements and sessions](unit-hub.md) +- [First-time tutorial implementation](student-onboarding/README.md) + +## Shared contracts + +- [Theme contract](theme/THEME-CONTRACT.md) +- [Safe upload policy matrix](FILE-A01-safe-upload-policy-matrix.md) +- [Service worker behaviour](service-worker.md) +- [Peer progress unit-summary handover](ppi-f02-unit-summary-handover.md) + +## Team guidance + +The [team guide](https://github.com/ontrack-features-t2-2026/github-guide) +maintains setup walkthroughs, transcripts, tutorial guidance and contribution +instructions. Feature-specific implementation evidence stays beside the code +in its owning repository. diff --git a/docs/migration/analytics.md b/docs/migration/analytics.md new file mode 100644 index 0000000000..fa6b12e429 --- /dev/null +++ b/docs/migration/analytics.md @@ -0,0 +1,76 @@ +# Unit Statistics charts (MG-11) + +Unit Statistics now includes task-status bubbles, target-grade shares and task-completion +box plots alongside the existing CSV exports and tutor-time summary. The shared tutorial +selector switches between the whole unit and one tutorial. Task Status additionally +filters by task; Task Completion can show the selected unit/tutorial or all grade groups. +New routes/sub-states are outside this change. + +## Chart library compatibility + +Rendering tests uncovered that ngx-charts 20.5 injects Angular's removed +`ComponentFactoryResolver`. The dependency is upgraded to **25.0.2**, whose +[upstream changelog](https://github.com/swimlane/ngx-charts/blob/master/docs/changelog.md) +adds Angular 22 support. Existing burndown, task-status pie and gauge components +have real SVG smoke tests as well as the new chart tests. No tooltip injection +shim or disabled render assertion is used to hide the incompatibility. + +## Behaviour retained from the source + +- `SummaryTaskStatusScatterComponent` totals the existing + `TaskStatusStats[taskDefinitionId][tutorialId]` rows by status. The x axis is status, + the y axis is task, and bubble size is count. Invalid/unknown counts are excluded. +- `TargetGradePieChartComponent` totals rows by grade, uses configured unit grade + labels and the existing grade palette, and reports `round(count / total * 100)`. + Empty selections do not divide by zero. +- `TaskCompletionBoxPlotComponent` preserves the API's `min`, `lower`, `median`, + `upper`, `max` for unit, tutorial and grade groupings. The installed ngx-charts box + series computes quartiles from its input. Supplying these **five ordered summary + values** puts its 25th/50th/75th percentile positions exactly at the supplied + lower/median/upper values. The component spec asserts the rendered ngx-charts + quartiles and whiskers, rather than only asserting a mapper's output. These are + summary values, not fabricated student observations. + +The API's existing `Unit#_calculate_task_completion_stats` uses the 30% and 80% +array positions for lower/upper, and includes records with completed tasks. The UI +labels them **lower** and **upper**, not newly calculated population quartiles. +Changing the API population/percentile definition would be a separate behavioural change. +The new chart uses ngx-charts' labelled data range instead of the old fixed y domain. + +The status and grade endpoints group by tutorial enrolment. Whole-unit sums retain +the old aggregation and may count a student once per stream. The page discloses this; +these charts must not be represented as deduplicated student headcounts. The data +comes from the existing authenticated `/units/:id/stats/task_status_pct`, +`student_target_grade`, and `task_completion_stats` endpoints, all of which enforce +`:download_stats` on the API. No new endpoint or client cache is introduced. + +## Resilience and accessibility + +Changing units cancels the preceding request and clears old data and filter selection. +Failures leave successful charts available and show a retry action. Loading, failure, +empty and all-zero completion states are explicit. Each chart has a semantic values +table; chart drawing is hidden from screen readers to avoid duplicate/colour-only +information. Filters use labelled Material controls. No student names are added. + +## Verification + +Run the chart specs through Angular's test builder (Vitest): + +```sh +npm test -- --watch=false --include='src/app/visualisations/summary-task-status-scatter/*.spec.ts' --include='src/app/visualisations/target-grade-pie-chart/*.spec.ts' --include='src/app/visualisations/task-completion-box-plot/*.spec.ts' --include='src/app/units/states/analytics/*.spec.ts' +npm run typecheck +npm run build +npm run lint +``` + +Review with a convenor account: open Unit Statistics, switch tutorials and task, +compare the accessible tables with the endpoint payloads, try grade grouping, then +simulate a failed stats request and use Retry. Check narrow layouts and theme contrast. +A seeded/authenticated API session is required for that final real-data walkthrough; +unit fixtures are not evidence that a production account was exercised. + +See the [recorded validation results](validation.md) for the final full-suite run and source revisions. + +The [21 September authenticated walkthrough](acceptance-2026-09-21.md) now records +the seeded full-app exercise, actual API payload comparisons, filtering/retry and +settled responsive/theme checks, including the discovered SVG label contrast fix. diff --git a/docs/migration/legacy-audit.md b/docs/migration/legacy-audit.md new file mode 100644 index 0000000000..dc3d919f05 --- /dev/null +++ b/docs/migration/legacy-audit.md @@ -0,0 +1,104 @@ +# Legacy source and toolchain audit (MG-06, MG-08, MG-09) + +Baseline: web `11.0.x` `d16f6201caff78082e5e83c540320dbee5871404`, inspected 20 September 2026. + +## Migration guide: already resolved + +`MIGRATION-GUIDE.md` was deleted by `03bc2cdf377ec2a4e07cae668f0511c2d6c6b6fc` +on 15 September 2026. The accurate component instructions now live in +[CONTRIBUTING.md](../../CONTRIBUTING.md). Preserve that deletion; do not recreate the +AngularJS downgrade process. The complete former guide was inspected with +`git show 03bc2cdf3^:MIGRATION-GUIDE.md`. + +| Old guide lines | Reference / advice | Verified state | +| ------------------------ | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| 45–47 | `task-description-card.coffee`, `.tpl.html`, root `.scss` | Replaced by `.component.ts`, `.component.html`, `.component.scss` in the task-description-card directory. | +| 107 | `src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee` | Absent. | +| 110–115 | `src/app/doubtfire-angular.module.ts`, imports and declarations | Live and still necessary for non-standalone components. | +| 116–128 | `src/app/doubtfire-angularjs.module.ts`, `downgradeComponent`, `build/src/.../task-description-card.js` | Absent; AngularJS bootstrapping removed. | +| 167, 198–199 | `npm start`, accessibility labels, linting | Still applicable. Use `npm run lint` to preserve `--max-warnings 0`. | +| 17, 25, 184–188, 201–209 | Bootstrap-to-Material conversion | Runtime conversion complete; orphan markup remains in the catalogue. | + +For paths, `git cat-file -e : && echo LIVE || echo DEAD` gives an +explicit result. For short filenames use `git ls-tree -r --name-only ` first. +No `downgradeComponent` remains in `src`. The two Angular module names differ: +`doubtfire-angular.module.ts` **is live**; `doubtfire-angularjs.module.ts` is absent. + +## CoffeeScript inventory and resolution + +| Baseline path | Lines | Replacement / outcome | +| ----------------------------------------------------------- | ----: | ---------------------------------------------------------------------------------------- | +| `src/app/common/filters/filters.coffee` | 320 | Eight Angular pipes in `src/app/common/filters/`; remove dead source. | +| `src/app/units/states/tasks/definition/definition.coffee` | 31 | `app.routes.ts` definition routes and `UnitTaskInboxStateComponent`; remove dead source. | +| `src/app/visualisations/progress-burndown-chart.coffee` | 107 | `ProgressBurndownChartComponent`; remove dead source and root orphan SCSS. | +| `src/app/visualisations/summary-task-status-scatter.coffee` | 29 | MG-11: replaced by `SummaryTaskStatusScatterComponent`. | +| `src/app/visualisations/target-grade-pie-chart.coffee` | 48 | MG-11: replaced by `TargetGradePieChartComponent`. | +| `src/app/visualisations/task-completion-box-plot.coffee` | 66 | MG-11: replaced by `TaskCompletionBoxPlotComponent`. | +| `src/app/visualisations/task-status-pie-chart.coffee` | 46 | `TaskStatusPieChartComponent`; remove dead source. | +| `src/app/visualisations/visualisations.coffee` | 61 | Retained-for-reference module; delete after all three replacements are integrated. | + +The filter replacements are `filters.pipe.ts`, `order-by.pipe.ts`, +`task-definition-name.pipe.ts`, `tasks-by-tutor.pipe.ts`, `tasks-for-group-set.pipe.ts`, +`tasks-for-inbox-search.pipe.ts`, `tasks-in-tutorials.pipe.ts`, and +`tasks-of-task-definition.pipe.ts`. They are registered in the main Angular module. +The definition route still selects explorer mode, search options, task-definition mode +and `queryTasksForTaskExplorer` in the Angular inbox component. + +The `visualisations.coffee` header explicitly said it was unlinked and retained for +reference. Migration deletions preserve the history; the new chart specs capture the +computations. See [analytics behaviour](analytics.md) for the remaining API semantics. + +## Obsolete toolchain + +The baseline `package.json` has no `grunt`, `karma`, `jasmine` or `coffee` entry. +`angular.json` uses `@angular/build:application` and `@angular/build:unit-test` (Vitest). +`git grep -in grunt d16f6201c -- .github/` and +`git grep -nE '(import|require).*\.coffee' d16f6201c` produce no matches. + +| File | Baseline evidence | Resolution | +| -------------------------------- | -------------------------------------------------------------- | ---------------- | +| `Gruntfile.js` | Depends on uninstalled legacy task packages; no npm/CI caller. | Delete. | +| `build.config.js` | Consumed only by the obsolete task file. | Delete. | +| `env.config.js` | Consumed only by the obsolete task file. | Delete. | +| `karma/karma-unit.tpl.js` | No installed runner or builder uses it. | Delete. | +| `module.prefix`, `module.suffix` | Only referenced by the obsolete task file. | Delete. | +| `migration_progress.sh` | Counts AngularJS/CoffeeScript, which the app no longer builds. | Delete. | +| `setup.sh` | Unreferenced script installs Node 10 and global Sass. | Delete (DX-W02). | + +The README already documents Node, `npm ci`, Angular CLI, port 4200 and recursive +submodules. Its setup instructions required no rewrite. Build scripts lose the obsolete +`angular18` suffix (DX-W12); deploy callers use `npm start` and are unaffected. +`@eslint/js`, `typescript-eslint`, and `@sentry/cli` are development dependencies, with +matching lockfile flags. Production is the built `dist/browser` artifact. An +`--omit=dev` install cannot build the app because Angular CLI/compiler are development +dependencies; use a full `npm ci` in the build stage rather than claiming otherwise. + +## Remaining template inventory + +| Baseline template | Referrers / disposition | +| --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/app/projects/states/dashboard/dashboard.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/projects/states/dashboard/directives/student-task-list/student-task-list.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/units/states/analytics/analytics.tpl.html` | No source referrer; orphan, retained for a separately scoped cleanup. | +| `src/app/visualisations/visualisation.tpl.html` | `src/app/visualisations/progress-burndown-chart.coffee`; `src/app/visualisations/summary-task-status-scatter.coffee`; `src/app/visualisations/target-grade-pie-chart.coffee`; `src/app/visualisations/task-completion-box-plot.coffee`; `src/app/visualisations/task-status-pie-chart.coffee` | + +The chart `visualisation.tpl.html` is deleted with its final CoffeeScript users. +`about-doubtfire-modal-content.tpl.html`, called out as live in the August workbook, +is no longer present at this baseline. Do not recreate it. Remaining `.tpl.html` files +are excluded from ESLint and are not routed by the Angular build. + +## Existing setup fixes: verify, do not duplicate + +- **DX-W01:** `.nvmrc` is `v22.22.3`, `.tool-versions` is `nodejs 22.22.3`, and + `package.json` requires `>=22.22.3`. Fixing commit: + `27f1012bf7cd01f81a4fe5725a3be545288a03cd` (24 August 2026). +- **DX-W13:** README Getting Started and production instructions initialize + `JPlag-Report-Viewer`; the post-build check verifies the copied assets. Fixing commit: + `bd5e1d66b` (24 August 2026), followed by `8ef1be045`. `.gitmodules` and the + `angular.json` asset entry agree. No install hook is needed to duplicate the guide. + +These are source-verified outcomes; no Planner card or approval state was changed. diff --git a/docs/migration/recovery-register.md b/docs/migration/recovery-register.md new file mode 100644 index 0000000000..ced4b76c06 --- /dev/null +++ b/docs/migration/recovery-register.md @@ -0,0 +1,98 @@ +# Migration recovery register (MG-01) + +Read-only GitHub audit on 20 September 2026. Verdicts were checked against +**doubtfire-lms/doubtfire-web `11.0.x` `a51f22f6f18f41dfd1725d3c9f4c5c2abae0f42b`**, not the organisation fork. +No PR was merged, reopened, retargeted, closed, or commented on. No claim of +original authorship or approval is made by this audit. + +## Actionable LIVE rows + +| PR | Author | Fork / head | Verdict | Checked file and evidence | Rebasable | +| ------------------------------------------------------------------------------ | ------------ | ---------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| [doubtfire-lms#1143](https://github.com/doubtfire-lms/doubtfire-web/pull/1143) | WaelAlahamdi | `WaelAlahamdi/doubtfire-web` / `quality/migrate-visualisation-service` | LIVE | `src/app/visualisations/visualisations.coffee`: LIVE. Source still present; three charts needed Angular replacements. Addressed by MG-11 in this branch; compare before recovering. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1220](https://github.com/doubtfire-lms/doubtfire-web/pull/1220) | WaelAlahamdi | `WaelAlahamdi/doubtfire-web` / `quality/migrate-visualisation-service` | LIVE | `src/app/visualisations/visualisations.coffee`: LIVE. Source still present; three charts needed Angular replacements. Addressed by MG-11 in this branch; compare before recovering. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | + +The remaining `visualisations.coffee` references were an actual outstanding UI gap. +This branch implements the three charts; do not blindly rebase the old AngularJS +wiring. `SUPERSEDED` means the migration target is gone or already has an Angular +replacement, not that every idea in the PR was reviewed for feature merit. + +## Closed-unmerged 2026 cohort targeting 10.0.x + +The complete closed list was requested with `--limit 2000`, filtered by null +`mergedAt`, `baseRefName == "10.0.x"`, creation in 2026, and non-bot author. The +current result has 24 rows: the workbook's 22 plus #1085 (mentor moderation link) +and #1095 (submission history), both feature work. The original five feature PRs +#1148, #1160, #1193, #1212 and #1213 are explicitly retained as NOT MIGRATION. + +| PR | Author | Fork / head | Verdict | Checked file and evidence | Rebasable | +| ------------------------------------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [doubtfire-lms#1085](https://github.com/doubtfire-lms/doubtfire-web/pull/1085) | b0ink | `b0ink/doubtfire-web` / `refactor/show-moderation-to-mentors-only` | NOT MIGRATION | `src/app/common/header/task-dropdown/task-dropdown.component.html`: LIVE. Feature change, excluded from migration recovery. | Not tested; no recovery indicated. | +| [doubtfire-lms#1095](https://github.com/doubtfire-lms/doubtfire-web/pull/1095) | b0ink | `b0ink/doubtfire-web` / `feat/submission-diffs` | NOT MIGRATION | `src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html`: LIVE. Feature change, excluded from migration recovery. | Not tested; no recovery indicated. | +| [doubtfire-lms#1143](https://github.com/doubtfire-lms/doubtfire-web/pull/1143) | WaelAlahamdi | `WaelAlahamdi/doubtfire-web` / `quality/migrate-visualisation-service` | LIVE | `src/app/visualisations/visualisations.coffee`: LIVE. Source still present; three charts needed Angular replacements. Addressed by MG-11 in this branch; compare before recovering. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1147](https://github.com/doubtfire-lms/doubtfire-web/pull/1147) | Thirus224849242 | `Thirus224849242/doubtfire-web` / `migrate/root-controller` | SUPERSEDED | `src/app/config/root-controller/root-controller.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1148](https://github.com/doubtfire-lms/doubtfire-web/pull/1148) | jtalev | `jtalev/doubtfire-web` / `tutor-effort-input` | NOT MIGRATION | `src/app/api/models/task-definition.ts`: LIVE. Feature change, excluded from migration recovery. | Not tested; no recovery indicated. | +| [doubtfire-lms#1156](https://github.com/doubtfire-lms/doubtfire-web/pull/1156) | Thirus224849242 | `Thirus224849242/doubtfire-web` / `migrate/root-controller` | SUPERSEDED | `src/app/config/root-controller/root-controller.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1160](https://github.com/doubtfire-lms/doubtfire-web/pull/1160) | faithque | `faithque/doubtfire-web (fork unavailable)` / `feature/performance-bands-web` | NOT MIGRATION | `src/app/visualisations/performance-bands/performance-bands.component.ts`: GONE. Feature change, excluded from migration recovery. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1174](https://github.com/doubtfire-lms/doubtfire-web/pull/1174) | m1chael-pappas | `m1chael-pappas/doubtfire-web` / `migrate/error-states` | SUPERSEDED | `src/app/errors/states/timeout/timeout.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1177](https://github.com/doubtfire-lms/doubtfire-web/pull/1177) | m1chael-pappas | `m1chael-pappas/doubtfire-web` / `migrate/student-dashboard` | SUPERSEDED | `src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1193](https://github.com/doubtfire-lms/doubtfire-web/pull/1193) | faithque | `faithque/doubtfire-web` / `feature/performance-bands` | NOT MIGRATION | `src/app/projects/states/performance-bands/performance-bands.coffee`: GONE. Feature change, excluded from migration recovery. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1212](https://github.com/doubtfire-lms/doubtfire-web/pull/1212) | b0ink | `b0ink/doubtfire-web` / `feat/communication-system` | NOT MIGRATION | `src/app/api/models/communication.ts`: LIVE. Feature change, excluded from migration recovery. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1213](https://github.com/doubtfire-lms/doubtfire-web/pull/1213) | jtalev | `jtalev/doubtfire-web` / `allow-prediction-flag-on-units` | NOT MIGRATION | `src/app/api/models/project.ts`: LIVE. Feature change, excluded from migration recovery. | Not tested; no recovery indicated. | +| [doubtfire-lms#1215](https://github.com/doubtfire-lms/doubtfire-web/pull/1215) | WaelAlahamdi | `WaelAlahamdi/doubtfire-web` / `quality/migrate-timeout-final` | SUPERSEDED | `src/app/errors/states/timeout/timeout.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1216](https://github.com/doubtfire-lms/doubtfire-web/pull/1216) | WaelAlahamdi | `WaelAlahamdi/doubtfire-web` / `migrate-students-list` | SUPERSEDED | `src/app/units/states/students-list/students-list.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1220](https://github.com/doubtfire-lms/doubtfire-web/pull/1220) | WaelAlahamdi | `WaelAlahamdi/doubtfire-web` / `quality/migrate-visualisation-service` | LIVE | `src/app/visualisations/visualisations.coffee`: LIVE. Source still present; three charts needed Angular replacements. Addressed by MG-11 in this branch; compare before recovering. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1221](https://github.com/doubtfire-lms/doubtfire-web/pull/1221) | Thirus224849242 | `Thirus224849242/doubtfire-web` / `migrate/root-controller` | SUPERSEDED | `src/app/config/root-controller/root-controller.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1223](https://github.com/doubtfire-lms/doubtfire-web/pull/1223) | Thirus224849242 | `Thirus224849242/doubtfire-web` / `migrate/tasks` | SUPERSEDED | `src/app/tasks/tasks.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1224](https://github.com/doubtfire-lms/doubtfire-web/pull/1224) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/units-states-tasks-coffee` | SUPERSEDED | `src/app/units/states/tasks/tasks.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1230](https://github.com/doubtfire-lms/doubtfire-web/pull/1230) | sheshankarvapally | `sheshankarvapally/doubtfire-web` / `project-outcome-alignment-clean-v2` | SUPERSEDED | `src/app/projects/states/outcomes/outcomes.coffee`: GONE. Legacy source absent on upstream 11.0.x. | Not tested; no recovery indicated. | +| [doubtfire-lms#1231](https://github.com/doubtfire-lms/doubtfire-web/pull/1231) | sheshankarvapally | `sheshankarvapally/doubtfire-web` / `investigate-config-dependencies-10x-clean` | SUPERSEDED | `src/app/config/root-controller/root-controller.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1232](https://github.com/doubtfire-lms/doubtfire-web/pull/1232) | sheshankarvapally | `sheshankarvapally/doubtfire-web` / `clean-upload-submission-modal-10x` | SUPERSEDED | `src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee`: GONE. Legacy source absent on upstream 11.0.x. | Not tested; no recovery indicated. | +| [doubtfire-lms#1233](https://github.com/doubtfire-lms/doubtfire-web/pull/1233) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/unit-groups-state-clean` | SUPERSEDED | `src/app/units/states/groups/groups.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1236](https://github.com/doubtfire-lms/doubtfire-web/pull/1236) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/units-states-tasks-definition` | SUPERSEDED | `src/app/units/states/tasks/definition/definition.coffee`: LIVE. Orphan source remains; definition routes and UnitTaskInboxStateComponent already implement the route. No route recovery needed. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [doubtfire-lms#1237](https://github.com/doubtfire-lms/doubtfire-web/pull/1237) | sakethsram8888 | `sakethsram8888/doubtfire-web` / `migrate/utilService` | SUPERSEDED | `src/common/utilService/utilService.ts`: GONE. AngularJS dependency/utility wiring removed; no AngularJS bootstrap remains. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | + +## Current-team thoth-tech PRs + +All nine named by the ticket remain represented. Even a superseded target must be +discussed with its author before any maintainer action. Seven edit the removed +AngularJS module: #452, #457, #458, #486, #491, #493 and #495. #484 and #485 do not. +Rebasability is a separate column from migration relevance, not an inference from +GitHub's mergeable flag. + +| PR | Author | Fork / head | Verdict | Checked file and evidence | Rebasable | +| ---------------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [thoth-tech#452](https://github.com/thoth-tech/doubtfire-web/pull/452) | Thirus224849242 | `Thirus224849242/doubtfire-web` / `migrate/root-controller` | SUPERSEDED | `src/app/config/root-controller/root-controller.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [thoth-tech#457](https://github.com/thoth-tech/doubtfire-web/pull/457) | Thirus224849242 | `Thirus224849242/doubtfire-web` / `migrate/tasks` | SUPERSEDED | `src/app/tasks/tasks.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [thoth-tech#458](https://github.com/thoth-tech/doubtfire-web/pull/458) | sakethsram8888 | `sakethsram8888/doubtfire-web` / `migrate/vendor-dependencies` | SUPERSEDED | `src/app/config/vendor-dependencies/vendor-dependencies.ts`: GONE. AngularJS dependency/utility wiring removed; no AngularJS bootstrap remains. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [thoth-tech#484](https://github.com/thoth-tech/doubtfire-web/pull/484) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `investigate/f-unit-task-list-v2` | SUPERSEDED | `src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee`: GONE. Legacy source absent on upstream 11.0.x. | Not tested; no recovery indicated. | +| [thoth-tech#485](https://github.com/thoth-tech/doubtfire-web/pull/485) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/viewer-wrapper-clean` | SUPERSEDED | `src/app/units/states/tasks/viewer/viewer.coffee`: GONE. Legacy source absent on upstream 11.0.x. | Not tested; no recovery indicated. | +| [thoth-tech#486](https://github.com/thoth-tech/doubtfire-web/pull/486) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/unit-groups-state-clean` | SUPERSEDED | `src/app/units/states/groups/groups.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [thoth-tech#491](https://github.com/thoth-tech/doubtfire-web/pull/491) | sakethsram8888 | `sakethsram8888/doubtfire-web` / `migrate/utilService` | SUPERSEDED | `src/common/utilService/utilService.ts`: GONE. AngularJS dependency/utility wiring removed; no AngularJS bootstrap remains. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [thoth-tech#493](https://github.com/thoth-tech/doubtfire-web/pull/493) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/units-states-tasks-coffee` | SUPERSEDED | `src/app/units/states/tasks/tasks.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | +| [thoth-tech#495](https://github.com/thoth-tech/doubtfire-web/pull/495) | Sujay-Deakin | `Sujay-Deakin/doubtfire-web` / `migrate/units-states-tasks-definition` | SUPERSEDED | `src/app/units/states/tasks/definition/definition.coffee`: LIVE. Orphan source remains; definition routes and UnitTaskInboxStateComponent already implement the route. No route recovery needed. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | + +## Out-of-cohort referral + +#957 was opened in 2025 and is not included in the 2026 counts. The lead should +decide whether any non-migration feature content deserves separate review. + +| PR | Author | Fork / head | Verdict | Checked file and evidence | Rebasable | +| ---------------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| [doubtfire-lms#957](https://github.com/doubtfire-lms/doubtfire-web/pull/957) | disururathnayake | `disururathnayake/doubtfire-web` / `migrate/task-alignment-rater` | SUPERSEDED | `src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee`: GONE. Legacy source absent on upstream 11.0.x. | No: edits deleted `src/app/doubtfire-angularjs.module.ts`; any useful change needs a clean port. | + +## Reproduce + +```sh +gh pr list --repo doubtfire-lms/doubtfire-web --state closed --limit 2000 --json number,title,author,baseRefName,mergedAt,createdAt,headRefName,headRepositoryOwner +gh api --paginate 'repos/thoth-tech/doubtfire-web/pulls?state=open&per_page=100' +gh api --paginate repos/doubtfire-lms/doubtfire-web/pulls/1143/files +git fetch https://github.com/doubtfire-lms/doubtfire-web.git 11.0.x +git cat-file -e a51f22f6f18f41dfd1725d3c9f4c5c2abae0f42b:src/app/visualisations/visualisations.coffee && echo LIVE || echo GONE +``` + +Substitute the row's owner, PR and checked path. For #1236/#495, also inspect +`src/app/app.routes.ts` definition routes and +`src/app/units/states/tasks/inbox/unit-task-inbox-state.component.ts` at the same SHA; +the retained CoffeeScript alone is not evidence that the route still needs migration. +Follow the [open-PR triage](open-pr-triage.md) for maintainer handover. diff --git a/docs/migration/ui-catalogue.md b/docs/migration/ui-catalogue.md new file mode 100644 index 0000000000..903dcabdca --- /dev/null +++ b/docs/migration/ui-catalogue.md @@ -0,0 +1,93 @@ +# Layout migration catalogue (MG-04) + +## Current status — 21 September 2026 + +Rechecked against merged web `11.0.x` at +`283b493367681d1abab23e5fe7c633ba3a57e5c2`: **zero Flex Layout directive +occurrences, zero `FlexLayoutModule`/`MediaObserver` imports, and no +`ng-flex-layout` dependency**. Commit `0caa10617` completed the teardown on +20 September. There are no remaining templates to convert for this inventory. + +The following audit is retained at its original source revision so its counts +remain reproducible. Its conversion order and live-import warnings describe +that historical revision, not work still required on the current branch. + +## Historical inventory — 20 September 2026 + +Verified on 20 September 2026 against web `11.0.x` at `d16f6201caff78082e5e83c540320dbee5871404`. +This is a source inventory, not measured student usage. The workbook's August counts +were already historical: **10 occurrences across 2 templates** remained at this SHA. +Ranking uses route frequency and task relevance, not attribute counts or invented analytics. + +| Priority | Template | fx occurrences | Rationale | +| -------- | -------------------------------------------------------------------------------------- | -------------: | ------------------------------------------------------- | +| 1 | `src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.html` | 9 | Student project dashboard: a frequent task entry point. | +| 2 | `src/app/welcome/welcome.component.html` | 1 | Welcome screen: primarily first entry. | + +The recommended order at this revision was project dashboard first, welcome second. +There was no third candidate. Both conversions have since been completed. + +## TypeScript and package wiring + +- `src/app/doubtfire-angular.module.ts` (production). + +Only the main module still imports `FlexLayoutModule`, which remains in its `imports` +array and supplies the two templates. It is live, so do not remove the dependency yet. +No production or spec `MediaObserver` imports remain at this revision. Earlier audits +reported four production consumers and three specs; those have been migrated. +`MediaObserver` would require Angular CDK `BreakpointObserver`/application logic, not +a direct Tailwind class substitution, if encountered on an older branch. + +## Attribute mapping + +`fxLayoutAlign` is **main axis first, cross axis second**. For a row, +`fxLayoutAlign="center start"` maps to `justify-center items-start`, not the reverse. + +| Directive | Current occurrences | Replacement / exception | +| --------------- | ------------------: | ------------------------------------------------------------------------------------------------------------ | +| `fxLayout` | 2 | `flex flex-row` or `flex flex-col`; preserve breakpoint overrides. | +| `fxFlex` | 3 | Per-value decision: `flex-1`, `grow`, `basis-*`, or explicit flex-basis. Do not map every value to `w-full`. | +| `fxLayoutAlign` | 1 | Map main-axis value to `justify-*`, cross-axis value to `items-*`. | +| `fxFlexFill` | 0 | `w-full h-full` with appropriate flex growth after checking the parent. | +| `fxFlexAlign` | 0 | `self-*`. | +| `fxFill` | 2 | `w-full h-full`; verify parent dimensions. | +| `fxShow` | 1 | Conditional visibility / responsive display utilities; expressions need application state. | +| `fxHide` | 1 | `hidden` plus intended breakpoint display; expressions need application state. | + +## Bootstrap and Material leftovers + +No Bootstrap package is installed. These names are not evidence that Bootstrap still +runs; check component styles before replacing a custom class that happens to share a name. + +| File at the audited revision | Status | +| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.html` | Live Angular template with old class names; replace deliberately with Material/utilities. | +| `src/app/projects/states/dashboard/directives/student-task-list/student-task-list.tpl.html` | Dead legacy source; see the legacy map. | +| `src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.tpl.html` | Dead legacy source; see the legacy map. | +| `src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html` | Dead legacy source; see the legacy map. | +| `src/app/tasks/task-comment-composer/task-comment-composer.component.html` | Live Angular template with old class names; replace deliberately with Material/utilities. | +| `src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.html` | Live Angular template with old class names; replace deliberately with Material/utilities. | +| `src/app/units/states/analytics/analytics.tpl.html` | Dead legacy source; see the legacy map. | +| `src/app/visualisations/progress-burndown-chart.coffee` | Dead legacy source; see the legacy map. | + +Material legacy appearance remains at `src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html:21`. This is the older button-toggle appearance, not the removed form-field appearance. + +## Reproduce the counts + +Current status (the first two commands produce no matches): + +```sh +git grep -nE 'fx[A-Z][A-Za-z]*' 283b49336 -- 'src/**/*.html' +git grep -nE 'ng-flex-layout|FlexLayoutModule|MediaObserver' 283b49336 -- package.json 'src/**/*.ts' +``` + +Historical inventory: + +```sh +git grep -nE 'fx[A-Z][A-Za-z]*' d16f6201c -- '*.html' +git grep -n "from 'ng-flex-layout'" d16f6201c -- '*.ts' +git grep -nE 'btn btn-|col-sm-|form-group|panel panel-|input-group|glyphicon' d16f6201c -- src +``` + +Count regex **matches**, not matching lines, when updating this table. Follow the +[CSS style guide](../css-style-guide.md) and [theme contract](../theme/THEME-CONTRACT.md). diff --git a/docs/student-onboarding/README.md b/docs/student-onboarding/README.md new file mode 100644 index 0000000000..1d753c2b31 --- /dev/null +++ b/docs/student-onboarding/README.md @@ -0,0 +1,96 @@ +# First-Time OnTrack Tutorial implementation + +TUT-W01–W04, TUT-S01, TUT-T01, TUT-Q01 and TUT-MVP01. This is the reviewable +implementation handover, not a claim of release or human approval. The tutorial +is disabled unless authenticated settings explicitly return `tutorialEnabled: true`. + +The [student and contributor guides](https://github.com/ontrack-features-t2-2026/github-guide/tree/main/docs/student-onboarding) +remain the central written guidance. The account menu now contains **Tutorial and Help** +for authenticated students after profile and data loading, when the institution +has enabled the feature. The written guide is also linked from every tutorial panel. + +## Architecture and maintenance + +- [Shell](../../src/app/student-onboarding/student-onboarding.component.ts): welcome, + four guided steps, confirmation and completion. Existing CDK focus trapping is + used for modal panels; guided steps remain non-modal. Escape follows the + [prototype keyboard contract](https://github.com/ontrack-features-t2-2026/github-guide/blob/main/docs/design/student-onboarding-prototype-review/keyboard-and-focus.md). +- [Registry](../../src/app/student-onboarding/student-onboarding.steps.ts): version, + stable ID, route context, target, exact DOC-11 title/body, locate-action label and + missing-target fallback. Route is explanatory metadata, never an automatic + navigation instruction. Students choose their own unit and navigate themselves. +- [Service](../../src/app/student-onboarding/student-onboarding.service.ts): readiness, + own-user history check, browser progress, transitions and replay. It never writes + the existing profile setup flag, assessment data or Calendar settings. +- [Target resolver](../../src/app/student-onboarding/student-onboarding-target.ts): + visible explicit `data-onboarding-target` attributes; rejects selector input. + DOM changes, resize and scroll refresh highlights without activating controls. + +| Step | Stable target | Real location / fallback | +| --- | --- | --- | +| Choose Your Unit | `unit-selector` | Header unit dropdown (both selected and unselected states). No units: explain enrolment support. | +| Find Your Tasks | `task-dashboard` | Current project navigation menu trigger. No selected project: explain Select Unit then Dashboard. | +| Check Your Target Grade | `target-grade` | Dashboard Select Target Grade control; unavailable/hidden: explain its location and continue. | +| Use the Calendar | `calendar` | Existing desktop Calendar button or visible account-menu Calendar entry. Narrow screens: explain account menu, then Calendar. | + +`account-menu` is the permanent focus-return fallback, not a fifth tutorial step. +The tutorial does not click a menu, select a unit, navigate a route, change a grade, +create a subscription or submit a task. **Find…** only scrolls the current target +into view following an explicit user click. No extra tour dependency was added. + +To update a step, edit the approved [DOC-11 copy source](https://github.com/ontrack-features-t2-2026/github-guide/blob/main/onboarding-tutorial-step-copy.md) +and registry together. Keep IDs stable when reordering. A material add/remove, +reorder, route/target move or meaning change increments `ONBOARDING_VERSION`. +Spelling corrections do not. Existing completion/dismissal survives version changes; +incomplete older versions restart from the beginning. Update target, fallback and +version tests before retiring an ID. Never reuse an old ID for another purpose. + +## Eligibility, persistence and failures + +The [DOC-10 / TUT-D03 contract](https://github.com/ontrack-features-t2-2026/github-guide/blob/main/onboarding-tutorial-trigger-and-state-rules.md) +contains the decision table. Automatic eligibility requires observing incomplete +profile setup and a successful current-user history request returning **zero** +projects, including withdrawn and inactive projects. The request uses the +authenticated `GET /projects/history` response `{hasProjects:boolean}`. Only the +exact response `{hasProjects:false}` establishes empty history. Authentication +supplies the owner; no user ID or query arguments are sent and no project details +are returned or retained. A missing/older endpoint is replay-only. + +This intentionally excludes some newly enrolled students who already have a +project. Unknown/error/non-empty history is replay-only. An empty active-project +cache does not establish newness. Profile and globals must then finish loading; +welcome, profile editing, sign-in/out and SCORM routes cannot be interrupted. + +Browser progress is exactly `{version,state,step}` under +`ontrack:student-onboarding:`. States are `new`, +`in-progress`, `skipped`, `dismissed`, `completed`; step IDs are from the registry. +The key scopes normal use to the current account but is not an authorization +boundary against another person using the same browser profile. + +Start/Back/Next save `in-progress`; a new session resumes its current-version step. +Skip/Close save `skipped`, suppressing repeats this session. The skip confirmation +separately explains permanent dismissal. Completed/dismissed decisions are not +re-prompted, including after a version bump. Replay always begins at the welcome +panel and leaves the entire saved automatic decision unchanged. Sign-out or flag +off closes the shell and clears only memory. Storage corruption, future schemas, +read/write failures and a failed/timed-out history request fail open to normal use. + +## Validation and handover + +See [test traceability and QA](validation.md), the [portable browser fixture](browser-qa/README.md) +the [targeted full-application acceptance and recording](full-app-qa/README.md), +and the [threat model](threat-model.md). +The original welcome/profile component and its tests remain in place. The API +change PR-TUT-17 supplies a default-false environment switch; no tutorial progress +endpoint, database migration or deployment change is required by this frontend. +Automatic eligibility does require the new bounded own-user `/projects/history` +read endpoint, which includes withdrawn history; the ordinary projects list cannot +prove its absence. + +Status: implemented for PR review, with release/pilot decisions pending. Existing +TUT-UX01 prototype documents explicitly record pending human approvals. Do not +report those as completed, and do not enable the tutorial for a cohort solely +because the automated checks pass. TUT-U01-RUN needs real representative reviewers; +TUT-U01-FIXES depends on actual findings. No participant feedback, recordings, +second-contributor review or approval has been invented here. TUT-D04's final +walkthrough and manual acceptance evidence remain review handover items. diff --git a/docs/student-onboarding/threat-model.md b/docs/student-onboarding/threat-model.md new file mode 100644 index 0000000000..71d5eb14e2 --- /dev/null +++ b/docs/student-onboarding/threat-model.md @@ -0,0 +1,58 @@ +# Tutorial threat model and privacy boundary + +TUT-S01. Review candidate; approval is recorded by human reviewers on the PR. + +```mermaid +flowchart LR + A[Authenticated user and protected settings] --> B[Student role and enabled flag] + B --> C[Observe existing incomplete profile] + C --> D[GET own projects/history: hasProjects boolean] + D --> E[Empty history only: remember new candidate] + E --> F[Wait for complete profile and globals] + F --> G[Tutorial shell and trusted step registry] + G --> H[Per-account browser progress] + H --> G + I[Account menu replay] --> G +``` + +The API/session boundary authorizes `/projects/history` using `current_user`. +The tutorial supplies no owner or query parameter. The endpoint returns only +`{hasProjects:boolean}`, including withdrawn and inactive history. No project +summary, assessment data or history result is stored in progress. The ordinary +`/projects` endpoint excludes withdrawn enrolments and is unsuitable for this gate. The browser boundary is untrusted: progress +can be read or edited by scripts on the same origin or people sharing a browser +profile. Tutorial state grants no access or authority over units or assessment. + +| Data | Purpose | Storage | +| --- | --- | --- | +| Authenticated account ID | Isolate normal account progress | Browser key namespace only; no names, emails or tokens | +| Role, authentication readiness | Exclude staff and anonymous users | Existing account memory only | +| `hasRunFirstTimeSetup` | Observe existing profile boundary | Read only; tutorial never updates it | +| `tutorialEnabled` | Institution off switch | Existing authenticated settings subject; false after sign-out/failure | +| Own project existence | No-prior-units gate | Boolean response only; no project details returned or history retained | +| Tutorial version, state, stable step | Resume, prompt suppression | Validated three-field browser record | +| DOM target position | Show current control | Transient component memory; no DOM text copied | + +No detailed marks, submitted grades, feedback, extension/disability information, +assessment content, click analytics, learning history or tutorial telemetry are +stored or logged. There are no new third-party scripts, packages or remote HTML. +Copy and targets are reviewed source constants rendered with Angular text binding. + +| Threat | Control and test evidence | Residual risk | +| --- | --- | --- | +| Cross-user state mutation / IDOR | No owner argument on `/projects/history`; account-derived storage key; account/flag generation checks discard stale history results; service isolation tests | Same-origin code and shared browser profiles can inspect non-sensitive local state. Server progress would require a new authorization design. | +| Broken/forged progress | Exact key set, bounded JSON length, integer supported version, allowlisted state/step; malformed/future records rejected; tests cover unknown and extra fields | Browser state is a usability preference, not proof of identity or authorization. | +| Profile corruption | Service has no profile update path and never writes setup fields; existing profile/welcome tests and no-action regression | Future refactors must preserve the independent setup meaning. | +| Repeated interruption | Only confirmed new candidates; once per session; skip/dismiss/complete/replay rules; bounded history timeout and no retry loop | Clearing browser storage loses progress; new browser/device is replay-only once profile setup is complete. | +| Stale target / unsafe action | Stable attributes, visible-target resolution, missing-target text, no auto clicks or route changes; all four targets tested | Manual responsive/browser review is still required when shared controls move. | +| Overlay blocks application | Modal entry/confirmation only; guided steps non-modal; Close/Escape, focus return, missing-data fallbacks | Screen reader, real browser zoom and full app route QA require explicit evidence. | +| Dependency / telemetry leak | Reuse Angular Material/CDK already in the lockfile; no additional dependency or analytics call | Existing dependency advisories are owned by the security/migration workstreams. | + +A future tour package must have an explicit licence, maintenance, telemetry, +accessibility, bundle-size and supply-chain review. A future progress API requires +own-user authorization, unauthenticated/invalid/cross-user tests and a reviewed +storage justification; none is needed for this browser-only implementation. + +Security tests are in `student-onboarding.service.spec.ts`, +`student-onboarding.component.spec.ts` and `doubtfire-constants.spec.ts`. These are +controls and evidence, not a claim that a security reviewer has approved rollout. diff --git a/docs/student-onboarding/validation.md b/docs/student-onboarding/validation.md new file mode 100644 index 0000000000..ba225c0f9b --- /dev/null +++ b/docs/student-onboarding/validation.md @@ -0,0 +1,125 @@ +# Tutorial validation and evidence + +TUT-T01 / TUT-Q01 / TUT-MVP01. Feature branch base: web `11.0.x` +`d16f6201caff78082e5e83c540320dbee5871404`, then rebased onto `a35f2826d`. The implementation is proposed in [web PR #263](https://github.com/ontrack-features-t2-2026/doubtfire-web/pull/263); +[guide PR #12](https://github.com/ontrack-features-t2-2026/github-guide/pull/12) records the coordinated rules and handover. Do not interpret a workbook checklist as evidence of execution. + +## Automated traceability + +| Requirement / ticket | Evidence | +| --- | --- | +| TUT-W01 shell and controls | Component tests render welcome, step, confirmation, completion, Back/Next/Skip/Dismiss/Close/Finish and accessible names. | +| TUT-W02 new/returning/staff eligibility | Service tests require incomplete profile plus explicit false own-history boolean; suppress true/unknown/error history (API includes withdrawn/inactive enrolments); wait readiness and safe route. | +| TUT-W02 persistence / privacy | Service tests exercise resume, skip, dismiss, completion, replay preservation, account replacement, malformed/future records, blocked reads/writes and stale history. | +| TUT-W03 real targets / fallback | Component/target tests resolve all four attribute contracts; ignore hidden, inert, detached and zero-size targets; show written fallback. Header regression tests compile the shared controls. | +| TUT-W03 no automatic actions | Service state snapshots and route spy; component locate action scrolls without clicking; no write API exists in tutorial source. | +| TUT-W04 replay | Service tests start completed/dismissed users from the beginning and preserve storage; header menu supplies one entry. | +| TUT-S01 off switch | Constants regression accepts explicit true only; missing setting and sign-out reset false; service rejects disabled/unauthenticated/staff launch. | +| Existing profile setup | Run unchanged WelcomeComponent and EditProfileFormComponent specifications alongside tutorial tests. | +| TUT-Q01 accessibility | Component tests verify heading focus, labels, polite step announcements, modal/non-modal distinction, Escape dispatch and control order. Real assistive-technology testing remains separate below. | + +## Reproducible commands + +Use the Node version in `.nvmrc` or a compatible Node 22 runtime and dependencies +from `npm ci --legacy-peer-deps`. Run: + +```sh +npm run typecheck +npm run lint +npm run build -- --configuration production +npm run test:ci -- --include='src/app/student-onboarding/*.spec.ts' --include='src/app/config/constants/doubtfire-constants.spec.ts' --include='src/app/welcome/welcome.component.spec.ts' --include='src/app/common/edit-profile-form/edit-profile-form.component.spec.ts' --include='src/app/common/header/header.component.spec.ts' +``` + +On a busy host, Vitest may need one worker. An external runner config can set +`test: {maxWorkers: 1, fileParallelism: false, pool: 'threads'}` and be passed with +`--runner-config=`. This changes scheduling, not assertions. Browser progress remains local. The API switch and authenticated own-history +endpoint have separate API tests in the companion change. + +Actual command results and limitations are recorded in [verification.txt](verification.txt). + +## Browser fixture and manual QA matrix + +The [portable fixture](browser-qa/README.md) mounts the production tutorial with +its real global theme and synthetic surrounding controls. + +Record browser/version, viewport/zoom, input method, synthetic scenario, result, +evidence and owner for each run. Automated jsdom checks do not constitute browser, +screen-reader or pilot sign-off. + +| Scenario | Required checks | Status | +| --- | --- | --- | +| New student, zero history, after setup | Welcome; all four steps; Back/Next; completion; reload | State/control tests and synthetic browser matrix passed; full-app manual acceptance pending | +| Existing/in-progress/skipped/completed/dismissed | Eligibility; resume; skip later; permanent suppression; manual replay | Unit tests and synthetic browser lifecycle checks passed; real session acceptance pending | +| Student/staff/anonymous, disabled flag | Correct visibility; no interrupted setup/auth | Service tests plus synthetic staff/disabled cases passed; deployed setting verification pending | +| No unit/project, missing/hidden/delayed target | Explanation; Next/Close; normal page remains usable | Target tests passed; synthetic missing-unit/control screenshots captured | +| History timeout/failure, storage failure | Normal use, no retries/loop; replay/warning | Service tests and synthetic browser storage failure/replay checks passed; institution browser-policy QA pending | +| Keyboard only | Heading focus; visible focus; tab order; modal exit; non-modal step navigation; return focus | Component tests plus browser Tab, Escape and narrow-panel keyboard scrolling checked; full-app keyboard acceptance pending | +| Screen reader / accessibility inspector | Named dialogs/controls; step title/count; decorative highlight ignored | Browser accessibility-tree snapshots captured; human assistive-technology run pending | +| Desktop / 390px / 320px / 200% zoom | All controls reachable; no lost target; readable panel; scrolling | Synthetic viewports and 200% CSS zoom checked; native 200% welcome is verified below; remaining panels need native zoom acceptance | +| Reduced motion | No essential animation; explicit locate scroll is instant | Synthetic browser context uses reduced-motion preference and asserts no panel animation | +| Chrome / Edge / Firefox / Safari where available | Same flows and failure states | Individual engine/version results in the linked evidence; WebKit is not a native Safari run | +| Route changes and browser Back | Current target refreshes or falls back; no tutorial navigation side effects | Service tests and synthetic real Angular Router/browser Back checks passed; full-app history acceptance pending | + +The [browser evidence](browser-qa/evidence/README.md) records the exact engine +versions and assertions against production code `0ff843477`. It mounts production +tutorial code, real global theme and real Angular Router events with synthetic +accounts, history responses and surrounding controls. It does not establish full +application routing, API authorization, actual enrolled projects, human screen +reader behavior or native browser zoom. + +## Combined compatibility check + +A temporary local tree combined tutorial `0ff843477` and migration `66e3e2056`. +Its full suite passed **1,158 tests in 151 files** and its default development build passed. +The tree and exact heads are recorded in [combined-validation.json](browser-qa/evidence/combined-validation.json). +This snapshot was neither pushed nor merged on GitHub. The branch combination +checks and test evidence support review; they do not substitute for reviewer approval. + +## Pilot and closure boundaries + +Use the existing [pilot script and de-identified templates](https://github.com/ontrack-features-t2-2026/github-guide/tree/main/docs/evidence/student-onboarding-pilot). +TUT-U01-RUN requires at least three representative people, including a novice. +TUT-U01-FIXES requires actual triaged findings before a fix can be attributed to the +pilot. No names, assessments or invented observations belong in evidence. + +Human prototype/security approvals, second-contributor review, independent review of the versioned fallback +recording, full-application/native-browser-zoom/assistive-technology QA and pilot sessions remain pending. +This handover completes the repository implementation and reproducible test assets; +it does not claim those external review activities happened or that PRs merged. + +## Remaining workbook ticket mapping + +| Ticket | Repository deliverable / remaining boundary | +| --- | --- | +| TUT-W01 | Reusable standalone shell, typed registry, explicit targets, focus and fallback behavior implemented. | +| TUT-W02 | Auth/profile/history gate, browser state, resume, skip, completion, dismissal, version handling and failure behavior implemented. | +| TUT-W03 | Four existing controls annotated; exact DOC-11 copy and safe fallbacks implemented; no automatic actions. | +| TUT-W04 | One role/flag-gated account-menu replay entry and written guide link implemented. | +| TUT-T01 | 104 unique focused tests, unchanged welcome/profile regressions, full lint/typecheck/build, combined 1,158-test compatibility run and command evidence supplied. | +| TUT-S01 | Data flow, fields, trust boundaries, dependency decision and security tests supplied; human approval pending. | +| TUT-Q01 | Automated semantics/state/route checks, reproducible browser fixture, engine evidence and manual matrix supplied; synthetic evidence is supplemental to full-app and assistive-technology QA. | +| TUT-D04 | Implementation locations, maintenance/version rules, troubleshooting and handover supplied; central guides updated in github-guide; second human review/video pending. | +| TUT-MVP01 | Evidence index and honest handover supplied; PR review, pilot and full release validation pending, no merge claimed. | +| PR-TUT-17 | Separate API default-false runtime flag and bounded own-history endpoint change; this frontend consumes the authenticated response and resets on sign-out. | +| TUT-U01-RUN | Existing pilot script/templates linked; real representative participants cannot be fabricated by repository work. | +| TUT-U01-FIXES | Requires actual pilot findings; no invented critical/high issues, participant findings or approvals. | + +## Full application follow-up + +The [full-app acceptance package](full-app-qa/evidence/README.md) adds actual +synthetic sign-in/profile completion followed by the automatic welcome, a native +Chrome 200% welcome capture, and the enrolled-student four-step/completion/replay +flow against real API data in Chrome, Edge and Firefox. A 29-second annotated +fallback recording is supplied, pending a second human review. + +This uses the pinned local combined **default/development** artifact, whose exact +tree can be reconstructed from published parents as documented. It does not claim +a deployed production acceptance run. The earlier combined build label was corrected +from production to default/development after inspecting the original command log. +The separate original tutorial production build result remains unchanged. + +The [follow-up limits and exact next steps](full-app-qa/evidence/README.md#exact-limits-and-remaining-acceptance) +identify integrated state/failure/native keyboard/zoom checks still outstanding, +the native Mac-lock interruption, unsupported native Safari, and the human reviews +and representative-user pilot. Previously passed fixture and automated state tests +remain distinct evidence. diff --git a/docs/task-sheet-widget-feedback.md b/docs/task-sheet-widget-feedback.md new file mode 100644 index 0000000000..a427d5791a --- /dev/null +++ b/docs/task-sheet-widget-feedback.md @@ -0,0 +1,190 @@ +# PPI-D03: Proof of Concept, Feedback and Design Evaluation + +**Ticket:** PPI-D03 - Gather feedback and document the task-sheet PPI design decisions +**Branch:** `docs/ppi-task-sheet-widget-feedback` +**Group:** Feature/Peer Progress Indicator +**Resource:** `PPI-task-sheet-widget-v1.png` +**Resource Attribution:** Eloise Ridder-Strickland + +![Original task-sheet Peer Progress Indicator design showing the proposed indicator options and placement concepts](images/PPI-task-sheet-widget-v1.png) + +**Purpose** +> To identify design recommendations for the development of the Peer Progress Indicator widget, in line with user, stakeholder and project scope/expectations and accessibility, usability and style guidelines. + +### Implementation status + +*Proof of concept.* This document does not claim that a live unit-level component or API is implemented or already exists. The proposed widget could show an authorised, anonymous cohort aggregate. Live accuracy, refresh timing and the required unit-level data source remain future implementation and validation work. + +### Privacy and evidence handling + +Reviewer evidence is de-identified. Reviewer names are omitted unless consent to publish them is recorded. The evidence does not include student IDs, marks, student assessment feedback, progress records, credentials, secrets or real student data. + +### Acceptance Criteria Coverage + +| | Requirement | Result | Evidence | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------------------------------------------------------------------------- | +| `1.` | The original ticket, mock-up and author are clearly identified. | *Met* | Header and Section 7. | +| `2.` | At least two people review the design. A third reviewer is encouraged but not required. | *Met* | Two formal reviewers represented by general roles in Section 2. | +| `3.` | One reviewer represents implementation or technical knowledge and one represents likely users, tutors, UX or documentation. | *Met* | Section 2. | +| `4.` | The same core questions are used for each review. | *Met* | Sections 1.2 and 2.1; both formal reviewers answered the same follow-up question. | +| `5.` | The Markdown file clearly separates reviewer feedback from Eloise's conclusions. | *Met* | Sections 2 and 3. | +| `6.` | The evidence does not expose student IDs, marks, feedback, progress records or other unnecessary personal information. | *Met* | Privacy statement and de-identified roles in Section 2. | +| `7.` | The review covers clarity, placement, usefulness, anonymity, accessibility, possible stress or competition, and the user preference. | *Met* | Sections 3 to 5. | +| `8.` | Each suggested change is marked as accepted, deferred or not adopted, with a short reason. | *Met* | Section 3. | +| `9.` | Any revised design is optional and does not block completion. | *Met* | Section 6. | +| `10.` | If a revision is made, the original image remains in the repository so the design history is preserved. | *Met* | `docs/images/PPI-task-sheet-widget-v1.png`. | +| `11.` | The documentation does not claim that the design has been implemented or connected to live data. | *Met* | Implementation status and Sections 4 to 6. | +| `12.` | The pull request links the original Planner ticket, this follow-up ticket and the supporting evidence. | *Met* | PR description and Section 7. | + +## 1. Methodology + +Two formal reviewers evaluated the same mock-up using the same survey question set, including the same follow-up improvement question. Earlier project discussions and general Teams comments were retained as supplementary discovery evidence and were not counted as formal reviews. + +### 1.1 Evidence sources + +1. **Supplementary discovery discussion** before the mock-up was developed, involving project leadership and mentors: [Ontrack - Sprint Planning (Fortnightly)](https://deakin365.sharepoint.com/:v:/r/sites/ThothTech2/Shared%20Documents/Product%20-%20OnTrack/Recordings/Ontrack%20-%20Sprint%20Planning%20\(Fortnightly\)-20260720_180013-Meeting%20Recording.mp4?d=w0b96f239856c4927b309d448a06b7e8c&csf=1&web=1&e=e0uYqS&nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJTdHJlYW1XZWJBcHAiLCJyZWZlcnJhbFZpZXciOiJTaGFyZURpYWxvZy1MaW5rIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXcifX0%3D) +2. **Supplementary open group discussion** after the initial mock-up was shared in Teams. + +![Redacted Teams feedback discussing the PPI options, placement and local visibility control](images/teams-review-doc.png) + +3. **Formal survey review** completed by two reviewers using the same evaluation questions: [Peer Progress Indicator Mock-up Evaluation](https://docs.google.com/forms/d/e/1FAIpQLSeXGCQzNpgdOlEv686l91L1ko58_SnHXD6obsdpx---WXpTzA/viewform?usp=dialog) + +### 1.2 Evaluation questions + +#### Feedback profile + +1. Which feature group are you a part of? +2. Do you have previous UI or general design experience? (Yes/No) + +#### Context and design + +3. Without context, what do you think the Peer Progress Indicator primarily shows? +4. Which indicator design option is more useful or comfortable to view, and why? +5. ~~Which placement option is more useful or comfortable to view, and why?~~ - *Removed as redundant with Question 6.* +6. Which placement is easiest to notice and understand without cluttering the task sheet? + +#### Accessibility and privacy + +7. Is it clear that this is an anonymous cohort value rather than a ranking? +8. Is the local show/hide option clear for users who do not want to view peer progress? +9. If this feature were available, would you choose to show or hide it by default? +10. What is the main reason for that preference? + +#### General considerations + +11. Are there accessibility concerns that are not addressed in the mock-up? +12. Is a local On/Off control appropriate, and could you suggest a better location or label? +13. What is the single most useful improvement you would make? - *Completed as the same follow-up question for both formal reviewers.* + +## 2. Aggregated Feedback Summary + +The following are de-identified extracts from the two formal survey reviews and supplementary team communications about *PPI-task-sheet-widget-v1.png*. + +The formal reviewer roles were: + +- A **technical/design reviewer** with relevant implementation or design knowledge. +- A **likely end-user reviewer** representing student usability needs. + +Earlier PPI contributor and team discussions were retained as supplementary discovery evidence and were not counted as additional formal reviews. + +![Survey respondent distribution showing the two formal reviewers across the recorded design-experience categories](images/survey-distribution-chart.png) + +### 2.1 Common improvement question + +Both formal reviewers were asked the same missed question as a follow-up. + +- **Technical/design reviewer:** The single most useful improvement I would make is to keep the richer Option B concept, but introduce it gradually rather than showing every task status immediately. I would start with a simple anonymous comparison, keep the Mockup 02 placement, and include a clear show/hide preference. Once the data and behaviour are proven to be accurate and useful, the additional status breakdowns could be added later. +- **Likely end-user reviewer:** The single most useful improvement for me would be giving students clear control over whether the indicator is shown. I like the Mockup 02 placement because it is visible without getting in the way, but I would keep the wording neutral and make it easy to hide so students who find peer comparison distracting or stressful do not have to see it. + +| Strength | Concerns | Suggestion | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| Option B - it gives a clearer outline. | Option B - gives a lot more information but could scare people who aren't ready to see a lot of information. | Option A - with a toggle for Option B called 'Advanced'. for people who would like to see an advanced view (Option B). | +| Option B - the data is helpful for me. | Option B - could cause issues for subjects that have tutors that mark stuff really quickly and others that don't so it might cause undue stress on students. | An overall dark mode would be very interesting / helpful | +| Placement 1 - the information is where you want it when scanning over a page. | 'InProgress' aren't used by many students so it's not useful data. | Having the Opt-In/Opt-out in the same area as the 'placement' would likely be easier for most/all students as it's simple to find. | +| Placement 1 - does not over cutter the reading space. | Option B - I don't think it would be practical to share that much information with students. | As far as placement goes I think 1 or 3. | +| Opt-in, because it is useful and clear. | Example Toggle - The wording is wrong, we won't have an opt-out for statistics. | Option B - looks good but I think that's more of a stretch goal. | +| Opt-in, I'd would like see the data & would want others to benefit off it as well. | The indicator itself might give some students anxiety because they feel behind. | | +| It is a good feature and is appropriate. | | | +| I'm a big fan of Option B, Mock-up 2 placement and the toggle. | | | +| If people don't like it they can just turn it off. | | | + +## 3. Decision Table + +Each item is marked as **Accepted**, **Deferred**, or **Not adopted**, with a short reason. Wellbeing concerns are treated as real design risks rather than dismissed because a visibility control exists. + +| Status | Reviewer feedback | Design conclusion and reason | +| --- | --- | --- | +| **Accepted** | *Placement 1 - the information is where you want it when scanning over a page.* | Use Placement 1 as the baseline because it is central, easy to notice and contextually relevant. | +| **Accepted** | *Placement 1 - does not overly clutter the reading space.* | Keep the first view compact so the submission workflow remains the main focus. | +| **Accepted** | *If people do not like it they can turn it off.* | Provide a clear local show/hide control beside the widget. This reduces exposure but does not remove the underlying wellbeing risk. | +| **Accepted** | *Option B gives a clearer outline.* | Use this to inform a possible future advanced view while keeping Option A as the simpler baseline. | +| **Accepted** | *'InProgress' is not used by many students, so it is not useful data.* | Exclude this metric from the first proof of concept because reviewers did not identify enough value to justify the extra detail. | +| **Accepted** | *Place the visibility control in the same area as the widget.* | A local control is easier to find and clearly communicates that it hides only the visual widget. | +| **Accepted** | *The example toggle wording suggests an opt-out from statistics.* | Correct the interface wording. Users may show or hide the widget locally; this does not imply opting out of authorised anonymous aggregation. | +| **Accepted as risk** | *Option B could scare people who are not ready to see that much information.* | Valid wellbeing and cognitive-load risk. Start with Option A, avoid ranking language and validate the default visibility before implementation. | +| **Accepted as risk** | *Different tutor marking speeds could create undue stress.* | Valid interpretation risk. Any future implementation needs neutral wording, explanation of data limits and testing across different unit workflows. | +| **Accepted as risk** | *The indicator might make some students anxious because they feel behind.* | Valid wellbeing risk. A show/hide control reduces but does not remove it; the feature needs safe defaults and user testing. | +| **Deferred** | *Add an Advanced option that expands Option A into Option B.* | Useful future extension, but outside the first proof-of-concept scope. | +| **Deferred** | *Add an overall dark mode.* | Potentially useful but outside this feature's scope. Existing OnTrack accessibility and theme work should be considered separately. | +| **Deferred** | *Option B looks good but is a stretch goal.* | Retain as a future option after the simple baseline and data contract are validated. | +| **Not adopted** | *It is a good feature and is appropriate.* | Positive but too general to create a separate design action. | +| **Not adopted** | *Option B data is helpful to me.* | Recorded as useful feedback, but one personal preference does not justify making the detailed view the default. | +| **Not adopted** | *I would show it because it is useful and clear.* | Supports perceived usefulness but does not create a separate design change. | +| **Not adopted** | *Option B is not practical to share with students.* | Addressed through the simpler Option A baseline and the deferred advanced view rather than rejecting the whole concept. | +| **Not adopted** | *Placement 1 or 3.* | Too broad to act on directly; the more specific placement feedback supports Placement 1. | +| **Not adopted** | *I would show it and want others to benefit from it.* | Recorded as a preference, but it does not determine the default for all students. | + +## 4. Impact Consideration Summary + +| Area | Functional considerations | Non-functional considerations | +| --- | --- | --- | +| **Clarity** | Option A provides a simple baseline. Option B adds a detailed stage breakdown and may be considered later. | Reduce visual clutter, use neutral wording and clearly explain that the value is an anonymous cohort aggregate rather than a ranking. | +| **Placement** | Consider Placement 1 as the initial design, with the local show/hide control beside the widget. | Keep the primary task submission workflow central and place the indicator near related information. | +| **Usefulness** | The proposed widget could show an authorised, anonymous aggregate comparison. Live accuracy, refresh timing and the required data source still need implementation and validation. | The feature may support pacing and reassurance, but it must not imply that cohort progress determines an individual student's success. | +| **Anonymity** | Use aggregated percentage metrics and prevent access to individual student records. | Protect psychological safety and test authorisation so users cannot infer or access another student's progress. | +| **Accessibility** | Provide accessible text equivalents, screen-reader labels and keyboard operation for controls and tooltips. | Do not rely on colour alone. Use sufficient contrast, patterns or labels, and test the design with accessibility tooling and users where possible. | +| **Wellbeing** | Give users a clear local show/hide control and avoid ranking or competitive language. | Seeing peer completion rates may create stress, competition or a false sense of security. The control reduces but does not remove these risks. | +| **User preference** | Allow students to show or hide the widget locally without implying that they can opt out of authorised anonymous aggregation. | Use a considered default, preserve user agency and minimise sensitive information shown outside the main OnTrack context. | + +## 5. Summary of Findings + +Based on the available feedback, the first proof-of-concept direction should include: + +- **Placement 1:** It places the information where students naturally scan while keeping it near the relevant task context. +- **Option A as the baseline:** Option B received positive feedback, but Option A is a more realistic first step. A detailed advanced view can remain future work. +- **Local show/hide control:** Place the control on or beside the widget. Make it clear that this controls local visibility only. +- **Neutral and accessible wording:** Explain that the value is anonymous and is not a ranking. Avoid competitive language and do not expose individual student information. +- **Focused metrics:** Omit `InProgress` from the first proof of concept because the reviewers did not identify enough value to include it. +- **Wellbeing safeguards:** Treat stress, anxiety, tutor marking-speed differences and false reassurance as design risks that require safe defaults and validation. + +Avoid: + +- **Placement 2 as the initial choice:** The specific feedback more strongly supports Placement 1. +- **Global statistical opt-out wording:** The local control hides the widget; it does not imply an opt-out from authorised anonymous aggregation. +- **Mandatory persistent display:** A forced view could negatively affect some students and removes user agency. +- **Claims of live or real-time accuracy:** No live unit-level data source or implemented widget is claimed in this document. + +### Implementation notes + +> **User agency and wellbeing:** Put a binary show/hide control directly on or beside the widget and validate the default before implementation. + +> **Data and security:** Use authorised aggregate metrics, filter out irrelevant statuses, minimise exposed information and test access controls. + +> **Accessibility:** Provide high-contrast visual cues, text equivalents, screen-reader labels, keyboard operation and clear tooltips where applicable. + +> **Validation:** Test comprehension, accessibility, privacy, different tutor marking patterns and possible stress effects before treating the design as production-ready. + +## 6. Handover and Recommendations + +1. **Implement Option A only as a future baseline**, keeping Option B or an Advanced View as a later stretch goal after the data source and user need are validated. +2. **Use correct interface wording:** state that the control shows or hides the visual widget locally rather than suggesting an opt-out from authorised anonymous aggregation. +3. **Prototype Placement 1:** place the compact widget and local control near the task context, then test that it does not interfere with submission work. +4. **Define the data contract and security boundary:** document the authorised aggregate fields, refresh behaviour, cohort rules, suppression thresholds and access-control tests before connecting live data. +5. **Validate accessibility and wellbeing:** test neutral wording, keyboard and screen-reader support, colour independence, default visibility and responses across different marking speeds. +6. **Preserve design history and evidence:** keep the original image, de-identified feedback, decisions, risks and follow-up reviewer answers so future contributors can understand why the design changed. + +## 7. Reference Links +--- +*Links* +- [Planner - PPI-D03 - Gather feedback and document the task-sheet PPI design decisions](https://teams.microsoft.com/l/entity/com.microsoft.teamspace.tab.planner/planner.v1.df787fb0-1a0b-4bb5-9bc9-9b92a428eb13_p_KuipOoIL0E-7orl64gLd0cgAGv3z?tenantId=d02378ec-1688-46d5-8540-1c28b5f470f6&webUrl=https%3A%2F%2Ftasks.teams.microsoft.com%2Fteamsui%2FpersonalApp%2Falltasklists&context=%7B%22subEntityId%22%3A%22%2Fv1%2Fplan%2FKuipOoIL0E-7orl64gLd0cgAGv3z%2Fview%2Fboard%2Ftask%2FU0BJo6N2Mkq_kw3IYFFWuMgAAM_I%22%2C%22channelId%22%3A%2219%3Abd20175d09414f079490a2403f7fca74%40thread.tacv2%22%7D) +- [Planner - PPI - Draft a small task-sheet indicator/widget interactions](https://teams.microsoft.com/l/entity/com.microsoft.teamspace.tab.planner/planner.v1.df787fb0-1a0b-4bb5-9bc9-9b92a428eb13_p_KuipOoIL0E-7orl64gLd0cgAGv3z?tenantId=d02378ec-1688-46d5-8540-1c28b5f470f6&webUrl=https%3A%2F%2Ftasks.teams.microsoft.com%2Fteamsui%2FpersonalApp%2Falltasklists&context=%7B%22subEntityId%22%3A%22%2Fv1%2Fplan%2FKuipOoIL0E-7orl64gLd0cgAGv3z%2Fview%2Fboard%2Ftask%2FYQjP6F8GfkaDmq5IgUryQcgAL9Ay%22%2C%22channelId%22%3A%2219%3Abd20175d09414f079490a2403f7fca74%40thread.tacv2%22%7D) diff --git a/env.config.js b/env.config.js deleted file mode 100644 index 60f507f0f5..0000000000 --- a/env.config.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - env: { - options: { - // Global options - EXTERNAL_NAME: process.env.DF_EXTERNAL_NAME || 'Doubtfire', - }, - - development: { - // Use the computer's IP address -- if using localhost then - // testing on mobile devices will fail as it cannot point to - // localhost (this would be the device itself!) - API_URL: 'http://' + require('ip').address() + ':3000/api', - }, - - production: { - // Provide a set API_URL, otherwise window.location will be used - // to locate the API dynamically (see api-url.coffee) - API_URL: process.env.DF_API_URL, - }, - - docker: { - // API URL should use the DF_DOCKER_MACHINE_IP set - API_URL: 'http://' + process.env.DF_DOCKER_MACHINE_IP + ':3000/api', - }, - - devcontainer: { - // API URL should use the DF_DOCKER_MACHINE_IP set - API_URL: 'http://' + process.env.DF_DOCKER_MACHINE_IP + ':4200/api', - }, - }, -}; diff --git a/karma/karma-unit.tpl.js b/karma/karma-unit.tpl.js deleted file mode 100644 index 5b390cb14f..0000000000 --- a/karma/karma-unit.tpl.js +++ /dev/null @@ -1,60 +0,0 @@ -module.exports = function ( karma ) { - karma.set({ - /** - * From where to look for files, starting with the location of this file. - */ - basePath: '../', - - /** - * This is the list of file patterns to load into the browser during testing. - */ - files: [ - <% scripts.forEach( function ( file ) { %>'<%= file %>', - <% }); %> - 'src/**/*.js', - 'src/**/*.coffee', - ], - - frameworks: [ 'jasmine' ], - plugins: [ 'karma-jasmine', 'karma-firefox-launcher', 'karma-chrome-launcher', 'karma-coffee-preprocessor' ], - preprocessors: { - '**/*.coffee': 'coffee', - }, - - /** - * How to report, by default. - */ - reporters: 'dots', - - /** - * On which port should the browser connect, on which port is the test runner - * operating, and what is the URL path for the browser to use. - */ - port: 9018, - runnerPort: 9100, - urlRoot: '/', - - /** - * Disable file watching by default. - */ - autoWatch: false, - - /** - * The list of browsers to launch to test on. This includes only "Firefox" by - * default, but other browser names include: - * Chrome, ChromeCanary, Firefox, Opera, Safari, PhantomJS - * - * Note that you can also use the executable name of the browser, like "chromium" - * or "firefox", but that these vary based on your operating system. - * - * You may also leave this blank and manually navigate your browser to - * http://localhost:9018/ when you're running tests. The window/tab can be left - * open and the tests will automatically occur there during the build. This has - * the aesthetic advantage of not launching a browser every time you save. - */ - browsers: [ - 'Firefox' - ] - }); -}; - diff --git a/migration_progress.sh b/migration_progress.sh deleted file mode 100755 index da81aabd49..0000000000 --- a/migration_progress.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/zsh - -function count_to_migrate() { grep -Ril --include \*.coffee "angular.module" ./src/app | sed 's/^/- [ ] /'; } -function count_migrated() { grep -Ril --include \*.ts "@Component" ./src/app | sed 's/^/- [x] /';} -function print_progress() { echo $1 / $2 components migrated; } - -echo "\nSUMMARY:\n" -print_progress `count_migrated | wc -l` `count_to_migrate | wc -l` - -echo "\nMIGRATED:\n" -count_migrated - -echo "\nTODO:\n" -count_to_migrate diff --git a/module.prefix b/module.prefix deleted file mode 100644 index c52d9b7229..0000000000 --- a/module.prefix +++ /dev/null @@ -1 +0,0 @@ -(function ( window, angular, undefined ) { diff --git a/module.suffix b/module.suffix deleted file mode 100644 index f6354bac28..0000000000 --- a/module.suffix +++ /dev/null @@ -1 +0,0 @@ -})( window, window.angular ); diff --git a/nginx.conf b/nginx.conf index 0914d0a344..298c817cb1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,18 +1,71 @@ -worker_processes 1; +worker_processes auto; -events { } +events { worker_connections 2048; } http { include /etc/nginx/mime.types; + types { application/manifest+json webmanifest; } + + # The Angular app shell and service-worker control files must always be + # revalidated. `$uri` excludes query parameters, so cache-busting query + # strings cannot bypass this policy. An empty value is not emitted for + # fingerprinted bundles and other static assets. + map $uri $ontrack_control_file_cache_control { + default ""; + /index.html "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + /ngsw.json "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + /ngsw-worker.js "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + /safety-worker.js "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + /worker-basic.min.js "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + /manifest.webmanifest "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + } sendfile on; + tcp_nopush on; + tcp_nodelay on; server { root /usr/share/nginx/html/; index index.html; listen 80; + # src/index.html's inline pre-boot theme script depends on unsafe-inline below. + # Before tightening CSP, allow its exact SHA-256 in script-src or serve a + # per-response nonce so saved dark themes still paint before Angular boots. add_header Content-Security-Policy "default-src https: 'unsafe-inline' 'unsafe-eval' blob: data: ws:; worker-src 'self' blob:; child-src 'self' blob:" always; add_header Permissions-Policy "microphone=(self),fullscreen=(self),payment=()" always; + add_header Cache-Control $ontrack_control_file_cache_control always; + + # Do not serve the SPA fallback under a service-worker or manifest URL. + # A missing control file must be a real 404, otherwise browsers can try + # to execute index.html as JavaScript. Internal SPA fallback redirects + # still resolve through the exact /index.html location below. + location = /index.html { + try_files $uri =404; + } + + location = /ngsw.json { + try_files $uri =404; + } + + location = /ngsw-worker.js { + try_files $uri =404; + } + + location = /safety-worker.js { + try_files $uri =404; + } + + location = /worker-basic.min.js { + try_files $uri =404; + } + + location = /manifest.webmanifest { + try_files $uri =404; + } + + location /assets/ { + try_files $uri =404; + } location / { try_files $uri $uri/ $uri/index.html /index.html; @@ -20,7 +73,7 @@ http { } gzip on; - gzip_types text/css application/javascript; + gzip_types text/css application/javascript application/manifest+json application/json; gzip_proxied any; gzip_buffers 32 8k; diff --git a/package-lock.json b/package-lock.json index d4ffed0764..7af36165d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,10 @@ "@angular/router": "^22.0.3", "@angular/service-worker": "^22.0.3", "@ctrl/ngx-emoji-mart": "^9.3.0", - "@eslint/js": "^10.0.1", "@ngneat/hotkeys": "^4.0.0", "@ngstack/code-editor": "^9.0.0", "@sentry/angular": "^10.61.0", - "@sentry/cli": "^3.5.1", - "@swimlane/ngx-charts": "^20.5.0", + "@swimlane/ngx-charts": "^25.0.2", "@tailwindcss/postcss": "^4.3.2", "@worktile/gantt": "^21.0.0", "angular-calendar": "^0.32.2", @@ -35,7 +33,7 @@ "canvas-confetti": "^1.9", "d3": "^7.9.0", "date-fns": "^4.4.0", - "dompurify": "^3.4.11", + "dompurify": "^3.4.14", "html2canvas": "^1.0.0-rc.7", "html5-qrcode": "^2.3.8", "jszip": "^3.10.1", @@ -43,8 +41,7 @@ "marked": "^18.0.5", "moment": "^2.30", "monaco-editor": "^0.55.1", - "ng-flex-layout": "^17.3.7-beta.1", - "ng2-pdf-viewer": "10.2.2", + "ng2-pdf-viewer": "10.4.0", "ngx-entity-service": "^0.0.44", "ngx-lottie": "^22.0.0", "ngx-monaco-editor-v2-alternative": "^22.0.0", @@ -52,7 +49,6 @@ "qrcode": "^1.5.4", "rxjs": "~7.8.2", "tslib": "^2.8.1", - "typescript-eslint": "^8.62.0", "zone.js": "~0.16.2" }, "devDependencies": { @@ -61,12 +57,14 @@ "@angular-eslint/eslint-plugin-template": "^22.0.0", "@angular-eslint/schematics": "^22.0.0", "@angular-eslint/template-parser": "^22.0.0", - "@angular/build": "^22.0.4", - "@angular/cli": "^22.0.4", + "@angular/build": "^22.0.9", + "@angular/cli": "^22.0.9", "@angular/compiler-cli": "^22.0.3", "@angular/language-service": "^22.0.3", "@commitlint/cli": "^21.1.0", "@commitlint/config-conventional": "^21.1.0", + "@eslint/js": "^10.0.1", + "@sentry/cli": "^3.5.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/canvas-confetti": "^1.6.0", "@types/d3": "^7.4.3", @@ -74,24 +72,26 @@ "@types/node": "^26.0.1", "@typescript-eslint/eslint-plugin": "^8.60.1", "@typescript-eslint/parser": "^8.62.1", + "@vitest/coverage-v8": "^4.1.9", "angular-eslint": "^22.0.0", "autoprefixer": "^10.5.2", - "concurrently": "^10.0.3", + "axe-core": "^4.13.0", + "concurrently": "^10.0.5", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-tailwindcss": "^4.0.4", "husky": "^9.1.7", - "ip": "^2.0.1", "jsdom": "^29.1.1", "npm-run-all2": "^9.0.2", - "postcss": "^8.5.16", + "postcss": "^8.5.26", "postcss-scss": "^4.0.9", "prettier": "^3.8.4", "sass": "^1.101.0", "tailwindcss": "^4.3.1", "ts-node": "~10.9", "typescript": "~6.0.3", + "typescript-eslint": "^8.62.0", "vitest": "^4.1.9" }, "engines": { @@ -343,13 +343,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2200.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.4.tgz", - "integrity": "sha512-X/iEQiZ0pRmpjUt11jCM+mtOLRl4XxI9hnM0IC9aAcsm5AzRBb9WY6QIEqOSficjxC/+MI7MGwrerrcP6QN8UA==", + "version": "0.2200.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.9.tgz", + "integrity": "sha512-kwiyEFfJsnJ4o7ondW8KswwJbp+qri5yWLS0wMMG+uBwOolZfrQReorqsQNCALhwmojhDNZc0wEWXEHEhKnotw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "22.0.4", + "@angular-devkit/core": "22.0.9", "rxjs": "7.8.2" }, "bin": { @@ -362,9 +362,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "22.0.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.4.tgz", - "integrity": "sha512-zA2UJSMAU3su5uJTOn5ul/gCLRcw6/uIQ6EC5v/Ju/ePjgDIw9R3y3MAvWQ4Ibi/fXiq0FVxpF8hE7RUclYmJA==", + "version": "22.0.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.9.tgz", + "integrity": "sha512-u53kltrh1ZKHSqGfEDbPW2RzSC1WBPpan3LN61TuzjAkiHqqaQXrN6EmUvYkX3CIT5PKhO6RPRsptMH9y7hs0w==", "dev": true, "license": "MIT", "dependencies": { @@ -390,13 +390,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "22.0.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.4.tgz", - "integrity": "sha512-VRxL1hD/Q3TQglM6EfQ0ksAW4OIvtKyZgtaUpyGsJRfD6tGmLFn7MDnmyyq1ceLX/Clq+3tzH/wN0tF6rcE0jA==", + "version": "22.0.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.9.tgz", + "integrity": "sha512-r7tPuiXI7pnwcaLQY4hQLOM9R5rkKRc1mKhQQb074Shh8XzTcTaw2ktDXLeiJm/SUUWQPhaxClnjjO3qU8AJRw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "22.0.4", + "@angular-devkit/core": "22.0.9", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.4.0", @@ -547,16 +547,16 @@ } }, "node_modules/@angular/build": { - "version": "22.0.4", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.4.tgz", - "integrity": "sha512-hst/KhP5mMPajY32l7qmyW/5fuqrMHCjHEeNhMMNqP82+j8jPBFfMEL26AH9w2kIIdKGpC3WKN5JgLotyFD7tQ==", + "version": "22.0.9", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.9.tgz", + "integrity": "sha512-j5eYaMTGFXdPVSX0xtWRRWHE2Ezz2CFiBPsAKrWHitvzccv95nURQYk5jbB+FHCYuN60zJNoTZMIlT7rXYAvag==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2200.4", - "@babel/core": "7.29.0", - "@babel/helper-annotate-as-pure": "7.27.3", + "@angular-devkit/architect": "0.2200.9", + "@babel/core": "7.29.7", + "@babel/helper-annotate-as-pure": "7.29.7", "@babel/helper-split-export-declaration": "7.24.7", "@inquirer/confirm": "6.0.12", "@vitejs/plugin-basic-ssl": "2.3.0", @@ -576,7 +576,7 @@ "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.16", - "vite": "7.3.5", + "vite": "7.3.6", "watchpack": "2.5.1" }, "engines": { @@ -595,7 +595,7 @@ "@angular/platform-browser": "^22.0.0", "@angular/platform-server": "^22.0.0", "@angular/service-worker": "^22.0.0", - "@angular/ssr": "^22.0.4", + "@angular/ssr": "^22.0.9", "istanbul-lib-instrument": "^6.0.0", "karma": "^6.4.0", "less": "^4.2.0", @@ -1602,19 +1602,19 @@ } }, "node_modules/@angular/cli": { - "version": "22.0.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.4.tgz", - "integrity": "sha512-3eJy6VoNlgskKFzvqy3AJsYXFRhSBLLGObF2iTpJymsukuxWUen7hlVVVWrO5++bW1LEgd6PTCCD5fdT2UuRiA==", + "version": "22.0.9", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.9.tgz", + "integrity": "sha512-nkdz9sc9OjTLBG2QwPH8HuFc4fOl489AzrKInM2y+Q0u1+/HFt9QpSeupPnDJeOYGImDIP0B2VJJ3teDRM1DGg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2200.4", - "@angular-devkit/core": "22.0.4", - "@angular-devkit/schematics": "22.0.4", + "@angular-devkit/architect": "0.2200.9", + "@angular-devkit/core": "22.0.9", + "@angular-devkit/schematics": "22.0.9", "@inquirer/prompts": "8.4.2", "@listr2/prompt-adapter-inquirer": "4.2.3", "@modelcontextprotocol/sdk": "1.29.0", - "@schematics/angular": "22.0.4", + "@schematics/angular": "22.0.9", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.52.0", "ini": "6.0.0", @@ -1697,54 +1697,6 @@ } } }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@angular/core": { "version": "22.0.3", "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.3.tgz", @@ -1986,21 +1938,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -2051,13 +2003,13 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2253,6 +2205,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -2728,9 +2690,9 @@ "peer": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -2745,9 +2707,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -2762,9 +2724,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -2779,9 +2741,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -2796,9 +2758,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -2813,9 +2775,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -2830,9 +2792,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -2847,9 +2809,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -2864,9 +2826,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -2881,9 +2843,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -2898,9 +2860,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -2915,9 +2877,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -2932,9 +2894,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -2949,9 +2911,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -2966,9 +2928,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -2983,9 +2945,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -3000,9 +2962,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -3017,9 +2979,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -3034,9 +2996,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -3051,9 +3013,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -3068,9 +3030,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -3085,9 +3047,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -3102,9 +3064,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -3119,9 +3081,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -3136,9 +3098,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -3153,9 +3115,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -3173,6 +3135,7 @@ "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -3191,6 +3154,7 @@ "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -3200,6 +3164,7 @@ "version": "0.23.5", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^3.0.5", @@ -3214,27 +3179,30 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -3250,6 +3218,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^1.2.1" @@ -3262,6 +3231,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -3274,6 +3244,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -3294,6 +3265,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -3303,6 +3275,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^1.2.1", @@ -3349,9 +3322,9 @@ "optional": true }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", "dev": true, "license": "MIT", "engines": { @@ -3365,6 +3338,7 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/types": "^0.15.0" @@ -3377,6 +3351,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.2", @@ -3391,6 +3366,7 @@ "version": "0.15.0", "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -3400,6 +3376,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -3413,6 +3390,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3939,215 +3917,6 @@ "win32" ] }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC", - "optional": true - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, "node_modules/@mattlewis92/dom-autoscroller": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@mattlewis92/dom-autoscroller/-/dom-autoscroller-2.4.2.tgz", @@ -4802,16 +4571,16 @@ } }, "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@npmcli/package-json/node_modules/glob": { @@ -5666,14 +5435,14 @@ "license": "Apache-2.0" }, "node_modules/@schematics/angular": { - "version": "22.0.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.4.tgz", - "integrity": "sha512-P3V3tkqIR+n0GJSv0ibf34/zMtKbFp6kaTjBe5cm/RyXuHbmdaPYjk8PNphkGMypDtWCof1RtNnW/hl832Wnew==", + "version": "22.0.9", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.9.tgz", + "integrity": "sha512-moQI3UnqUt6GZ81ykxJRUbf+IpM6SDFRyMMj6A2VNSiUGVpSAETNFIuTk/6YDzNG8yhMN9zOICeBwvvPH0ylbA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "22.0.4", - "@angular-devkit/schematics": "22.0.4", + "@angular-devkit/core": "22.0.9", + "@angular-devkit/schematics": "22.0.9", "jsonc-parser": "3.3.1", "typescript": "6.0.3" }, @@ -5735,6 +5504,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.5.1.tgz", "integrity": "sha512-h710aEXT8At4lg7GbXDZkatDDq0uA5QKZmSiF/8Hy6jJZ/9dh6EBDZZpTYnDpNrwRvE8tqhnwEjTZDlHjOhPNQ==", + "dev": true, "hasInstallScript": true, "license": "FSL-1.1-MIT", "dependencies": { @@ -5764,6 +5534,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.5.1.tgz", "integrity": "sha512-GxHAtZaXRA650egcepQXU0pR0dZ8ZNvQS8eq3wBxhCK8os5VQpLyBABIaN6AdrE/b8M7UvqGjsuVm0giI1GZ7w==", + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5780,6 +5551,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5798,6 +5570,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5817,6 +5590,7 @@ "x86", "ia32" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5835,6 +5609,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5853,6 +5628,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5870,6 +5646,7 @@ "x86", "ia32" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -5886,6 +5663,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -6058,36 +5836,36 @@ "license": "MIT" }, "node_modules/@swimlane/ngx-charts": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz", - "integrity": "sha512-PNBIHdu/R3ceD7jnw1uCBVOj4k3T6IxfdW6xsDsglGkZyoWMEEq4tLoEurjLEKzmDtRv9c35kVNOXy0lkOuXeA==", + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-25.0.2.tgz", + "integrity": "sha512-twXOkh3pdahSB4v5Wzorbmrh7FhoZy9GyK5wsxFsOxMNQ+VNQ70wx5VzxV9+C+eMpR7ky0urix7ls3GXA1frlA==", "license": "MIT", "dependencies": { - "d3-array": "^3.1.1", + "d3-array": "^3.2.0", "d3-brush": "^3.0.0", "d3-color": "^3.1.0", "d3-ease": "^3.0.1", "d3-format": "^3.1.0", - "d3-hierarchy": "^3.1.0", + "d3-hierarchy": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-shape": "^3.2.0", - "d3-time-format": "^3.0.0", + "d3-time-format": "^4.1.0", "d3-transition": "^3.0.1", - "rfdc": "^1.3.0", - "tslib": "^2.0.0" + "gradient-path": "^2.3.0", + "tslib": "^2.3.1" }, "peerDependencies": { - "@angular/animations": ">=12.0.0", - "@angular/cdk": ">=12.0.0", - "@angular/common": ">=12.0.0", - "@angular/core": ">=12.0.0", - "@angular/forms": ">=12.0.0", - "@angular/platform-browser": ">=12.0.0", - "@angular/platform-browser-dynamic": ">=12.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "@angular/animations": "^21.2.0 || ^22.0.0", + "@angular/cdk": "^21.2.0 || ^22.0.0", + "@angular/common": "^21.2.0 || ^22.0.0", + "@angular/core": "^21.2.0 || ^22.0.0", + "@angular/forms": "^21.2.0 || ^22.0.0", + "@angular/platform-browser": "^21.2.0 || ^22.0.0", + "@angular/platform-browser-dynamic": "^21.2.0 || ^22.0.0", + "rxjs": "7.x" } }, "node_modules/@tailwindcss/node": { @@ -6428,34 +6206,6 @@ "tailwindcss": "4.3.2" } }, - "node_modules/@tailwindcss/postcss/node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/@trivago/prettier-plugin-sort-imports": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.2.tgz", @@ -6560,16 +6310,16 @@ } }, "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@tufjs/models/node_modules/minimatch": { @@ -6911,12 +6661,14 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, "node_modules/@types/geojson": { @@ -6930,6 +6682,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -6942,6 +6695,12 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -6953,6 +6712,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", @@ -6981,6 +6741,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/scope-manager": "8.62.1", @@ -7005,6 +6766,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.1", @@ -7026,6 +6788,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.62.1", @@ -7043,6 +6806,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7059,6 +6823,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7072,6 +6837,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/project-service": "8.62.1", @@ -7099,6 +6865,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.62.1", @@ -7116,27 +6883,30 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -7149,6 +6919,7 @@ "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -7164,6 +6935,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.0", @@ -7185,6 +6957,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.62.0", @@ -7202,6 +6975,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7218,6 +6992,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.62.0", @@ -7242,6 +7017,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7255,6 +7031,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/project-service": "8.62.0", @@ -7282,27 +7059,30 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -7318,6 +7098,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", @@ -7341,6 +7122,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.62.0", @@ -7358,6 +7140,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -7379,6 +7162,37 @@ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", @@ -7551,6 +7365,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7563,6 +7378,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -7786,43 +7602,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -7857,6 +7636,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.5.2", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", @@ -7894,6 +7692,16 @@ "postcss": "^8.1.0" } }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -7908,7 +7716,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { @@ -7919,10 +7727,31 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/baseline-browser-mapping": { - "version": "2.10.38", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7963,6 +7792,33 @@ "require-from-string": "^2.0.2" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -8010,9 +7866,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -8020,9 +7876,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -8040,11 +7896,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -8053,6 +7909,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8103,16 +7984,16 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/cacache/node_modules/glob": { @@ -8232,9 +8113,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -8252,6 +8133,21 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/canvas-confetti": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", @@ -8262,6 +8158,13 @@ "url": "https://www.paypal.me/kirilvatev" } }, + "node_modules/canvas/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -8445,16 +8348,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -8475,23 +8368,16 @@ "dot-prop": "^5.1.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true - }, "node_modules/concurrently": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", - "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", + "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==", "dev": true, "license": "MIT", "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", - "shell-quote": "1.8.4", + "shell-quote": "1.9.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" @@ -8514,13 +8400,6 @@ "dev": true, "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -8695,6 +8574,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9169,38 +9049,17 @@ } }, "node_modules/d3-time-format": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", - "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-time": "1 - 2" - } - }, - "node_modules/d3-time-format/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-time-format/node_modules/d3-time": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", - "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", - "license": "BSD-3-Clause", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "d3-array": "2" + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/d3-time-format/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -9245,18 +9104,6 @@ "node": ">=12" } }, - "node_modules/d3/node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -9285,6 +9132,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -9314,12 +9162,39 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "license": "MIT" - }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delaunator": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", @@ -9329,13 +9204,6 @@ "robust-predicates": "^3.0.2" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9416,9 +9284,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", - "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz", + "integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -9475,9 +9343,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.378", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", - "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", "dev": true, "license": "ISC" }, @@ -9497,6 +9365,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.23.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", @@ -9609,9 +9487,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9622,32 +9500,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -9671,6 +9549,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, "license": "MIT", "workspaces": [ "packages/*" @@ -9801,6 +9680,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@types/esrecurse": "^4.3.1", @@ -9819,6 +9699,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -9831,6 +9712,7 @@ "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -9847,27 +9729,30 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9880,6 +9765,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -9892,6 +9778,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -9901,12 +9788,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -9922,6 +9811,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.16.0", @@ -9939,6 +9829,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -9951,6 +9842,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -9963,6 +9855,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -9975,6 +9868,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -9994,6 +9888,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -10032,6 +9927,16 @@ "node": ">=18.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -10123,6 +10028,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -10136,12 +10042,14 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, "license": "MIT" }, "node_modules/fast-string-truncated-width": { @@ -10162,9 +10070,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "dev": true, "funding": [ { @@ -10192,6 +10100,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -10209,6 +10118,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -10243,6 +10153,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -10259,6 +10170,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -10272,6 +10184,7 @@ "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, "license": "ISC" }, "node_modules/forwarded": { @@ -10308,6 +10221,13 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -10321,13 +10241,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -10353,83 +10266,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10518,32 +10354,18 @@ "node": ">=18" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -10559,30 +10381,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/global-directory": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", @@ -10618,6 +10416,25 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gradient-path": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gradient-path/-/gradient-path-2.3.0.tgz", + "integrity": "sha512-vZdF/Z0EpqUztzWXFjFC16lqcialHacYoRonslk/bC6CuujkuIrqx7etlzdYHY4SnUU94LRWESamZKfkGh7yYQ==", + "license": "MIT", + "dependencies": { + "tinygradient": "^1.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -10631,13 +10448,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -10652,9 +10462,9 @@ } }, "node_modules/hono": { - "version": "4.12.26", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", - "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", "dev": true, "license": "MIT", "engines": { @@ -10697,6 +10507,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html2canvas": { "version": "1.0.0-rc.7", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", @@ -10847,10 +10664,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -10880,16 +10719,16 @@ } }, "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/ignore-walk/node_modules/minimatch": { @@ -10915,9 +10754,9 @@ "license": "MIT" }, "node_modules/immutable": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", - "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", "dev": true, "license": "MIT" }, @@ -10952,23 +10791,12 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -10994,17 +10822,10 @@ "node": ">=12" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true, - "license": "MIT" - }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "dev": true, "license": "MIT", "engines": { @@ -11032,6 +10853,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11057,6 +10879,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -11138,8 +10961,61 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -11151,7 +11027,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -11175,9 +11051,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -11262,9 +11138,9 @@ } }, "node_modules/jsdom/node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -11288,6 +11164,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -11318,6 +11195,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -11366,6 +11244,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -11375,6 +11254,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -11755,6 +11635,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -11908,6 +11789,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -12053,6 +11962,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -12069,6 +11991,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -12202,6 +12134,13 @@ "node": ">= 18" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -12253,15 +12192,6 @@ "marked": "14.0.0" } }, - "node_modules/monaco-editor/node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/monaco-editor/node_modules/marked": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", @@ -12288,6 +12218,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/msgpackr": { @@ -12334,17 +12265,10 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/nan": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", - "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", - "license": "MIT", - "optional": true - }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -12359,10 +12283,18 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -12375,30 +12307,14 @@ "node": ">= 0.6" } }, - "node_modules/ng-flex-layout": { - "version": "17.3.7-beta.1", - "resolved": "https://registry.npmjs.org/ng-flex-layout/-/ng-flex-layout-17.3.7-beta.1.tgz", - "integrity": "sha512-MTjlQUldB/hEsn0DY/RoY2SKBQoyaKltlOOFjCjH2OfcYA3COHhkJS3PZe9NAjR7TBDb9Ve989m18bSucRzACQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/cdk": ">=17.0.0", - "@angular/common": ">=17.0.0", - "@angular/core": ">=17.0.0", - "@angular/platform-browser": ">=17.0.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/ng2-pdf-viewer": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.2.2.tgz", - "integrity": "sha512-GaKAvF0nXAiR9U4LFWuT54MM9nzp0ie8GGscp34W+lFsSOXdlwS0iFx5UPuVlODRm3YEUKx6xcK5oaJeBq0SAw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.4.0.tgz", + "integrity": "sha512-TPh1oLZoeARggreTG60Sl3ikSn+Z3+At9pLZ0o/vxPjc7mW2ok2XPyl2Oqz7VyP80ipVorldm1hsLPBmNe2zzA==", "license": "MIT", "dependencies": { - "pdfjs-dist": "^3.11.174", - "tslib": "^2.3.0" + "pdfjs-dist": "4.8.69", + "tslib": "^2.8.1" } }, "node_modules/ngx-entity-service": { @@ -12454,60 +12370,27 @@ "@angular/core": ">=19.0.0" } }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", "license": "MIT", "optional": true, "dependencies": { - "whatwg-url": "^5.0.0" + "semver": "^7.3.5" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-gyp": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", @@ -12576,9 +12459,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", - "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, "license": "MIT", "engines": { @@ -12769,20 +12652,6 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -12800,7 +12669,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12876,6 +12745,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -12924,6 +12794,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -12939,6 +12810,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -13150,20 +13022,11 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13207,14 +13070,14 @@ "url": "https://opencollective.com/express" } }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", - "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "node_modules/path2d": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz", + "integrity": "sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==", "license": "MIT", "optional": true, "engines": { - "node": ">=8" + "node": ">=6" } }, "node_modules/pathe": { @@ -13225,70 +13088,16 @@ "license": "MIT" }, "node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "version": "4.8.69", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.8.69.tgz", + "integrity": "sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==", "license": "Apache-2.0", "engines": { "node": ">=18" }, "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, - "node_modules/pdfjs-dist/node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pdfjs-dist/node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pdfjs-dist/node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pdfjs-dist/node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "canvas": "^3.0.0-rc2", + "path2d": "^0.2.1" } }, "node_modules/picocolors": { @@ -13301,6 +13110,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -13373,10 +13183,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", - "dev": true, + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -13393,7 +13202,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -13509,10 +13318,39 @@ "dev": true, "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -13567,6 +13405,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -13590,8 +13429,20 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -13816,6 +13667,39 @@ "node": ">= 0.10" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-package-json-fast": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", @@ -13942,25 +13826,9 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -14122,6 +13990,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -14200,6 +14069,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14212,15 +14082,16 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, "license": "MIT", "engines": { @@ -14365,6 +14236,32 @@ "license": "MIT", "optional": true }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -14690,9 +14587,9 @@ } }, "node_modules/tar": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", - "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -14706,6 +14603,58 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -14723,6 +14672,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -14737,6 +14692,7 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -14749,6 +14705,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", + "license": "MIT", + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", @@ -14826,6 +14792,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.12" @@ -14906,10 +14873,24 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -14955,6 +14936,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14968,6 +14950,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", @@ -14991,6 +14974,7 @@ "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", @@ -15019,9 +15003,10 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", - "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.17" @@ -15045,9 +15030,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { @@ -15079,6 +15064,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -15088,6 +15074,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -15127,13 +15114,13 @@ } }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -15365,6 +15352,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -15399,68 +15387,11 @@ "node": ">=8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wide-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15665,6 +15596,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 7013576f44..3ec720d60e 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,36 @@ { "name": "doubtfire", "version": "11.0.0-45", - "homepage": "http://github.com/doubtfire-lms/", + "homepage": "https://github.com/ontrack-features-t2-2026/doubtfire-web", "description": "Learning and feedback tool.", "license": "AGPL-3.0", - "repository": {}, + "repository": { + "type": "git", + "url": "https://github.com/ontrack-features-t2-2026/doubtfire-web.git" + }, "engines": { "node": ">=22.22.3" }, "scripts": { "build": "ng build", - "build:angular18": "ng build", "lint:fix": "ng lint --fix", "lint": "ng lint --max-warnings 0", - "serve:angular18": "export NG_FORCE_TTY=false && export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --poll=2000 --configuration $NODE_ENV --proxy-config proxy.conf.json", "format": "prettier --write .", - "start": "npm-run-all serve:angular18", + "start": "npm-run-all serve", "deploy:build2api": "ng build --delete-output-path=true --optimization=true --configuration production --output-path dist", "deploy:build2api:sourcemaps": "ng build --delete-output-path=true --optimization=true --configuration production --source-map=true --output-path dist", "sentry:sourcemaps": "node scripts/upload-sentry-sourcemaps.js", + "verify:deployment-config": "node scripts/verify-deployment-config.js", "deploy": "run-s -l deploy:build2api", "prepare": "husky install", "test": "ng test", "test:ci": "ng test --no-watch --no-progress", - "typecheck": "ngc -p src/tsconfig.app.json --noEmit" + "test:a11y": "ng test --no-watch --no-progress --include=\"**/*.accessibility.spec.ts\" --include=\"**/testing/accessibility.spec.ts\"", + "typecheck": "ngc -p src/tsconfig.app.json --noEmit", + "serve": "export NG_FORCE_TTY=false && export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --poll=2000 --configuration $NODE_ENV --proxy-config proxy.conf.json", + "verify:pwa": "node scripts/verify-pwa.js", + "verify:pwa:build": "node scripts/verify-pwa.js --dist dist/browser", + "test:coverage": "ng test --no-watch --no-progress --coverage" }, "keywords": [], "author": "", @@ -41,12 +48,10 @@ "@angular/router": "^22.0.3", "@angular/service-worker": "^22.0.3", "@ctrl/ngx-emoji-mart": "^9.3.0", - "@eslint/js": "^10.0.1", "@ngneat/hotkeys": "^4.0.0", "@ngstack/code-editor": "^9.0.0", "@sentry/angular": "^10.61.0", - "@sentry/cli": "^3.5.1", - "@swimlane/ngx-charts": "^20.5.0", + "@swimlane/ngx-charts": "^25.0.2", "@tailwindcss/postcss": "^4.3.2", "@worktile/gantt": "^21.0.0", "angular-calendar": "^0.32.2", @@ -54,7 +59,7 @@ "canvas-confetti": "^1.9", "d3": "^7.9.0", "date-fns": "^4.4.0", - "dompurify": "^3.4.11", + "dompurify": "^3.4.14", "html2canvas": "^1.0.0-rc.7", "html5-qrcode": "^2.3.8", "jszip": "^3.10.1", @@ -62,8 +67,7 @@ "marked": "^18.0.5", "moment": "^2.30", "monaco-editor": "^0.55.1", - "ng-flex-layout": "^17.3.7-beta.1", - "ng2-pdf-viewer": "10.2.2", + "ng2-pdf-viewer": "10.4.0", "ngx-entity-service": "^0.0.44", "ngx-lottie": "^22.0.0", "ngx-monaco-editor-v2-alternative": "^22.0.0", @@ -71,7 +75,6 @@ "qrcode": "^1.5.4", "rxjs": "~7.8.2", "tslib": "^2.8.1", - "typescript-eslint": "^8.62.0", "zone.js": "~0.16.2" }, "devDependencies": { @@ -80,12 +83,14 @@ "@angular-eslint/eslint-plugin-template": "^22.0.0", "@angular-eslint/schematics": "^22.0.0", "@angular-eslint/template-parser": "^22.0.0", - "@angular/build": "^22.0.4", - "@angular/cli": "^22.0.4", + "@angular/build": "^22.0.9", + "@angular/cli": "^22.0.9", "@angular/compiler-cli": "^22.0.3", "@angular/language-service": "^22.0.3", "@commitlint/cli": "^21.1.0", "@commitlint/config-conventional": "^21.1.0", + "@eslint/js": "^10.0.1", + "@sentry/cli": "^3.5.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/canvas-confetti": "^1.6.0", "@types/d3": "^7.4.3", @@ -93,24 +98,26 @@ "@types/node": "^26.0.1", "@typescript-eslint/eslint-plugin": "^8.60.1", "@typescript-eslint/parser": "^8.62.1", + "@vitest/coverage-v8": "^4.1.9", "angular-eslint": "^22.0.0", "autoprefixer": "^10.5.2", - "concurrently": "^10.0.3", + "axe-core": "^4.13.0", + "concurrently": "^10.0.5", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-tailwindcss": "^4.0.4", "husky": "^9.1.7", - "ip": "^2.0.1", "jsdom": "^29.1.1", "npm-run-all2": "^9.0.2", - "postcss": "^8.5.16", + "postcss": "^8.5.26", "postcss-scss": "^4.0.9", "prettier": "^3.8.4", "sass": "^1.101.0", "tailwindcss": "^4.3.1", "ts-node": "~10.9", "typescript": "~6.0.3", + "typescript-eslint": "^8.62.0", "vitest": "^4.1.9" }, "optionalDependencies": { @@ -121,5 +128,10 @@ "@nx/nx-win32-x64-msvc": "^23.0.1", "@rollup/rollup-linux-arm64-gnu": "^4.62.2", "@rollup/rollup-linux-x64-gnu": "^4.62.2" + }, + "overrides": { + "monaco-editor": { + "dompurify": "3.4.14" + } } } diff --git a/scripts/test-nginx.py b/scripts/test-nginx.py new file mode 100644 index 0000000000..7b20a2c90b --- /dev/null +++ b/scripts/test-nginx.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Exercise the production Nginx configuration with disposable static fixtures. + +Run with `python3 scripts/test-nginx.py`. Docker is required; no app build, +database, external port, or production credentials are used. +""" + +import gzip +from http.client import HTTPException +import json +from pathlib import Path +import re +import subprocess +import tempfile +import time +import unittest +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + + +ROOT = Path(__file__).resolve().parent.parent + + +def docker(*arguments): + return subprocess.check_output(["docker", *arguments], text=True).strip() + + +class NginxTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.container = None + cls.fixture = tempfile.TemporaryDirectory(prefix="ontrack-nginx-") + cls.addClassCleanup(cls.fixture.cleanup) + cls.files = Path(cls.fixture.name) + # Linux bind mounts retain mkdtemp's 0700 mode. Nginx workers must be + # able to traverse this directory of public, synthetic static fixtures. + cls.files.chmod(0o755) + (cls.files / "assets").mkdir() + (cls.files / "index.html").write_text("OnTrack test shell") + cls.manifest = (ROOT / "src/manifest.webmanifest").read_bytes() + (cls.files / "manifest.webmanifest").write_bytes(cls.manifest) + (cls.files / "assets/test.js").write_text("/*" + "asset fixture " * 100 + "*/") + (cls.files / "assets/test.json").write_text(json.dumps({"message": "fixture " * 100})) + image = re.search(r"^FROM (nginx:\S+)$", (ROOT / "deploy.Dockerfile").read_text(), re.M)[1] + cls.container = docker( + "run", "--rm", "--detach", "--publish", "127.0.0.1::80", + "--mount", f"type=bind,src={ROOT / 'nginx.conf'},dst=/etc/nginx/nginx.conf,readonly", + "--mount", f"type=bind,src={cls.files},dst=/usr/share/nginx/html,readonly", + image, + ) + cls.addClassCleanup(lambda: docker("rm", "--force", cls.container)) + docker("exec", cls.container, "nginx", "-t") + cls.base = "http://" + docker("port", cls.container, "80/tcp") + for _ in range(50): + try: + cls.request("/") + break + except (URLError, ConnectionError, TimeoutError, HTTPException): + # Docker can publish the port before the Nginx worker is ready, + # including a reset after TCP connects but before HTTP headers. + time.sleep(0.1) + else: + raise RuntimeError("Nginx did not become ready") + + @classmethod + def request(cls, path, **headers): + try: + response = urlopen(Request(cls.base + path, headers=headers), timeout=5) + except HTTPError as error: + response = error + with response: + return response.status, response.headers, response.read() + + def test_manifest_type_and_compression_preserve_other_mime_types(self): + status, headers, body = self.request("/manifest.webmanifest", **{"Accept-Encoding": "gzip"}) + self.assertEqual(200, status) + self.assertEqual("application/manifest+json", headers.get_content_type()) + self.assertEqual("gzip", headers.get("Content-Encoding")) + self.assertEqual(self.manifest, gzip.decompress(body)) + for path, expected_type in [("/assets/test.js", "application/javascript"), + ("/assets/test.json", "application/json")]: + with self.subTest(path=path): + status, headers, body = self.request(path, **{"Accept-Encoding": "gzip"}) + self.assertEqual(200, status) + self.assertEqual(expected_type, headers.get_content_type()) + self.assertEqual("gzip", headers.get("Content-Encoding")) + self.assertEqual((self.files / path.lstrip("/")).read_bytes(), gzip.decompress(body)) + + def test_missing_assets_and_control_files_return_real_404s(self): + for path in ["/assets/missing.js", "/assets/nested/missing.css", "/ngsw.json", + "/ngsw-worker.js", "/safety-worker.js", "/worker-basic.min.js"]: + with self.subTest(path=path): + status, headers, body = self.request(path) + self.assertEqual(404, status) + self.assertNotIn(b"OnTrack test shell", body) + self.assertIn("default-src", headers["Content-Security-Policy"]) + manifest = self.files / "manifest.webmanifest" + manifest.unlink() + try: + self.assertEqual(404, self.request("/manifest.webmanifest")[0]) + finally: + manifest.write_bytes(self.manifest) + + def test_deep_routes_keep_the_uncached_application_shell(self): + for path in ["/", "/projects/42/tasks/13", "/projects/42/tasks/13?refresh=1"]: + with self.subTest(path=path): + status, headers, body = self.request(path) + self.assertEqual(200, status) + self.assertIn(b"OnTrack test shell", body) + self.assertIn("no-store", headers["Cache-Control"]) + + def test_auto_workers_use_available_processors(self): + processes = docker("exec", self.container, "ps", "-o", "args") + workers = sum(line.startswith("nginx: worker process") for line in processes.splitlines()) + processors = int(docker("exec", self.container, "getconf", "_NPROCESSORS_ONLN")) + self.assertEqual(processors, workers) + print(f"Nginx started {workers} workers for {processors} available processors") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/verify-pwa.js b/scripts/verify-pwa.js new file mode 100644 index 0000000000..314a5fa955 --- /dev/null +++ b/scripts/verify-pwa.js @@ -0,0 +1,124 @@ +// Verify source assets, or the deployable Angular output with --dist . +// Browser installation and institutional SSO still require the documented smoke test. +const assert = require('node:assert/strict'); +const {createHash} = require('node:crypto'); +const {readFileSync} = require('node:fs'); +const {resolve} = require('node:path'); + +const root = resolve(__dirname, '..'); +const args = process.argv.slice(2); +assert( + args.length === 0 || (args.length === 2 && args[0] === '--dist'), + 'Usage: node scripts/verify-pwa.js [--dist dist/browser]', +); +const built = args.length > 0; +const assets = built ? resolve(root, args[1]) : resolve(root, 'src'); +const read = (path) => readFileSync(resolve(assets, path.replace(/^\//, ''))); +const manifest = JSON.parse(read('manifest.webmanifest')); +const origin = 'https://ontrack.example'; +const localPath = (value) => { + const url = new URL(value, origin); + assert.equal(url.origin, origin, `PWA URL must be same-origin: ${value}`); + assert(!url.search && !url.hash, `PWA URL must not contain credentials or tracking: ${value}`); + return url.pathname; +}; + +// Existing installations used start_url as their implicit id. Preserve both. +assert.equal(manifest.id, '/index.html', 'Preserve the identity of existing installations'); +assert.equal(manifest.start_url, '/index.html'); +assert.equal(manifest.scope, '/'); +assert.equal(manifest.display, 'standalone'); +assert.equal(manifest.prefer_related_applications, false); +assert(manifest.name && manifest.short_name && manifest.description); +assert.match(read('index.html').toString(), /href="\/manifest.webmanifest"\s+rel="manifest"/); + +for (const size of [192, 512]) { + assert( + manifest.icons.some( + (icon) => + icon.sizes === `${size}x${size}` && + (!icon.purpose || icon.purpose.split(' ').includes('any')), + ), + `Missing general-purpose ${size}px app icon`, + ); +} +assert(manifest.icons.some((icon) => icon.purpose?.split(' ').includes('maskable'))); +for (const icon of manifest.icons) { + assert.equal(icon.type, 'image/png'); + const bytes = read(localPath(icon.src)); + assert.equal(bytes.subarray(0, 8).toString('hex'), '89504e470d0a1a0a', `${icon.src} must be PNG`); + assert.equal(bytes.toString('ascii', 12, 16), 'IHDR'); + assert.equal(`${bytes.readUInt32BE(16)}x${bytes.readUInt32BE(20)}`, icon.sizes); +} +assert.deepEqual( + manifest.shortcuts.map((entry) => localPath(entry.url)), + ['/home', '/notifications', '/unit-hub'], +); +const routes = readFileSync(resolve(root, 'src/app/app.routes.ts'), 'utf8'); +for (const shortcut of manifest.shortcuts) { + assert(shortcut.name); + assert( + routes.includes(`path: '${localPath(shortcut.url).slice(1)}'`), + `Shortcut has no Angular route: ${shortcut.url}`, + ); +} + +const swConfig = JSON.parse(readFileSync(resolve(root, 'ngsw-config.json'), 'utf8')); +assert(swConfig.navigationUrls.includes('!/api')); +assert(swConfig.navigationUrls.includes('!/api/**')); +assert( + swConfig.assetGroups.some( + (group) => + group.installMode === 'prefetch' && group.resources.files?.includes('/manifest.webmanifest'), + ), +); +const angular = JSON.parse(readFileSync(resolve(root, 'angular.json'), 'utf8')); +assert.equal( + angular.projects.doubtfire.architect.build.configurations.production.serviceWorker, + 'ngsw-config.json', +); + +if (built) { + const sw = JSON.parse(read('ngsw.json')); + assert.equal(sw.index, '/index.html'); + assert(read('ngsw-worker.js').length > 1000, 'Angular worker must be present'); + const files = new Set([ + '/index.html', + '/manifest.webmanifest', + ...manifest.icons.map((icon) => localPath(icon.src)), + ...sw.assetGroups + .filter((group) => group.installMode === 'prefetch') + .flatMap((group) => group.urls), + ]); + for (const file of files) { + assert.equal( + createHash('sha1').update(read(file)).digest('hex'), + sw.hashTable[file], + `Missing or stale service-worker hash for ${file}`, + ); + } + const routesToCheck = [ + ['/home', true], + ['/notifications', true], + ['/unit-hub', true], + ['/api', false], + ['/api/d2l/callback', false], + ['/api/projects/123', false], + ['/sidekiq', false], + ['/JPlag/report', false], + ]; + for (const [path, expected] of routesToCheck) { + const positive = sw.navigationUrls.some( + (entry) => entry.positive && new RegExp(entry.regex).test(path), + ); + const excluded = sw.navigationUrls.some( + (entry) => !entry.positive && new RegExp(entry.regex).test(path), + ); + assert.equal( + positive && !excluded, + expected, + `Incorrect service-worker navigation fallback: ${path}`, + ); + } +} +console.log(`PWA ${built ? 'production build' : 'source'} verification passed.`); diff --git a/setup.sh b/setup.sh deleted file mode 100755 index 6ddabb0b7e..0000000000 --- a/setup.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -#!/bin/bash - -# -# Detects if system is macOS -# -is_mac() { - if [[ `uname` == "Darwin" ]]; then - return 0 - else - return 1 - fi -} - -# -# Detects if system is Linux -# -is_linux() { - if [[ `uname` == "Linux" ]]; then - return 0 - else - return 1 - fi -} - -# -# Detects if environment is ZSH -# -is_zsh() { - if [ -n "$ZSH_VERSION" ]; then - return 0 - elif [ -n "$BASH_VERSION" ]; then - return 1 - fi -} - -# -# Resets the color -# -msg_reset () { - RESET='\033[0m' - printf "${RESET}\n" -} - -# -# Log an error -# -error () { - RED_FORE='\033[0;31m' - printf "${RED_FORE}ERROR: $1" - msg_reset -} - -# -# Log verbose message -# -verbose () { - CYAN_FORE='\033[0;36m' - printf "${CYAN_FORE}INFO: $1" - msg_reset -} - -# -# Log message -# -msg () { - printf "$1\n" -} - -install_web() { - msg "Install Web Interface..." - curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - - sudo apt-get install -y nodejs - gem install sass - rbenv rehash - npm install -} - -install_web -msg "You should now be able to launch the doubtfire web interface using 'npm start'" - diff --git a/src/environments/environment.prod.spec.ts b/src/environments/environment.prod.spec.ts new file mode 100644 index 0000000000..5b0dc75d17 --- /dev/null +++ b/src/environments/environment.prod.spec.ts @@ -0,0 +1,12 @@ +import {describe, expect, it} from 'vitest'; +import {environment} from './environment.prod'; + +describe('production environment', () => { + it('fails closed for demos and unresolved optional telemetry settings', () => { + expect(environment.production).toBe(true); + expect(environment.enableDemoTools).toBe(false); + expect(environment.sentryDsn).toBe(''); + expect(environment.sentryRelease).toBe(''); + expect(environment.sentryDist).toBe(''); + }); +}); diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index d225c3761d..21dbe97419 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,6 +1,22 @@ +/** + * Docker replaces optional build values before Angular compiles this file. + * A direct production build leaves the `${...}` marker intact; normalising + * unresolved markers to an empty string keeps optional telemetry disabled. + */ +const optionalBuildValue = (value: string): string => + value.startsWith('${') && value.endsWith('}') ? '' : value; + export const environment = { production: true, - sentryDsn: '${SENTRY_DSN}', - sentryRelease: '${SENTRY_RELEASE}', - sentryDist: '${SENTRY_DIST}', + + // Not read in production: `production: true` already enables the service + // worker. Present so both environment files have the same shape. + enableServiceWorker: false, + + // Demo mode must never be enabled by a production build. + enableDemoTools: false, + + sentryDsn: optionalBuildValue('${SENTRY_DSN}'), + sentryRelease: optionalBuildValue('${SENTRY_RELEASE}'), + sentryDist: optionalBuildValue('${SENTRY_DIST}'), }; diff --git a/src/vitest-setup.ts b/src/vitest-setup.ts index 9ee7140bc8..e2b9a2ab21 100644 --- a/src/vitest-setup.ts +++ b/src/vitest-setup.ts @@ -1 +1,10 @@ +import {vi} from 'vitest'; import 'zone.js/plugins/vitest-patch'; + +// The full Angular suite compiles and instantiates many large TestBed modules in +// parallel. Give those hooks enough headroom under CI load while keeping a +// finite timeout so genuine hangs still fail the run. +vi.setConfig({ + hookTimeout: 30_000, + testTimeout: 30_000, +}); From 48a9c3fce02e3ae3ef4b97710eb0231207a3551f Mon Sep 17 00:00:00 2001 From: Clupai8o0 Date: Mon, 28 Sep 2026 00:28:56 +1000 Subject: [PATCH 2/2] feat(platform): bring in org web PR 281 for the platform files Brings ontrack-features-t2-2026/doubtfire-web#281 to the files this PR already carries, so every file stays in exactly one PR. It carries the test setup that gives the suite a working localStorage on newer Node. --- src/vitest-setup.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/vitest-setup.ts b/src/vitest-setup.ts index e2b9a2ab21..93e87c3ab3 100644 --- a/src/vitest-setup.ts +++ b/src/vitest-setup.ts @@ -8,3 +8,33 @@ vi.setConfig({ hookTimeout: 30_000, testTimeout: 30_000, }); + +// Node 22.4 added its own localStorage, gated behind --localstorage-file, so on +// Node 24 and up globalThis.localStorage exists as a getter that returns +// undefined. Vitest's jsdom environment skips any key already present on the +// global, so jsdom's real localStorage is never copied across and every spec +// that touches it fails on the first line. sessionStorage is unaffected, which +// is what makes the failure look like a jsdom bug rather than a Node one. +// +// CI is on Node 22 without the flag, so it never sees this. Give local runs a +// working store so the suite means the same thing on both. +if (typeof globalThis.localStorage === 'undefined') { + const store: Map = new Map(); + + const storage: Storage = { + get length() { + return store.size; + }, + clear: () => store.clear(), + getItem: (key) => (store.has(String(key)) ? store.get(String(key))! : null), + key: (index) => Array.from(store.keys())[index] ?? null, + removeItem: (key) => void store.delete(String(key)), + setItem: (key, value) => void store.set(String(key), String(value)), + }; + + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: storage, + writable: true, + }); +}