From b3aca3e9f425a5e27d6e9408fa15e9a025f3b6d5 Mon Sep 17 00:00:00 2001 From: tempus2016 Date: Mon, 10 Aug 2026 22:08:58 +0000 Subject: [PATCH 1/2] chore(ci): add data/release gates, CodeQL, dependency review and dev tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repo-specific gates: - scripts/check_translations.py + "Translation parity" job — fails the build if any locale's key set differs from en.json, in both the HA translations catalogue and the card/panel locales - scripts/check_data_files.py + "Data files" job — parses every shipped blueprint, custom sentence, locale and metadata file, and cross-checks manifest/hacs packaging fields - scripts/check_release.py + release-guard.yml — asserts manifest.json's version equals the release tag exactly, and that taskmate.zip actually landed on the release (HACS installs break without it) Security and supply chain: - codeql.yml — Python + JavaScript analysis on push/PR and weekly - dependency-review.yml — blocks PRs introducing a vulnerable dependency - workflow-lint.yml — actionlint + zizmor audit of the CI config itself - fixed the findings that audit surfaced: template injection via the release tag in release-zip.yml, and persist-credentials on every checkout - dependabot: 7/14-day cooldown on new releases, plus a pip entry so a newer test-harness pin surfaces as one PR a month instead of never CI quality and speed: - pip and npm dependency caching (the HA harness install was the slow step) - pytest-cov with a per-module coverage table in the job summary and an HTML report artifact - dropped the duplicate hassfest job from tests.yml (hassfest.yaml already runs it on every push/PR) - ruff now covers scripts/ too Contributor experience: - .devcontainer — Codespaces/VS Code container that installs the toolchain and creates a scratch HA config with the integration symlinked in - .pre-commit-config.yaml (+ pre-commit.ci) running ruff, the data checks, ESLint and whitespace hygiene - .github/CODEOWNERS and .github/release.yml (label-grouped generated notes) - CONTRIBUTING.md documents all of it; README gains the new status badges --- .devcontainer/devcontainer.json | 39 + .devcontainer/setup.sh | 48 ++ .github/CODEOWNERS | 13 + .github/CONTRIBUTING.md | 39 +- .github/ISSUE_TEMPLATE/bug_report.yml | 8 +- .github/ISSUE_TEMPLATE/feature_request.yml | 4 +- .github/dependabot.yml | 22 + .github/release.yml | 46 ++ .github/workflows/codeql.yml | 56 ++ .github/workflows/data-checks.yml | 59 ++ .github/workflows/dependency-review.yml | 34 + .github/workflows/hassfest.yaml | 2 + .github/workflows/labels.yml | 2 + .github/workflows/lint.yml | 12 +- .github/workflows/release-guard.yml | 66 ++ .github/workflows/release-zip.yml | 7 +- .github/workflows/tests.yml | 65 +- .github/workflows/validate.yml | 2 + .github/workflows/workflow-lint.yml | 69 ++ .gitignore | 8 + .pre-commit-config.yaml | 65 ++ README.md | 727 +++++++++--------- .../taskmate/www/taskmate-child-card.js | 2 +- pyproject.toml | 15 + requirements_test.txt | 3 + scripts/check_data_files.py | 120 +++ scripts/check_release.py | 47 ++ scripts/check_translations.py | 110 +++ 28 files changed, 1303 insertions(+), 387 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/setup.sh create mode 100644 .github/CODEOWNERS create mode 100644 .github/release.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/data-checks.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/release-guard.yml create mode 100644 .github/workflows/workflow-lint.yml create mode 100644 .pre-commit-config.yaml create mode 100644 scripts/check_data_files.py create mode 100644 scripts/check_release.py create mode 100644 scripts/check_translations.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..c064b175 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +{ + "name": "TaskMate dev", + "image": "mcr.microsoft.com/devcontainers/python:1-3.13-bookworm", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + } + }, + "forwardPorts": [8123], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "postCreateCommand": "bash .devcontainer/setup.sh", + "remoteUser": "vscode", + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "ms-python.python", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "redhat.vscode-yaml", + "github.vscode-github-actions" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "editor.formatOnSave": false, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true + } + } + } +} diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100644 index 00000000..3102c0aa --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Provisions the dev container: test harness, linters, and a throwaway Home +# Assistant config with TaskMate symlinked in. Runs once, on container create. +set -euo pipefail + +echo "==> Installing Python tooling" +python -m pip install --upgrade pip +pip install -r requirements_test.txt +pip install ruff==0.15.12 pre-commit homeassistant + +echo "==> Installing Node tooling" +npm ci + +echo "==> Installing git hooks" +pre-commit install || echo "pre-commit install skipped" + +echo "==> Preparing a Home Assistant config at ./dev-config" +mkdir -p dev-config/custom_components +# Symlink, not copy — edits to the integration are live in the running HA. +ln -sfn "$(pwd)/custom_components/taskmate" dev-config/custom_components/taskmate + +if [ ! -f dev-config/configuration.yaml ]; then + cat > dev-config/configuration.yaml <<'YAML' +# Minimal Home Assistant config for TaskMate development. +default_config: + +logger: + default: info + logs: + custom_components.taskmate: debug + +# Cards are served by the integration itself at /taskmate/.js and +# registered automatically, so no lovelace resources block is needed here. +YAML +fi + +cat <<'EOF' + +Setup complete. + + Run tests pytest -v + Lint ruff check custom_components/taskmate tests scripts && npm run lint + Data checks python3 scripts/check_translations.py && python3 scripts/check_data_files.py + Start Home Assistant hass -c dev-config + then open the forwarded port 8123 and add the + TaskMate integration from Settings -> Devices & services + +EOF diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..e71bcc47 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# Auto-requests review on every PR (including from forks) and makes the +# ownership explicit. Matches the "codeowners" field in manifest.json. + +* @tempus2016 + +# Areas worth an explicit line — these are the ones where an outside PR is most +# likely to land, and where a silent change is most expensive. +/.github/ @tempus2016 +/custom_components/taskmate/www/ @tempus2016 +/custom_components/taskmate/translations/ @tempus2016 +/blueprints/ @tempus2016 +/hacs.json @tempus2016 +/custom_components/taskmate/manifest.json @tempus2016 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 34aaa4d1..ad2368d3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -40,6 +40,16 @@ custom_components/taskmate/ ## Development setup +### One-click: dev container / Codespaces + +The repo ships a dev container. Open it in GitHub Codespaces, or in VS Code with +the Dev Containers extension ("Reopen in Container"). Setup installs the test +harness, ruff, ESLint and pre-commit, and creates a scratch HA config at +`dev-config/` with the integration symlinked in — so `hass -c dev-config` gives +you a real Home Assistant on port 8123 with your working copy live. + +### Manual + The fastest loop is a real Home Assistant instance with the integration bind-mounted: @@ -52,18 +62,35 @@ bind-mounted: ## Code quality -Two checks gate every PR into `main`. Run them locally before pushing: +Run these locally before pushing: ```bash # Lint (matches the "Ruff" CI check) -ruff check . +ruff check custom_components/taskmate tests scripts + +# Cards and panel (matches the "ESLint" CI check) +npm ci && npm run lint # Tests (matches the "Run tests" CI check) pytest + +# Every locale matches en.json (matches the "Translation parity" CI check) +python3 scripts/check_translations.py + +# Blueprints, sentences and packaging metadata parse (matches "Data files") +python3 scripts/check_data_files.py ``` -`hassfest` and HACS validation also run in CI to keep the integration -compliant. +Or install the hooks once and let them run on commit: + +```bash +pip install pre-commit +pre-commit install +pre-commit run --all-files +``` + +`hassfest`, HACS validation, CodeQL, dependency review and a workflow-security +audit (`zizmor`) also run in CI. ## Translations @@ -75,6 +102,10 @@ be asked to include the translations. Backend strings live in `custom_components/taskmate/translations/` and `strings.json`; card strings live under `www/locales/`. +This is enforced in CI: `scripts/check_translations.py` fails the build if any +locale's key set differs from `en.json` in either catalogue. Run it locally to +see exactly which keys are missing. + ## Submitting a pull request 1. Fork and create a feature branch. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4a18b342..0d64a78b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -175,15 +175,15 @@ body: attributes: label: Home Assistant Logs description: | - Paste any relevant log output here. - + Paste any relevant log output here. + To get debug logs, add this to configuration.yaml, restart HA, reproduce the issue, then copy the logs from Settings → System → Logs: ```yaml logger: logs: custom_components.taskmate: debug ``` - + Filter by "taskmate" to find relevant entries. render: text placeholder: | @@ -223,7 +223,7 @@ body: label: Additional Context description: | Anything else that might be relevant — screenshots, screen recordings, recent changes to your setup, etc. - + You can drag and drop images directly into this text box. - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index c224dee9..1ab49df9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -10,7 +10,7 @@ body: attributes: value: | Thanks for suggesting a new feature! Please fill in as much detail as you can — the more context you give, the easier it is to understand and implement. - + Before submitting, please search [existing issues](https://github.com/tempus2016/taskmate/issues) to check if this has already been requested. # ── Feature Summary ─────────────────────────────────────────────────────────── @@ -132,7 +132,7 @@ body: label: Additional Context description: | Anything else that helps explain your situation — ages of children, how you currently use TaskMate, screenshots of what you'd like to see, mockups, etc. - + You can drag and drop images directly into this box. # ── Checklist ───────────────────────────────────────────────────────────────── diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 318145ec..117168cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,10 @@ updates: labels: - "dependencies" - "ci" + # Don't bump onto a release that's hours old — a freshly published action + # version is the one most likely to be yanked or compromised. + cooldown: + default-days: 7 # Dev tooling only (eslint) — nothing here ships in the integration. # Grouped so routine bumps arrive as one PR rather than one per package. @@ -19,9 +23,27 @@ updates: labels: - "dependencies" - "ci" + cooldown: + default-days: 7 groups: npm-dev-dependencies: dependency-type: "development" update-types: - "minor" - "patch" + + # The HA test harness in requirements_test.txt is deliberately pinned so a new + # upstream release can't flip the required "Run tests" check on its own. This + # entry doesn't undo that — it just raises one PR when a newer pin exists, so + # the decision to move is explicit instead of never noticed. Limit 1 on + # purpose: this should be a quiet trickle, not a queue. + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 1 + labels: + - "dependencies" + - "ci" + cooldown: + default-days: 14 diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..28d90daf --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,46 @@ +# Groups GitHub's auto-generated release notes by label. This does not replace +# the hand-written release notes — it shapes the "What's Changed" list that +# GitHub appends when you click "Generate release notes", so dependency bumps +# collapse into one section instead of padding the top of the list. +changelog: + exclude: + labels: + - duplicate + - invalid + - wontfix + - stale + authors: + - dependabot + categories: + - title: New features + labels: + - enhancement + + - title: Bug fixes + labels: + - bug + + - title: Cards & UI + labels: + - cards + + - title: Translations + labels: + - translations + + - title: Documentation + labels: + - documentation + + - title: CI & maintenance + labels: + - ci + - tests + + - title: Dependencies + labels: + - dependencies + + - title: Other changes + labels: + - "*" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..60cdf0b3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,56 @@ +name: CodeQL + +# GitHub's own static analysis. Findings land in the repo's Security tab +# (Security → Code scanning), not in the PR checks list, so this is advisory +# signal rather than a merge gate. + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Weekly, so newly published queries get applied to unchanged code too. + - cron: "17 4 * * 1" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + + permissions: + security-events: write + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: ["python", "javascript-typescript"] + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + languages: ${{ matrix.language }} + # security-and-quality adds maintainability queries on top of the + # default security set — worth it on a codebase this size. + queries: security-and-quality + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/data-checks.yml b/.github/workflows/data-checks.yml new file mode 100644 index 00000000..7092a621 --- /dev/null +++ b/.github/workflows/data-checks.yml @@ -0,0 +1,59 @@ +name: Data checks + +# Guards the two classes of breakage the Python test suite can't see: +# 1. a locale that drifted out of sync with en.json (renders raw keys in the UI) +# 2. a shipped YAML/JSON file that doesn't parse (blows up at install time) + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + translations: + name: Translation parity + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Check every locale matches en.json + run: python3 scripts/check_translations.py + + data-files: + name: Data files + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Install PyYAML + run: python -m pip install --upgrade pip pyyaml + + - name: Parse blueprints, sentences, locales and metadata + run: python3 scripts/check_data_files.py diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..08f79493 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,34 @@ +name: Dependency review + +# Fails a PR that introduces a dependency with a known vulnerability, or one +# under a licence we don't want to ship. Only the npm dev tooling and the test +# requirements are in scope today — the integration itself has no runtime +# requirements — but this is the gate that keeps it that way. + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Review dependency changes + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + comment-summary-in-pr: on-failure diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 84eed992..23402678 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -18,4 +18,6 @@ jobs: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1 + with: + persist-credentials: false - uses: home-assistant/actions/hassfest@a7c616ce81ccda50150bf1595786c71b1883fabb # master diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 9d7d3552..c981298b 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Apply labels uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d # v6.0.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8181ee00..09782c3b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,9 @@ concurrency: permissions: contents: read +env: + RUFF_VERSION: "0.15.12" + jobs: ruff: name: Ruff @@ -22,6 +25,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -29,10 +34,10 @@ jobs: python-version: "3.12" - name: Install ruff - run: python -m pip install --upgrade pip "ruff==0.15.12" + run: python -m pip install --upgrade pip "ruff==${RUFF_VERSION}" - name: Run ruff - run: ruff check custom_components/taskmate tests + run: ruff check custom_components/taskmate tests scripts eslint: name: ESLint (cards + panel) @@ -41,11 +46,14 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20" + cache: "npm" - name: Install dependencies run: npm ci diff --git a/.github/workflows/release-guard.yml b/.github/workflows/release-guard.yml new file mode 100644 index 00000000..3626801f --- /dev/null +++ b/.github/workflows/release-guard.yml @@ -0,0 +1,66 @@ +name: Release guard + +# Two release rules that are otherwise only enforced by remembering them: +# +# 1. manifest.json's "version" must equal the tag exactly (including any +# -beta.N suffix) — HACS reads the manifest, not the tag. +# 2. the release must carry taskmate.zip — hacs.json sets zip_release, so a +# release without that asset is uninstallable, and the asset's download +# count is the number HACS displays. +# +# release-zip.yml builds and uploads the zip on the same `published` event; +# this workflow waits for it to land rather than racing it. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag to re-check (e.g. v5.1.0)" + required: true + type: string + +permissions: + contents: read + +jobs: + manifest-version: + name: Manifest version matches tag + runs-on: ubuntu-latest + + steps: + - name: Check out the released tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag || github.event.release.tag_name }} + persist-credentials: false + + - name: Compare manifest.json against the tag + env: + TAG: ${{ inputs.tag || github.event.release.tag_name }} + run: python3 scripts/check_release.py "$TAG" + + zip-asset: + name: taskmate.zip attached + runs-on: ubuntu-latest + + steps: + - name: Wait for the HACS zip to be attached + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ inputs.tag || github.event.release.tag_name }} + run: | + for attempt in $(seq 1 10); do + assets=$(gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name' || true) + echo "attempt $attempt — assets: ${assets:-}" + if printf '%s\n' "$assets" | grep -qx 'taskmate.zip'; then + echo "taskmate.zip is attached to $TAG." + exit 0 + fi + sleep 30 + done + echo "::error::taskmate.zip is missing from release $TAG — HACS installs will fail." + echo "Re-run the 'Attach HACS zip to release' workflow, then re-run this check." + exit 1 diff --git a/.github/workflows/release-zip.yml b/.github/workflows/release-zip.yml index b78d21a4..3cc0be1b 100644 --- a/.github/workflows/release-zip.yml +++ b/.github/workflows/release-zip.yml @@ -22,6 +22,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.release.tag_name }} + persist-credentials: false - name: Build taskmate.zip run: | @@ -35,4 +36,8 @@ jobs: - name: Upload taskmate.zip to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release upload "${{ github.event.release.tag_name }}" taskmate.zip --clobber --repo "${{ github.repository }}" + # Via env, not inline ${{ }} — a tag name is attacker-influenced text + # and inline expansion would splice it straight into the shell. + TAG: ${{ github.event.release.tag_name }} + REPO: ${{ github.repository }} + run: gh release upload "$TAG" taskmate.zip --clobber --repo "$REPO" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10fe572e..043eab11 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,16 +15,9 @@ permissions: contents: read jobs: - validate: - name: Validate integration - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Hassfest validation - uses: home-assistant/actions/hassfest@a7c616ce81ccda50150bf1595786c71b1883fabb # master + # NOTE: hassfest lives in hassfest.yaml and runs on its own schedule + on + # every push/PR. It used to be duplicated here as a "validate" job; that ran + # the same action twice per PR for no extra signal. test: name: Run tests @@ -33,11 +26,18 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" + # The HA test harness pulls in homeassistant and its whole dependency + # tree — easily the slowest step in CI. Cache it against the pinned + # requirements file. + cache: "pip" + cache-dependency-path: requirements_test.txt - name: Install dependencies run: | @@ -46,4 +46,47 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Run tests - run: pytest -v + run: pytest -v --cov --cov-report=term-missing --cov-report=xml --cov-report=html + + - name: Coverage summary + if: always() + run: | + if [ ! -f coverage.xml ]; then + echo "No coverage.xml produced — tests likely failed before finishing." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + python - <<'PY' >> "$GITHUB_STEP_SUMMARY" + import xml.etree.ElementTree as ET + + root = ET.parse("coverage.xml").getroot() + overall = float(root.get("line-rate", 0)) * 100 + print("## Test coverage\n") + print(f"**Overall: {overall:.1f}%**\n") + print("| Module | Coverage | Missed |") + print("|---|---:|---:|") + + rows = [] + for cls in root.iter("class"): + lines = cls.find("lines") + if lines is None: + continue + entries = list(lines) + if not entries: + continue + missed = sum(1 for line in entries if line.get("hits") == "0") + rate = float(cls.get("line-rate", 0)) * 100 + rows.append((rate, cls.get("filename"), missed)) + + # Worst-covered first — that's where the next test is worth writing. + for rate, filename, missed in sorted(rows)[:25]: + print(f"| `{filename}` | {rate:.0f}% | {missed} |") + PY + + - name: Upload HTML coverage report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-html + path: htmlcov/ + retention-days: 14 + if-no-files-found: ignore diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3baa8e18..46116af8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -18,6 +18,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: HACS validation uses: hacs/action@1ebf01c408f29afcb6406bd431bc98fd8cbb15aa # main with: diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml new file mode 100644 index 00000000..0ac358b4 --- /dev/null +++ b/.github/workflows/workflow-lint.yml @@ -0,0 +1,69 @@ +name: Workflow lint + +# Lints the CI configuration itself: +# actionlint — YAML/expression/shell errors inside workflow files +# zizmor — security audit (template injection, credential persistence, +# over-broad permissions, unpinned actions) +# +# Path-filtered: only runs when the CI config actually changes. + +on: + push: + branches: [main] + paths: + - ".github/workflows/**" + - ".github/dependabot.yml" + pull_request: + branches: [main] + paths: + - ".github/workflows/**" + - ".github/dependabot.yml" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + actionlint: + name: actionlint + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run actionlint + run: | + bash <(curl -sSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) + ./actionlint -color + + zizmor: + name: zizmor + runs-on: ubuntu-latest + + permissions: + contents: read + security-events: write + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Install zizmor + run: python -m pip install --upgrade pip "zizmor==1.29.0" + + - name: Run zizmor + run: zizmor --no-progress --persona regular --min-severity medium . diff --git a/.gitignore b/.gitignore index 0c1dbe0d..430a482f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,11 @@ scripts/screenshots/ # Node / JS tooling (CI-3) node_modules/ + +# Dev container scratch HA config +dev-config/ + +# Coverage output +.coverage +coverage.xml +htmlcov/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..3612efee --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,65 @@ +# Local pre-commit hooks — catch in a second what CI would otherwise catch in +# five minutes. Optional for contributors; CI remains the source of truth. +# +# pip install pre-commit && pre-commit install +# pre-commit run --all-files # one-off sweep +# +# pre-commit.ci (the GitHub App) runs the same hooks on PRs and pushes an +# autofix commit when a hook rewrites a file. + +ci: + autofix_commit_msg: "style: pre-commit.ci autofixes" + autoupdate_commit_msg: "chore(deps): pre-commit autoupdate" + autoupdate_schedule: monthly + # Node isn't available in the pre-commit.ci runner, and the data checks need + # the repo's own Python — both already run in GitHub Actions. + skip: [eslint, translation-parity, data-files] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + # Preserve the two-space markdown line break. + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + # Blueprints use HA's !input tag, which plain YAML can't resolve. + exclude: ^blueprints/ + - id: check-merge-conflict + - id: check-case-conflict + - id: check-added-large-files + args: [--maxkb=1024] + - id: mixed-line-ending + args: [--fix=lf] + + # Keep this rev in step with RUFF_VERSION in .github/workflows/lint.yml. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 + hooks: + - id: ruff + args: [--fix] + + - repo: local + hooks: + - id: eslint + name: ESLint (cards + panel) + entry: npm run lint --silent + language: system + pass_filenames: false + files: ^custom_components/taskmate/www/.*\.js$ + + - id: translation-parity + name: Every locale matches en.json + entry: python3 scripts/check_translations.py + language: system + pass_filenames: false + files: ^custom_components/taskmate/(translations|www/locales)/.*\.json$ + + - id: data-files + name: Blueprints, sentences and metadata parse + entry: python3 scripts/check_data_files.py + language: system + pass_filenames: false + files: ^(blueprints/|custom_sentences/|hacs\.json|custom_components/taskmate/manifest\.json) diff --git a/README.md b/README.md index 63beaf55..e3ae1d99 100755 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@

TaskMate

- +

TaskMate

- +

Turn chores into a game your kids actually want to play.
A Home Assistant integration for family chore management, smart rewards, and streak tracking.

- +

Latest Release HACS Default @@ -22,14 +22,17 @@ hassfest Tests Lint + Data checks + CodeQL + pre-commit.ci

- + > Originally created by [vinnybad/choremander](https://github.com/vinnybad/choremander). This fork adds 20 Lovelace cards, a bonus points system, streak tracking, reward approval flow, a penalty system, and much more. - + --- - + ## Contents - + - [How It Works](#how-it-works) - [Installation](#installation) - [Setup](#setup) @@ -79,47 +82,47 @@ - [Finding IDs](#finding-ids) - [Troubleshooting](#troubleshooting) - [Tips](#tips) - + --- - + ## How It Works - + 1. **Create chores** — assign them to children, set point values and schedules 2. **Kids complete chores** — tap on the child card to tick off chores, earn points, build streaks 3. **Parents approve** — chores set to "requires approval" go into a pending queue 4. **Kids claim rewards** — when they have enough points, they claim a reward 5. **Parents approve claims** — points are only deducted once a parent approves 6. **Bonus points** — weekend multipliers, streak milestones, and perfect week bonuses add extra motivation - + All data is stored locally in Home Assistant. Nothing leaves your instance. - + --- - + ## Installation - + ### Via HACS (Recommended) - + TaskMate is a **default HACS integration** — no custom repository needed: - + 1. Open **HACS** → search **"TaskMate"** 2. Click **Download** 3. **Restart Home Assistant** 4. Add the integration: **Settings → Devices & Services → Add Integration → TaskMate** - + Or use the one-click buttons: - + [![Open TaskMate in HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=tempus2016&repository=taskmate&category=integration)   [![Add TaskMate integration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=taskmate) - + > Already installed TaskMate as a custom repository from before it was accepted into HACS? It keeps working and keeps receiving updates — you can safely remove the custom repository entry, HACS now tracks it by default. - + ### Manual - + 1. Download the [latest release](https://github.com/tempus2016/taskmate/releases/latest) 2. Copy the `taskmate` folder to `/config/custom_components/taskmate/` 3. **Restart Home Assistant** - + ### Requirements - **Home Assistant** 2024.1 or newer @@ -130,20 +133,20 @@ Or use the one-click buttons: All TaskMate data — children, chores, points, reward claims, completion history — is stored inside your Home Assistant instance via HA's native storage helpers. Nothing is sent to any external service. --- - + ## Setup - + ### Add the Integration - + 1. **Settings** → Devices & Services → **Add Integration** → search "TaskMate" 2. Choose your **points name** (Stars, Coins, Points, Bucks — whatever motivates your children) and an **icon**. Both can be changed later in the panel. - + That's the only thing the config flow asks. There is **no "Configure" button** on the integration card — all day-to-day management lives in the TaskMate panel below. (The legacy options/configure flow was removed in v4.0.) - + ### The TaskMate Panel - + After installing, a **TaskMate** entry appears in the Home Assistant sidebar. This is the management hub for everything — open it and you'll find: - + - **Children** — add children, set avatars, gift points - **Chores** — create and edit chores, reorder, bulk-add, save/apply templates - **Rewards** — create rewards with a fixed point cost (standard, jackpot, or savings-pool) @@ -154,29 +157,29 @@ After installing, a **TaskMate** entry appears in the Home Assistant sidebar. Th - **Templates** — reusable chore sets - **Notifications** — route approval alerts to parent devices and per child - **Settings** — points name & icon, **default card design**, history retention, streak mode, weekend multiplier, difficulty multipliers, and the bonus-points system - + The **Activity** section shows a live feed, and chore/reward approvals are handled right in the panel. It's fully translated in all supported languages and updates in real time via WebSocket. - +

TaskMate admin panel

- + See the [Admin Panel wiki page](https://github.com/tempus2016/taskmate/wiki/Admin-Panel) for details. ### Add Cards to Your Dashboard - + Lovelace resources are registered automatically on startup — no manual setup needed. - + 1. Edit your dashboard → **Add Card** 2. Search "taskmate" or scroll to **Custom** cards 3. Select a card, set `sensor.taskmate_overview` as the entity, configure options, save - + --- - + ## Chores & Rewards - + ### Chore Fields - + | Field | Description | |-------|-------------| | **Name** | Display name shown on the child card | @@ -192,37 +195,37 @@ Lovelace resources are registered automatically on startup — no manual setup n | **Visibility Entity** | *(Optional)* Home Assistant entity ID that controls when this chore appears on the child card. Leave empty to always show the chore. Examples: `binary_sensor.dishwasher`, `sensor.soil_moisture`, `input_boolean.guest_mode`. See [Dynamic Chore Visibility](#dynamic-chore-visibility) for details. | | **Visibility Operator** | How to compare the entity's current state with your target value. Options: `Equals`, `Not Equal`, `≥`, `≤`, `>`, `<`. See [Dynamic Chore Visibility](#dynamic-chore-visibility) for guidance. | | **Visibility State** | The value to compare against. For text operators (Equals, Not Equal), enter any state value like `on`, `home`, or `away`. For numeric operators, enter a number like `30` or `50.5`. | - + ### Reward Types - + | Type | How to Set | Description | |------|-----------|-------------| | **Standard** | Set `Points Cost` | Fixed cost set by the parent | | **Jackpot** | Enable "Jackpot" toggle | A shared family goal — everyone deposits into one pooled jar (jackpots are always pool-mode) and it's redeemed once the combined total reaches the cost | - + ### Reward Approval Flow - + Claiming is a two-step process — children can't instantly redeem rewards without parental oversight: - + 1. Child taps **Claim** → pending claim created, points **not yet deducted** 2. Parent sees the claim in the **Claims** tab of the Parent Dashboard card 3. **Approve** → points deducted, reward granted 4. **Reject** → claim cancelled, no points affected - + --- - + ## Chore Scheduling - + Chores have two scheduling modes set in **Step 1** of the add/edit chore flow. - + ### Mode A — Specific Days - + Choose which days of the week the chore appears on the child card. Leave empty to show every day. Use `due_days_mode` on the child card to hide or dim non-scheduled days. - + ### Mode B — Recurring - + The chore has a rolling recurrence window. Once completed, it cannot be done again until the window expires — measured in days from the date of last completion (midnight-rounded). - + | Recurrence | Window | |---|---| | Every 2 days | 2 days | @@ -231,45 +234,45 @@ The chore has a rolling recurrence window. Once completed, it cannot be done aga | Monthly | 30 days | | Every 3 months | 90 days | | Every 6 months | 180 days | - + **Optional settings for recurring chores:** - **Day of Week** — for Weekly/Every 2 Weeks, pin the chore to a specific day - **Start Date** — for Every 2 Days, set the anchor date for the rhythm - **First Occurrence** — Available Immediately (default) or Wait for First Scheduled Occurrence - + **Child card behaviour:** Use `recurrence_done_mode` on the child card to control what happens when a recurring chore has been completed and is waiting to reset — `dim` (default), `hide`, or `show`. - + --- - + ## Chore Dependencies - + Chain chores together so one only becomes available after others are finished. A chore with dependencies stays **locked** until **every** chore it depends on has been completed and approved **today** by the **same child** — then it unlocks for that child. This turns a loose list into an ordered routine: tidy the room *before* vacuuming, clear the table *before* loading the dishwasher. - + - Set prerequisites in the chore editor (**Admin Panel → Chores → Edit chore**) via the **"Depends on"** picker — pick one or more of your other chores (stored as the chore's `depends_on` list). All selected prerequisites must be satisfied before the dependent chore unlocks. - Each prerequisite needs an **approved** completion (a pending one doesn't count), made **today**, by the **same child**. Parent completions count; bonus sub-tasks do not. - The check is **per day** — dependencies reset every night. If a prerequisite is rejected or its approval is undone, the dependent chore locks again. - Dependencies are one more gate on top of scheduling, rotation, vacation status, and [Dynamic Chore Visibility](#dynamic-chore-visibility). While locked, the chore follows the same hide/dim child-card rules as any other unavailable chore. - + See the [Chore Dependencies wiki page](https://github.com/tempus2016/taskmate/wiki/Chore-Dependencies) for full details. - + --- - + ## Dynamic Chore Visibility - + Show or hide chores based on the state of a Home Assistant entity. Chores only appear on the child card when the visibility condition is met. - + ### How It Works - + When you create or edit a chore, set the **Visibility Entity** (e.g., `binary_sensor.dishwasher`), **Visibility Operator** (e.g., `Equals`), and **Visibility State** (e.g., `running`). The chore appears on the child card only when the entity's current state matches your condition. - + **Examples:** - **Dishwasher chores** — Set entity to `binary_sensor.dishwasher` with operator `Equals` and state `on`. Chores only appear when the dishwasher is running. - **Soil moisture** — Set entity to `sensor.soil_moisture` with operator `≤` and state `30`. Chores only appear when moisture is 30% or lower. - **Guest mode** — Set entity to `input_boolean.guest_mode` with operator `Not Equal` and state `on`. Chores only appear when guests are not over. - **Temperature threshold** — Set entity to `sensor.temperature` with operator `>=` and state `25`. Chores only appear when it's warm enough. - + ### Visibility Operators - + | Operator | Use When | Example | |----------|----------|---------| | **Equals** | Entity state matches exactly (case-insensitive) | Entity: `binary_sensor.dishwasher`, State: `on` — show when entity is "on" | @@ -278,69 +281,69 @@ When you create or edit a chore, set the **Visibility Entity** (e.g., `binary_se | **≤** (Less or Equal) | Numeric entity value is at or below threshold | Entity: `sensor.soil_moisture`, State: `30` — show when moisture ≤ 30% | | **>** (Greater Than) | Numeric entity value is above threshold | Entity: `sensor.temperature`, State: `25` — show when temperature > 25°C | | **<** (Less Than) | Numeric entity value is below threshold | Entity: `sensor.snow_depth`, State: `10` — show when snow < 10 cm | - + ### Key Points - + - **Optional** — Leave Visibility Entity empty to always show the chore - **Entity types** — Works with any entity: `binary_sensor`, `sensor`, `input_boolean`, `switch`, `number`, etc. - **Update frequency** — Visibility is checked every 30 seconds (coordinator refresh interval) - **Safe fallback** — If the entity becomes unavailable, the chore defaults to **visible** - **Frontend only** — Visibility only hides chores from the child card. Parent services like `taskmate.complete_chore` will still work on hidden chores - **Numeric handling** — Numeric operators automatically convert entity state to a number. If conversion fails, the operators fall back to string matching. - + ### Common Use Cases - + **Chores that only appear when equipment is in use:** - Dishwasher loading chores when `binary_sensor.dishwasher` is `on` - Laundry folding when `binary_sensor.dryer` is `on` - + **Seasonal or conditional chores:** - Snow shovelling when `sensor.snow_depth` > 5 cm - Watering plants when `sensor.soil_moisture` ≤ 40% - Yard work when `binary_sensor.guest_mode` is `off` - + **Time-based or system state:** - Tasks only when home alone (entity not set to `away`) - Tasks only during school days (custom `input_boolean.school_day`) - Tasks only when a family member is home (entity is `home`) - + --- - + ## Weather-Aware Chores - + Outdoor chores can hide themselves when the weather is unsuitable. Open a chore and expand **Advanced — weather conditions**. - + | Setting | Effect | |---|---| | **Weather entity** | The `weather.*` entity to read. Leave empty to ignore the weather entirely. | | **Hide when the weather is** | Any number of conditions — rainy, pouring, snowy, sleet, hail, lightning, thunderstorm, fog, windy, very windy, cloudy, severe. | | **Minimum / maximum temperature** | Hide below / above this. Leave empty for no limit. | | **Maximum wind speed** | Hide above this. Leave empty for no limit. | - + Temperature and wind limits are read from the weather entity's `temperature` and `wind_speed` attributes, in whatever units your entity reports. - + ### Key Points - + - **A rained-off chore is not a missed chore.** The check sits in the same availability path as scheduling, so a hidden chore does not raise a mandatory-miss review item and does not break a streak. - **Fail-open.** A missing entity, an `unavailable`/`unknown` state, or an absent attribute all leave the chore visible. A weather integration going offline never hides the family's chores. - **0 is a real limit.** Leave a limit blank to disable it — `0` means 0°, not "off". - Conditions are checked first, then temperature, then wind. - + ### Common Use Cases - + - Mow the lawn — hide on `rainy` and `pouring` - Wash the car — hide below 2° - Put the bins out — hide above 40 km/h wind - Water the plants — hide on `rainy` (nature did it for you) - + --- - + ## Reactive Chores (Deadlines & Speed Bonus) - + A chore raised by an automation that must be done *now*: "the washing machine finished — empty it within 30 minutes." - + Call `taskmate.add_chore` with `expires_in_minutes`: - + ```yaml triggers: - trigger: state @@ -356,36 +359,36 @@ actions: expires_in_minutes: 30 speed_bonus_points: 5 ``` - + | Field | Effect | |---|---| | `expires_in_minutes` | Deadline this many minutes from now. `0` = no deadline (a normal chore). Max 10080 (a week). | | `speed_bonus_points` | Extra points if the chore is completed before the deadline. | - + ### How It Works - + - A chore with a deadline is automatically **one-shot** — it exists to be done now and never carries into tomorrow. - The child card shows a live **countdown badge** — amber, turning red under five minutes — with the speed bonus alongside it. - Beat the deadline and `speed_bonus_points` is added on top of the normal award. This **stacks** with the early-bonus/late-penalty of `due_time` if the chore has one. - Miss it and the chore disappears and is soft-disabled. The sweep runs on the 30-second poll, so a 30-minute chore doesn't sit on the card until midnight. - Expiry fires a `taskmate_chore_expired` event (`chore_id`, `chore_name`, `deadline_at`, `timestamp`) so an automation can nag, re-raise it, or just log the miss. - + --- - + ## Scheduled Config Changes - + Queue an edit to take effect on a future date — *"from 1 September this chore is worth 20 points"*, *"from November it's disabled for the winter"*. - + Open a chore in the TaskMate panel and expand **Advanced — scheduled changes**. Pick a date, a field, and the new value. Queued changes are listed with a count badge on the section, and can be removed before they fire. - + ### What Can Be Scheduled - + Points · Assigned to · Enabled · Requires approval · Daily limit · Days · Mandatory · Penalty points · Expires on · Description · Time of day · Difficulty - + Runtime state (rotation anchors, skip dates, calendar publish history) deliberately **cannot** be scheduled — a queued change can only touch configuration. - + ### How It Works - + - Changes are applied during **midnight maintenance**. - **Missed days catch up.** If Home Assistant was off on the day a change was due, it's applied on the next start — the parent still expects "from 1 September" to have happened. - **Values are validated when you queue them**, not at midnight weeks later, so a bad value fails in front of you. @@ -393,15 +396,15 @@ Runtime state (rotation anchors, skip dates, calendar publish history) deliberat - **Applied changes are kept**, not deleted, and shown under "Already applied". A config change that happens silently is worse than one that doesn't happen at all. - Applying fires a `taskmate_scheduled_change_applied` event (`change_id`, `chore_id`, `chore_name`, `changes`, `timestamp`). - Deleting a chore removes its queued changes. - + --- - + ## Routine Mode - + A guided, one-task-at-a-time flow for morning and bedtime routines. The child card is a checklist — good for scanning, poor for walking a five-year-old through getting ready. Routine mode shows a single task at a time with a big **Done** button, a progress bar and a celebration at the end. - + Add the **TaskMate Routine** card: - + ```yaml type: custom:taskmate-routine-card entity: sensor.taskmate_overview @@ -409,39 +412,39 @@ child_id: time_category: morning # morning | afternoon | evening | night | anytime | all title: Vaiha's morning # optional ``` - + | Control | Effect | |---|---| | **Done** | Completes the chore and moves to the next one. | | **Skip for now** | Moves on without completing — the task stays outstanding. | | **Back** | Returns to the previous task, so a mis-tap is recoverable. | - + ### Key Points - + - Availability comes from the integration's own chore-availability matrix, so the weather gate, reactive deadlines, dependencies, rotation and vacation mode are all honoured automatically. - Tasks appear in the child's configured chore order. - **`time_category` is exact.** A `morning` routine shows only morning chores — `anytime` chores are *not* mixed in, because a routine is a specific sequence. Use `all` if you want everything. - Chores needing approval show "waiting for a grown-up" and their points are totalled separately on the finish screen. - The finish screen totals what was earned **in that run**, not the whole day. - An empty period shows a "nothing to do" state rather than an empty list. - + --- - + ## Chore Roulette - + An opt-in nudge for the child who has stalled: spin once, get a random outstanding chore, and earn a multiplier on it if they do it. - + Enable it in **Settings** (off by default) and set the multiplier and how many spins a child gets per day. Then add `show_roulette: true` to a child card: - + ```yaml type: custom:taskmate-child-card entity: sensor.taskmate_overview child_id: show_roulette: true ``` - + ### Key Points - + - **Doubly opt-in.** It needs both the global setting *and* `show_roulette: true` on the card, so an existing dashboard never sprouts a new button unasked. - Roulette only ever picks a chore the child is **actually allowed to do right now** — it runs the same availability check as everything else, so the weather gate, deadlines, dependencies and rotation are all respected. - The pick is recorded **per child, per day**. It survives a reload, can't be re-rolled past the daily allowance, and expires overnight. @@ -449,27 +452,27 @@ show_roulette: true - The multiplier is applied at completion, and **stacks** with the difficulty multiplier and any speed bonus. - Spinning fires `taskmate_roulette_spun` (`child_id`, `child_name`, `chore_id`, `chore_name`, `multiplier`, `timestamp`). - A multiplier below 1 is clamped to 1 — spinning should never punish the child. - + Children spin via the `taskmate.spin_roulette` service (`child_id`), which the card calls for them. - + --- - + ## Timed Unlock Rewards - + Spend points to unlock something for a while — the TV, the console socket, a wifi group. Approving the claim turns the entity on; a timer turns it back off. - + **Two deliberate limits** keep this from becoming "a reward can do anything to your house": - + 1. A reward can only **turn one entity on and back off**. There is no free-form service call, no payload, no template. 2. The entity must be on your **allowlist**. - + ### Setting It Up - + 1. **Settings → Unlock allowlist** — add the entities a reward may touch. An entry can be a full entity id (`switch.xbox`) or a bare domain (`switch`) to permit everything in it. **An empty allowlist permits nothing** — this gates household devices, so the safe default when unconfigured is "no". 2. Open a reward, expand **Advanced — timed unlock**, pick an allowlisted entity and a duration (up to 24 hours; `0` leaves it on for you to turn off yourself). - + ### Key Points - + - The allowlist is checked **when the reward is saved and again when it fires** — you can revoke an entity later and any reward pointing at it quietly stops unlocking rather than breaking. - Active unlocks are **persisted**. A Home Assistant restart mid-unlock re-arms the timer; anything already past due is turned off at startup. A restart can never strand the television on. - Turning something **off** is never gated by the allowlist — a revert is always safe. @@ -478,45 +481,45 @@ Spend points to unlock something for a while — the TV, the console socket, a w --- ## Insights — Fairness, Friction, Week Ahead & Health - + **TaskMate panel → Insights.** Answers the question the raw numbers don't: *am I dumping everything on the eldest?* - + For a chosen window (7, 14 or 30 days) it shows each child's completed chores, points earned, share of the family total, and how many distinct days they were active — with a marker showing where an even split would sit. - + ### Key Points - + - **Judged on chore count, not points.** A pricier chore shouldn't be able to hide an uneven split. Points are shown alongside because the two can disagree: three quick jobs versus one hard one is balanced by points and lopsided by count, and only you can say which you meant. - Flagged as *doing more* / *doing less* when a child is more than **15 percentage points** off an even share. Wide enough that normal week-to-week variation doesn't nag; narrow enough to catch a real imbalance. - **Only approved, non-bonus completions count.** Unapproved work isn't yet work you've agreed happened, and bonus sub-tasks hang off a chore that's already counted. - Computed on demand, never cached — a stale report is worse than a slow one. - + ### Friction — what isn't working - + The second Insights view judges each **chore** rather than each child: how often it actually gets done versus how often it came up, over 14 / 30 / 90 days. - + | Verdict | Meaning | Suggestion | |---|---|---| | **Never done** | No completion on record, ever | Retire it | | **Stalling** | Done under 20% of the time it came up | Retire if long dead, otherwise raise the points or reassign | | **Patchy** | Done 20–60% of the time | Raise the points | | **Fine** | Done 60%+ of the time | Leave it alone | - + Chores with outstanding mandatory misses show a ⚠ count, and the report leads with a **suggestion** rather than just a diagnosis — a stalling chore that already pays well doesn't need more points, it needs a different child. - + **What it can't tell you:** TaskMate deletes a completion when you reject it, and removes a mandatory miss once you resolve it, so neither rejection counts nor historical miss counts exist to be reported. The report says so rather than quietly omitting them. Expected counts are approximate — they don't replay rotation, dependencies, weather or vacations. - + ### Week ahead — what's coming up - + The third view projects the next 7 / 14 / 28 days from each chore's schedule: how many chores and points are heading each child's way, what balance they'd reach, and a day-by-day grid. - + Rotation is projected using the same daily-assignment computation the integration itself uses, so alternating / random / balanced picks match what will really happen rather than an independent guess that could drift. - + **It's a ceiling, not a forecast.** A chore open to everyone is counted for *each* eligible child, because the schedule cannot know who'll get there first. Weather, dependencies and availability aren't projected either — they depend on the day. Points that aren't assigned to anyone are called out separately. - + ### Health — is anything broken? - + The fourth view checks the setup itself and reports storage size and entity counts. - + | Check | Severity | |---|---| | A chore depends on a chore that no longer exists (it can never unlock) | **Error** | @@ -524,15 +527,15 @@ The fourth view checks the setup itself and reports storage size and entity coun | A chore references a visibility/weather entity that doesn't exist | Warning | | A reward unlocks an entity no longer on the allowlist (nothing will happen) | Warning | | A child has no chores; completion records for deleted chores; a very large history | Note | - + Every issue carries a plain sentence and a **Show me** button that jumps to where it lives — a diagnostic that only says "3 problems" isn't one you can act on. Notes alone still count as healthy. - + --- - + ## Pre-Reader Mode - + A picture-only child card for children who can't read yet. No chore names, no numbers — a big icon, a row of stars for the points, and a huge tick when it's done. - + ```yaml type: custom:taskmate-child-card entity: sensor.taskmate_overview @@ -540,20 +543,20 @@ child_id: pre_reader: true pre_reader_labels: false # optional; adds the chore name back under each tile ``` - + ### Key Points - + - **Chores now have a picture.** Set one per chore in the panel (next to the description). Without it a tile falls back to the time-of-day icon — fine for one chore, useless for telling "brush teeth" from "get dressed", so set them. - **Points are shown as stars, not digits** (1–5, scaled from the chore's value). A four-year-old can count pictures. - Tiles are at least 128 px with a large tap target, and a **done tile stays tappable** so a mis-tap can be undone, exactly as on the standard card. - **Opt-in.** An existing dashboard never turns into pictures on its own, and names stay off unless you ask for them. - + --- - + ## Read Aloud - + Speak a child's outstanding chores to a media player — *"Ella, you have three things left: make your bed, brush your teeth and pack your bag."* - + ```yaml action: taskmate.read_aloud data: @@ -562,142 +565,142 @@ data: tts_entity: tts.piper # optional; falls back to Settings, then the only one installed message: "Dinner is ready" # optional; says this instead of the summary ``` - + ### Wording - + The sentence comes from **parent-editable templates** in Settings, not from TaskMate's translations. The frontend locales don't reach the backend, and a family may well want phrasing that isn't one of the eight shipped languages. - + | Setting | Default | Placeholders | |---|---|---| | `read_aloud_template` | `{name}, you have {count} things left: {chores}.` | `{name}` `{count}` `{chores}` | | `read_aloud_one_template` | `{name}, you have one thing left: {chores}.` | same | | `read_aloud_done_template` | `{name}, you're all done. Nice one!` | same | | `read_aloud_joiner` | `and` | joins the last two chores | - + A template with a bad placeholder logs a warning and falls back to the built-in wording rather than silencing the feature. - + Fires `taskmate_read_aloud` (`child_id`, `media_player`, `tts_entity`, `message`). - + --- - + ## Accessible Design Style - + A fifth per-card design alongside Classic, Playroom, Console and Clean Pro — pick it per card or as the global default. - + - **Colour-blind safe.** Uses the Okabe-Ito palette, which stays distinguishable under protanopia, deuteranopia and tritanopia. - **High contrast.** Near-black on white (~19:1, comfortably past WCAG AAA), with heavy borders so meaning never rests on hue alone. - **Dyslexia-friendly type.** Atkinson Hyperlegible, drawn for low vision — its letterforms stay distinct where similar glyphs (I/l/1, O/0) usually collapse. - **Dark variant included.** `#0A0A0A` rather than pure black, which blooms on OLED and is harsh with astigmatism. - + --- - + ## Multi-Parent Approval Routing - + By default every approval buzzes every parent. Two other modes are available via the `parent_routing` setting: - + | Mode | Behaviour | |---|---| | `all` *(default)* | Every enabled parent, as before | | `home` | Only parents whose presence entity says they're here | | `round_robin` | One parent per notification, rotating | - + Give each parent a **presence entity** (`device_tracker.*`, `person.*`, anything that reads `home`/`on`/`true`/`present`) in the notification settings. - + ### Every fallback errs towards over-notifying - + An unseen approval is worse than a redundant buzz, so: - + - **Nobody home** → everyone is told, not nobody. - **No presence entity set** → that parent counts as available and is never silently cut out. - **Broken or unavailable presence sensor** → fails open. - **Round-robin state pointing at a deleted parent** → starts from the beginning rather than wedging. - + Round-robin position is tracked **per notification type**, so a reward claim doesn't advance the chore-approval rotation. Child notifications are never affected by any of this — a reminder for a child must always reach that child. - + --- - + ## Sharing Template Packs - + Export your custom chore templates as a JSON pack and import one somebody else made. - + - **Export** — `taskmate/templates/export` returns a pack (all custom templates, or a chosen subset). Built-ins are excluded: they ship with TaskMate, so exporting them would only create duplicates on the other end. - **Import** — `taskmate/templates/import` takes a pack object. - + ### Import is treated as untrusted - + A pack is arbitrary JSON from someone else, so: - + - **Unknown chore fields are dropped**, not carried through. A shared pack cannot set runtime state (rotation anchors, skip dates) or anything the panel wouldn't let you set by hand. - Format and version are checked. A pack from a newer TaskMate says so plainly rather than being half-imported. - Sizes are capped (50 templates, 200 chores each) and long names truncated rather than rejected. - **A clashing name is suffixed, never overwritten** — `Morning routine (2)`. An import must not silently replace something your family built. - A pack that fails validation writes **nothing**. - + **No URL importing.** Packs are imported from pasted or uploaded JSON only. Fetching arbitrary URLs from inside your home network would make TaskMate an SSRF vector; you can still paste a gist's raw contents. - + --- - + ## Printable Weekly Chart - + A fridge-ready A4 chore chart with a box to tick against every task. - + `taskmate/print/weekly_chart` returns a **standalone HTML page** — open it in a tab and print. No external assets, so it prints identically offline. - + | Option | Values | |---|---| | `orientation` | `portrait` *(default)* or `landscape` — **your choice**: two children with short names fit portrait, five need the width | | `week_start` | ISO date anywhere in the week you want (defaults to this week) | | `title` | Heading text (defaults to "This week") | - + ### Key Points - + - **Schedule only.** A paper chart can't know next Thursday's weather, so entity-driven gates aren't applied — a chart that quietly omitted a chore would be worse than one that lists it. - Chores with no assignee appear for every child; assigned ones only for theirs. - Children with nothing assigned are left off entirely rather than printing an empty row. - Chore names are HTML-escaped: they're user input landing in a document. - + --- - + ## Guest Child Profiles - + A visiting cousin gets a temporary child profile that expires on its own and stays out of the family leaderboard. - + Set **Guest** and an end date on a child (`is_guest`, `guest_expires_on`). - + ### Key Points - + - **Guests don't compete.** They're excluded from the leaderboard — a cousin here for a week shouldn't win the month, and their leaving shouldn't read as a loss. - **No end date means no expiry.** You may not know how long the visit is, and silently archiving someone mid-stay would be worse than leaving the profile up. - **Expired guests are archived, not deleted.** The visit's completions stay in history, and next summer the same guest can be reactivated rather than rebuilt. Archiving reuses the existing availability plumbing, so every chore and streak path already treats them as away. - **Promoting a guest to a family member** clears the end date and un-archives them — someone who moves in shouldn't stay invisible. - Archiving fires `taskmate_guest_archived`. - + --- - + ## Bonus Points System - + All bonus settings live in the **Settings** tab of the TaskMate panel (sidebar → TaskMate → Settings).

TaskMate panel settings

- + ### Weekend Points Multiplier - + Children earn extra points for completing chores on Saturdays and Sundays. - + - Default multiplier: **2.0×** — a 10-point chore on Saturday earns 20 points - The multiplier applies to the **completion date**, not the approval date - Configure: `1.0` to `5.0` (set `1.0` to disable) - + ### Streak Milestone Bonuses - + Bonus points are awarded when a child hits a streak milestone. Fully configurable — enter your own milestones as `days:points` pairs: - + ``` 3:5, 7:10, 14:20, 30:50, 60:100, 100:200 ``` - + | Default Streak | Default Bonus | |----------------|---------------| | 3 days | +5 pts | @@ -706,24 +709,24 @@ Bonus points are awarded when a child hits a streak milestone. Fully configurabl | 30 days | +50 pts | | 60 days | +100 pts | | 100 days | +200 pts | - + - Milestones are **re-earnable** after a streak resets - Use the **Streak Milestone Bonuses** toggle as a master on/off switch - Leave the configuration field empty to disable all milestones without turning off the toggle - Invalid formats fall back to the default list with a validation error shown on save - + ### Perfect Week Bonus - + Children earn a bonus when they complete at least one chore every day Monday–Sunday. - + - Checked automatically every **Monday at midnight** - Default: **50 points** — configurable from 10 to 500 - Enable **Perfect week needs all tasks** to require *every* chore due that day to be done (not just one) before the day counts > **Stricter streaks:** the matching **Streak needs all tasks** toggle applies the same rule to daily streaks — a day only extends the streak once every chore due that day is complete. Both default to off, preserving the original "any one chore" behaviour. - + ### Settings Reference - + | Setting | Default | Description | |---------|---------|-------------| | Weekend Points Multiplier | `2.0` | Multiplier on Sat/Sun (1.0 = off) | @@ -735,79 +738,79 @@ Children earn a bonus when they complete at least one chore every day Monday–S | Perfect week needs all tasks | `off` | A day counts only when every chore due that day is done | | Streak Reset Mode | `reset` | `reset` — streak drops to 0 on a missed day; `pause` — streak is preserved | | History Days to Keep | `90` | Completion history retention, 30–365 days | - + --- - + ## Notifications - + TaskMate can notify parents when a chore requiring approval has been completed. - + ### How It Works - + When a child completes a chore that has **Requires Approval** turned on: - + 1. A **persistent notification** is always created in HA — visible in the notification bell in the sidebar 2. If a **notification target** is configured in the panel's Notifications tab, a push notification is also sent - + ### Configuring Push Notifications - + Open the **Notifications** tab of the TaskMate panel (sidebar → TaskMate → Notifications). There you can: - + - Add one or more **parent notification targets**, each pointing at a notify service such as `notify.mobile_app_your_phone`, with an individual enable toggle - Route notifications **per child** to a specific notify service - Send a **test notification** to confirm a target works - + Leave all targets empty to use persistent (in-app) notifications only. > **Note:** Targets must be in the `notify` domain (e.g. `notify.mobile_app_...`). Services from other domains are ignored with a warning in the HA logs. > **Tip:** Use `binary_sensor.taskmate_has_pending_approvals` in your own automations for more customised notification logic. - + --- - + ## Quiet Hours - + Set a per-child do-not-disturb window so TaskMate doesn't ping a child during school or after bedtime. While a child's local time is inside their window, **that child's** notifications are silently held back. Parent notifications are never affected. - + - Set the window per child in the **Admin Panel → Notifications** tab — a **Quiet hours** start and end time alongside each child's notify service. Fill in **both** (`quiet_hours_start` / `quiet_hours_end`, 24-hour `HH:MM`) to enable; clear either to turn it off. - The **end is exclusive**; an equal start and end is treated as disabled. - If the start is later than the end, the window runs **overnight** (e.g. `20:00`–`07:00` covers the evening through to the next morning). Earlier start than end is a same-day window (e.g. `08:30`–`15:30` for the school day). - + See the [Quiet Hours wiki page](https://github.com/tempus2016/taskmate/wiki/Quiet-Hours) for full details. - + --- - + ## Reminder Escalation - + When a mandatory chore is left incomplete, TaskMate can step up its reminders instead of staying silent. Each open same-day mandatory miss climbs a three-rung ladder: - + | Stage | Fires | Audience | When | |---|---|---|---| | 1 — Nudge | `mandatory_reminder` | The child | As soon as the miss is raised | | 2 — Reminder | `mandatory_reminder` | The child | After the **reminder** threshold | | 3 — Parent alert | `mandatory_parent_alert` | Parents | After the **parent** threshold | - + - The two thresholds, both in minutes from when the miss was raised, are set in the **Admin Panel → Notifications** tab under the mandatory-reminder controls: `mandatory_escalation_reminder_minutes` (default `30`) and `mandatory_escalation_parent_minutes` (default `120`), each 1–1440. - The child rungs go only to the affected child (and respect [Quiet Hours](#quiet-hours)); the parent alert goes to the routed parent recipients and is never suppressed. - Both notification types are **off by default** — enable and route the **Mandatory reminder** and **Mandatory parent alert** types. If the child completes the chore, that miss stops climbing. - + See the [Reminder Escalation wiki page](https://github.com/tempus2016/taskmate/wiki/Reminder-Escalation) for full details. - + --- - + ## Weekly Digest & Monthly Report - + TaskMate can send parents a periodic recap of each child's activity. - + - **Weekly digest** (`weekly_digest`) — fires **Sundays at 18:00**, one line per child showing chores completed and points earned this week (approved completions only; bonus subtasks and pending completions excluded). - **Monthly report** (`monthly_report`) — fires on the **1st of each month at 18:00**, recapping the **previous calendar month** per child: chores completed, points earned, level, and best streak. - + Both have the **parent** audience, deliver through the standard notification system, and are **off by default**. Enable and route the **Weekly digest** / **Monthly report** types on the Notifications tab, and use **Send test** to verify routing without waiting for the schedule. - + See the [Weekly Digest wiki page](https://github.com/tempus2016/taskmate/wiki/Weekly-Digest) for full details. - + --- - + ## Penalties Deduct points from a child for unwanted behaviour — the flip side of the reward system. @@ -1075,9 +1078,9 @@ See the [Allowance wiki page](https://github.com/tempus2016/taskmate/wiki/Allowa --- ## Photo Proof - + Evidence photos are attached to the **approval push notification** on the HA companion app, so a parent can approve from the lock screen while actually looking at the tidied room. The URL is signed (24h) because the app fetches attachments without the user's bearer token — an unsigned URL returns 401. Android reads `data.image` and iOS reads `data.attachment.url`; both are sent so one payload works on either. Non-mobile backends (Telegram, email, persistent) get no attachment, since they'd render a raw payload rather than a picture. - + Photo proof lets a chore require evidence before it counts. Turn on **Require photo proof** (`require_photo`) in the chore dialog and that chore's completions **always** go through parent approval — even if Requires Approval is off — and any photo attached is shown to the parent as a thumbnail when they review it. @@ -1187,13 +1190,13 @@ See the [Automations wiki page](https://github.com/tempus2016/taskmate/wiki/Auto --- ## Dashboard Cards - + > **Header colours:** Every card has a configurable `header_color` option in the visual editor, with its own vibrant default. Change it to match your dashboard theme or differentiate kid vs parent cards. - + > **Design styles (v4.2.0+):** Every card also has a `card_design` option — choose **Classic** (the original look), **Playroom** (warm, rounded, picture-book), **Console** (dark, neon game-HUD), or **Clean Pro** (minimal, flat). Set it per card in the visual editor or in YAML (`card_design: playroom`), or leave it on **Global default** (`card_design: global`) to follow the integration-wide style set in the panel's **Settings → Default card design**. Styles are scoped per card, so you can mix and match across a dashboard. - + ### Card Overview - + | Card | Best For | Purpose | |------|----------|---------| | [Child Card](#child-card) | Kids | Complete chores — big buttons, sounds, celebrations | @@ -1215,17 +1218,17 @@ See the [Automations wiki page](https://github.com/tempus2016/taskmate/wiki/Auto | [Calendar Card](#calendar-card) | Both | One-day view of chores assigned to each child | | [Family Goal Card](#family-goal-card) | Both | Live progress toward a shared family-wide points goal | | [Photo Gallery Card](#photo-gallery-card) | Parents | Grid of proof photos from chore completions | - + --- - + ### Child Card - + Kid-friendly chore completion. The entire row is tappable — no small targets. Supports colourful animated badges, confetti celebrations, and completion sounds. Tapping a completed chore undoes it. - +

Child Card

- + ```yaml type: custom:taskmate-child-card entity: sensor.taskmate_overview @@ -1244,17 +1247,17 @@ header_color: "#9b59b6" **`elapsed_time_mode`** — controls what happens to time-of-day chores once that time window has passed without completion. Set to `dim` (default) to grey them out and make them non-interactive, `hide` to remove them entirely, or `show` to leave them active. Chores set to `Anytime` are never affected. Chores that were completed still show with their green done style regardless. Time-of-day periods are fully customisable in **Settings → Time-of-day boundaries** in the TaskMate admin panel: rename the built-in four, change their hours and icons, or add as many of your own (school run, bedtime, …) as you like. Periods can't overlap; gaps between them fall back to Anytime, and a period that still has chores assigned can't be deleted until they're reassigned. A chore's or card's `time_category` accepts any period id. - + --- - + ### Rewards Card - + Shows all available rewards with progress bars and claim buttons. After claiming, the button shows "Awaiting parent approval" until approved. If you don't set `child_id`, the card shows a **child picker** at the top so you can choose who is claiming (for households with more than one child). Jackpot rewards show pooled **deposit** controls and a colour-coded contribution bar per child instead of a single claim button. - +

Rewards Card

- + ```yaml type: custom:taskmate-rewards-card entity: sensor.taskmate_overview @@ -1265,17 +1268,17 @@ header_color: "#e67e22" ``` > **Pool deposits:** Jackpot and savings-pool rewards show quick-deposit buttons plus an **Amount** field where a child can type any custom amount — handy for large goals. Set `deposit_amounts` to change the quick buttons (e.g. `[10, 50, 100]`); the custom field always accepts any value and is capped to the child's spendable balance and the pool's remaining room. - + --- - + ### Approvals Card - + Review and approve or reject chore completions requiring parent sign-off. Items are grouped by date and time of day. - +

Pending Approvals

- + ```yaml type: custom:taskmate-approvals-card entity: sensor.taskmate_overview @@ -1283,19 +1286,19 @@ title: Pending Approvals # optional child_id: a8c8376a # optional — filter to one child header_color: "#27ae60" ``` - + --- - + ### Points Card - + Manually award bonus points or deduct points for consequences — useful for situations outside the normal chore flow. - +

Manage Points

- + Each child row shows two rows of quick-tap buttons — one for adding, one for removing. Tap a button to apply instantly with no dialog. The `⋯` button opens a dialog for a custom amount with an optional reason. - + ```yaml type: custom:taskmate-points-card entity: sensor.taskmate_overview @@ -1305,17 +1308,17 @@ quick_remove_amounts: [1, 5, 10] # configurable remove buttons show_dialog: true # show ⋯ for custom amount + reason header_color: "#2980b9" ``` - + --- - + ### Reorder Card - + Drag-and-drop interface to set the order chores appear for each child. - +

Reorder Card

Saves per-child. - + ```yaml type: custom:taskmate-reorder-card entity: sensor.taskmate_overview @@ -1323,22 +1326,22 @@ child_id: a8c8376a title: Reorder Chores header_color: "#16a085" ``` - + --- - + ### Parent Dashboard Card - + The most useful parent card — four tabs in one: - + - **Overview** — all children's progress and points - **Approvals** — pending chore completions with inline approve/reject - **Claims** — pending reward claims with approve/reject - **Points** — quick +/- buttons per child - +

Parent Dashboard

- + ```yaml type: custom:taskmate-parent-dashboard-card entity: sensor.taskmate_overview @@ -1347,34 +1350,34 @@ quick_points_amount: 5 # points per +/- button press show_claims: true # show the Claims tab header_color: "#c0392b" ``` - + --- - + ### Overview Card - + At-a-glance view of every child — today's chore progress bars, current points, and a pulsing red badge when approvals are pending. Progress counts only chores due today — chores with `due_days` set are excluded from the total on days they are not scheduled. - +

Overview

- + ```yaml type: custom:taskmate-overview-card entity: sensor.taskmate_overview title: TaskMate header_color: "#8e44ad" ``` - + --- - + ### Activity Card - + Scrollable timeline of everything — chore completions, manual point adjustments, bonus point events (weekends, streaks, perfect weeks), and reward claims. Grouped by Today / Yesterday / date. - +

Activity

- + ```yaml type: custom:taskmate-activity-card entity: sensor.taskmate_overview @@ -1383,17 +1386,17 @@ max_items: 30 show_undo: true # optional — set false to hide undo buttons (kid-friendly dashboards) header_color: "#2471a3" ``` - + --- - + ### Streak Card - + Per-child streak display with a dot history grid, current and best streak, and achievement badges. - +

Streak

- + ```yaml type: custom:taskmate-streak-card entity: sensor.taskmate_overview @@ -1401,34 +1404,34 @@ child_id: a8c8376a # optional — filter to one child streak_days_shown: 14 # days shown in the dot history grid header_color: "#e74c3c" ``` - + --- - + ### Weekly Card - + Monday–Sunday bar chart - +

Weekly Card

with headline stats (chores completed, points earned, days active). Counts only approved completions. - + ```yaml type: custom:taskmate-weekly-card entity: sensor.taskmate_overview child_id: a8c8376a # optional — filter to one child header_color: "#27ae60" ``` - + --- - + ### Points Graph Card - + Canvas-based line graph - +

Points Graph

of points over time. Supports multiple children with colour-coded lines and a hover/touch tooltip. - + ```yaml type: custom:taskmate-graph-card entity: sensor.taskmate_overview @@ -1436,17 +1439,17 @@ child_id: a8c8376a # optional — filter to one child days: 14 # date range: 3–90 header_color: "#d35400" ``` - + --- - + ### Reward Progress Card - + Full-screen motivational display - +

Reward Progress

for a single reward — animated progress bar, floating reward icon, and a pulsing "Ready to claim!" badge. Designed for wall-mounted tablets. - + ```yaml type: custom:taskmate-reward-progress-card entity: sensor.taskmate_overview @@ -1455,17 +1458,17 @@ child_id: a8c8376a # optional — show one child's contribution title: Reward Goal header_color: "#7d3c98" ``` - + --- - + ### Leaderboard Card - + Competitive ranking - +

Leaderboard

of all children. Top 3 get gold/silver/bronze styling. For single-child households, automatically shows a personal bests display instead. - + ```yaml type: custom:taskmate-leaderboard-card entity: sensor.taskmate_overview @@ -1586,7 +1589,7 @@ header_color: "#5d6d7e" --- ## Services - + TaskMate exposes services you can call from automations, scripts, or Developer Tools. > **v3.7.0+:** All entity ID fields (`child_id`, `chore_id`, `reward_id`, etc.) now show as **dropdown selectors** in the HA automation editor with entity names as labels. You can still type raw hex IDs if you prefer. @@ -1675,7 +1678,7 @@ data: See the [Services wiki page](https://github.com/tempus2016/taskmate/wiki/Services) for full parameter details, examples, and side effects. **Example — award bonus points from an automation:** - + ```yaml service: taskmate.add_points data: @@ -1683,21 +1686,21 @@ data: points: 10 reason: "Helped with the shopping" ``` - + --- - + ## Jackpot Rewards - + Enable **Jackpot** mode on a reward for big family goals — a holiday, a theme-park trip, a board game everyone wants. Jackpots are **always pooled**: each assigned child **deposits** points into the shared jar (deposited points are locked in), and once the combined total reaches the cost the reward can be **redeemed** — so a goal no single child could afford on their own is reached together. The rewards card shows each child's contribution as a colour-coded bar segment. - + Because jackpots are inherently pooled, the reward editor manages pool mode for them automatically — there's no separate Pool toggle to set. See [Pool Mode (Savings Jars)](#pool-mode-savings-jars) for how depositing and redeeming work. - + --- - + ## Completion Sounds - + The child card plays a sound when a chore is ticked off. All synthesised sounds are generated via the Web Audio API — no external files needed. - + | Sound | Type | Description | |-------|------|-------------| | `coin` | Synth | Classic video game coin collect | @@ -1709,15 +1712,15 @@ The child card plays a sound when a chore is ticked off. All synthesised sounds | `fart1`–`fart10` | Audio file | Real fart sounds (CC0 — BigSoundBank.com) | | `fart_random` | Audio file | Random fart sound 1–10 | | `none` | — | Silence | - + **Fart sounds** require the audio files placed at `/config/www/taskmate/fart1.mp3` through `fart10.mp3`. - + Priority order: **chore-level sound** → **card `default_sound`** → `coin` - + --- - + ## Finding IDs - + Several card options and service calls require a `child_id`, `chore_id`, `reward_id`, etc. ### Method 1 — Show IDs Toggle (easiest) @@ -1770,40 +1773,40 @@ Beyond the sensors above, TaskMate also exposes: | `number.taskmate_perfect_week_bonus` | Perfect-week bonus points (0–1000) | | `select.taskmate_streak_reset_mode` | Streak reset mode (`reset` / `pause`) | | `select.taskmate_card_design` | Global default card design (`classic` / `playroom` / `console` / `cleanpro`) | - + --- - + ## Troubleshooting - + **Cards show "Custom element doesn't exist"** - Hard refresh the browser (Cmd+Shift+R / Ctrl+Shift+R) - Check Settings → Dashboards → Resources — the `/taskmate/*.js` resources should be listed - If resources are missing, restart Home Assistant — they are registered automatically on startup - + **Cards show "Entity not found"** - Make sure you're using `sensor.taskmate_overview` as the entity (not `sensor.taskmate_pending_approvals`) - Verify the TaskMate integration is loaded: Settings → Devices & Services → TaskMate - + **Chore description not showing** - Enable **Show chore description** in the child card editor - Make sure the chore actually has a description set in Settings → Manage Chores - + **Points not updating after completing a chore** - If the chore requires approval, points are held in "pending" until a parent approves - The pending points are shown separately in the child card header - + **Streak not incrementing** - Streaks update at midnight. If you complete chores late at night and check before midnight, the streak counter won't have updated yet - If Streak Reset Mode is set to `reset`, missing a single day resets the streak to 0 - + **Resources keep disappearing after restart** - Restart Home Assistant — Lovelace resources are registered automatically on startup - If the problem persists after restarting, check the HA logs for errors from the `taskmate` integration - + --- - + ## Tips - + - **Two dashboards** — One for children (Child Card + Rewards Card), one for parents (Parent Dashboard). Children don't need to see the approval queue - **Completion %** — Set this lower for optional or weekly chores. If a chore is done twice a week, set it to ~30%. This prevents infrequent chores from inflating reward costs - **Due Days** — Use these so Monday's homework doesn't appear on Saturday. Set `due_days_mode: hide` on the child card @@ -1814,11 +1817,11 @@ Beyond the sensors above, TaskMate also exposes: - **Header colours** — Each card has its own default colour. Customise them in the visual editor to make the children's dashboard bright and fun, and the parent dashboard more neutral - **Per-chore sounds** — Set `completion_sound: fanfare` on harder chores to make completing them feel more rewarding than easy ones - **Time-of-day cards** — Set `time_category: morning` on a card for the breakfast routine and `elapsed_time_mode: dim` so missed morning chores grey out automatically once it's afternoon — no clutter, no guilt trips - + --- - + ## Changelog - + ### v5.1.0 Two features aimed at the two things you do most: **give a chore a real photograph**, and **adjust a child's points without leaving the panel**. Plus a correlation id for anyone wiring TaskMate into something else. No configuration changes — upgrade is drop-in. @@ -1865,7 +1868,7 @@ A fix release for the v5.0.2 tap-to-open notification feature. - CI and dev-dependency updates. ([#736](https://github.com/tempus2016/taskmate/pull/736), [#737](https://github.com/tempus2016/taskmate/pull/737), [#738](https://github.com/tempus2016/taskmate/pull/738)) ### v5.0.2 - + Adds a navigation target to notifications, so tapping one opens a place of your choosing. **New** @@ -1874,7 +1877,7 @@ Adds a navigation target to notifications, so tapping one opens a place of your **Note on upgrade:** notification taps now open the TaskMate panel (`/taskmate-admin`) by default. If you preferred the previous behaviour (open wherever the app was last), clear the **When tapped, open** field or set it to `noAction`. ### v5.0.1 - + A bug-fix release. All of v5.0.0's new features work as documented; upgrade is drop-in with no configuration changes. **Fixes** @@ -1884,9 +1887,9 @@ A bug-fix release. All of v5.0.0's new features work as documented; upgrade is d - **Pre-reader mode now works on every card design** — `pre_reader: true` was silently ignored on any style but Classic. The picture tiles now render under all five designs, so pre-reader pairs correctly with the Accessible style. ([#728](https://github.com/tempus2016/taskmate/pull/728)) - **The Photo Gallery and Family Goal cards now follow `card_design`** — both cards ignored the design setting and always rendered in the Classic look. They now take the active design's colours and fonts like every other card. ([#731](https://github.com/tempus2016/taskmate/pull/731)) - **The avatar picker works on every card design** — tapping a child's avatar to switch it worked on the Classic child card only. It now opens under all five designs. ([#731](https://github.com/tempus2016/taskmate/pull/731)) - + ### v5.0.0 - + The largest release so far: fourteen new features across chores, rewards, cards, notifications and the admin panel. Nothing is removed and no configuration changes — existing setups upgrade untouched. The major version marks the size of the addition, not a breaking change. **New — chores** @@ -1915,57 +1918,57 @@ The largest release so far: fourteen new features across chores, rewards, cards, - **Shareable template packs** — export your custom chore templates as a JSON pack, and import one somebody else made. ([#707](https://github.com/tempus2016/taskmate/pull/707)) - **Printable weekly chart** — a fridge-ready A4 chore chart with a box to tick against every task. ([#708](https://github.com/tempus2016/taskmate/pull/708)) - **Guest child profiles** — a visiting cousin gets a temporary profile that expires on its own and stays out of the family leaderboard. ([#710](https://github.com/tempus2016/taskmate/pull/710)) - + **Fixes** - **Routine mode now honours the card design setting** — the new routine card shipped without design-system wiring, so it accepted `card_design` and silently ignored it, rendering identically under every style. It now follows the per-card and global design setting like every other card, including the new Accessible style, and has a design picker in its editor. ([#722](https://github.com/tempus2016/taskmate/pull/722), fixes [#721](https://github.com/tempus2016/taskmate/issues/721)) - **Two high-severity dev-dependency advisories resolved** — build tooling only; nothing shipped to Home Assistant was affected. ([#711](https://github.com/tempus2016/taskmate/pull/711)) - + ### v4.5.1 - + **Improved** - **Easier mobile navigation in the admin panel** — on phones, the admin panel no longer packs every section into a single horizontally-scrolling tab strip that you had to swipe across to reach Settings. Instead a **section picker** shows the current section; tap it to open a grouped list (Today / Manage / System) that mirrors the desktop sidebar — with the same count badges and highlight — and jump straight to any section in one tap. Desktop is unchanged. ([#669](https://github.com/tempus2016/taskmate/pull/669)) - **Clickable template packs** — on the **Templates** tab, built-in and custom pack cards are now clickable and open the preview (chore list + **Create N chores**); the chores toolbar button is clearer as **Create from template** (all locales). ([#665](https://github.com/tempus2016/taskmate/pull/665)) - + **Fixes** - **Mobile styling sweep** — audited every card and the admin panel at phone widths and fixed the overflow, clipping and broken-layout issues found: the Children stat grid now lays out as a clean 2×2 instead of leaving empty cells, the points-card add dialog and toast no longer run off-screen on narrow phones, the Parents (no-admin) rows sit inline, and more. ([#666](https://github.com/tempus2016/taskmate/pull/666), [#665](https://github.com/tempus2016/taskmate/pull/665)) - + ### v4.5.0 - + **New** - **Parent access without admin rights** — you can now give a second parent day-to-day control of TaskMate without making them a Home Assistant admin. In **Settings → Parents (no admin rights)**, tick any existing non-admin Home Assistant user; they can then approve or reject chores, adjust points, confirm rewards and allowance payouts, award badges, complete chores on a child's behalf, and undo — straight from the cards. They still **cannot** open the admin panel or change any configuration, and every parent action is recorded in the audit log. Ideal for a partner who wants to run the daily routine but shouldn't have full Home Assistant admin rights. ([#662](https://github.com/tempus2016/taskmate/pull/662), fixes [#661](https://github.com/tempus2016/taskmate/issues/661)) - + ### v4.4.4 - + **New** - **Undo a bonus sub-task by tapping it again** — a completed bonus sub-task on the Child Card can now be un-completed with a second tap, mirroring the tap-to-undo that top-level chores already had. Reverses only that sub-task's points; the parent chore stays done. Works on the classic and all designed card styles. ([#656](https://github.com/tempus2016/taskmate/pull/656), fixes [#653](https://github.com/tempus2016/taskmate/issues/653)) - **Approval notifications clear from your phone when reviewed** — approving or rejecting a chore (or reward) — including with **Approve All** — now dismisses its push notification from the Home Assistant companion app, so a big approval sweep no longer leaves a pile of stale alerts. Clearing is limited to `mobile_app.*` notify targets, so other channels (Telegram, email, persistent) are never spammed. ([#658](https://github.com/tempus2016/taskmate/pull/658), fixes [#655](https://github.com/tempus2016/taskmate/issues/655)) - **In-page lightbox for chore evidence photos** — tapping a chore evidence photo now opens it in an in-page lightbox instead of jumping to the raw image in a new browser tab. Applies to the Approvals card, Parent Dashboard card, the panel's pending-approvals view, and the Photo Gallery card (classic and designed styles). ([#652](https://github.com/tempus2016/taskmate/pull/652)) - + **Fixes** - **Badges can be created and edited from the UI again** — saving a badge failed every time with `extra keys not allowed @ data['combinator']`. The badge editor always sends the `combinator` field (AND/OR), but the `add_badge`/`update_badge` service schemas never allowed it. Both schemas now accept it. ([#657](https://github.com/tempus2016/taskmate/pull/657), fixes [#654](https://github.com/tempus2016/taskmate/issues/654)) - + ### v4.4.3 - + **New** - **Approve All on pending approvals** — when several chore completions are waiting for a parent's sign-off, you can now clear the whole queue with a single **Approve All** button instead of approving each one. The button appears on both the TaskMate panel's pending-approvals view and the **Approvals card**, and runs every award, badge, quest and celebration side-effect exactly as approving each chore by hand would. Backed by a new `taskmate.approve_all_chores` service (approves all pending, or a specific list of `completion_ids`) and a matching `taskmate/approve_all_chores` WebSocket command. ([#647](https://github.com/tempus2016/taskmate/pull/647), [#648](https://github.com/tempus2016/taskmate/pull/648)) - + **Changed** - **Dashboard card-picker suggestions (HA 2026.6+)** — every TaskMate card now implements `getEntitySuggestion`, so Home Assistant's "add card" picker pre-fills a sensible TaskMate entity instead of leaving the card blank when you drop it onto a dashboard. ([#646](https://github.com/tempus2016/taskmate/pull/646)) - + ### v4.4.2 - + **Fixes** - **Rewards Card: depositing points into a pool/jackpot reward no longer crashes** — `allocate_points_to_pool` failed with `'TaskMateCoordinator' object has no attribute 'get_children'` whenever the kiosk cross-child safety check ran (a non-admin/shared-tablet session depositing to an unlinked child). The child roster is now looked up on the correct object, so deposits work again. ([#642](https://github.com/tempus2016/taskmate/pull/642), fixes [#641](https://github.com/tempus2016/taskmate/issues/641)) - + ### v4.4.1 - + **Fixes** - **Photo-proof upload no longer fails on long sessions** — the child card's photo upload sent a manually-cached access token that expires after ~30 min, so an aged session got a `401` shown as the misleading *"Upload failed. Check your connection."* It now refreshes the token when expired and retries once on a `401`. ([#636](https://github.com/tempus2016/taskmate/pull/636)) - **No more `taskmate-panel` "already defined" console error after upgrades** — guarded the panel's `customElements.define()` so a browser briefly holding both the old and new panel module (different `?v=` cache-busters) no longer throws an uncaught error. ([#636](https://github.com/tempus2016/taskmate/pull/636)) - **Config-entity defaults now match the applied bonus** — the `weekend_multiplier` and `perfect_week_bonus` number entities defaulted to `1.0`/`0`, but the bonus logic falls back to `2.0`/`50` when unset, so a fresh install displayed values that didn't match what was actually applied. Defaults aligned. ([#635](https://github.com/tempus2016/taskmate/pull/635)) - + ### v4.4.0 - + **New features** - **Allowance** — convert points into a real-money pocket-money payout at a fixed conversion rate, with a payout ledger and the `taskmate.record_allowance_payout` service. See [Allowance](#allowance-real-money-payouts). - **Family Goals** — a single shared, family-wide points target with a one-time goal-reached notification and the new **Family Goal Card**. See [Family Goals](#family-goals). @@ -1981,14 +1984,14 @@ The largest release so far: fourteen new features across chores, rewards, cards, - **Photo Gallery Card** — browse past photo-proof images. See [Photo Proof](#photo-proof). - **Automation blueprint pack** — ready-made blueprints for common TaskMate events. See [Automation Blueprints](#automation-blueprints). - **Negative-balance policy** — optional `allow_negative_balance` to let penalties push a balance below zero. - + **Fixes & polish** - Card count is now 20; sensors expose new `family_goal`, `season_*`, and `photo_gallery` attributes; the Leaderboard Card gains `career` and `season` sort modes. - + See [GitHub Releases](https://github.com/tempus2016/taskmate/releases) for the full changelog. - + --- - +

License: MIT · Data stays local in your Home Assistant instance

diff --git a/custom_components/taskmate/www/taskmate-child-card.js b/custom_components/taskmate/www/taskmate-child-card.js index d3bf56de..205df8b5 100644 --- a/custom_components/taskmate/www/taskmate-child-card.js +++ b/custom_components/taskmate/www/taskmate-child-card.js @@ -4668,4 +4668,4 @@ console.info( "%c TASKMATE CHILD CARD %c v" + _tmVersion + " ", "background:#9b59b6;color:white;font-weight:bold;padding:2px 4px;border-radius:4px 0 0 4px;", "background:#2c3e50;color:white;font-weight:bold;padding:2px 4px;border-radius:0 4px 4px 0;" -); \ No newline at end of file +); diff --git a/pyproject.toml b/pyproject.toml index b8d52613..fab10550 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,3 +9,18 @@ ignore = [ "E501", # line length handled by formatter preference, not strict "E731", # allow `_m = lambda ...` localisation helpers inside flow steps ] + +[tool.coverage.run] +source = ["custom_components/taskmate"] +branch = true +omit = [ + "custom_components/taskmate/www/*", + "tests/*", +] + +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] diff --git a/requirements_test.txt b/requirements_test.txt index d9e32559..8f2e9669 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,3 +9,6 @@ # 0.13.205 is the newest release compatible with the CI Python (3.12); 0.13.206+ # require >=3.13. This is exactly what the previous unpinned install resolved to. pytest-homeassistant-custom-component==0.13.205 + +# Coverage reporting for CI (Tests workflow renders a per-module summary). +pytest-cov diff --git a/scripts/check_data_files.py b/scripts/check_data_files.py new file mode 100644 index 00000000..eb57cfc5 --- /dev/null +++ b/scripts/check_data_files.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Parse-check every shipped data file, and cross-check manifest/hacs metadata. + +A malformed blueprint or locale file doesn't break the Python tests — it breaks +at install time on a user's Home Assistant, which is the worst place to find out. +This walks every YAML and JSON file we ship and simply proves it parses, then +runs a few cheap consistency checks on the packaging metadata. + +Run: python3 scripts/check_data_files.py +Exit code 0 = everything parses, 1 = at least one problem. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +INTEGRATION = REPO_ROOT / "custom_components" / "taskmate" + +YAML_DIRS = (REPO_ROOT / "blueprints", REPO_ROOT / "custom_sentences") +JSON_DIRS = ( + INTEGRATION / "translations", + INTEGRATION / "www" / "locales", +) +JSON_FILES = (INTEGRATION / "manifest.json", REPO_ROOT / "hacs.json") + +# Home Assistant loads blueprints with its own loader, which understands !input. +# Plain yaml.safe_load would choke on it, so register a passthrough. +yaml.SafeLoader.add_constructor("!input", lambda loader, node: loader.construct_scalar(node)) +yaml.SafeLoader.add_constructor("!secret", lambda loader, node: loader.construct_scalar(node)) + + +def check_yaml(problems: list[str]) -> None: + for directory in YAML_DIRS: + if not directory.is_dir(): + continue + for path in sorted(directory.rglob("*.y*ml")): + rel = path.relative_to(REPO_ROOT) + try: + yaml.safe_load(path.read_text(encoding="utf-8")) + print(f" OK {rel}") + except yaml.YAMLError as err: + print(f" FAIL {rel}") + problems.append(f"{rel}: invalid YAML — {err}") + + +def check_json(problems: list[str]) -> None: + paths = list(JSON_FILES) + for directory in JSON_DIRS: + if directory.is_dir(): + paths += sorted(directory.glob("*.json")) + + for path in paths: + rel = path.relative_to(REPO_ROOT) + if not path.is_file(): + print(f" FAIL {rel} (missing)") + problems.append(f"{rel}: file not found") + continue + try: + json.loads(path.read_text(encoding="utf-8")) + print(f" OK {rel}") + except json.JSONDecodeError as err: + print(f" FAIL {rel}") + problems.append(f"{rel}: invalid JSON — {err}") + + +def check_metadata(problems: list[str]) -> None: + manifest_path = INTEGRATION / "manifest.json" + hacs_path = REPO_ROOT / "hacs.json" + + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + hacs = json.loads(hacs_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return # already reported by check_json + + for key in ("domain", "name", "version", "documentation", "issue_tracker", "codeowners"): + if not manifest.get(key): + problems.append(f"manifest.json: missing required key '{key}'") + + if manifest.get("domain") != "taskmate": + problems.append(f"manifest.json: domain is '{manifest.get('domain')}', expected 'taskmate'") + + # HACS is configured for zip_release — release-zip.yml must keep producing + # this exact filename or the integration becomes uninstallable. + if hacs.get("zip_release") and hacs.get("filename") != "taskmate.zip": + problems.append(f"hacs.json: zip_release is on but filename is '{hacs.get('filename')}', expected 'taskmate.zip'") + + print(f" OK manifest version {manifest.get('version')}, hacs filename {hacs.get('filename')}") + + +def main() -> int: + problems: list[str] = [] + + print("YAML (blueprints, custom sentences)") + check_yaml(problems) + + print("\nJSON (manifest, hacs, translations, locales)") + check_json(problems) + + print("\nPackaging metadata") + check_metadata(problems) + + print() + if problems: + print(f"FAIL — {len(problems)} problem(s):") + for problem in problems: + print(f" - {problem}") + return 1 + + print("PASS — all shipped data files parse and metadata is consistent.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_release.py b/scripts/check_release.py new file mode 100644 index 00000000..d31b6e6d --- /dev/null +++ b/scripts/check_release.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Verify manifest.json's version matches the release tag exactly. + +HACS reads the version out of manifest.json, not out of the git tag. If the two +disagree, HACS shows one version and installs another — and for pre-releases the +full tag string matters, so tag v5.2.0-beta.1 must be version "5.2.0-beta.1", +not "5.2.0". + +Run: python3 scripts/check_release.py v5.2.0 +Exit code 0 = match, 1 = mismatch. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +MANIFEST = REPO_ROOT / "custom_components" / "taskmate" / "manifest.json" + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print("usage: check_release.py ") + return 2 + + tag = argv[1] + expected = tag[1:] if tag.startswith("v") else tag + version = json.loads(MANIFEST.read_text(encoding="utf-8")).get("version") + + print(f"tag: {tag}") + print(f"expected version: {expected}") + print(f"manifest version: {version}") + + if version != expected: + print() + print(f"FAIL — manifest.json says '{version}' but the tag implies '{expected}'.") + print("Fix manifest.json (full tag string, including any -beta.N suffix), then re-tag.") + return 1 + + print("\nPASS — manifest version matches the release tag.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/check_translations.py b/scripts/check_translations.py new file mode 100644 index 00000000..efc654d3 --- /dev/null +++ b/scripts/check_translations.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Check that every locale carries exactly the same keys as English. + +TaskMate ships two independent string catalogues: + + custom_components/taskmate/translations/ — nested, used by Home Assistant + (config flow, services, entities) + custom_components/taskmate/www/locales/ — flat dotted keys, used by the + Lovelace cards and admin panel + +Both must stay in lockstep with ``en.json``. A missing key renders as a raw +key (or English) in the UI; a stray key is dead weight that hides a typo. +This script is the CI gate behind "new strings ship translated in the same PR". + +Run: python3 scripts/check_translations.py +Exit code 0 = all locales match English, 1 = drift found. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +BASE_LOCALE = "en" + +CATALOGUES = ( + ("Home Assistant translations", REPO_ROOT / "custom_components" / "taskmate" / "translations"), + ("Card/panel locales", REPO_ROOT / "custom_components" / "taskmate" / "www" / "locales"), +) + + +def flatten(obj, prefix=""): + """Flatten a nested dict into dotted key paths, so both catalogue shapes compare alike.""" + keys = set() + if isinstance(obj, dict): + for key, value in obj.items(): + path = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + keys |= flatten(value, path) + else: + keys.add(path) + return keys + + +def load_keys(path: Path) -> set[str]: + with path.open(encoding="utf-8") as handle: + return flatten(json.load(handle)) + + +def check_catalogue(label: str, directory: Path) -> list[str]: + problems: list[str] = [] + base_file = directory / f"{BASE_LOCALE}.json" + + if not base_file.is_file(): + return [f"{label}: missing base locale {base_file.relative_to(REPO_ROOT)}"] + + base_keys = load_keys(base_file) + locales = sorted(p for p in directory.glob("*.json") if p.stem != BASE_LOCALE) + + print(f"\n{label} ({directory.relative_to(REPO_ROOT)})") + print(f" {BASE_LOCALE}.json: {len(base_keys)} keys (reference)") + + for locale_file in locales: + try: + locale_keys = load_keys(locale_file) + except json.JSONDecodeError as err: + problems.append(f"{label}: {locale_file.name} is not valid JSON — {err}") + print(f" {locale_file.name}: INVALID JSON") + continue + + missing = sorted(base_keys - locale_keys) + extra = sorted(locale_keys - base_keys) + + if not missing and not extra: + print(f" {locale_file.name}: OK ({len(locale_keys)} keys)") + continue + + print(f" {locale_file.name}: {len(missing)} missing, {len(extra)} extra") + for key in missing: + print(f" missing: {key}") + problems.append(f"{label}: {locale_file.name} is missing key '{key}'") + for key in extra: + print(f" extra: {key}") + problems.append(f"{label}: {locale_file.name} has key '{key}' not present in {BASE_LOCALE}.json") + + return problems + + +def main() -> int: + problems: list[str] = [] + for label, directory in CATALOGUES: + if not directory.is_dir(): + problems.append(f"{label}: directory {directory} not found") + continue + problems += check_catalogue(label, directory) + + print() + if problems: + print(f"FAIL — {len(problems)} translation key problem(s).") + print("Every user-facing string must ship translated into all locales in the same PR.") + return 1 + + print("PASS — all locales match English key-for-key.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 750fd56bc92b4159d9b93dc4643e1b56f9bcd709 Mon Sep 17 00:00:00 2001 From: tempus2016 Date: Mon, 10 Aug 2026 22:12:02 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(ci):=20drop=20the=20CodeQL=20workflow?= =?UTF-8?q?=20=E2=80=94=20default=20setup=20is=20already=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Advanced CodeQL configurations can't upload results while GitHub's default setup is configured, so both Analyze jobs failed. The repo already scans actions, javascript-typescript and python via default setup; its query suite has been raised from 'default' to 'extended' instead, which is what the workflow was adding. --- .github/CONTRIBUTING.md | 4 +-- .github/workflows/codeql.yml | 56 ------------------------------------ README.md | 1 - 3 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ad2368d3..f17e589a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -89,8 +89,8 @@ pre-commit install pre-commit run --all-files ``` -`hassfest`, HACS validation, CodeQL, dependency review and a workflow-security -audit (`zizmor`) also run in CI. +`hassfest`, HACS validation, dependency review and a workflow-security audit +(`zizmor`) also run in CI, alongside GitHub's CodeQL code scanning. ## Translations diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 60cdf0b3..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: CodeQL - -# GitHub's own static analysis. Findings land in the repo's Security tab -# (Security → Code scanning), not in the PR checks list, so this is advisory -# signal rather than a merge gate. - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - # Weekly, so newly published queries get applied to unchanged code too. - - cron: "17 4 * * 1" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - - permissions: - security-events: write - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - language: ["python", "javascript-typescript"] - - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 - with: - languages: ${{ matrix.language }} - # security-and-quality adds maintainability queries on top of the - # default security set — worth it on a codebase this size. - queries: security-and-quality - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 - with: - category: "/language:${{ matrix.language }}" diff --git a/README.md b/README.md index e3ae1d99..c92d60ec 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ Tests Lint Data checks - CodeQL pre-commit.ci