6 Commits
Author SHA1 Message Date
tothbt 9739ea55c6 docs: initialize NVM once per shell session 2026-11-02 20:55:23 +01:00
tothbt 99cbbd7a07 chore: release 2.1.0
Build and Release Court Pilot / draft (push) Canceled after 0s
Build and Release Court Pilot / windows (push) Canceled after 0s
Build and Release Court Pilot / macos (aarch64-apple-darwin) (push) Canceled after 0s
Build and Release Court Pilot / macos (x86_64-apple-darwin) (push) Canceled after 0s
Build and Release Court Pilot / snap (push) Canceled after 0s
Build and Release Court Pilot / publish (push) Canceled after 0s
Build and Release Court Pilot / github-release (push) Canceled after 0s
2026-11-02 19:31:59 +01:00
tothbt 7c5384621a feat: add waiting-list off-duty and return quick buttons 2026-11-02 18:01:13 +01:00
tothbt 848e12e2d6 fix: configure test typings and root database linting 2026-11-02 18:00:01 +01:00
tothbt 2f7f5ba5af ci: publish release packages to GitHub Releases 2026-11-02 17:30:32 +01:00
tothbt e7d9fe0efb fix: align Tauri development server with configured port 2026-10-24 10:53:08 +02:00
28 changed files with 654 additions and 252 deletions

No files matched your search

+50
View File
@@ -66,6 +66,15 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm run tauri build run: npm run tauri build
- name: Upload Windows packages as GitHub artifact
uses: actions/upload-artifact@v4
with:
name: court-pilot-windows
if-no-files-found: error
path: |
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
- name: Upload release assets to CrabNebula - name: Upload release assets to CrabNebula
uses: crabnebula-dev/cloud-release@v0 uses: crabnebula-dev/cloud-release@v0
with: with:
@@ -116,6 +125,13 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm run tauri build -- --target ${{ matrix.target }} --bundles app,dmg run: npm run tauri build -- --target ${{ matrix.target }} --bundles app,dmg
- name: Upload macOS packages as GitHub artifact
uses: actions/upload-artifact@v4
with:
name: court-pilot-macos-${{ matrix.target }}
if-no-files-found: error
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
- name: Upload release assets to CrabNebula - name: Upload release assets to CrabNebula
uses: crabnebula-dev/cloud-release@v0 uses: crabnebula-dev/cloud-release@v0
with: with:
@@ -228,3 +244,37 @@ jobs:
api-key: ${{ secrets.CN_API_KEY }} api-key: ${{ secrets.CN_API_KEY }}
path: ./src-tauri path: ./src-tauri
working-directory: ./src-tauri working-directory: ./src-tauri
# ---------------------------------------------------------
# Publish GitHub release after CrabNebula publication succeeds
# ---------------------------------------------------------
github-release:
needs: publish
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Download release packages
uses: actions/download-artifact@v4
with:
pattern: court-pilot-*
path: release-assets
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.ref_name }}
shell: bash
run: |
mapfile -d '' assets < <(find release-assets -type f -print0)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release packages found" >&2
exit 1
fi
gh release create "$RELEASE_TAG" "${assets[@]}" \
--verify-tag \
--title "$RELEASE_TAG" \
--generate-notes
+2 -2
View File
@@ -90,7 +90,7 @@ Do not manually edit generated output or installed dependencies:
- Node.js must use the `lts/jod` NVM alias, as declared in `.nvmrc`. - Node.js must use the `lts/jod` NVM alias, as declared in `.nvmrc`.
- Documented commands target Bash on Linux, macOS, or Windows WSL. Use Bash explicitly for agent commands; fish's `nvm.fish` is unavailable in Bash. - Documented commands target Bash on Linux, macOS, or Windows WSL. Use Bash explicitly for agent commands; fish's `nvm.fish` is unavailable in Bash.
- Before every npm command, explicitly load Bash NVM and select Node in the same shell session: `export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"; . "$NVM_DIR/nvm.sh" && nvm use lts/jod && npm run build`. Do not rely on `.bashrc` loading in noninteractive shells or fall back to system Node. - Initialize Bash NVM once at the start of each shell session: `export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"; . "$NVM_DIR/nvm.sh" && nvm use` from the project root. `nvm use` reads `lts/jod` from `.nvmrc`. Reuse that initialized shell for subsequent npm commands. If a tool starts a fresh shell for each execution, initialize each new shell or use a persistent shell; environment changes do not carry between independent shells. Stop if initialization fails. Do not rely on `.bashrc` loading in noninteractive shells or fall back to system Node.
- If `${NVM_DIR:-$HOME/.nvm}/nvm.sh` is missing, ask the user for permission before installing Bash NVM. Follow `docs/development.md#shell-and-nvm-setup` after approval. Keep existing fish NVM separate. - If `${NVM_DIR:-$HOME/.nvm}/nvm.sh` is missing, ask the user for permission before installing Bash NVM. Follow `docs/development.md#shell-and-nvm-setup` after approval. Keep existing fish NVM separate.
- Install exact dependencies: `npm ci` - Install exact dependencies: `npm ci`
- Start the Ionic development server: `npm start` - Start the Ionic development server: `npm start`
@@ -99,7 +99,7 @@ Do not manually edit generated output or installed dependencies:
- Run linting: `npm run lint` - Run linting: `npm run lint`
- Run Tauri CLI commands: `npm run tauri -- <command>` - Run Tauri CLI commands: `npm run tauri -- <command>`
Tauri development intentionally uses port `8103`. `src-tauri/tauri.conf.json` expects that port, so its `beforeDevCommand` must start Ionic on `8103`; the current `npm start` script does not explicitly select it. Verify the full `npm run tauri dev` flow before claiming it works. Tauri development intentionally uses port `8103`. `src-tauri/tauri.conf.json` expects that port, so its `beforeDevCommand` starts the project's Angular CLI with `serve --host=0.0.0.0 --port=8103`. Keep the command aligned with `devUrl`. Verify the full `npm run tauri dev` flow before claiming it works.
## Release versioning ## Release versioning
+21 -26
View File
@@ -38,49 +38,48 @@ For desktop development, also install:
These commands target **Bash** (Linux, macOS, or Windows WSL). From fish, These commands target **Bash** (Linux, macOS, or Windows WSL). From fish,
start `bash` first. Follow the [shell and NVM setup](docs/development.md#shell-and-nvm-setup) start `bash` first. Follow the [shell and NVM setup](docs/development.md#shell-and-nvm-setup)
before continuing. If Bash NVM is missing, agents must ask permission before before continuing. If Bash NVM is missing, agents must ask permission before
installing it. `.nvmrc` declares `lts/jod`: installing it. `.nvmrc` declares `lts/jod`.
Initialize NVM once at the start of each Bash session, from the project root:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
. "$NVM_DIR/nvm.sh" . "$NVM_DIR/nvm.sh" && nvm use
nvm install lts/jod
nvm use lts/jod && npm ci
``` ```
Each npm command block loads Bash NVM explicitly and selects the required Node On first setup, run `nvm install` after loading NVM if the required Node version
version, including in fresh noninteractive shells. is not installed. Stop if setup fails; do not use the system Node as a fallback.
The commands below reuse this session. Repeat the setup when opening a new shell.
Install the project dependencies:
```bash
npm ci
```
If Ionic CLI is not already installed: If Ionic CLI is not already installed:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm install --global @ionic/cli
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm install --global @ionic/cli
``` ```
## Web development ## Web development
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm start
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm start
``` ```
Build the Angular application: Build the Angular application:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run build
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm run build
``` ```
Run linting and tests: Run linting and tests:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run lint
. "$NVM_DIR/nvm.sh" npm test
nvm use lts/jod && npm run lint
nvm use lts/jod && npm test
``` ```
`npm test` starts Karma in its configured mode and may remain open in watch mode. `npm test` starts Karma in its configured mode and may remain open in watch mode.
@@ -96,18 +95,14 @@ For VS Code, the recommended extension is `esbenp.prettier-vscode`. Use **Format
The Tauri CLI is available through: The Tauri CLI is available through:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run tauri -- <command>
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm run tauri -- <command>
``` ```
For example: For example:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run tauri -- dev
. "$NVM_DIR/nvm.sh" npm run tauri -- build
nvm use lts/jod && npm run tauri -- dev
nvm use lts/jod && npm run tauri -- build
``` ```
> Tauri development intentionally uses `http://localhost:8103`. The current `npm start` script does not explicitly select that port, so verify that Ionic is listening on `8103` when using `tauri dev`. > Tauri development intentionally uses `http://localhost:8103`. The current `npm start` script does not explicitly select that port, so verify that Ionic is listening on `8103` when using `tauri dev`.
+1 -1
View File
@@ -122,7 +122,7 @@
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] "lintFilePatterns": ["src/**/*.ts", "src/**/*.html", "db.ts"]
} }
} }
} }
+20
View File
@@ -1,5 +1,7 @@
import Dexie, { type EntityTable } from 'dexie'; import Dexie, { type EntityTable } from 'dexie';
export type WaitingRole = 'umpire' | 'serviceJudge';
export interface Umpire { export interface Umpire {
id: number; id: number;
firstName: string; firstName: string;
@@ -7,6 +9,7 @@ export interface Umpire {
country: string; country: string;
gender: string; gender: string;
courtNo?: number; courtNo?: number;
returnToQueue?: WaitingRole;
} }
export interface CourtUmpire { export interface CourtUmpire {
@@ -38,6 +41,7 @@ export interface Settings {
withServiceJudge: boolean; withServiceJudge: boolean;
numberOfCourts: number; numberOfCourts: number;
showAlert: boolean; showAlert: boolean;
waitingListQuickButtons?: boolean;
} }
const db = new Dexie('CourtPilot') as Dexie & { const db = new Dexie('CourtPilot') as Dexie & {
@@ -58,4 +62,20 @@ db.version(1).stores({
waitingServiceJudges: '++id, order, serviceJudgeId' waitingServiceJudges: '++id, order, serviceJudgeId'
}); });
db.version(2)
.stores({
umpires: '++id, lastName',
courtUmpires: '++id, courtNo, umpireId',
courtServiceJudges: '++id, courtNo, umpireId',
settings: '++id',
waitingUmpires: '++id, order, umpireId',
waitingServiceJudges: '++id, order, serviceJudgeId'
})
.upgrade(async (transaction) => {
await transaction
.table('settings')
.toCollection()
.modify({ waitingListQuickButtons: false });
});
export { db }; export { db };
+21 -6
View File
@@ -21,11 +21,11 @@ flowchart TD
The root route lazy-loads the tab routes. The three primary routes are: The root route lazy-loads the tab routes. The three primary routes are:
| Route | Source | Responsibility | | Route | Source | Responsibility |
|---|---|---| | ---------------- | --------------- | ---------------------------------------------------------------------- |
| `/tabs/pilot` | `src/app/tab1/` | Waiting queues, court assignments, drag and drop, and rotation actions | | `/tabs/pilot` | `src/app/tab1/` | Waiting queues, court assignments, drag and drop, and rotation actions |
| `/tabs/umpires` | `src/app/tab2/` | Court-official roster management | | `/tabs/umpires` | `src/app/tab2/` | Court-official roster management |
| `/tabs/settings` | `src/app/tab3/` | Court count, theme, service-judge mode, alerts, and language | | `/tabs/settings` | `src/app/tab3/` | Court count, theme, service-judge mode, alerts, and language |
Pages are standalone Angular components and directly import the Ionic standalone components they use. Services are root-provided and injected with Angular's `inject()` function. Pages are standalone Angular components and directly import the Ionic standalone components they use. Services are root-provided and injected with Angular's `inject()` function.
@@ -37,7 +37,7 @@ Persistence services use Dexie's `liveQuery()` and Angular's `toSignal()` to exp
## Persistence model ## Persistence model
`db.ts` declares a `CourtPilot` Dexie database at schema version 1. `db.ts` declares a `CourtPilot` Dexie database at schema version 2.
```mermaid ```mermaid
erDiagram erDiagram
@@ -85,6 +85,21 @@ component starts and toggles Ionic's `ion-palette-dark` class. Device theme chan
apply while the app is open when device-preferred is selected. This does not apply while the app is open when device-preferred is selected. This does not
change the Dexie schema or existing settings records. change the Dexie schema or existing settings records.
### Waiting-list quick buttons
The optional waiting-list quick buttons default to disabled and require service
judges. Disabling service judges disables this setting. Both waiting lists have
an off-duty action; off-duty officials with a saved origin have a return action. Quick-button alerts
apply to both actions. Dragging an official from either waiting list to Not on Duty
also saves the origin, even when quick buttons are disabled. Returning appends to the saved role queue.
Schema version 2 adds `waitingListQuickButtons` to settings and optional
`returnToQueue` metadata to officials. The upgrade explicitly sets the setting to
false for existing settings rows. Existing officials need no transformation;
officials without return metadata have no return action. Queue indexes and
existing records are preserved. Older application versions do not understand the
new metadata; downgrading an IndexedDB database is not supported.
### Schema-change policy ### Schema-change policy
Once data may exist in released clients, do not modify schema version 1 in place. Add a new `db.version(n)` declaration and define upgrade behavior when existing records need transformation. Once data may exist in released clients, do not modify schema version 1 in place. Add a new `db.version(n)` declaration and define upgrade behavior when existing records need transformation.
+70 -25
View File
@@ -27,18 +27,21 @@ NVM. After approval, follow the official
Use the default `~/.nvm` location, or set `NVM_DIR` to your existing Bash NVM Use the default `~/.nvm` location, or set `NVM_DIR` to your existing Bash NVM
installation. Keep an existing fish installation separate. installation. Keep an existing fish installation separate.
After NVM is installed, install the required Node release from the project root: Initialize NVM once at the start of each Bash session, from the project root:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
. "$NVM_DIR/nvm.sh" . "$NVM_DIR/nvm.sh" && nvm use
nvm install lts/jod
``` ```
Every npm command block below explicitly loads NVM and selects `lts/jod`. `nvm use` reads the required `lts/jod` version from `.nvmrc`. On first setup,
This also works in noninteractive Bash sessions that do not load `.bashrc`. run `nvm install` after loading NVM if that version is not installed.
Run each block from the project root. Stop if loading NVM or selecting Node fails; Stop if loading NVM or selecting Node fails; do not fall back to system Node.
do not fall back to the system Node version.
Run the commands below in that initialized shell. Repeat setup for each new
shell, including noninteractive shells that do not load `.bashrc`. Agents should
reuse an initialized shell for subsequent npm commands; separate command
executions do not necessarily share environment changes.
Angular 22 requires TypeScript 6.0 and Node.js 22.22.3 or newer on the Angular 22 requires TypeScript 6.0 and Node.js 22.22.3 or newer on the
Node.js 22 line. If your installed `lts/jod` is older, run `nvm install lts/jod` Node.js 22 line. If your installed `lts/jod` is older, run `nvm install lts/jod`
@@ -47,17 +50,13 @@ after loading Bash NVM.
Install exact project dependencies with: Install exact project dependencies with:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm ci
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm ci
``` ```
If needed, install Ionic CLI globally: If needed, install Ionic CLI globally:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm install --global @ionic/cli
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm install --global @ionic/cli
``` ```
The committed `.nvmrc` uses the named `lts/jod` alias. Do not replace it with another Node release or add `.tool-versions` unless the team intentionally changes version-management tools. The committed `.nvmrc` uses the named `lts/jod` alias. Do not replace it with another Node release or add `.tool-versions` unless the team intentionally changes version-management tools.
@@ -71,17 +70,13 @@ Install Rust stable and the Tauri 2 prerequisites for your operating system. `sr
Start the Ionic development server: Start the Ionic development server:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm start
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm start
``` ```
Build the Angular application: Build the Angular application:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run build
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm run build
``` ```
The production web output is generated under `www/`. Do not edit that directory manually. The production web output is generated under `www/`. Do not edit that directory manually.
@@ -126,21 +121,64 @@ uses Vite 8.3.5 or newer within v8, and Capacitor's Xcode parser uses UUID 11.1.
or newer within v11. Xcode uses the compatible CommonJS `uuid.v4()` API. or newer within v11. Xcode uses the compatible CommonJS `uuid.v4()` API.
Revisit these overrides when upstream packages include the patched versions. Revisit these overrides when upstream packages include the patched versions.
The audit overrides also select patched `minimatch` 11 for modern consumers,
`glob` 14 for consumers of version 9 or newer, and `follow-redirects` 1.16.2 or
newer for Karma's HTTP proxy. These overrides retain Node.js 22 support. Keep
Karma's legacy `minimatch` 3 and `glob` 7 dependency paths intact: their callable
CommonJS APIs differ from the modern packages. npm still reports moderate
findings through those legacy test dependencies; resolving them requires an
upstream-compatible test-tooling update. Do not use `npm audit fix --force`,
which proposes incompatible downgrades of Angular and the test tooling.
## Tauri workflow ## Tauri workflow
Run Tauri commands through the npm script: Run Tauri commands through the npm script:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run tauri dev
. "$NVM_DIR/nvm.sh" npm run tauri build
nvm use lts/jod && npm run tauri dev
nvm use lts/jod && npm run tauri build
``` ```
Tauri development intentionally expects the frontend at `http://localhost:8103`. `devUrl` only tells Tauri where to connect; the Ionic command must also listen on port `8103`. The current `npm start` script does not explicitly select that port, so verify the complete development flow when changing Tauri or serve configuration. Tauri development intentionally expects the frontend at `http://localhost:8103`. `devUrl` tells Tauri where to connect, and `beforeDevCommand` runs `npm run ng -- serve --host=0.0.0.0 --port=8103` to serve the frontend on the matching port without opening a browser. This uses the project's Angular CLI and does not require a global Ionic CLI installation. Keep these settings aligned when changing Tauri or serve configuration.
The desktop wrapper is under `src-tauri/`. Do not edit generated `src-tauri/target/` or `src-tauri/gen/` content. The desktop wrapper is under `src-tauri/`. Do not edit generated `src-tauri/target/` or `src-tauri/gen/` content.
### Local updater signing
Tauri builds require the private key matching the updater public key in
`src-tauri/tauri.conf.json`. Set the key path in the build shell; Tauri does not
load these values from `.env` files. Keep private keys and passwords outside the
repository.
```bash
export TAURI_SIGNING_PRIVATE_KEY="$HOME/.tauri/myapp.key"
# For a key without a password:
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=""
npm run tauri build
```
For a password-protected key, replace the empty-password export with a hidden
prompt in your terminal:
```bash
read -r -s -p "Signing key password: " TAURI_SIGNING_PRIVATE_KEY_PASSWORD
echo
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD
```
If you keep the password in `$HOME/.tauri/signing-password`, restrict the file to
your account with `chmod 600 "$HOME/.tauri/signing-password"`, then load it for
each build without printing it:
```bash
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat "$HOME/.tauri/signing-password")"
```
This stores the password as plaintext outside the repository. The export applies
only to the current shell and its child processes.
See the [Tauri updater signing documentation](https://v2.tauri.app/plugin/updater/#building).
## Data storage ## Data storage
Court Pilot has no backend API. It stores domain data in IndexedDB using Dexie. The database and schema are declared in root-level `db.ts`. Court Pilot has no backend API. It stores domain data in IndexedDB using Dexie. The database and schema are declared in root-level `db.ts`.
@@ -214,12 +252,19 @@ GIT_EDITOR=true git tag v1.2.0
Replace `1.2.0` with the confirmed release version and verify the tag points to the intended commit. Do not overwrite or move an existing release tag. Replace `1.2.0` with the confirmed release version and verify the tag points to the intended commit. Do not overwrite or move an existing release tag.
Creating the tag is local. Pushing it starts `.github/workflows/release.yml`, which builds Windows and macOS artifacts for CrabNebula and builds/publishes the Linux Snap. Push only after explicit confirmation: Creating the tag is local. Pushing it starts `.github/workflows/release.yml`, which builds Windows and macOS artifacts for CrabNebula and builds/publishes the Linux Snap. After CrabNebula publication succeeds, it also creates a GitHub Release for the pushed tag with generated release notes and attaches the Windows installers, both macOS DMGs, and the Linux Snap. Push only after explicit confirmation:
```sh ```sh
git push origin v1.2.0 git push origin v1.2.0
``` ```
The GitHub Release job uses the built-in `GITHUB_TOKEN` with job-scoped
`contents: write` permission; no additional secret is required. Existing
CrabNebula, updater-signing, and Snap Store secrets are still required. Manual
workflow runs on branches skip the GitHub Release job; runs on `v*` tags include
it. Use a new version tag for each release: creation fails if a GitHub Release
already exists for that tag.
## Generated and local directories ## Generated and local directories
Do not manually edit or commit generated/dependency content unless a workflow explicitly requires an artifact: Do not manually edit or commit generated/dependency content unless a workflow explicitly requires an artifact:
+7 -16
View File
@@ -12,22 +12,19 @@ as the app rather than installing an older nested copy.
These examples target Bash and run from the project root. Complete the These examples target Bash and run from the project root. Complete the
[shell and NVM setup](development.md#shell-and-nvm-setup) first. Agents must ask [shell and NVM setup](development.md#shell-and-nvm-setup) first. Agents must ask
permission before installing missing Bash NVM. Each npm block loads NVM explicitly. permission before installing missing Bash NVM. Initialize NVM once per Bash
session as described there, then run the commands below in that same shell.
Run the interactive test suite: Run the interactive test suite:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm test
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm test
``` ```
Run the configured non-watching CI test mode: Run the configured non-watching CI test mode:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm test -- --configuration=ci
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm test -- --configuration=ci
``` ```
The test environment requires an available Chrome installation. Test coverage output, when enabled by the test runner, is written under `coverage/app/`. The test environment requires an available Chrome installation. Test coverage output, when enabled by the test runner, is written under `coverage/app/`.
@@ -35,9 +32,7 @@ The test environment requires an available Chrome installation. Test coverage ou
Run the suite once in headless Chrome with a coverage report: Run the suite once in headless Chrome with a coverage report:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
``` ```
If Chrome is not auto-detected, set `CHROME_BIN` to its executable path If Chrome is not auto-detected, set `CHROME_BIN` to its executable path
@@ -53,17 +48,13 @@ spies and signals to isolate assignments, confirmation, and role swaps.
Lint TypeScript and Angular templates: Lint TypeScript and Angular templates:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run lint
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm run lint
``` ```
Build the production Angular application: Build the production Angular application:
```bash ```bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" npm run build
. "$NVM_DIR/nvm.sh"
nvm use lts/jod && npm run build
``` ```
Check Rust changes from the Tauri directory: Check Rust changes from the Tauri directory:
+2 -2
View File
@@ -4,11 +4,11 @@ const angular = require('angular-eslint');
module.exports = defineConfig( module.exports = defineConfig(
{ ignores: ['projects/**', 'www/**', 'coverage/**'] }, { ignores: ['projects/**', 'www/**', 'coverage/**'] },
{ {
files: ['src/**/*.ts'], files: ['src/**/*.ts', 'db.ts'],
extends: angular.configs.tsRecommended, extends: angular.configs.tsRecommended,
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.json'], project: ['./tsconfig.eslint.json'],
tsconfigRootDir: __dirname tsconfigRootDir: __dirname
} }
}, },
+26 -164
View File
@@ -1,12 +1,12 @@
{ {
"name": "court-pilot", "name": "court-pilot",
"version": "2.0.0", "version": "2.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "court-pilot", "name": "court-pilot",
"version": "2.0.0", "version": "2.1.0",
"dependencies": { "dependencies": {
"@angular/cdk": "^22.2.4", "@angular/cdk": "^22.2.4",
"@angular/common": "^22.2.3", "@angular/common": "^22.2.3",
@@ -4834,6 +4834,13 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@isaacs/nonrecursive-glob-match": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz",
"integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
@@ -5737,9 +5744,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5757,9 +5761,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5777,9 +5778,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5797,9 +5795,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5817,9 +5812,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5837,9 +5829,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5857,9 +5846,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6095,9 +6081,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6115,9 +6098,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6135,9 +6115,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6155,9 +6132,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6175,9 +6149,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6195,9 +6166,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6215,9 +6183,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6235,9 +6200,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6452,9 +6414,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6476,9 +6435,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6500,9 +6456,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6524,9 +6477,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6548,9 +6498,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6572,9 +6519,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6939,9 +6883,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6959,9 +6900,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6979,9 +6917,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -6999,9 +6934,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7019,9 +6951,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7039,9 +6968,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7142,9 +7068,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7158,9 +7081,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7174,9 +7094,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7190,9 +7107,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7379,9 +7293,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7399,9 +7310,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7419,9 +7327,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7439,9 +7344,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7459,9 +7361,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -10835,9 +10734,9 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.16.1", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -13087,9 +12986,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -13111,9 +13007,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -13135,9 +13028,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -13159,9 +13049,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -13680,16 +13567,17 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.2.6", "version": "11.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.4.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "integrity": "sha512-/UJ1dEoVQTjPNDh6SGlUo+ueLMmoHTas4q+8yaAR/5t6nLAIiBDJtoYUh20IYleix7ehiQkgczb84PE3uzHkxg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"brace-expansion": "^5.0.8" "@isaacs/nonrecursive-glob-match": "^1.0.3",
"brace-expansion": "^5.0.12"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -14514,9 +14402,9 @@
} }
}, },
"node_modules/path-scurry": { "node_modules/path-scurry": {
"version": "2.0.2", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-3.0.0.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "integrity": "sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
@@ -15324,15 +15212,15 @@
} }
}, },
"node_modules/rimraf/node_modules/glob": { "node_modules/rimraf/node_modules/glob": {
"version": "13.0.6", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.3.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "integrity": "sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"minimatch": "^10.2.2", "minimatch": "^11.0.2",
"minipass": "^7.1.3", "minipass": "^7.1.3",
"path-scurry": "^2.0.2" "path-scurry": "^3.0.0"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
@@ -15660,7 +15548,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": "glibc",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15678,7 +15565,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": "glibc",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15696,7 +15582,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": "musl",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15714,7 +15599,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": "musl",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15732,7 +15616,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": "musl",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15750,7 +15633,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": "musl",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15768,7 +15650,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": "glibc",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15786,7 +15667,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": "glibc",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17457,9 +17337,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17477,9 +17354,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17497,9 +17371,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17517,9 +17388,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17537,9 +17405,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17557,9 +17422,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
+6 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "court-pilot", "name": "court-pilot",
"version": "2.0.0", "version": "2.1.0",
"author": "Richard Toth", "author": "Richard Toth",
"homepage": "https://github.com/tricsusz/court-pilot", "homepage": "https://github.com/tricsusz/court-pilot",
"scripts": { "scripts": {
@@ -76,6 +76,11 @@
}, },
"xcode": { "xcode": {
"uuid": "^11.1.1" "uuid": "^11.1.1"
},
"minimatch@>=9": "^11.0.1",
"glob@>=9": "^14.0.2",
"http-proxy": {
"follow-redirects": "^1.16.2"
} }
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@ license: MIT
base: core24 base: core24
icon: snap/gui/court-pilot.png icon: snap/gui/court-pilot.png
version: '2.0.0' version: '2.1.0'
summary: Badminton umpire rotation application summary: Badminton umpire rotation application
+1 -1
View File
@@ -519,7 +519,7 @@ dependencies = [
[[package]] [[package]]
name = "court-pilot" name = "court-pilot"
version = "2.0.0" version = "2.1.0"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "court-pilot" name = "court-pilot"
version = "2.0.0" version = "2.1.0"
description = "Court Pilot" description = "Court Pilot"
authors = ["Richard Toth"] authors = ["Richard Toth"]
license = "MIT" license = "MIT"
+2 -2
View File
@@ -2,12 +2,12 @@
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "court-pilot", "productName": "court-pilot",
"mainBinaryName": "court-pilot", "mainBinaryName": "court-pilot",
"version": "2.0.0", "version": "2.1.0",
"identifier": "com.courtpilot.dev", "identifier": "com.courtpilot.dev",
"build": { "build": {
"frontendDist": "../www", "frontendDist": "../www",
"devUrl": "http://localhost:8103", "devUrl": "http://localhost:8103",
"beforeDevCommand": "npm start", "beforeDevCommand": "npm run ng -- serve --host=0.0.0.0 --port=8103",
"beforeBuildCommand": "ionic build" "beforeBuildCommand": "ionic build"
}, },
"app": { "app": {
+16
View File
@@ -9,6 +9,14 @@ export const translations = {
themeLight: 'Light', themeLight: 'Light',
themeDark: 'Dark', themeDark: 'Dark',
themeSystem: 'Device-preferred', themeSystem: 'Device-preferred',
waitingListQuickButtons: 'Quick buttons for waiting lists',
waitingListQuickButtonsHelp:
'Move waiting officials off duty and return them to the end of their previous list. Only officials with a saved list have a return button. Requires Service Judge to be enabled.',
moveOffDuty: 'Move off duty',
returnToQueue: 'Return to waiting list',
confirmMoveOffDuty: 'Move this official to Not on Duty?',
confirmReturnToQueue:
'Return this official to the bottom of the waiting list?',
showAlerts: 'Show alerts on quick buttons', showAlerts: 'Show alerts on quick buttons',
serviceJudge: 'Service Judge', serviceJudge: 'Service Judge',
language: 'Language', language: 'Language',
@@ -62,6 +70,14 @@ export const translations = {
themeLight: 'Világos', themeLight: 'Világos',
themeDark: 'Sötét', themeDark: 'Sötét',
themeSystem: 'Eszközbeállítás szerint', themeSystem: 'Eszközbeállítás szerint',
waitingListQuickButtons: 'Gyors gombok a várólistákhoz',
waitingListQuickButtonsHelp:
'A várakozó tisztségviselőket pihenőre küldheted, majd visszaküldheted őket az előző várólistájuk végére. A visszaküldés gomb csak mentett várólista esetén jelenik meg. Az Adogatásbíró beállítás bekapcsolása szükséges.',
moveOffDuty: 'Pihenőre küldés',
returnToQueue: 'Vissza a várólistára',
confirmMoveOffDuty: 'Pihenőre küldöd ezt a tisztségviselőt?',
confirmReturnToQueue:
'Visszaküldöd ezt a tisztségviselőt a várólista végére?',
showAlerts: 'Figyelmeztetések megjelenítése a gyors gomboknál', showAlerts: 'Figyelmeztetések megjelenítése a gyors gomboknál',
serviceJudge: 'Adogatásbíró', serviceJudge: 'Adogatásbíró',
language: 'Nyelv', language: 'Nyelv',
+8 -2
View File
@@ -29,7 +29,12 @@ export class SettingsService {
return; return;
} }
await db.settings.update(1, changes); await db.settings.update(1, {
...changes,
...(!(changes.withServiceJudge ?? current.withServiceJudge)
? { waitingListQuickButtons: false }
: {})
});
} }
private async ensureSettings() { private async ensureSettings() {
@@ -40,7 +45,8 @@ export class SettingsService {
id: 1, id: 1,
numberOfCourts: 5, numberOfCourts: 5,
withServiceJudge: true, withServiceJudge: true,
showAlert: true showAlert: true,
waitingListQuickButtons: false
}); });
} }
} }
@@ -0,0 +1,95 @@
import { TestBed } from '@angular/core/testing';
import { db } from 'db';
import { WaitingDutyService } from './waiting-duty.service';
describe('WaitingDutyService', () => {
let service: WaitingDutyService;
beforeEach(async () => {
await Promise.all([
db.settings.clear(),
db.umpires.clear(),
db.waitingUmpires.clear(),
db.waitingServiceJudges.clear(),
db.courtUmpires.clear(),
db.courtServiceJudges.clear()
]);
await db.settings.put({
id: 1,
withServiceJudge: true,
waitingListQuickButtons: true,
showAlert: false,
numberOfCourts: 5
});
await db.umpires.put({
id: 1,
firstName: 'Alex',
lastName: 'Smith',
gender: 'M',
country: 'GB'
});
service = TestBed.inject(WaitingDutyService);
});
afterEach(async () => {
TestBed.resetTestingModule();
await Promise.all([
db.settings.clear(),
db.umpires.clear(),
db.waitingUmpires.clear(),
db.waitingServiceJudges.clear(),
db.courtUmpires.clear(),
db.courtServiceJudges.clear()
]);
});
for (const role of ['umpire', 'serviceJudge'] as const) {
it(`remembers the ${role} queue and returns at its bottom only once`, async () => {
if (role === 'umpire')
await db.waitingUmpires.add({ umpireId: 1, order: 1 });
else await db.waitingServiceJudges.add({ serviceJudgeId: 1, order: 1 });
await service.move(1, 'rest');
expect((await db.umpires.get(1))?.returnToQueue).toBe(role);
expect(
(await db.waitingUmpires.count()) +
(await db.waitingServiceJudges.count())
).toBe(0);
if (role === 'umpire')
await db.waitingUmpires.add({ umpireId: 2, order: 1 });
else await db.waitingServiceJudges.add({ serviceJudgeId: 2, order: 1 });
await Promise.all([service.move(1, 'umpire'), service.move(1, 'umpire')]);
const items =
role === 'umpire'
? await db.waitingUmpires.orderBy('order').toArray()
: await db.waitingServiceJudges.orderBy('order').toArray();
expect(items.map((item) => item.order)).toEqual([1, 2]);
expect((await db.umpires.get(1))?.returnToQueue).toBeUndefined();
});
}
for (const role of ['umpire', 'serviceJudge'] as const) {
it(`saves the ${role} origin on a drag move even with quick buttons disabled`, async () => {
await db.settings.update(1, { waitingListQuickButtons: false });
if (role === 'umpire')
await db.waitingUmpires.add({ umpireId: 1, order: 1 });
else await db.waitingServiceJudges.add({ serviceJudgeId: 1, order: 1 });
await service.moveToRest(1);
expect((await db.umpires.get(1))?.returnToQueue).toBe(role);
expect(await db.waitingUmpires.count()).toBe(0);
expect(await db.waitingServiceJudges.count()).toBe(0);
});
}
it('does not return officials without a saved origin', async () => {
await service.move(1, 'umpire');
expect(await db.waitingUmpires.count()).toBe(0);
expect(await db.waitingServiceJudges.count()).toBe(0);
});
it('ignores stale return actions for assigned officials', async () => {
await db.courtUmpires.add({ umpireId: 1, courtNo: 1 });
await service.move(1, 'umpire');
expect(await db.waitingUmpires.count()).toBe(0);
});
it('does nothing when service judges are disabled', async () => {
await db.settings.update(1, { withServiceJudge: false });
await service.move(1, 'umpire');
expect(await db.waitingUmpires.count()).toBe(0);
});
});
+81
View File
@@ -0,0 +1,81 @@
import { Injectable, inject } from '@angular/core';
import { db, WaitingRole } from 'db';
import { WaitingUmpiresService } from './waiting-umpires.service';
import { WaitingServiceJudgesService } from './waiting-service-judges.service';
@Injectable({ providedIn: 'root' })
export class WaitingDutyService {
private readonly umpires = inject(WaitingUmpiresService);
private readonly judges = inject(WaitingServiceJudgesService);
async moveToRest(officialId: number): Promise<void> {
await this.moveOfficial(officialId, 'rest', false);
}
async move(
officialId: number,
destination: WaitingRole | 'rest'
): Promise<void> {
await this.moveOfficial(officialId, destination, true);
}
private async moveOfficial(
officialId: number,
destination: WaitingRole | 'rest',
requireQuickButtons: boolean
): Promise<void> {
await db.transaction(
'rw',
[
db.settings,
db.umpires,
db.waitingUmpires,
db.waitingServiceJudges,
db.courtUmpires,
db.courtServiceJudges
],
async () => {
const settings = await db.settings.get(1);
const official = await db.umpires.get(officialId);
if (
!official ||
(requireQuickButtons &&
(!settings?.withServiceJudge || !settings.waitingListQuickButtons))
)
return;
const umpire = await db.waitingUmpires
.where('umpireId')
.equals(officialId)
.first();
const judge = await db.waitingServiceJudges
.where('serviceJudgeId')
.equals(officialId)
.first();
const onCourt =
(await db.courtUmpires
.where('umpireId')
.equals(officialId)
.count()) ||
(await db.courtServiceJudges
.where('umpireId')
.equals(officialId)
.count());
if (onCourt) return;
if (destination === 'rest') {
if (!umpire && !judge) return;
await db.umpires.update(officialId, {
returnToQueue: umpire ? 'umpire' : 'serviceJudge'
});
await this.umpires.removeByUmpireId(officialId);
await this.judges.removeByUmpireId(officialId);
} else {
if (umpire || judge || !official.returnToQueue) return;
const role = official.returnToQueue;
if (role === 'serviceJudge') await this.judges.add(officialId);
else await this.umpires.add(officialId);
await db.umpires.update(officialId, { returnToQueue: undefined });
}
}
);
}
}
+36
View File
@@ -161,6 +161,18 @@
<ion-label>{{ umpire|fullname }}</ion-label> <ion-label>{{ umpire|fullname }}</ion-label>
</div> </div>
<ion-label>{{ umpire|fullname }}</ion-label> <ion-label>{{ umpire|fullname }}</ion-label>
@if (waitingQuickButtonsEnabled()) {
<ion-button
class="waiting-duty-button"
size="small"
fill="solid"
[attr.aria-label]="('moveOffDuty' | translate) + ': ' + (umpire | fullname)"
[attr.title]="'moveOffDuty' | translate"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation(); changeWaitingDuty(umpire, true)">
<ion-icon slot="icon-only" name="exit-outline"></ion-icon>
</ion-button>
}
</div> </div>
} }
</ion-list> </ion-list>
@@ -179,6 +191,18 @@
<ion-label>{{ umpire|fullname }}</ion-label> <ion-label>{{ umpire|fullname }}</ion-label>
</div> </div>
<ion-label>{{ umpire|fullname }}</ion-label> <ion-label>{{ umpire|fullname }}</ion-label>
@if (waitingQuickButtonsEnabled()) {
<ion-button
class="waiting-duty-button"
size="small"
fill="solid"
[attr.aria-label]="('moveOffDuty' | translate) + ': ' + (umpire | fullname)"
[attr.title]="'moveOffDuty' | translate"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation(); changeWaitingDuty(umpire, true)">
<ion-icon slot="icon-only" name="exit-outline"></ion-icon>
</ion-button>
}
</div> </div>
} }
</ion-list> </ion-list>
@@ -198,6 +222,18 @@
<ion-label>{{ umpire|fullname }}</ion-label> <ion-label>{{ umpire|fullname }}</ion-label>
</div> </div>
<ion-label>{{ umpire|fullname }}</ion-label> <ion-label>{{ umpire|fullname }}</ion-label>
@if (waitingQuickButtonsEnabled() && umpire.returnToQueue) {
<ion-button
class="waiting-duty-button"
size="small"
fill="solid"
[attr.aria-label]="('returnToQueue' | translate) + ': ' + (umpire | fullname)"
[attr.title]="'returnToQueue' | translate"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation(); changeWaitingDuty(umpire, false)">
<ion-icon slot="icon-only" name="enter-outline"></ion-icon>
</ion-button>
}
</div> </div>
} }
</ion-list> </ion-list>
+15
View File
@@ -38,3 +38,18 @@
background-color: var(--ion-color-step-150, #d9d9d9); background-color: var(--ion-color-step-150, #d9d9d9);
} }
} }
.bottom-lists .custom-ion-item {
display: flex;
align-items: center;
gap: 4px;
ion-label {
flex: 1;
min-width: 0;
}
.waiting-duty-button {
flex-shrink: 0;
margin: 0;
}
}
+75
View File
@@ -1,7 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { signal } from '@angular/core'; import { signal } from '@angular/core';
import { AlertController } from '@ionic/angular'; import { AlertController } from '@ionic/angular';
import { CourtServiceJudge, CourtUmpire, Settings, Umpire } from 'db'; import { CourtServiceJudge, CourtUmpire, Settings, Umpire } from 'db';
import { WaitingDutyService } from '../services/waiting-duty.service';
import { Tab1Page } from './tab1.page'; import { Tab1Page } from './tab1.page';
import { SettingsService } from '../services/settings-service'; import { SettingsService } from '../services/settings-service';
import { UmpireService } from '../services/umpire.service'; import { UmpireService } from '../services/umpire.service';
@@ -34,6 +36,7 @@ describe('Tab1Page court rotation', () => {
let waitingJudge: jasmine.SpyObj<WaitingServiceJudgesService>; let waitingJudge: jasmine.SpyObj<WaitingServiceJudgesService>;
let umpireCourt: jasmine.SpyObj<CourtUmpireService>; let umpireCourt: jasmine.SpyObj<CourtUmpireService>;
let judgeCourt: jasmine.SpyObj<CourtServiceJudgeService>; let judgeCourt: jasmine.SpyObj<CourtServiceJudgeService>;
let duty: jasmine.SpyObj<WaitingDutyService>;
let alerts: jasmine.SpyObj<AlertController>; let alerts: jasmine.SpyObj<AlertController>;
beforeEach(() => { beforeEach(() => {
@@ -72,6 +75,9 @@ describe('Tab1Page court rotation', () => {
['save', 'removeByUmpireId'], ['save', 'removeByUmpireId'],
{ umpires: courtJudges } { umpires: courtJudges }
); );
duty = jasmine.createSpyObj('WaitingDutyService', ['move', 'moveToRest']);
duty.move.and.resolveTo();
duty.moveToRest.and.resolveTo();
alerts = jasmine.createSpyObj('AlertController', ['create']); alerts = jasmine.createSpyObj('AlertController', ['create']);
waitingUmpire.getCurrentUmpire.and.resolveTo(official(1)); waitingUmpire.getCurrentUmpire.and.resolveTo(official(1));
waitingJudge.getCurrentUmpire.and.resolveTo(official(2)); waitingJudge.getCurrentUmpire.and.resolveTo(official(2));
@@ -85,6 +91,7 @@ describe('Tab1Page court rotation', () => {
judgeCourt.removeByUmpireId.and.resolveTo(); judgeCourt.removeByUmpireId.and.resolveTo();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
{ provide: WaitingDutyService, useValue: duty },
{ provide: SettingsService, useValue: { settings } }, { provide: SettingsService, useValue: { settings } },
{ provide: UmpireService, useValue: { umpires } }, { provide: UmpireService, useValue: { umpires } },
{ provide: WaitingUmpiresService, useValue: waitingUmpire }, { provide: WaitingUmpiresService, useValue: waitingUmpire },
@@ -98,6 +105,74 @@ describe('Tab1Page court rotation', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
}); });
for (const source of [
'list-waiting-umpires',
'list-waiting-service-judges'
]) {
it(`saves the waiting origin when dropped from ${source} to rest`, async () => {
await component.dropToRest({
previousContainer: { id: source },
container: { id: 'list-on-rest' },
item: { data: official(1) }
} as CdkDragDrop<Umpire[]>);
expect(duty.moveToRest).toHaveBeenCalledOnceWith(1);
expect(waitingUmpire.removeByUmpireId).not.toHaveBeenCalled();
expect(waitingJudge.removeByUmpireId).not.toHaveBeenCalled();
});
}
it('moves directly when quick-button alerts are disabled', async () => {
settings.update((value) => ({ ...value, waitingListQuickButtons: true }));
await component.changeWaitingDuty(official(1), true);
expect(duty.move).toHaveBeenCalledOnceWith(1, 'rest');
expect(alerts.create).not.toHaveBeenCalled();
});
it('ignores return actions without a saved origin', async () => {
settings.update((value) => ({
...value,
waitingListQuickButtons: true,
showAlert: true
}));
await component.changeWaitingDuty(official(5), false);
expect(duty.move).not.toHaveBeenCalled();
expect(alerts.create).not.toHaveBeenCalled();
});
it('confirms returning to the saved service-judge queue', async () => {
settings.update((value) => ({
...value,
waitingListQuickButtons: true,
showAlert: true
}));
alerts.create.and.resolveTo({
present: async () => undefined
} as unknown as HTMLIonAlertElement);
await component.changeWaitingDuty(
{ ...official(5), returnToQueue: 'serviceJudge' },
false
);
expect(duty.move).not.toHaveBeenCalled();
const buttons = alerts.create.calls.mostRecent().args[0]?.buttons;
const confirm = buttons?.find(
(button) => typeof button !== 'string' && button.role === 'confirm'
);
if (typeof confirm === 'object' && typeof confirm.handler === 'function')
confirm.handler(undefined);
expect(duty.move).toHaveBeenCalledOnceWith(5, 'serviceJudge');
});
it('hides and ignores quick actions when service judges are disabled', async () => {
settings.update((value) => ({
...value,
waitingListQuickButtons: true,
withServiceJudge: false
}));
expect(component.waitingQuickButtonsEnabled()).toBeFalse();
await component.changeWaitingDuty(official(1), true);
expect(duty.move).not.toHaveBeenCalled();
});
it('waits for confirmation before assigning officials', async () => { it('waits for confirmation before assigning officials', async () => {
settings.update((value) => ({ ...value, showAlert: true })); settings.update((value) => ({ ...value, showAlert: true }));
const present = jasmine.createSpy('present').and.resolveTo(); const present = jasmine.createSpy('present').and.resolveTo();
+45 -1
View File
@@ -22,6 +22,7 @@ import {
import { SettingsService } from '../services/settings-service'; import { SettingsService } from '../services/settings-service';
import { UmpireService } from '../services/umpire.service'; import { UmpireService } from '../services/umpire.service';
import { Umpire } from 'db'; import { Umpire } from 'db';
import { WaitingDutyService } from '../services/waiting-duty.service';
import { WaitingUmpiresService } from '../services/waiting-umpires.service'; import { WaitingUmpiresService } from '../services/waiting-umpires.service';
import { WaitingServiceJudgesService } from '../services/waiting-service-judges.service'; import { WaitingServiceJudgesService } from '../services/waiting-service-judges.service';
import { FullnamePipe } from '../fullname-pipe'; import { FullnamePipe } from '../fullname-pipe';
@@ -71,6 +72,8 @@ export class Tab1Page {
addIcons({ exitOutline, enterOutline }); addIcons({ exitOutline, enterOutline });
} }
readonly waitingDutyService = inject(WaitingDutyService);
readonly settingsService = inject(SettingsService); readonly settingsService = inject(SettingsService);
readonly umpireService = inject(UmpireService); readonly umpireService = inject(UmpireService);
readonly waitingUmpireService = inject(WaitingUmpiresService); readonly waitingUmpireService = inject(WaitingUmpiresService);
@@ -285,12 +288,53 @@ export class Tab1Page {
return this.settings()?.withServiceJudge ?? false; return this.settings()?.withServiceJudge ?? false;
}); });
dropToRest(event: CdkDragDrop<Umpire[]>) { readonly waitingQuickButtonsEnabled = computed(
() =>
this.serviceJudgeEnabled() && !!this.settings()?.waitingListQuickButtons
);
async changeWaitingDuty(umpire: Umpire, toRest: boolean): Promise<void> {
if (!this.waitingQuickButtonsEnabled()) return;
const destination = toRest ? 'rest' : umpire.returnToQueue;
if (!destination) return;
const move = () => this.waitingDutyService.move(umpire.id, destination);
if (!this.settings()?.showAlert) {
await move();
return;
}
const alert = await this.alertController.create({
header: this.translationService.translate('confirmation'),
subHeader: this.translationService.translate(
toRest ? 'confirmMoveOffDuty' : 'confirmReturnToQueue'
),
message: this.fullnamePipe.transform(umpire),
buttons: [
{ text: this.translationService.translate('no'), role: 'cancel' },
{
text: this.translationService.translate('yes'),
role: 'confirm',
handler: () => {
void move();
}
}
]
});
await alert.present();
}
async dropToRest(event: CdkDragDrop<Umpire[]>): Promise<void> {
if (event.previousContainer === event.container) { if (event.previousContainer === event.container) {
return; return;
} else { } else {
const comingFrom = event.previousContainer.id; const comingFrom = event.previousContainer.id;
const umpireToMove = event.item.data; const umpireToMove = event.item.data;
if (
comingFrom === 'list-waiting-umpires' ||
comingFrom === 'list-waiting-service-judges'
) {
await this.waitingDutyService.moveToRest(umpireToMove.id);
return;
}
this.removeFromOriginalPlace(umpireToMove, comingFrom); this.removeFromOriginalPlace(umpireToMove, comingFrom);
} }
} }
+16 -1
View File
@@ -35,6 +35,21 @@
</ion-toggle> </ion-toggle>
</ion-item> </ion-item>
<ion-item>
<div class="waiting-list-setting">
<ion-toggle
[checked]="currentSettings.waitingListQuickButtons ?? false"
[disabled]="!currentSettings.withServiceJudge"
aria-describedby="waiting-list-help"
(ionChange)="updateWaitingListQuickButtons($event)">
{{ 'waitingListQuickButtons' | translate }}
</ion-toggle>
<p id="waiting-list-help" class="setting-help">
{{ 'waitingListQuickButtonsHelp' | translate }}
</p>
</div>
</ion-item>
<ion-item> <ion-item>
<ion-toggle <ion-toggle
[checked]="currentSettings.showAlert" [checked]="currentSettings.showAlert"
@@ -79,7 +94,7 @@
<ion-col sizeLg="6" pushLg="3"> <ion-col sizeLg="6" pushLg="3">
<ion-item> <ion-item>
<ion-label class="ion-text-center" <ion-label class="ion-text-center"
>{{ 'version' | translate }}: 2.0.0</ion-label >{{ 'version' | translate }}: 2.1.0</ion-label
> >
</ion-item> </ion-item>
</ion-col> </ion-col>
+15
View File
@@ -0,0 +1,15 @@
.waiting-list-setting {
width: 100%;
padding-block: 12px;
ion-toggle {
width: 100%;
}
}
.setting-help {
margin: 8px 0 0;
color: var(--ion-color-medium-shade);
font-size: 0.875rem;
line-height: 1.4;
}
+8
View File
@@ -99,6 +99,14 @@ export class Tab3Page {
} }
} }
async updateWaitingListQuickButtons(
event: CustomEvent<{ checked: boolean }>
): Promise<void> {
await this.settingsService.update({
waitingListQuickButtons: event.detail.checked
});
}
async updateShowAlert(event: CustomEvent): Promise<void> { async updateShowAlert(event: CustomEvent): Promise<void> {
await this.settingsService.update({ await this.settingsService.update({
showAlert: event.detail.checked showAlert: event.detail.checked
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["jasmine"]
},
"include": ["src/**/*.ts", "db.ts"]
}
+5
View File
@@ -1,6 +1,11 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */ /* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"compileOnSave": false, "compileOnSave": false,
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.spec.json" }
],
"compilerOptions": { "compilerOptions": {
"paths": { "db": ["./db.ts"] }, "paths": { "db": ["./db.ts"] },
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",