Compare commits
37 Commits
agent/clau
...
9ec15fdc6c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ec15fdc6c | ||
|
|
74456a03b6 | ||
|
|
a5acad4ca4 | ||
|
|
7dfeb78c37 | ||
|
|
9506f1b0ad | ||
|
|
f7c00ca818 | ||
|
|
8443ffbef5 | ||
|
|
7d6caabe8c | ||
|
|
d7dfe1061b | ||
|
|
b517d0112e | ||
|
|
8097dac558 | ||
|
|
6ae37633d7 | ||
|
|
3eab28771b | ||
|
|
8fcfab1cb3 | ||
|
|
54e3e1ce7a | ||
|
|
9ab305855f | ||
|
|
585a723154 | ||
|
|
aeba8de6f4 | ||
|
|
dccdedf568 | ||
|
|
af5462eb0f | ||
|
|
b8a04ed953 | ||
|
|
3911ef424e | ||
|
|
da974f6a77 | ||
|
|
b7a4751520 | ||
|
|
bbf68e10ed | ||
|
|
9b20380c63 | ||
|
|
1ab1799276 | ||
|
|
49dd9f0e4e | ||
|
|
b453c60c33 | ||
|
|
aceeaf36b4 | ||
|
|
9faae5cc90 | ||
|
|
d3d3476986 | ||
|
|
c0aa5af0ce | ||
|
|
87c9b60be0 | ||
|
|
82ac7957c8 | ||
|
|
f0d20d77fa | ||
|
|
988cecb122 |
364
.claude/skills/hassio-addon-workflow/SKILL.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
name: hassio-addon-workflow
|
||||
description: >-
|
||||
End-to-end workflow for alexbelgium/hassio-addons add-on work — scope the change, diagnose
|
||||
against the live add-on with real measurements, get an independent Codex (gpt-5.6-sol) review
|
||||
of the plan, implement, have Codex review the code adversarially, open a PR, resolve the
|
||||
CodeRabbit / Copilot / Codex-connector review comments, and verify the merged result actually
|
||||
works. Use this whenever the task touches a Home Assistant add-on in this repo — fixing a bug
|
||||
or reported issue, tuning RAM, CPU or performance, editing a Dockerfile, config.yaml,
|
||||
cont-init.d script or s6 service, bumping an add-on version, or opening and iterating a PR
|
||||
against hassio-addons. Also use it when asked to "check with codex", "verify with chatgpt", or
|
||||
to resolve bot review comments on an add-on PR. Small tasks (typo fixes, version bumps,
|
||||
one-file tweaks, simple coding questions) route through a light path that skips measurement
|
||||
and Codex reviews — invoking this skill is cheap for small asks too.
|
||||
---
|
||||
|
||||
# Home Assistant add-on workflow
|
||||
|
||||
Triage first, then one of two paths:
|
||||
|
||||
- **Light path** (small, contained tasks): scope → implement the simplest mechanism →
|
||||
validate → PR → resolve bot comments → report honestly.
|
||||
- **Full loop** (diagnosis, performance, defaults, new mechanisms): scope → measure → plan →
|
||||
Codex attacks the plan → implement → simplify → Codex attacks the code → PR → resolve bot
|
||||
comments → verify in production → report with calibrated confidence.
|
||||
|
||||
**The standing rule on both paths:** when a simple solution and a more efficient-but-more-complex
|
||||
one both work, ship the simple one. Complexity is only paid for by a **measurement** showing the
|
||||
simple version has a concrete, user-visible cost on a real host — never by reasoning about
|
||||
hypothetical performance. Slightly less efficient and obviously correct beats faster and harder
|
||||
to review.
|
||||
|
||||
The disciplines below exist because each one, when skipped, produced a specific failure in this
|
||||
repo — the examples throughout are real, not illustrative.
|
||||
|
||||
| Discipline | The failure it prevents |
|
||||
|---|---|
|
||||
| **Triage before you start** | Full ceremony spent on a one-line fix |
|
||||
| **Scope before you work** | Solving the wrong problem, elegantly |
|
||||
| **Evidence before reasoning** | Confident claims that measurement contradicts |
|
||||
| **Reason adversarially** | Shipping something that only works on your host |
|
||||
| **Simplify to the smallest thing that works** | 500 lines of machinery where an option would do |
|
||||
| **Verify before declaring done** | "This should work" — and it doesn't |
|
||||
| **Calibrate and report** | Verified and assumed presented as the same thing |
|
||||
|
||||
**Where things are.** The repo is `alexbelgium/hassio-addons`; each add-on is a top-level
|
||||
directory (`claude_desktop/`, `birdnet-go/`, …). This skill and its scripts are checked into
|
||||
the repo at `.claude/skills/hassio-addon-workflow/` — invoke scripts from the repo root, e.g.
|
||||
`bash "$(git rev-parse --show-toplevel)/.claude/skills/hassio-addon-workflow/scripts/preflight.sh"`.
|
||||
(In the claude_desktop add-on environment the checkout lives at `/data/claude/hassio-addons`;
|
||||
a copy of this skill may also exist under `~/.claude/skills/` — the checked-in copy is
|
||||
canonical.)
|
||||
|
||||
**Three facts to know before you touch anything**, because each is silent when violated:
|
||||
|
||||
- **You cannot test the Docker build** — dockerd does not start here. CI is the only gate.
|
||||
- **Never `git stash` under `/data/claude`** — `refs/stash` is shared across worktrees and
|
||||
sessions, so it is not isolated even in your own worktree.
|
||||
- **Work in a worktree under `/data`, not `/tmp`** (`/tmp` is `noexec`).
|
||||
|
||||
Repo-specific traps live in `references/traps.md`. Read it before implementing — it is the
|
||||
accumulated cost of previous sessions. The repo's own `CLAUDE.md` covers structure, Dockerfile
|
||||
conventions, CI and lint rules.
|
||||
|
||||
---
|
||||
|
||||
## 0. Triage — pick the path
|
||||
|
||||
Classify the task before doing anything else.
|
||||
|
||||
**Light path** — typo/doc fixes, CHANGELOG edits, version bumps, one-file small edits at
|
||||
mechanism levels 1–3 (the ladder in step 3), simple coding questions. Flow: one-sentence scope →
|
||||
pick the simplest mechanism → implement → `scripts/validate.sh <addon> --vs-master` → PR
|
||||
(version bump + CHANGELOG still required) → resolve bot comments. Skip measurement, both Codex
|
||||
reviews, and post-deploy measurement — but still label claims Verified / Checked / Assumed
|
||||
honestly in the report.
|
||||
|
||||
**Full loop** — performance/RAM/CPU work, diagnosis ("why is X slow/broken"), anything changing
|
||||
a shipped default, changes spanning several scripts or mechanisms (a version bump's
|
||||
config.yaml + CHANGELOG + Dockerfile touch is still light), anything at ladder levels 4–6, or
|
||||
when the user asks for a Codex check.
|
||||
|
||||
**Escalation rule** — if a light task grows mid-flight (touches a default, needs a new script or
|
||||
service, reveals a deeper problem), stop and upgrade to the full loop rather than continuing
|
||||
light.
|
||||
|
||||
`references/traps.md` is required reading on **both** paths — traps bite one-liners too.
|
||||
|
||||
## 1. Scope before you work
|
||||
|
||||
Write this down before generating anything. Two sentences is enough, but they must be explicit:
|
||||
|
||||
- **Goal** — the observable outcome, in the user's terms.
|
||||
- **Non-goals** — what you are deliberately not touching. This is the load-bearing half.
|
||||
- **Constraints** — what cannot change (defaults for other users, upgrade paths, arch support).
|
||||
- **Definition of done** — what evidence will demonstrate success.
|
||||
|
||||
A diagnosis task and a fix task have different scopes. "Why is it slow?" is answered by
|
||||
measurement and a written finding; it does not automatically authorize a rewrite. When the user's
|
||||
ask genuinely spans several changes, list them and say which you are doing now.
|
||||
|
||||
Ask about defaults when the answer changes who is affected. Changing a shipped default touches
|
||||
every user of a 120-add-on repo; making it opt-in touches nobody until they choose. That is the
|
||||
user's call, not yours, and it is cheap to ask before implementing rather than after.
|
||||
|
||||
## 2. Evidence before reasoning
|
||||
|
||||
State no cause you have not observed. On a live add-on (`$BUILD_VERSION` set, `HOME=/data/data`)
|
||||
measure the running system rather than reasoning from source — reviewers will hold you to the
|
||||
numbers, and source-derived guesses are where wrong plans come from.
|
||||
|
||||
Pick the tool to the question:
|
||||
|
||||
- **"what is consuming RAM/CPU?"** → `scripts/measure.sh` (PSS and private, not summed RSS; keep
|
||||
the sample at ≥20 s — a 3 s sample measured 2.3% where 20 s measured 21.6%).
|
||||
- **"I set an option and nothing happened"** → `scripts/env_trace.sh <VAR> <process>`, which
|
||||
walks all four stages of the option plumbing and names the one that dropped the value.
|
||||
- **"is this flag/driver/package actually present?"** → look at the artifact itself:
|
||||
`/proc/<pid>/cmdline`, `command -v`, `/var/log/apt/history.log`.
|
||||
|
||||
`references/traps.md#measurement` explains why summed RSS and reserved-vs-resident both matter.
|
||||
|
||||
Before asserting anything, ask what would show it false, then go look:
|
||||
|
||||
- "This process is duplicated" → is it? `ps -ef --forest`, compare parents and start times.
|
||||
- "This costs 500 MB" → is it resident? `grep Rss /proc/<pid>/smaps`.
|
||||
- "This block never runs" → is its payload in the image? `command -v`, `apt` history.
|
||||
- "The flag isn't set" → `tr '\0' '\n' < /proc/<pid>/cmdline`.
|
||||
|
||||
**Verify the revision you are reading.** `scripts/preflight.sh` compares the checkout's
|
||||
`config.yaml` version against the running `$BUILD_VERSION`. A stale branch reads as completely
|
||||
normal and has already cost one full analysis pass.
|
||||
|
||||
When you correct yourself mid-analysis, keep the correction visible in your notes and in what you
|
||||
report. A retracted claim that stays retracted is worth more than one quietly dropped.
|
||||
|
||||
## 3. Plan, then have Codex attack it
|
||||
|
||||
### Choose the mechanism level first (both paths)
|
||||
|
||||
Rank mechanisms and start from the top — choose the level **before writing code** and state it
|
||||
in the plan. Each step down costs more to write, more to review, and more to keep working across
|
||||
base-image upgrades:
|
||||
|
||||
1. **A config value** — an option, a schema constraint, an existing env var.
|
||||
2. **An existing knob** the base image already reads (`MAX_RES`, `DRINODE`, `SELKIES_*`).
|
||||
3. **A few lines in an existing script**, at the point that already runs.
|
||||
4. **A new init script.**
|
||||
5. **A new service, wrapper, or long-running process.**
|
||||
6. **Custom protocol code, or patching someone else's internals.**
|
||||
|
||||
Levels 4–6 automatically mean full loop, and need a reason that survives being said out loud.
|
||||
"Upstream has no knob for this, and I checked" is a reason. "It felt cleaner" is not. If two
|
||||
levels both solve it, the higher (simpler) level wins even when the lower one would be more
|
||||
efficient — see the standing rule at the top.
|
||||
|
||||
### Codex review of the plan (full loop only — skip on the light path)
|
||||
|
||||
Write the plan around the measurements — each proposed change tied to a number — then get an
|
||||
independent read **before** writing code. Codex is a genuinely different model reading the files
|
||||
itself; on this workload it has repeatedly been worth the minutes.
|
||||
|
||||
**Use the CLI, not the MCP tool, for prompts of this size.** `mcp__codex__codex` timed out
|
||||
twice on ~4 KB prompts (2026-08-03); the CLI with the same content succeeded. This overrides the
|
||||
global CLAUDE.md note recommending the MCP tool — that guidance still holds for short questions.
|
||||
Run it backgrounded (`--sandbox read-only` means Codex cannot run anything, so paste every number
|
||||
into the prompt; `- <` feeds the prompt file on stdin):
|
||||
|
||||
```bash
|
||||
codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
|
||||
-c approval_policy='"never"' - < prompt.md > codex_out.txt 2>&1
|
||||
```
|
||||
|
||||
Write the prompt to a file. Include the files to read, your measurements **with numbers**, the
|
||||
proposed changes, and explicit instructions to challenge you. Ask direct questions ("is this
|
||||
really add-on-fixable?", "give the precise flag set") rather than "review this". Codex's sandbox
|
||||
often cannot run local commands and falls back to reading GitHub, so paste the evidence in rather
|
||||
than assuming it will find it.
|
||||
|
||||
**Codex agrees with confident premises.** It has confirmed a wrong conclusion stated too
|
||||
confidently, and separately caught a genuine methodology error in the same review. Treat its
|
||||
confirmations with the same scepticism as its objections — especially about the build.
|
||||
|
||||
### Attack your own plan too
|
||||
|
||||
Before implementing, spend a moment actively trying to break it:
|
||||
|
||||
- What does this do on a host **unlike this one** — no GPU, small `/dev/shm`, aarch64, a VM?
|
||||
- What happens on **upgrade** to someone who configured this by hand?
|
||||
- What is the **blast radius** if the assumption underneath it is wrong?
|
||||
- What am I **inferring** that I could instead **detect at runtime** or **record explicitly**?
|
||||
|
||||
That last question is the highest-yield one here; see [the recurring failure
|
||||
mode](#the-failure-mode-this-loop-keeps-producing).
|
||||
|
||||
## 4. Implement
|
||||
|
||||
Read `references/traps.md` first; the bashio, s6-env, arch-guard and versioning traps are all
|
||||
live and each has shipped a bug.
|
||||
|
||||
Validate with `scripts/validate.sh <addon> --vs-master`.
|
||||
|
||||
Write behavioural tests for anything with branches. Extract an embedded Python heredoc and drive
|
||||
it against fixtures with stubbed env vars; stub `bashio::*` and `df` to exercise shell paths. Test
|
||||
**the regression a reviewer described**, not just the happy path — a test that only covers the
|
||||
case you were already thinking about adds little.
|
||||
|
||||
## 5. Simplify — is this the simplest thing that works?
|
||||
|
||||
Do this once you have something working and before you ask anyone to review it. The question is
|
||||
not "is this good code" but **"what is the smallest change that makes the symptom go away, and
|
||||
why isn't that enough?"** If you cannot answer the second half, the smaller change is the answer.
|
||||
And restating the standing rule: obviously correct and slightly less efficient beats faster and
|
||||
harder to review — efficiency only buys complexity when a measurement shows it matters.
|
||||
|
||||
Checks worth running against your own diff:
|
||||
|
||||
- **Did the diff stay at the ladder level chosen in step 3?** If it crept up a level, either
|
||||
justify that out loud or redo it at the level you chose.
|
||||
- **Can this be solved by deleting instead of adding?** A flag that shouldn't be passed, a
|
||||
process that shouldn't start, a registration that shouldn't be duplicated. Removals cannot
|
||||
regress on hosts you can't test.
|
||||
- **Is the fix bigger than the thing it fixes?** That is a smell, not a rule — but it usually
|
||||
means the problem was framed one level too deep.
|
||||
- **How does this fail in three years**, when the base image, Electron, or upstream has moved?
|
||||
Code that reads a documented knob keeps working. Code that reaches into private internals
|
||||
does not.
|
||||
|
||||
The evidence from this repo is blunt:
|
||||
|
||||
- A rejected PR spent a **388-line TCP proxy plus a 142-line monkeypatch of a private upstream
|
||||
method** to reclaim 159 MB — placing custom transport code in the path of every API request.
|
||||
Both independent reviewers said close it rather than iterate on it.
|
||||
- A ~180-line `ctypes` probe was written to decide whether to enable GPU flags. It worked
|
||||
perfectly, proved the driver was fine, and the change **still did nothing**, because the
|
||||
question it answered was not the question that mattered.
|
||||
- A resolution cap shipped as a **new init script writing an s6 envdir** — the wrong mechanism
|
||||
entirely. Renaming the option to the env var the service already reads (level 1) would have
|
||||
worked, and the new script did not.
|
||||
|
||||
In all three cases the simpler option existed and was skipped. Being able to build the
|
||||
complicated thing is not a reason to.
|
||||
|
||||
## 6. Codex attacks the code (full loop only)
|
||||
|
||||
Same invocation, pointed at `git diff origin/master...HEAD` plus the reasoning behind each hunk.
|
||||
Ask specifically what breaks: upgrade paths, hosts unlike this one, users who configured things
|
||||
by hand. Ask it directly whether a simpler mechanism would achieve the same thing — an outside
|
||||
reader spots one-level-too-deep framing far more easily than the person who just built it.
|
||||
|
||||
## 7. Open the PR
|
||||
|
||||
What CI actually gates on a PR: **`CHANGELOG.md` updated** (hard `exit 1`), the **HA add-on
|
||||
linter** (`frenck/action-addon-linter` in `onpr_check-pr.yaml`, no `continue-on-error` — a
|
||||
config.yaml schema error blocks the PR), and the **add-on image build**. The non-blocking lint
|
||||
is the *weekly Super-Linter*, not the PR checks — don't confuse the two. Nothing checks the
|
||||
version bump — but bump it
|
||||
anyway (`X.Y.Z.N`, never `X.Y.Z-N`, see `references/traps.md#versioning`), because Supervisor will
|
||||
not offer a rebuild without it, and update `README.md` if you added options. Match the existing
|
||||
CHANGELOG heading format, `## X.Y (DD-MM-YYYY)`.
|
||||
|
||||
Write the body to a file and use `gh pr create --body-file`. State what was measured, what
|
||||
changed, **what is not verified**, and how to roll back the riskiest hunk on its own.
|
||||
|
||||
## 8. Resolve review comments
|
||||
|
||||
`scripts/pr_review.sh list|reply|resolve|status|watch <PR>`.
|
||||
|
||||
For every comment, **reproduce the claim before agreeing or disagreeing.** A CodeRabbit finding
|
||||
that `grep -E '^…$'` anchors per line — letting a multi-line value pass validation — was real and
|
||||
provable in one command. A finding that a changelog heading needed a blank line was a false
|
||||
positive against this repo's `.markdownlint.yaml`.
|
||||
|
||||
Reply with the evidence, then resolve the thread. **Push back when you are right**, on the thread,
|
||||
so the maintainer can overrule you — a resolved-but-wrong thread is worse than an open one.
|
||||
Equally, when a reviewer is right, fix the cause rather than papering over the symptom.
|
||||
|
||||
## 9. Verify before declaring done
|
||||
|
||||
Do not write "this should work". Either it was exercised, or say plainly that it wasn't.
|
||||
|
||||
On the light path, verification is `validate.sh` plus CI — anything beyond that is **Assumed**,
|
||||
and the report must say so plainly.
|
||||
|
||||
Distinguish three states and never let them blur:
|
||||
|
||||
- **Verified** — you ran it and observed the result.
|
||||
- **Checked but not exercised** — it parses, lints, type-checks.
|
||||
- **Assumed** — reasoning only. Name the assumption.
|
||||
|
||||
**CI passing and the PR merging prove the build works, not that the change does anything.** Once
|
||||
the rebuilt add-on is running, re-run the measurement that motivated the work. Both changes in the
|
||||
session that produced this skill passed CI, merged, and were **inert**:
|
||||
|
||||
- The Xvfb resolution cap wrote its env file correctly and Xvfb still started at the base-image
|
||||
default — wrong env mechanism for that service.
|
||||
- The GPU flags reached Chromium's command line exactly as intended, and the GPU process still
|
||||
reported `--use-gl=disabled`, having overridden them after its own init failed.
|
||||
|
||||
Cheap post-deploy checks: `tr '\0' '\n' < /proc/<pid>/cmdline` for flags, `/proc/<pid>/environ`
|
||||
for env vars, `scripts/env_trace.sh <VAR> <process>` for the whole option plumbing, and a repeat
|
||||
CPU/PSS sample against the pre-change numbers.
|
||||
|
||||
Some fixes cannot be self-verified. A service reads its environment only at start, so an env-var
|
||||
fix is unproven until the add-on restarts — which needs the user, or `ha-cli` with their
|
||||
agreement. If you cannot restart, the change is **Assumed**, not Verified, and must be reported
|
||||
that way.
|
||||
|
||||
## 10. Calibrate and report
|
||||
|
||||
Close against the scope from step 1, not against what you ended up doing. Structure:
|
||||
|
||||
```
|
||||
What was asked / what shipped — mapped to the original scope
|
||||
Evidence — the numbers, before and after
|
||||
Verified — observed, with how
|
||||
Not verified — and why (e.g. no dockerd locally; CI is the gate)
|
||||
Known broken / left out — explicitly, including anything descoped
|
||||
Risk + rollback — the riskiest hunk and how to revert it alone
|
||||
```
|
||||
|
||||
Lead with anything that did not work. A merged PR that achieved nothing is the single most
|
||||
important sentence in the report, and it must not appear after the summary of what went well.
|
||||
|
||||
Give confidence per claim, not one blanket number, and make it mean something: "measured", "CI
|
||||
verified", "unverified — reasoning only". If a number came from one host, say so.
|
||||
|
||||
---
|
||||
|
||||
## The failure mode this loop keeps producing
|
||||
|
||||
Every bug shipped from the source session came from one move: **measuring this host correctly,
|
||||
then generalising it to all hosts.**
|
||||
|
||||
- `/dev/shm` was 7.7 GB here, so a flag looked useless — but Home Assistant ignores `shm_size`, so
|
||||
elsewhere it is Docker's 64 MB default and removing the flag reintroduces a crash loop.
|
||||
- An MCP entry was identified by its URL — but that URL is the documented default, so the rule
|
||||
would have deleted a user's hand-written configuration.
|
||||
- A GPU probe created a hardware context — but that proved the driver worked, not that Chromium's
|
||||
GPU path did.
|
||||
|
||||
The pattern is always *inference standing in for detection*. Before changing a default, ask what
|
||||
this is like on a host unlike yours. Prefer detecting the condition at runtime over asserting it.
|
||||
When ownership matters, **record it rather than infer it**.
|
||||
|
||||
## Token efficiency
|
||||
|
||||
`rtk` wraps commands via hook automatically. Compress large structured output you will re-read
|
||||
with `mcp__headroom__headroom_compress` (skip error/stack output). Use
|
||||
`mcp__tokensave__tokensave_context` for code exploration. Redirect big output to a file and read
|
||||
only what you need, and poll CI in a **background** task rather than blocking.
|
||||
|
||||
## Bundled files
|
||||
|
||||
| File | Use |
|
||||
|---|---|
|
||||
| `scripts/preflight.sh` | Tools, live-add-on check, revision-vs-running-image check. Exits 2 on mismatch |
|
||||
| `scripts/measure.sh` | RAM (PSS/private) + CPU snapshot; reserved vs resident. Sample ≥20 s |
|
||||
| `scripts/env_trace.sh` | Trace one env var through all four plumbing stages — for "my option did nothing" |
|
||||
| `scripts/validate.sh` | Local linters + CI gates; `--vs-master` shows only findings your diff added |
|
||||
| `scripts/pr_review.sh` | Fetch / reply to / resolve PR review threads; watch checks |
|
||||
| `references/traps.md` | Repo-specific traps — read before implementing |
|
||||
|
||||
Each script's header explains its reasoning; read the script when you use it.
|
||||
212
.claude/skills/hassio-addon-workflow/references/traps.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Repo-specific traps
|
||||
|
||||
Things that look correct and are not. Each cost real time or shipped broken. Read this before
|
||||
implementing; skim the headings, read the ones you're about to touch.
|
||||
|
||||
The repo's own `CLAUDE.md` documents structure, Dockerfile conventions, `updater.json`, CI
|
||||
workflows and lint rules — that is not repeated here.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Environment and workspace](#environment-and-workspace)
|
||||
- [Measurement](#measurement)
|
||||
- [Passing values into base-image services](#passing-values-into-base-image-services)
|
||||
- [Shell and bashio](#shell-and-bashio)
|
||||
- [Dockerfile and architecture](#dockerfile-and-architecture)
|
||||
- [Versioning](#versioning)
|
||||
- [Chromium / Electron under Xvfb](#chromium--electron-under-xvfb)
|
||||
- [CI and review bots](#ci-and-review-bots)
|
||||
|
||||
---
|
||||
|
||||
## Environment and workspace
|
||||
|
||||
**The checkout is probably on the wrong branch.** Checkouts under `/data/claude` are shared and
|
||||
persistent; another session leaves them wherever it finished. A stale branch looks entirely
|
||||
normal. Compare the add-on's `config.yaml` `version` against the running `$BUILD_VERSION` before
|
||||
trusting anything you read. `scripts/preflight.sh` does this.
|
||||
|
||||
**Never run `git stash` under `/data/claude`.** `refs/stash` is shared across every worktree and
|
||||
concurrent session, so it is *not* isolated even in your own worktree. A bare `stash` / `stash
|
||||
pop` pair in a clean worktree once restored another session's stash, producing conflict markers
|
||||
in six untouched files. To compare a file against another revision use
|
||||
`git show <rev>:<path> > /tmp/x`. If a pop does go wrong: a conflicted pop **keeps** the stash
|
||||
entry, so nothing is lost — confirm `git rev-parse HEAD` matches what you pushed, then
|
||||
`git reset --hard HEAD`.
|
||||
|
||||
**Work in a worktree under `/data`, not `/tmp`** — `/tmp` is `noexec`, so scripts there won't run.
|
||||
|
||||
```bash
|
||||
git worktree add --detach /data/claude/.work/<task> origin/master
|
||||
```
|
||||
|
||||
**You cannot test the Docker build.** dockerd does not start in this environment. CI is the only
|
||||
gate. One observed run took ~3 hours, with 20+ runs queued against 2 executing — that was account
|
||||
runner contention, not the diff. Check `gh run list` before concluding your PR is stuck. Poll in
|
||||
a background task, and never claim the build is verified when it hasn't run.
|
||||
|
||||
## Measurement
|
||||
|
||||
**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
|
||||
a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
|
||||
882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB —
|
||||
which completely changes which duplicate is worth removing. Quote private when arguing "removing
|
||||
this saves N MB".
|
||||
|
||||
**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
|
||||
506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
|
||||
|
||||
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
|
||||
Never attribute those totals to the add-on.
|
||||
|
||||
**`rtk` filters some command output.** For a complete listing, redirect to a file and read that
|
||||
(`ps ... > $SP/ps.txt`), or use `rtk proxy <cmd>`.
|
||||
|
||||
## Passing values into base-image services
|
||||
|
||||
The plumbing has four stages. `scripts/env_trace.sh <VAR> <process>` walks all four and tells
|
||||
you which one drops the value — use it rather than reasoning about this from memory.
|
||||
|
||||
1. `/data/options.json` — the user's saved options.
|
||||
2. **Injected export block** — `.templates/00-global_var.sh` writes a literal
|
||||
`export <option>='<value>'` block into *every* service `run` script, using the option name
|
||||
**verbatim**. So `max_resolution` *is* injected; it just isn't a name any service reads.
|
||||
`MAX_RES` would be both injected and read.
|
||||
3. `container_environment` — s6's envdir, read **only** by services whose shebang is
|
||||
`#!/usr/bin/with-contenv`.
|
||||
4. The running process — the only stage that decides behaviour.
|
||||
|
||||
**Name the option exactly as the env var the service reads** (uppercase), the way `DRINODE`,
|
||||
`KEYBOARD` and `TZ` already do. Verified live: `DRINODE` appears as `export DRINODE=…` in all 16
|
||||
service run scripts including `svc-xorg`, and Xvfb runs with `-vfbdevice /dev/dri/renderD128`.
|
||||
|
||||
**Two consequences that are easy to get wrong:**
|
||||
|
||||
- `00-global_var.sh` is cont-init **00**. Any cont-init script numbered higher runs *after* the
|
||||
injection, so it cannot change what a service will see through stage 2.
|
||||
- LSIO's `svc-xorg` starts `#!/usr/bin/env bashio`, **not** `with-contenv`, so it never reads
|
||||
stage 3 at all. Writing `container_environment` for it is a silent no-op — that shipped: the
|
||||
file was written 6 seconds before Xvfb started, and Xvfb still came up at the base-image
|
||||
default.
|
||||
|
||||
**Renaming an option to match a base-image env var moves validation out of your script and into
|
||||
the schema.** `00-global_var.sh` exports empty strings (only objects/arrays/nulls are dropped),
|
||||
and base-image scripts typically test `${VAR+x}` — *set*-ness, not emptiness. So an empty
|
||||
`MAX_RES` becomes `Xvfb -screen 0 "x24"` and the X server does not start. If you make this move,
|
||||
constrain the value in `config.yaml` (`match(^[0-9]{1,5}x[0-9]{1,5}$)?`) in the same commit, or
|
||||
keep a guard script.
|
||||
|
||||
**Open question, unresolved:** what Supervisor does with a stored `options.json` key that no
|
||||
longer exists in the new schema — error, warn, or silently drop. This decides whether renaming an
|
||||
option is safe on upgrade. The `monica` add-on shipped exactly such a rename
|
||||
(`MEILISEARCH_KEY` → `meilisearch_key`) with no migration, which is weak evidence it is
|
||||
tolerated. The base image has an `init-migrations` oneshot reading `/migrations` if a migration
|
||||
is needed. Confirm before renaming a shipped option.
|
||||
|
||||
Whichever mechanism you use, verify the service actually received it:
|
||||
|
||||
```bash
|
||||
tr '\0' '\n' < /proc/<pid>/environ | grep <VAR>
|
||||
```
|
||||
|
||||
**`cont-init.d` runs as root before s6 services start** — that part is true and is the right
|
||||
place for filesystem and permission setup.
|
||||
|
||||
**Anything needing an X display must not run in `cont-init.d`** — Xvfb isn't up yet. Put it in
|
||||
the openbox autostart. (ANGLE's OpenGL backend, for instance, fails with "Could not open the
|
||||
default X display".)
|
||||
|
||||
## Shell and bashio
|
||||
|
||||
**`bashio::config` for lists**: `while read ... < <(bashio::config ...)` silently yields an empty
|
||||
list under errexit — bashio's internals return non-zero and process substitution inherits the
|
||||
failure. Capture with `$(...)` first, then feed a here-string.
|
||||
|
||||
**Scripts shared by symlink**: `80-configuration.sh` and friends are shared with the webtop
|
||||
add-ons. Put add-on-specific logic in a new numbered script instead of editing them.
|
||||
|
||||
**`grep -E '^…$'` anchors per line.** A multi-line config value passes validation on its first
|
||||
line and is then used verbatim. Use bash's `[[ =~ ]]`, which anchors the whole string.
|
||||
|
||||
## Dockerfile and architecture
|
||||
|
||||
**Prefer `BUILD_ARCH` over `TARGETARCH`** — the repo's builder passes `BUILD_ARCH` explicitly,
|
||||
while `TARGETARCH` is BuildKit-provided and may or may not be populated.
|
||||
|
||||
Either way the variable must be declared with `ARG <NAME>` **in the build stage that uses it**;
|
||||
without that it expands empty, the guard never matches, and the block silently does nothing —
|
||||
which is the same dead-`if` failure described just below, and the usual cause of it.
|
||||
|
||||
**Verify a guarded block actually ran** rather than assuming. Check whether its payload exists in
|
||||
the running image (`command -v <tool>`), and cross-check `/var/log/apt/history.log` for the
|
||||
matching `apt-get install` line. An `if` block whose condition never matched leaves no trace and
|
||||
no error — one such block sat dead for weeks while appearing to guarantee driver verification.
|
||||
|
||||
**Don't test for distro-specific filenames.** A guard on
|
||||
`/usr/share/vulkan/icd.d/intel_icd.x86_64.json` named a file Debian does not ship (it installs
|
||||
`intel_icd.json`), so fixing the arch variable alone would have turned dead code into a failing
|
||||
build.
|
||||
|
||||
## Versioning
|
||||
|
||||
**`X.Y.Z.N`, never `X.Y.Z-N`.** A hyphen parses as a semver pre-release, which Supervisor treats
|
||||
as *older* than `X.Y.Z` — the update is never offered.
|
||||
|
||||
Date-based versions (`2026.08.03`) are common here. Check whether master has already moved to the
|
||||
version you were about to use.
|
||||
|
||||
## Chromium / Electron under Xvfb
|
||||
|
||||
**Xvfb offers only indirect/software GLX**, so Chromium probes it, fails, and falls back to CPU
|
||||
rendering — the GPU process runs `--use-gl=disabled` and the renderer `--disable-gpu-compositing`.
|
||||
|
||||
**Passing ANGLE flags is not sufficient.** `--ozone-platform=x11 --use-gl=angle
|
||||
--use-angle=gl-egl` reached Chromium's command line exactly as intended and the GPU process
|
||||
*still* reported `--use-gl=disabled`, having overridden the flag after its own init failed.
|
||||
|
||||
**A standalone ANGLE probe proves less than it appears to.** Loading Claude Desktop's bundled
|
||||
`libEGL.so`, initializing the OpenGL backend and reading back
|
||||
`ANGLE (Intel, Mesa Intel(R) Graphics (ADL-N), OpenGL 4.6)` proves the driver and device work —
|
||||
not that Chromium's GPU process, sandbox, dmabuf import and X11 presentation path work. Codex
|
||||
flagged this distinction during review and was right.
|
||||
|
||||
`--use-angle=gles-egl` is rejected outright by Mesa ("Intel or NVIDIA OpenGL ES drivers are not
|
||||
supported").
|
||||
|
||||
**`--disable-dev-shm-usage`** is a workaround for Docker's 64 MB default `/dev/shm`. Home
|
||||
Assistant **ignores** the add-on's `shm_size`, so the real size varies per install — it was 7.7 GB
|
||||
on one host. Detect at runtime rather than assuming either way; keep the flag when the size
|
||||
cannot be determined, because the crash it prevents is worse than its overhead.
|
||||
|
||||
## CI and review bots
|
||||
|
||||
**What CI actually gates** — checked against the workflows, because assuming costs a cycle:
|
||||
|
||||
- **CHANGELOG updated** — the only hard gate (`onpr_check-pr.yaml`, its single `exit 1`).
|
||||
- **Add-on image build** — real, and slow; one run took ~3 h.
|
||||
- **Lint** — `lint.yml` runs super-linter with `continue-on-error: true` at both call sites, so
|
||||
it *cannot* fail a PR. Fix real findings anyway, but do not treat lint as a blocker.
|
||||
- **Version bump** — no workflow checks it. It is repo convention, and required for Supervisor to
|
||||
offer the rebuild, but it will not fail CI.
|
||||
|
||||
**CI rewrites your shell scripts.** `lint.yml` runs `shfmt -w -i 4 -ci -bn -sr` over every `*.sh`
|
||||
and `run`, plus a `chmod +x` pass, on schedule. Repo-wide reformatting commits land on master
|
||||
without your involvement — another reason a shared checkout goes stale mid-task.
|
||||
|
||||
**Reviewers**: CodeRabbit (deepest — often runs scripts to prove a claim; reviews ~9 minutes
|
||||
after the PR opens, or on `@coderabbitai review`), chatgpt-codex-connector, Copilot, Codacy.
|
||||
|
||||
**Codacy `action_required` is this repo's normal state.** Other open PRs show the same. It
|
||||
exposes no annotations via the API, so its findings are only visible in the maintainer's Codacy
|
||||
account. Note it and move on rather than guessing.
|
||||
|
||||
**Resolving a review thread requires GraphQL** (`resolveReviewThread`); the REST API cannot do it.
|
||||
`scripts/pr_review.sh` wraps fetch / reply / resolve.
|
||||
|
||||
**The repo's `.markdownlint.yaml` does not disable MD022/MD032**, so a CHANGELOG will show
|
||||
dozens of pre-existing heading/list findings. They are noise because lint is `continue-on-error`,
|
||||
not because the config exempts them — don't cite the config as a reason to ignore a finding.
|
||||
|
||||
**Separate new lint findings from pre-existing ones** by linting the same file at `origin/master`
|
||||
and diffing the result sets — otherwise you chase warnings that were already there.
|
||||
`scripts/validate.sh --vs-master` does this.
|
||||
136
.claude/skills/hassio-addon-workflow/scripts/env_trace.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
# Trace one env var through the whole add-on plumbing, to answer "I set the option and nothing
|
||||
# happened".
|
||||
#
|
||||
# This is the single highest-value diagnostic for this repo, because the plumbing has four
|
||||
# separate stages and a value can be present at stage 3 and absent at stage 4 while every script
|
||||
# involved reports success. That exact case shipped: MAX_RES was correctly written to
|
||||
# /run/s6/container_environment/MAX_RES six seconds before Xvfb started, and Xvfb still came up
|
||||
# at the base-image default.
|
||||
#
|
||||
# The four stages:
|
||||
# 1. /data/options.json the user's saved add-on options
|
||||
# 2. injected export block .templates/00-global_var.sh writes `export <option>='<v>'`
|
||||
# into every service run script — as cont-init 00, i.e. BEFORE
|
||||
# any higher-numbered cont-init script can influence it
|
||||
# 3. container_environment s6's envdir, read only by services using `with-contenv`
|
||||
# 4. the running process the only stage that actually matters
|
||||
#
|
||||
# Two consequences worth internalising:
|
||||
# * A cont-init.d script numbered >00 cannot change what stage 2 injected.
|
||||
# * A service starting `#!/usr/bin/env bashio` (LSIO's svc-xorg does) never reads stage 3, so
|
||||
# writing container_environment for it is a silent no-op.
|
||||
#
|
||||
# Usage: env_trace.sh <VAR> [process-name-or-pid]
|
||||
# env_trace.sh MAX_RES Xvfb
|
||||
# env_trace.sh DRINODE Xvfb # a working example, for comparison
|
||||
set -uo pipefail
|
||||
|
||||
VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
|
||||
TARGET="${2:-}"
|
||||
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
|
||||
case "$VAR" in
|
||||
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { echo "invalid env var name: $VAR" >&2; exit 1; } ;;
|
||||
*) echo "invalid env var name: $VAR" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "== tracing ${VAR} =="
|
||||
echo
|
||||
|
||||
echo "1. /data/options.json (the user's saved options)"
|
||||
if [ -f /data/options.json ]; then
|
||||
python3 - "$VAR" <<'PY'
|
||||
import json, sys
|
||||
var = sys.argv[1]
|
||||
try:
|
||||
opts = json.load(open('/data/options.json'))
|
||||
except Exception as err:
|
||||
print(f" could not parse: {err}"); raise SystemExit
|
||||
hit = {k: v for k, v in opts.items() if k.lower() == var.lower()}
|
||||
if hit:
|
||||
for k, v in hit.items():
|
||||
shown = '<empty string>' if v == '' else repr(v)
|
||||
print(f" {k} = {shown}")
|
||||
if k != var:
|
||||
print(f" NOTE: option is named '{k}', not '{var}' — the injected export uses the")
|
||||
print(f" option name verbatim, so a service reading ${var} will not see it.")
|
||||
else:
|
||||
print(f" absent (so the add-on default from config.yaml applies, if any)")
|
||||
PY
|
||||
else
|
||||
echo " /data/options.json not present (not running as an add-on?)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "2. injected 'ADDON ENV' export block in service run scripts"
|
||||
found=0
|
||||
for d in /etc/s6-overlay/s6-rc.d /etc/services.d; do
|
||||
[ -d "$d" ] || continue
|
||||
while IFS= read -r rs; do
|
||||
if grep -qE "^export ${VAR}=" "$rs" 2> /dev/null; then
|
||||
echo " $(grep -E "^export ${VAR}=" "$rs" | head -1) <- $rs"
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$d" -name run -type f 2> /dev/null)
|
||||
done
|
||||
[ "$found" -eq 0 ] && echo " ${VAR} not injected into any service run script"
|
||||
|
||||
echo
|
||||
echo "3. s6 container_environment (only read by services using #!/usr/bin/with-contenv)"
|
||||
seen3=0
|
||||
for d in /var/run/s6/container_environment /run/s6/container_environment; do
|
||||
if [ -f "$d/$VAR" ]; then
|
||||
echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1
|
||||
fi
|
||||
done
|
||||
[ "$seen3" -eq 0 ] && echo " not present in either envdir"
|
||||
|
||||
echo
|
||||
echo "4. the running process (the only stage that decides behaviour)"
|
||||
if [ -z "$TARGET" ]; then
|
||||
echo " no target given; pass a process name or pid as \$2"
|
||||
else
|
||||
# Prefer an exact process-name match. A -f substring match picks up this script's own shell
|
||||
# (its command line contains the name you searched for), which produces confusing noise.
|
||||
if [ -d "/proc/$TARGET" ]; then
|
||||
pids="$TARGET"
|
||||
else
|
||||
pids=$(pgrep -x "$TARGET" 2> /dev/null | head -3)
|
||||
[ -z "$pids" ] && pids=$(pgrep -f "$TARGET" 2> /dev/null | grep -vE "^($$|$PPID)$" | head -3)
|
||||
fi
|
||||
if [ -z "$pids" ]; then
|
||||
echo " no process matching '$TARGET'"
|
||||
else
|
||||
for pid in $pids; do
|
||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2> /dev/null)
|
||||
val=$(tr '\0' '\n' < "/proc/$pid/environ" 2> /dev/null | sed -n "s/^${VAR}=//p")
|
||||
if [ -n "$val" ]; then
|
||||
echo " pid=$pid ($comm): ${VAR}=[$val]"
|
||||
else
|
||||
echo " pid=$pid ($comm): ${VAR} NOT SET"
|
||||
# Naming the shebang is usually the whole answer.
|
||||
for d in /etc/s6-overlay/s6-rc.d /etc/services.d; do
|
||||
while IFS= read -r rs; do
|
||||
if grep -qiE "exec .*${comm}|${comm}" "$rs" 2> /dev/null; then
|
||||
echo " its service $rs starts: $(head -1 "$rs")"
|
||||
head -1 "$rs" | grep -q with-contenv \
|
||||
&& echo " -> uses with-contenv, so stage 3 WOULD reach it" \
|
||||
|| echo " -> NOT with-contenv, so stage 3 can never reach it"
|
||||
break 2
|
||||
fi
|
||||
done < <(find "$d" -name run -type f 2> /dev/null)
|
||||
done
|
||||
fi
|
||||
# What it was actually launched with beats any theory about its environment.
|
||||
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 |
|
||||
grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== reading the result =="
|
||||
echo " present at 4 -> the value reached the process; the bug is elsewhere"
|
||||
echo " at 1+2 but not 4 -> service started before injection, or reads a different name"
|
||||
echo " at 1+3 but not 2 or 4 -> classic silent no-op: wrong mechanism for this service"
|
||||
echo " at 1 only -> option name does not match any env var a service reads"
|
||||
99
.claude/skills/hassio-addon-workflow/scripts/measure.sh
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# RAM/CPU snapshot of the running add-on, built to avoid the two mistakes that make such
|
||||
# snapshots wrong:
|
||||
#
|
||||
# 1. Summed RSS double-counts shared pages. Removing a duplicate process frees its *private*
|
||||
# memory, not its RSS. So PSS and private are reported alongside, and private is the number
|
||||
# to quote when arguing "removing this saves N MB".
|
||||
# 2. A big mapping is not necessarily resident. Large SysV/tmpfs segments are lazily populated,
|
||||
# so reserved size is reported separately from resident.
|
||||
#
|
||||
# Note /proc/meminfo and free show HOST figures (no memory cgroup namespace) — never attribute
|
||||
# those to the add-on.
|
||||
#
|
||||
# Usage: measure.sh [cpu-sample-seconds] (default 20)
|
||||
set -uo pipefail
|
||||
SAMPLE="${1:-20}"
|
||||
if [ "$SAMPLE" -lt 20 ]; then
|
||||
echo "WARNING: a ${SAMPLE}s sample understates CPU badly (a 3s sample measured 2.3% where" >&2
|
||||
echo " 20s measured 21.6% for the same process). Use >=20s for anything you report." >&2
|
||||
fi
|
||||
OUT="${SCRATCH:-${TMPDIR:-/tmp}}/addon-measure.$$"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# rtk filters some output; redirect to a file to get the complete list.
|
||||
ps -eo pid,ppid,user,rss,pcpu,etimes,args --sort=-rss > "$OUT/ps.txt" 2>&1
|
||||
|
||||
echo "== totals =="
|
||||
awk 'NR>1{s+=$4; n++} END{printf " processes=%d summed RSS=%.0f MB (overstates: shared pages counted per-process)\n", n, s/1024}' "$OUT/ps.txt"
|
||||
awk '{t+=$2} END{printf " threads=%d\n", t}' <(ps -eo pid,nlwp --no-headers 2> /dev/null)
|
||||
|
||||
echo
|
||||
echo "== per-process memory (top 20 by PSS) =="
|
||||
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
|
||||
python3 - "$OUT" <<'PY'
|
||||
import os, sys
|
||||
rows = []
|
||||
for pid in filter(str.isdigit, os.listdir('/proc')):
|
||||
try:
|
||||
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\x00', b' ').decode(errors='replace').strip()
|
||||
if not cmd:
|
||||
continue
|
||||
rss = pss = priv = 0
|
||||
for line in open(f'/proc/{pid}/smaps_rollup'):
|
||||
k, _, v = line.partition(':')
|
||||
v = v.split()[0] if v.split() else '0'
|
||||
if k == 'Rss': rss = int(v)
|
||||
elif k == 'Pss': pss = int(v)
|
||||
elif k in ('Private_Dirty', 'Private_Clean'): priv += int(v)
|
||||
except Exception:
|
||||
continue
|
||||
rows.append((pss, rss, priv, pid, cmd))
|
||||
rows.sort(reverse=True)
|
||||
tr = tp = tv = 0
|
||||
for pss, rss, priv, pid, cmd in rows:
|
||||
tr += rss; tp += pss; tv += priv
|
||||
for pss, rss, priv, pid, cmd in rows[:20]:
|
||||
name = (cmd[:26] + '..') if len(cmd) > 28 else cmd
|
||||
print(f" {name:<28} {rss/1024:7.0f}M {pss/1024:7.0f}M {priv/1024:7.0f}M")
|
||||
print(f"\n {'TOTAL':<28} {tr/1024:7.0f}M {tp/1024:7.0f}M {tv/1024:7.0f}M")
|
||||
print(" ^ quote PRIVATE when claiming what removing a process would free.")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
|
||||
ipcs -m 2>/dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
||||
|
||||
echo
|
||||
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
|
||||
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
|
||||
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
|
||||
# number rather than failing.
|
||||
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2>/dev/null; }
|
||||
|
||||
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it
|
||||
HZ=$(getconf CLK_TCK 2>/dev/null) && [ "$HZ" -gt 0 ] 2>/dev/null || HZ=100
|
||||
|
||||
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
|
||||
# and the busiest process is not necessarily a big one.
|
||||
declare -A t0
|
||||
for d in /proc/[0-9]*; do
|
||||
pid=${d#/proc/}
|
||||
[ -r "$d/stat" ] && t0[$pid]=$(jiffies "$pid")
|
||||
done
|
||||
sleep "$SAMPLE"
|
||||
for pid in "${!t0[@]}"; do
|
||||
[ -r "/proc/$pid/stat" ] || continue
|
||||
t1=$(jiffies "$pid") || continue
|
||||
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
|
||||
delta=$(( t1 - ${t0[$pid]} ))
|
||||
[ "$delta" -gt 0 ] || continue
|
||||
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
|
||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2>/dev/null)
|
||||
echo "$pct $pid $comm"
|
||||
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
|
||||
|
||||
echo
|
||||
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2>/dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
||||
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
|
||||
echo " raw ps: $OUT/ps.txt"
|
||||
112
.claude/skills/hassio-addon-workflow/scripts/pr_review.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# Work through bot review comments on a PR. Resolving a thread needs the GraphQL API (the REST
|
||||
# API cannot do it), which is the only reason this script exists.
|
||||
#
|
||||
# pr_review.sh list <PR> every inline comment, grouped
|
||||
# pr_review.sh status <PR> checks + unresolved thread count
|
||||
# pr_review.sh reply <PR> <COMMENT_ID> <text|@file>
|
||||
# pr_review.sh resolve <PR> <THREAD_ID...|--all> --all = every unresolved, asks first
|
||||
# pr_review.sh watch <PR> [minutes] poll checks (run this backgrounded)
|
||||
#
|
||||
# Reviewers seen here: coderabbitai (deepest; reviews ~9 min after open, or on
|
||||
# "@coderabbitai review"), chatgpt-codex-connector, Copilot, Codacy.
|
||||
#
|
||||
# Verify every claim before agreeing. Bots are frequently right and occasionally confidently
|
||||
# wrong; a reproduction takes a minute and decides it either way. Push back with evidence when
|
||||
# you are right — a resolved-but-wrong thread is worse than an open one.
|
||||
set -uo pipefail
|
||||
|
||||
REPO="${HASSIO_REPO:-}"
|
||||
[ -z "$REPO" ] && REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2> /dev/null)
|
||||
[ -z "$REPO" ] && { echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2; exit 1; }
|
||||
echo "repo: $REPO" >&2
|
||||
CMD="${1:-}"; PR="${2:-}"
|
||||
[ -z "$CMD" ] || [ -z "$PR" ] && { sed -n '2,16p' "$0" | sed 's/^# \?//'; exit 1; }
|
||||
|
||||
case "$CMD" in
|
||||
list)
|
||||
echo "== inline comments on #$PR =="
|
||||
gh api "repos/$REPO/pulls/$PR/comments" --paginate \
|
||||
--jq 'sort_by(.created_at)[] | "=== [\(.id)] \(.user.login) | \(.path):\(.line // .original_line) ===\n\(.body)\n"'
|
||||
echo "== review bodies =="
|
||||
gh api "repos/$REPO/pulls/$PR/reviews" \
|
||||
--jq '.[] | select(.body != "") | "--- \(.user.login) (\(.state)) ---\n\(.body[0:4000])\n"'
|
||||
;;
|
||||
status)
|
||||
gh pr checks "$PR" 2>&1 | head -15
|
||||
echo
|
||||
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved path comments(first:1){nodes{author{login}}}}}}}}" \
|
||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | "\(if .isResolved then "resolved" else "OPEN " end) \(.id) \(.comments.nodes[0].author.login) \(.path)"'
|
||||
;;
|
||||
reply)
|
||||
ID="${3:?comment id}"; BODY="${4:?text or @file}"
|
||||
if [ "${BODY#@}" != "$BODY" ]; then
|
||||
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -F body=@"${BODY#@}" --jq '.id' 2>&1)
|
||||
rc=$?
|
||||
else
|
||||
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -f body="$BODY" --jq '.id' 2>&1)
|
||||
rc=$?
|
||||
fi
|
||||
# Silently "succeeding" here is worse than failing: a later session reads the transcript and
|
||||
# believes a reviewer was answered when they were not.
|
||||
if [ "$rc" -eq 0 ] && [ -n "$out" ]; then
|
||||
echo "replied to $ID (comment $out)"
|
||||
else
|
||||
echo "FAILED to reply to $ID: $out" >&2
|
||||
echo " (top-level review bodies have different ids and cannot take replies here)" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
resolve)
|
||||
shift 2
|
||||
ids="$*"
|
||||
if [ "${ids:-}" = "--all" ]; then
|
||||
echo "About to resolve EVERY unresolved thread. Only do this if you have read and"
|
||||
echo "answered each one — a resolved-but-wrong thread is worse than an open one."
|
||||
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:100){nodes{isResolved path comments(first:1){nodes{author{login} body}}}}}}}" \
|
||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | " - \(.comments.nodes[0].author.login) \(.path): \(.comments.nodes[0].body[0:90])"'
|
||||
printf 'Type "yes" to resolve all: '; read -r ok
|
||||
[ "$ok" = "yes" ] || { echo "aborted"; exit 1; }
|
||||
ids=""
|
||||
elif [ -z "$ids" ]; then
|
||||
echo "usage: pr_review.sh resolve <PR> <THREAD_ID...> (or --all, with confirmation)" >&2
|
||||
echo "resolve each thread as you answer it; get ids from: pr_review.sh status $PR" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$ids" ]; then
|
||||
ids=$(gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved}}}}}" \
|
||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id')
|
||||
fi
|
||||
[ -z "$ids" ] && { echo "nothing unresolved"; exit 0; }
|
||||
rfail=0
|
||||
for id in $ids; do
|
||||
r=$(gh api graphql -f query="mutation{resolveReviewThread(input:{threadId:\"$id\"}){thread{isResolved}}}" \
|
||||
--jq '.data.resolveReviewThread.thread.isResolved' 2>&1)
|
||||
echo " $id -> $r"
|
||||
[ "$r" = "true" ] || rfail=1
|
||||
done
|
||||
# exiting 0 on a failed mutation would let a session believe threads were resolved
|
||||
exit "$rfail"
|
||||
;;
|
||||
watch)
|
||||
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short
|
||||
for i in $(seq 1 "$MINS"); do
|
||||
c=$(gh pr checks "$PR" 2> /dev/null | awk '{print $1"="$2}' | tr '\n' ' ')
|
||||
if [ -z "$c" ]; then
|
||||
# Normal in the first minutes after `gh pr create`, and also whenever gh errors.
|
||||
# Calling that "settled" would report success for checks that never ran.
|
||||
echo "[$i] no checks reported yet (gh returned nothing) — still waiting"
|
||||
sleep 60; continue
|
||||
fi
|
||||
echo "[$i] $c"
|
||||
case "$c" in
|
||||
*pending*) sleep 60 ;;
|
||||
*fail* | *error* | *cancel*) echo "settled — with FAILURES (see above)"; wfail=1; break ;;
|
||||
*) echo "settled — all passing"; break ;;
|
||||
esac
|
||||
done
|
||||
echo "note: long queues here are usually account runner contention, not your diff."
|
||||
exit "${wfail:-0}"
|
||||
;;
|
||||
*) echo "unknown: $CMD"; exit 1 ;;
|
||||
esac
|
||||
87
.claude/skills/hassio-addon-workflow/scripts/preflight.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Orient before starting add-on work: what tools exist, are we inside the running add-on, and
|
||||
# — the one that actually bites — does the checkout match what is running?
|
||||
#
|
||||
# Usage: preflight.sh [repo-path] [addon-slug]
|
||||
set -uo pipefail
|
||||
|
||||
REPO="${1:-/data/claude/hassio-addons}"
|
||||
SLUG="${2:-}"
|
||||
|
||||
echo "== tools =="
|
||||
for c in gh git codex rtk headroom tokensave shellcheck hadolint yamllint python3 jq; do
|
||||
printf ' %-11s %s\n' "$c" "$(command -v "$c" > /dev/null 2>&1 && echo yes || echo MISSING)"
|
||||
done
|
||||
[ -x /data/codex/bin/codex-real ] && echo " codex-real yes (prefer: codex exec --model gpt-5.6-sol)"
|
||||
|
||||
echo
|
||||
echo "== running add-on =="
|
||||
if [ -n "${BUILD_VERSION:-}" ]; then
|
||||
echo " BUILD_VERSION=$BUILD_VERSION HOME=${HOME:-?}"
|
||||
echo " -> live measurement is possible; see measure.sh"
|
||||
else
|
||||
echo " not inside a running add-on (no BUILD_VERSION); source-only analysis"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== repo =="
|
||||
# git-aware check: in a worktree .git is a file, not a directory
|
||||
if ! git -C "$REPO" rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo " no git repo at $REPO"
|
||||
exit 0
|
||||
fi
|
||||
cd "$REPO" || exit 0
|
||||
branch=$(git branch --show-current 2> /dev/null || echo "(detached)")
|
||||
echo " path=$REPO"
|
||||
echo " branch=$branch"
|
||||
|
||||
# Another session may be mid-operation in this shared checkout.
|
||||
echo " recent reflog (entries you did not make mean another session is active):"
|
||||
git reflog --date=iso -3 2> /dev/null | sed 's/^/ /'
|
||||
|
||||
# The trap this exists for: a stale branch looks entirely normal.
|
||||
if [ -n "${BUILD_VERSION:-}" ]; then
|
||||
# Hostname is <8-hex>-<slug-with-dashes>. Anchor the hex to 8 chars: an unanchored
|
||||
# [0-9a-f]* also eats real prefixes (dab-radio -> radio, cafe-monitor -> monitor).
|
||||
# Slugs may legitimately contain dashes (birdnet-go), so try both forms.
|
||||
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
|
||||
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
|
||||
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
|
||||
[ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; }
|
||||
done
|
||||
[ -z "$SLUG" ] && SLUG="$base"
|
||||
fi
|
||||
cfg="$REPO/$SLUG/config.yaml"
|
||||
if [ ! -f "$cfg" ]; then
|
||||
echo
|
||||
echo " could not find $SLUG/config.yaml — pass the slug as \$2 to enable the"
|
||||
echo " revision check (this is the check the script exists for)." >&2
|
||||
exit 3
|
||||
fi
|
||||
if [ -f "$cfg" ]; then
|
||||
here=$(grep -E '^version:' "$cfg" | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||
echo
|
||||
echo " $SLUG/config.yaml version = $here"
|
||||
echo " running image BUILD_VERSION = $BUILD_VERSION"
|
||||
if [ "$here" = "$BUILD_VERSION" ]; then
|
||||
echo " MATCH — this checkout corresponds to the running image."
|
||||
else
|
||||
echo " MISMATCH — this branch is NOT what is running."
|
||||
git fetch origin master --quiet 2> /dev/null
|
||||
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null |
|
||||
grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||
echo " origin/master version = ${master:-unknown}"
|
||||
echo " -> work from origin/master; analysing this branch will mislead you."
|
||||
echo
|
||||
echo "== suggested isolated worktree (/tmp is noexec; use /data) =="
|
||||
echo " git worktree add --detach /data/claude/.work/<task> origin/master"
|
||||
echo " NOTE: never 'git stash' under /data/claude — refs/stash is shared."
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== suggested isolated worktree (/tmp is noexec; use /data) =="
|
||||
echo " git worktree add --detach /data/claude/.work/<task> origin/master"
|
||||
echo " NOTE: never 'git stash' under /data/claude — refs/stash is shared across worktrees."
|
||||
128
.claude/skills/hassio-addon-workflow/scripts/validate.sh
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run every linter that CI will run and that works locally. The Docker build is deliberately not
|
||||
# attempted: dockerd does not start in this environment, so CI is the only gate for it — say that
|
||||
# rather than implying the build was checked.
|
||||
#
|
||||
# --vs-master re-lints each changed file at origin/master and prints only findings your diff
|
||||
# ADDED. Without it you will chase warnings that were already in the file.
|
||||
#
|
||||
# Usage: validate.sh [addon-dir] [--vs-master]
|
||||
set -uo pipefail
|
||||
|
||||
ADDON="${1:-}"
|
||||
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
|
||||
VS_MASTER=false
|
||||
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
|
||||
|
||||
if [ -z "$ADDON" ]; then
|
||||
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null |
|
||||
cut -d/ -f1 | sort -u | grep -vE '^\.' )
|
||||
if [ "${#_dirs[@]}" -gt 1 ]; then
|
||||
echo "several changed dirs: ${_dirs[*]}"
|
||||
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1
|
||||
fi
|
||||
ADDON="${_dirs[0]:-}"
|
||||
fi
|
||||
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; }
|
||||
git rev-parse --verify origin/master > /dev/null 2>&1 || {
|
||||
echo "origin/master missing — run: git fetch origin master"; exit 1; }
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
echo "== validating $ADDON =="
|
||||
|
||||
fail=0
|
||||
note() { printf ' %-13s %s\n' "$1" "$2"; }
|
||||
|
||||
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does).
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
|
||||
done < <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
|
||||
[ "$fail" -eq 0 ] && note "bash -n" "ok"
|
||||
|
||||
if command -v shellcheck > /dev/null 2>&1; then
|
||||
sc=$(find "$ADDON" -type f \( -name '*.sh' -o -name 'autostart' -o -name 'run' -o -name 'finish' \) -print0 2> /dev/null |
|
||||
xargs -0 -r shellcheck -x -f gcc 2>&1)
|
||||
if [ -n "$sc" ]; then
|
||||
note "shellcheck" "$(printf '%s\n' "$sc" | grep -c .) finding(s)"
|
||||
printf '%s\n' "$sc" | sed 's/^/ /' | head -20
|
||||
else note "shellcheck" "clean"; fi
|
||||
fi
|
||||
|
||||
command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
|
||||
hl=$(hadolint "$ADDON/Dockerfile" 2>&1)
|
||||
[ -n "$hl" ] && { note "hadolint" "$(printf '%s\n' "$hl" | grep -c .) finding(s)"; printf '%s\n' "$hl" | sed 's/^/ /' | head -10; } || note "hadolint" "clean"
|
||||
}
|
||||
|
||||
if [ -f "$ADDON/config.yaml" ]; then
|
||||
# path passed as argv, never interpolated into Python source
|
||||
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; }
|
||||
import yaml,sys
|
||||
d=yaml.safe_load(open(sys.argv[1]))
|
||||
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
|
||||
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
|
||||
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
|
||||
PY
|
||||
command -v yamllint > /dev/null 2>&1 && {
|
||||
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
|
||||
note "yamllint" "$yl finding(s) (compare with --vs-master)"
|
||||
}
|
||||
fi
|
||||
|
||||
while IFS= read -r f; do
|
||||
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; }
|
||||
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
|
||||
|
||||
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
|
||||
md=$(npx --yes markdownlint-cli2 "$ADDON/CHANGELOG.md" 2>&1 | grep -cE "CHANGELOG.md:[0-9]+")
|
||||
note "markdownlint" "$md finding(s) in CHANGELOG (mostly pre-existing; lint is continue-on-error in CI)"
|
||||
}
|
||||
|
||||
echo
|
||||
echo "== CI requirements =="
|
||||
if git diff --name-only origin/master...HEAD 2> /dev/null | grep -q "$ADDON/CHANGELOG.md"; then
|
||||
note "CHANGELOG" "updated"
|
||||
else
|
||||
# This one IS gated: onpr_check-pr.yaml exits 1 without it.
|
||||
note "CHANGELOG" "NOT UPDATED — this is the one CI hard-gate"; fail=1
|
||||
fi
|
||||
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
|
||||
note "version" "bumped"
|
||||
else
|
||||
# Repo convention and required for the rebuild to be offered — but no workflow gates it,
|
||||
# so this is a warning, not a failure.
|
||||
note "version" "NOT bumped (convention; no rebuild will be offered) — not a CI gate"
|
||||
fi
|
||||
note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only gate"
|
||||
|
||||
if $VS_MASTER; then
|
||||
echo
|
||||
echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
|
||||
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||
git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null | while IFS= read -r f; do
|
||||
git show "origin/master:$f" > "$tmp/base" 2> /dev/null || continue
|
||||
# A missing linter must be a visible skip, not a silent "no new findings":
|
||||
# its "command not found" error is identical for base and head, so comm would
|
||||
# cancel it out and report a false clean.
|
||||
case "$f" in
|
||||
*.sh | *autostart | */run | */finish)
|
||||
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; }
|
||||
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;;
|
||||
*.yaml | *.yml)
|
||||
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; }
|
||||
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;;
|
||||
*Dockerfile)
|
||||
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; }
|
||||
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
cp "$tmp/base" "$tmp/base_f"; b=$(cmd "$tmp/base_f" | sort)
|
||||
a=$(cmd "$f" | sort)
|
||||
new=$(comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a") | grep -c .)
|
||||
[ "$new" -gt 0 ] && { echo " $f: $new NEW finding(s)"; comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a") | sed 's/^/ /' | head -5; }
|
||||
done
|
||||
echo " (nothing listed above = your diff introduced no new lint findings)"
|
||||
fi
|
||||
|
||||
echo
|
||||
[ "$fail" -eq 0 ] && echo "== local validation passed ==" || echo "== local validation FAILED =="
|
||||
exit "$fail"
|
||||
BIN
.github/stargazer_map.png
vendored
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 60 KiB |
BIN
.github/stats.png
vendored
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
.github/stats_addons.png
vendored
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.1 KiB |
2
.github/workflows/daily_stale.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
|
||||
2
.github/workflows/onpr_check-pr.yaml
vendored
@@ -91,7 +91,7 @@ jobs:
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: 🔎 Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2
|
||||
uses: frenck/action-addon-linter@f6bef06a4cee6c67924b0a70be643aacb8500a43
|
||||
with:
|
||||
path: "./${{ matrix.addon }}"
|
||||
|
||||
|
||||
2
.github/workflows/onpush_builder.yaml
vendored
@@ -114,7 +114,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2
|
||||
uses: frenck/action-addon-linter@f6bef06a4cee6c67924b0a70be643aacb8500a43
|
||||
with:
|
||||
path: "./${{ matrix.addon }}"
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ slug="${slug#*_}"
|
||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
||||
# New config location
|
||||
CONFIGLOCATION="/config"
|
||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/config.yaml"
|
||||
CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/config.yaml"
|
||||
else
|
||||
# Legacy config location
|
||||
CONFIGLOCATION="/config/addons_config/${slug}"
|
||||
|
||||
@@ -174,7 +174,7 @@ slug="${slug#*_}"
|
||||
# Check type of config folder
|
||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
||||
CONFIGLOCATION="/config"
|
||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||
CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||
else
|
||||
CONFIGLOCATION="/config/addons_autoscripts"
|
||||
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
|
||||
|
||||
@@ -70,7 +70,7 @@ version: "X.Y.Z" # upstream version (format varies; see Versioning sect
|
||||
ingress: true/false
|
||||
ingress_port: 8000
|
||||
map:
|
||||
- addon_config:rw # /addon_configs/<hostname>/
|
||||
- app_config:rw # /app_configs/<hostname>/
|
||||
- share:rw
|
||||
- media:rw
|
||||
- ssl
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.08 (2026-08-01)
|
||||
|
||||
- Addon versions written in config.yaml now always comply with Home Assistant versioning: an upstream tag Home Assistant cannot order (`version-bf9e0b4f`, `ubuntu-2026-06-01`, ...) or would sort as older (`1.2.3-2`, `1.2.3+4`) no longer lands in config.yaml. The addon number is incremented instead, while the raw upstream tag stays in updater.json so the same release is never published twice
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
## 3.19
|
||||
|
||||
- New HA config logic implemented. Files are now located in the addon config file, that can be accessed from the addon_configs folder from my filebrowser or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- New HA config logic implemented. Files are now located in the addon config file, that can be accessed from the app_configs folder from my filebrowser or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- Feat : if there is no releases in a github repo, check if there is a valid package and get the highest tag https://github.com/alexbelgium/hassio-addons/issues/1168
|
||||
- Feat : github_exclude applies to dockerhub
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ verbose: "false"
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **Environment variables**: Use the add-on `env_vars` option and see [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon) for details.
|
||||
|
||||
@@ -8,7 +8,7 @@ environment:
|
||||
image: ghcr.io/alexbelgium/addons_updater-{arch}
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
name: Repository Updater
|
||||
options:
|
||||
date_iso8601: true
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.0.3 (2026-08-01)
|
||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||
|
||||
@@ -21,7 +21,7 @@ ports_description:
|
||||
webui: http://[HOST]:[PORT:3001]
|
||||
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
|
||||
|
||||
BIN
aurral/stats.png
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
- The Home Assistant project has deprecated support for the armv7, armhf and i386 architectures. Support wil be fully dropped in the upcoming Home Assistant 2025.12 release
|
||||
|
||||
## 0.10.1-hafix4 (2025-11-18)
|
||||
@@ -21,7 +22,7 @@
|
||||
## 0.9.5 (2024-04-27)
|
||||
- Update to latest version from ckulka/baikal-docker (changelog : https://github.com/ckulka/baikal-docker/releases)
|
||||
## 0.9.4-3 (2024-04-26)
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/baikal to a folder only accessible from my Filebrowser addon called /addon_configs/something-baikal. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/baikal to a folder only accessible from my Filebrowser addon called /app_configs/something-baikal. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
|
||||
## 0.9.4-2 (2024-01-14)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Configurations can be done through the app webUI, except for the following optio
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **Environment variables**: See [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon)
|
||||
|
||||
@@ -66,7 +66,7 @@ devices:
|
||||
- /dev/nvme2
|
||||
image: ghcr.io/alexbelgium/baikal-{arch}
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- homeassistant_config:rw
|
||||
- share:rw
|
||||
- ssl:ro
|
||||
|
||||
BIN
baikal/stats.png
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.02.28 (28-02-2026)
|
||||
- Minor bugs fixed
|
||||
## 2026.02.25-2 (25-02-2026)
|
||||
|
||||
@@ -86,7 +86,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -64,7 +64,7 @@ ingress: true
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 1.6.0.2 (2026-07-27)
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
||||
|
||||
## 1.5.4-1 (2026-01-08)
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/addons_config/bazarr to a folder only accessible from my Filebrowser addon called /addon_configs/xxx-bazarr. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/addons_config/bazarr to a folder only accessible from my Filebrowser addon called /app_configs/xxx-bazarr. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
|
||||
## 1.5.4 (2026-01-08)
|
||||
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
||||
|
||||
@@ -76,7 +76,7 @@ ingress: true
|
||||
ingress_entry: bazarr
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- homeassistant_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
|
||||
@@ -23,7 +23,7 @@ fi
|
||||
slug=bazarr
|
||||
|
||||
if [ -d "/homeassistant/addons_config/$slug" ]; then
|
||||
echo "Migrating /homeassistant/addons_config/$slug to /addon_configs/xxx-$slug"
|
||||
echo "Migrating /homeassistant/addons_config/$slug to /app_configs/xxx-$slug"
|
||||
cp -rnf /homeassistant/addons_config/"$slug"/. /config/ || true
|
||||
mv /homeassistant/addons_config/"$slug" /homeassistant/addons_config/"$slug"_migrated
|
||||
fi
|
||||
|
||||
BIN
bazarr/stats.png
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 20260729.1 (2026-08-01)
|
||||
|
||||
- Version renamed from `source-20260729.1`, which Home Assistant could not order and therefore could not reliably offer as an update: every number of the previous version is kept, as a section of its own. The addon itself and the upstream version it tracks are unchanged
|
||||
|
||||
@@ -25,7 +25,7 @@ MQTT password : Ri5ahV1aipeiw0aelerooteixai5ohtoeNg6oo3mo0thi5te0phiezuge4Phoore
|
||||
MQTT broker : tcp://core-mosquitto:1883
|
||||
---
|
||||
|
||||
Edit this section of config.yaml found in addon_configs/db21ed7f_birdnet-go/:
|
||||
Edit this section of config.yaml found in app_configs/db21ed7f_birdnet-go/:
|
||||
mqtt:
|
||||
enabled: true # true to enable MQTT
|
||||
broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port)
|
||||
|
||||
@@ -82,7 +82,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -80,7 +80,7 @@ ingress_entry: "ui/dashboard"
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
name: Birdnet-go (customized and built from source)
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 20260716 (2026-07-16)
|
||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||
|
||||
@@ -25,7 +25,7 @@ MQTT password : Ri5ahV1aipeiw0aelerooteixai5ohtoeNg6oo3mo0thi5te0phiezuge4Phoore
|
||||
MQTT broker : tcp://core-mosquitto:1883
|
||||
---
|
||||
|
||||
Edit this section of config.yaml found in addon_configs/db21ed7f_birdnet-go/:
|
||||
Edit this section of config.yaml found in app_configs/db21ed7f_birdnet-go/:
|
||||
mqtt:
|
||||
enabled: true # true to enable MQTT
|
||||
broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port)
|
||||
|
||||
@@ -79,7 +79,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -81,7 +81,7 @@ ingress_entry: "ui/dashboard"
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
name: Birdnet-go
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.07.10-2 (10-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 2026.07.10 (10-07-2026)
|
||||
|
||||
@@ -93,7 +93,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -61,7 +61,7 @@ image: ghcr.io/alexbelgium/birdnet-pi-zach-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,8 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.08.02 (02-08-2026)
|
||||
- Fix: ingress returned "502 Bad Gateway" because Caddy never listened on :8082. `91-nginx_ingress.sh` hooked the ingress site into `update_caddyfile.sh` with a sed anchored on `sudo caddy fmt --overwrite`, but 2026.07.10-1 strips `sudo` from every BirdNET-Pi script at build time, so the anchor stopped matching. `update_caddyfile.sh` then rewrote the Caddyfile from scratch just before Caddy started, dropping the ingress site
|
||||
- Fix: `caddy_ingress.sh` no longer appends a second `:8082` block when it runs twice (a duplicate site address makes Caddy refuse to start)
|
||||
- Fix: `02-caddy.sh` re-adds the ingress site if it is missing from the Caddyfile just before starting Caddy
|
||||
## 2026.07.22 (22-07-2026)
|
||||
- Fix: health-check the WebUI port (8081) instead of port 80, so the standalone Docker container no longer reports "unhealthy" when ssl=false
|
||||
- Fix: health-check now probes https when ssl is enabled, and no longer silently reports "healthy" regardless of the actual result (the previous check's `&>` redirection is a bash-ism that dash, the image's /bin/sh, parses as background + no-op, discarding curl's exit status)
|
||||
@@ -148,7 +153,7 @@
|
||||
- [ALL] Fix non-avx2 cpu support
|
||||
|
||||
## 2025.02.23 (2025-02-16)
|
||||
- WARNING 2025.02.14/16 was buggy. If you installed it you need to restore a backup or delete manually your /addon_configs/xxx-birdnet-pi/birdnet.conf file and recreate it
|
||||
- WARNING 2025.02.14/16 was buggy. If you installed it you need to restore a backup or delete manually your /app_configs/xxx-birdnet-pi/birdnet.conf file and recreate it
|
||||
- Allow usage as a standalone container (thanks @gotschi) https://github.com/mcguirepr89/BirdNET-Pi/issues/211#issuecomment-2650095952
|
||||
- Corrected a bug preventing to create db
|
||||
- Corrected a bug to ensure the the most up-to-date birdnet.conf on fresh start
|
||||
|
||||
@@ -93,7 +93,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -61,7 +61,7 @@ image: ghcr.io/alexbelgium/birdnet-pi-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
@@ -116,5 +116,5 @@ tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi
|
||||
usb: true
|
||||
version: 2026.07.22
|
||||
version: 2026.08.02
|
||||
video: true
|
||||
|
||||
@@ -25,5 +25,24 @@ export TZ="${TZ_VALUE:-Etc/UTC}"
|
||||
# Update caddyfile with password
|
||||
"$HOME"/BirdNET-Pi/scripts/update_caddyfile.sh &> /dev/null || true
|
||||
|
||||
# update_caddyfile.sh rewrites the Caddyfile from scratch. 91-nginx_ingress.sh
|
||||
# hooks the ingress site back into it, but if that hook ever fails to apply,
|
||||
# caddy would start without a :8082 listener and ingress would answer 502.
|
||||
# 91-nginx_ingress.sh writes /ingress_url when ingress is on and removes it when
|
||||
# it is off, so this is a no-op in standalone mode.
|
||||
# Require the Caddyfile to exist: if it is missing something went badly wrong
|
||||
# earlier, and caddy failing on a missing config is easier to diagnose than an
|
||||
# ingress-only Caddyfile conjured up here.
|
||||
if [[ -f /ingress_url ]] && [[ -f /etc/caddy/Caddyfile ]] \
|
||||
&& ! grep -qE '^[[:space:]]*:8082[[:space:]]*\{' /etc/caddy/Caddyfile 2> /dev/null; then
|
||||
echo "Ingress site missing from the Caddyfile, re-adding it"
|
||||
if ! /helpers/caddy_ingress.sh; then
|
||||
# Start caddy anyway: ingress stays broken, but direct access on 8081
|
||||
# keeps working. Exiting here would only make s6 restart this service in
|
||||
# a loop and take the WebUI down completely.
|
||||
echo "Failed to re-add the ingress site, the ingress panel will return 502" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting service: caddy"
|
||||
exec /usr/bin/caddy run --config /etc/caddy/Caddyfile
|
||||
|
||||
@@ -26,6 +26,10 @@ ingress_entry=$(bashio::addon.ingress_entry)
|
||||
if [[ "$ingress_entry" != "/api"* ]]; then
|
||||
bashio::log.info "Ingress entry is not set, exiting configuration."
|
||||
sed -i "1a sleep infinity" /custom-services.d/02-nginx.sh
|
||||
# This script re-runs from /etc/scripts-init on an add-on restart, so drop
|
||||
# any marker left by an earlier run: it is what 02-caddy.sh reads to decide
|
||||
# whether the ingress site belongs in the Caddyfile.
|
||||
rm -f /ingress_url
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -68,9 +72,23 @@ sed -i "s|localhost|localhost:8082|g" "$HOME/BirdNET-Pi/scripts/utils/notificati
|
||||
|
||||
# Update the Caddyfile if update script exists
|
||||
caddy_update_script="$HOME/BirdNET-Pi/scripts/update_caddyfile.sh"
|
||||
if [ -f "$caddy_update_script" ]; then
|
||||
sed -i "/sudo caddy fmt --overwrite/i /helpers/caddy_ingress.sh" "$caddy_update_script"
|
||||
else
|
||||
if [ ! -f "$caddy_update_script" ]; then
|
||||
bashio::log.error "Caddy update script not found: $caddy_update_script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# update_caddyfile.sh rewrites /etc/caddy/Caddyfile from scratch, which drops
|
||||
# the ingress site added just above. 02-caddy.sh runs it right before starting
|
||||
# caddy, so the hook below has to re-add the site from inside that script, just
|
||||
# before it formats and reloads the config.
|
||||
# The anchor must not require "sudo": the Dockerfile strips it from every
|
||||
# BirdNET-Pi script at build time, so the shipped line is "caddy fmt --overwrite".
|
||||
if ! grep -qF "/helpers/caddy_ingress.sh" "$caddy_update_script"; then
|
||||
sed -i -E "/^[[:space:]]*(sudo[[:space:]]+)?caddy[[:space:]]+fmt[[:space:]]+--overwrite/i /helpers/caddy_ingress.sh" "$caddy_update_script"
|
||||
fi
|
||||
|
||||
# sed is silent when the anchor is missing; make sure the hook is really there
|
||||
if ! grep -qF "/helpers/caddy_ingress.sh" "$caddy_update_script"; then
|
||||
bashio::log.warning "Could not anchor the ingress site in $caddy_update_script, appending it instead"
|
||||
printf '\n/helpers/caddy_ingress.sh\n' >> "$caddy_update_script"
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,13 @@ set +u
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/birdnet/birdnet.conf
|
||||
|
||||
# Nothing to do if the ingress site is already there. This script runs both from
|
||||
# cont-init and from update_caddyfile.sh, and a duplicate ":8082" site address
|
||||
# makes caddy refuse to start.
|
||||
if grep -qE '^[[:space:]]*:8082[[:space:]]*\{' /etc/caddy/Caddyfile 2> /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create ingress configuration for Caddyfile
|
||||
cat << EOF >> /etc/caddy/Caddyfile
|
||||
:8082 {
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 0.8.5 (2026-08-01)
|
||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||
|
||||
@@ -47,7 +47,7 @@ This add-on supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This add-on supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This add-on supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -67,7 +67,7 @@ ingress: true
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- ssl:rw
|
||||
- share:rw
|
||||
name: BirdNET-PiPy
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 1.93.129-ls119 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||
|
||||
@@ -26,7 +26,7 @@ image: ghcr.io/alexbelgium/brave-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2026.08.01.1 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||
|
||||
@@ -26,7 +26,7 @@ image: ghcr.io/alexbelgium/chromium-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
- ssl
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.55.2 (2026-08-01)
|
||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||
|
||||
@@ -50,7 +50,7 @@ TIMEOUT: 60000
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -69,7 +69,7 @@ environment:
|
||||
image: ghcr.io/alexbelgium/browserless_chrome-{arch}
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
name: Browserless Chromium
|
||||
options:
|
||||
env_vars: []
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 9.12.0 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||
@@ -217,7 +218,7 @@
|
||||
|
||||
- Update to latest version from linuxserver/docker-calibre
|
||||
- BREAKING CHANGE : please relink your library according to https://github.com/alexbelgium/hassio-addons/issues/1154#issuecomment-1879182729
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre to a folder only accessible from my Filebrowser addon called /addon_configs/something-calibre. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1154
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre to a folder only accessible from my Filebrowser addon called /app_configs/something-calibre. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1154
|
||||
|
||||
## 7.2.0 (2023-12-16)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
@@ -78,7 +78,7 @@ init: false
|
||||
map:
|
||||
- media:rw
|
||||
- share:rw
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- homeassistant_config:rw
|
||||
- ssl
|
||||
name: Calibre
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 0.6.26-2 (2026-04-06)
|
||||
- Fix: Install calibre (calibredb) at build time to fix 500 error when downloading books
|
||||
@@ -10,7 +11,7 @@
|
||||
- Update to latest version from linuxserver/docker-calibre-web (changelog : https://github.com/linuxserver/docker-calibre-web/releases)
|
||||
## 0.6.24-10 (2025-05-28)
|
||||
- Fix migration script
|
||||
- Allow access to Calibre folder through /addon_configs/xxx-Calibre
|
||||
- Allow access to Calibre folder through /app_configs/xxx-Calibre
|
||||
|
||||
## 0.6.24 (2024-11-23)
|
||||
- Update to latest version from linuxserver/docker-calibre-web (changelog : https://github.com/linuxserver/docker-calibre-web/releases)
|
||||
@@ -26,7 +27,7 @@
|
||||
- Minor bugs fixed
|
||||
## 0.6.21-5 (2024-01-12)
|
||||
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre-web to a folder only accessible from my Filebrowser addon called /addon_configs/something-calibre-web. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1177
|
||||
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre-web to a folder only accessible from my Filebrowser addon called /app_configs/something-calibre-web. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1177
|
||||
|
||||
## 0.6.21-2 (2023-11-05)
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ image: ghcr.io/alexbelgium/calibre_web-{arch}
|
||||
ingress: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- all_addon_configs:rw
|
||||
- app_config:rw
|
||||
- all_app_configs:rw
|
||||
- homeassistant_config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,87 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
## 2026.08.04 (04-08-2026)
|
||||
- Fix: reverted the GPU acceleration added in 2026.08.03. It did not just fail to help — it was
|
||||
what disabled the GPU. `--use-gl=angle --use-angle=gl-egl` forces Mesa's EGL X11 platform,
|
||||
which offers no window-capable EGLConfig under this Xvfb, so the GPU process logged
|
||||
`gl_surface_egl.cc:262 No suitable EGL configs found`, abandoned GL, and was relaunched with
|
||||
`--use-gl=disabled` while every renderer got `--disable-gpu-compositing`.
|
||||
Chromium already renders on the GPU here with no flags at all, because LSIO's Xvfb runs with
|
||||
`-vfbdevice /dev/dri/renderD128` and its GLX is therefore backed by the real render node —
|
||||
the premise that Xvfb offers only a software path was wrong for this base image. Measured
|
||||
three times, including at the production 15360x8640 screen: with the flags the GPU process
|
||||
loads `libEGL_mesa` and holds 1 fd on the render node; without them it loads `libGLX_mesa`,
|
||||
holds 8, and no renderer carries `--disable-gpu-compositing`.
|
||||
Removes `claude-gpu-probe` and the `gpu_acceleration` option. The probe was not wrong about
|
||||
the hardware, it was answering the wrong question: it exercised ANGLE's default GLX path,
|
||||
which works, and so it passed while the flags it gated disabled the GPU.
|
||||
- Fix: `max_resolution` never did anything; the option is renamed to `MAX_RES`. It wrote
|
||||
`MAX_RES` into the s6 `container_environment`, but the base image's `svc-xorg` starts
|
||||
`#!/usr/bin/env bashio` rather than `with-contenv` and never reads that directory, so Xvfb
|
||||
kept starting at 15360x8640. Naming the option `MAX_RES` makes the add-on env layer inject
|
||||
it directly into every service `run` script, which is how `DRINODE` already reaches Xvfb.
|
||||
`MAX_RES` ships with no default, so nothing changes for anyone until it is set: the screen
|
||||
stays at the base image's 15360x8640. The old `max_resolution` default of `1920x1080` is
|
||||
deliberately not carried over — it had never taken effect, so shipping it now would have
|
||||
silently shrunk every existing desktop, including on 4K displays. Home Assistant drops
|
||||
options that are no longer in the schema (it logs a warning), so a saved `max_resolution`
|
||||
is discarded rather than migrated. The schema bounds each axis to 100-9999, which keeps the
|
||||
worst case (~400 MB of framebuffer) below the 15360x8640 default it replaces.
|
||||
Removes `22-display_tuning.sh`.
|
||||
- Removed the amd64 GPU driver install, which has never run in any release. The 2026.08.03
|
||||
build corrected its gate variable but the guard is `if [[ ... ]]` and there is no `SHELL`
|
||||
directive, so it runs under dash, which has no `[[` — the condition was false and the `RUN`
|
||||
still exited 0. Verified in the shipped 2026.08.03 image: no `vainfo`, no
|
||||
`intel-media-va-driver-non-free`, and no matching install in its apt history.
|
||||
It was deleted rather than repaired, because it had nothing to add. Hardware OpenGL already
|
||||
works without it: the GPU process loads the Mesa gallium megadriver over DRI3 and holds 8
|
||||
fds on `/dev/dri/renderD128`, with no swrast/llvmpipe in the GL path. `libgl1-mesa-dri` and
|
||||
`mesa-vulkan-drivers` already come from the LSIO base at a newer backports version (Mesa
|
||||
25.0.7) than reinstalling them would give. And `intel-media-va-driver-non-free`, the one
|
||||
genuinely new payload, was installed live on the running add-on and changed nothing: VA-API
|
||||
failed identically to the free driver (`iHD_drv_video.so init failed`) on both
|
||||
`/dev/dri/renderD128` and `card0`. That failure is below the add-on, in the host i915 stack.
|
||||
Repairing the guard would have turned dead code into an untested apt transaction that swaps
|
||||
a base-image package for no measured benefit.
|
||||
- A `[[` in the Dockerfile's legacy `/etc/services.d` shim is corrected to `[` for the same
|
||||
dash reason; that directory does not exist on this s6-overlay v3 base, so it is
|
||||
behaviour-neutral here.
|
||||
|
||||
## 2026.08.03 (03-08-2026)
|
||||
- Performance: Claude Desktop now uses the GPU instead of rendering on the CPU.
|
||||
Under Xvfb, Chromium probed GLX, found only Xvfb's software path, and fell back to
|
||||
`--use-gl=disabled` + `--disable-gpu-compositing`, making the renderer the add-on's largest
|
||||
CPU consumer. It is now launched with `--ozone-platform=x11 --use-gl=angle --use-angle=gl-egl`,
|
||||
but only when the new `claude-gpu-probe` confirms Claude Desktop's own bundled ANGLE can
|
||||
create a hardware GL context on this host. New `gpu_acceleration` option (`auto`/`on`/`off`,
|
||||
default `auto`); any probe failure keeps the previous software rendering unchanged.
|
||||
- Performance: new `max_resolution` option (default `1920x1080`) caps the virtual screen via the
|
||||
base image's `MAX_RES`. Xvfb previously ran at 15360x8640, so Xvfb and the Selkies capture
|
||||
loop tracked damage over a 133-megapixel area continuously, even with no browser connected.
|
||||
Selkies still resizes dynamically below the cap.
|
||||
- Performance: Claude Code now talks to the Home Assistant MCP server over its native HTTP
|
||||
transport instead of through the `mcp-proxy` stdio bridge, removing one Python process per
|
||||
Claude Code session (~45 MB of private resident memory each). Claude Desktop keeps the
|
||||
bridge, as the config schema for a remote entry there is not confirmed.
|
||||
- Performance: new `mcp_servers_desktop` / `mcp_servers_code` options select which MCP servers
|
||||
each client registers. Every stdio MCP server is a separate process per client, and Desktop
|
||||
starts another full set per Claude Code session it hosts. Defaults register all of them in
|
||||
both clients, i.e. the previous behaviour.
|
||||
- Fix: the Dockerfile's Intel graphics block was dead code. It was gated on `TARGETARCH`, which
|
||||
the repo's builder does not pass, so it never ran: the shipped amd64 image has no `vainfo` and
|
||||
no `intel-media-va-driver-non-free`. It now uses `BUILD_ARCH`, and its Vulkan ICD check no
|
||||
longer names `intel_icd.x86_64.json`, a file Debian does not ship.
|
||||
- Fix: stale Home Assistant MCP registrations (and the bearer token in them) are now removed
|
||||
from Claude Code's config when `enable_ha_mcp` is turned off. Ownership of an HTTP entry is
|
||||
recorded when the add-on writes it, so a manually configured `homeassistant` server is never
|
||||
claimed, overwritten, or deleted — not even when it sits on the default URL.
|
||||
- `--disable-dev-shm-usage` is now applied only when `/dev/shm` is actually small (under
|
||||
256 MB). It is a workaround for Docker's 64 MB default, but Home Assistant ignores the add-on's
|
||||
`shm_size` so the real size varies per install; the size is now read at startup, keeping the
|
||||
crash workaround where it is needed and dropping it where it only pushed Chromium's shared
|
||||
memory into ordinary files. If the size cannot be determined, the flag is kept.
|
||||
|
||||
## 2026.08.02 (02-08-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
## ubunturesolute-version-3a10bef7 (2026-08-01)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
|
||||
@@ -74,7 +74,7 @@ VOLUME [ "/sys/fs/cgroup" ]
|
||||
# hadolint ignore=SC2015,DL4006,SC2013,SC2086
|
||||
RUN \
|
||||
usermod --home /data/data abc && \
|
||||
if [[ -d /etc/services.d ]] && ls /etc/services.d/*/run 1> /dev/null 2>&1; then sed -i "1a set +e" /etc/services.d/*/run; fi
|
||||
if [ -d /etc/services.d ] && ls /etc/services.d/*/run 1> /dev/null 2>&1; then sed -i "1a set +e" /etc/services.d/*/run; fi
|
||||
|
||||
ARG TEMPLATE_BASE_URL="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates"
|
||||
|
||||
@@ -142,24 +142,29 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The Intel N150/Twin Lake iGPU uses the host's i915 kernel driver through the mapped
|
||||
# /dev/dri nodes. Explicitly install the amd64 userspace stack needed for accelerated
|
||||
# OpenGL rendering, VA-API video encoding, and Vulkan, then fail the build if any driver
|
||||
# payload is missing. Keep aarch64 unchanged because these Intel packages are amd64-only.
|
||||
RUN if [[ "${TARGETARCH}" == "amd64" ]]; then \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
intel-media-va-driver-non-free \
|
||||
libgl1-mesa-dri \
|
||||
mesa-vulkan-drivers \
|
||||
vainfo && \
|
||||
test -f /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so && \
|
||||
test -f /usr/lib/x86_64-linux-gnu/dri/iris_dri.so && \
|
||||
test -f /usr/share/vulkan/icd.d/intel_icd.x86_64.json && \
|
||||
command -v vainfo > /dev/null; \
|
||||
fi && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# No bespoke GPU driver install. The LSIO base image already provides the userspace stack that
|
||||
# hardware rendering actually uses, and it does so at a newer version than adding it here would:
|
||||
# libgl1-mesa-dri and mesa-vulkan-drivers come from bookworm-backports Mesa 25.0.7, where a
|
||||
# plain `apt-get install` of the same names is at best a no-op.
|
||||
#
|
||||
# There used to be an amd64 block here installing intel-media-va-driver-non-free, libgl1-mesa-dri,
|
||||
# mesa-vulkan-drivers and vainfo. It never ran in any release: it was guarded by
|
||||
# `if [[ ... ]]`, and with no SHELL directive RUN executes under /bin/sh — dash, which has no
|
||||
# `[[`. dash printed `[[: not found`, the condition was false, and the RUN still exited 0, so
|
||||
# every build silently skipped it and still passed. (An earlier fix corrected the gate variable
|
||||
# from TARGETARCH to BUILD_ARCH but left the `[[`, so it stayed dead.) Confirmed in the shipped
|
||||
# 2026.08.03 amd64 image: no vainfo, no intel-media-va-driver-non-free, no matching apt history.
|
||||
#
|
||||
# It was removed rather than repaired, because measurement showed it had nothing to add:
|
||||
# - Hardware OpenGL already works without it. In the shipped image the Chromium GPU process
|
||||
# loads the Mesa gallium megadriver over DRI3 and holds 8 fds on /dev/dri/renderD128, with
|
||||
# no swrast/llvmpipe in the GL path.
|
||||
# - The only genuinely new payload, intel-media-va-driver-non-free, does not fix anything
|
||||
# here. Installed live alongside vainfo on the running add-on, VA-API failed identically to
|
||||
# the free driver — `iHD_drv_video.so init failed` on both /dev/dri/renderD128 and card0,
|
||||
# with both 23.1.1 builds. The failure is below the add-on, in the host i915/GuC stack.
|
||||
# Repairing the guard would therefore have turned dead code into an untested apt transaction
|
||||
# that swaps a base-image package for no measured benefit.
|
||||
|
||||
# Install the current upstream hadolint and actionlint releases for both supported
|
||||
ARG HADOLINT_VERSION=v2.14.0
|
||||
|
||||
@@ -97,6 +97,7 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||
| `MAX_RES` | _(unset)_ | Optional cap on the virtual screen, as `WIDTHxHEIGHT` (100-9999 per axis). Unset means the base image default, 15360x8640 — Selkies resizes dynamically below whatever the cap is, so this only sets the ceiling. Named `MAX_RES` because that is the environment variable the base image's Xvfb service reads. Setting it lowers the area Xvfb and the Selkies capture loop track for damage; the framebuffer itself is lazily populated, so this is a CPU saving, not a memory one. |
|
||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||
@@ -106,6 +107,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. |
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
||||
| `mcp_servers_desktop` | all | Which managed MCP servers Claude Desktop registers (`headroom`, `tokensave`, `homeassistant`, `codex`). |
|
||||
| `mcp_servers_code` | all | Which managed MCP servers Claude Code registers. Each stdio server is a separate process per client, and Desktop starts another set per Claude Code session it hosts, so trimming this is the cheapest way to cut memory. |
|
||||
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
|
||||
| `install_codex_cli` | `false` | Install the latest stable OpenAI Codex CLI at startup and register its native MCP server so Claude can delegate work to ChatGPT Codex. |
|
||||
| `codex_sandbox_mode` | `workspace-write` | Filesystem scope Codex runs with: `read-only`, `workspace-write`, or `danger-full-access`. |
|
||||
|
||||
@@ -61,7 +61,7 @@ magic link into the in-session Chromium (not a phone).
|
||||
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
||||
`rootfs/etc/cont-init.d/80-configuration.sh`).
|
||||
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
|
||||
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
|
||||
`/app_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
|
||||
terminal).
|
||||
|
||||
---
|
||||
|
||||
@@ -34,7 +34,7 @@ hassio_role: manager
|
||||
homeassistant_api: true
|
||||
auth_api: true
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
- ssl
|
||||
@@ -65,6 +65,16 @@ options:
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
install_tokensave: true
|
||||
mcp_servers_desktop:
|
||||
- headroom
|
||||
- tokensave
|
||||
- homeassistant
|
||||
- codex
|
||||
mcp_servers_code:
|
||||
- headroom
|
||||
- tokensave
|
||||
- homeassistant
|
||||
- codex
|
||||
permission_mode: auto
|
||||
tokensave_project_paths: []
|
||||
panel_admin: false
|
||||
@@ -86,6 +96,7 @@ schema:
|
||||
data_location: str?
|
||||
DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)?
|
||||
KEYBOARD: list(da-dk-qwerty|de-de-qwertz|en-gb-qwerty|en-us-qwerty|es-es-qwerty|fr-ch-qwertz|fr-fr-azerty|it-it-qwerty|ja-jp-qwerty|pt-br-qwerty|sv-se-qwerty|tr-tr-qwerty)?
|
||||
MAX_RES: match(^[1-9][0-9]{2,3}x[1-9][0-9]{2,3}$)?
|
||||
PASSWORD: str?
|
||||
PGID: int
|
||||
PUID: int
|
||||
@@ -115,12 +126,15 @@ schema:
|
||||
install_headroom: bool
|
||||
install_rtk: bool
|
||||
install_tokensave: bool
|
||||
mcp_servers_desktop:
|
||||
- list(headroom|tokensave|homeassistant|codex)
|
||||
mcp_servers_code:
|
||||
- list(headroom|tokensave|homeassistant|codex)
|
||||
permission_mode: list(strict|auto|bypass)
|
||||
tokensave_project_paths:
|
||||
- str
|
||||
slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "ubunturesolute-version-3a10bef7"
|
||||
version: "2026.08.04"
|
||||
video: true
|
||||
|
||||
@@ -20,4 +20,46 @@
|
||||
# Headroom is intentionally not injected into the Desktop process: Claude Desktop
|
||||
# force-overrides ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom
|
||||
# MCP tools instead.
|
||||
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=basic
|
||||
|
||||
# GPU acceleration: deliberately no flags. Do not add ANGLE/EGL flags here.
|
||||
#
|
||||
# Chromium already renders on the GPU on this base image, because LSIO's Xvfb is started with
|
||||
# `-vfbdevice /dev/dri/renderD128` — so GLX here is backed by the real render node, not the
|
||||
# indirect/software path. Left alone, Chromium picks Mesa's GLX (libGLX_mesa), initialises the
|
||||
# GPU process, and composites on the GPU.
|
||||
#
|
||||
# Passing `--use-gl=angle --use-angle=gl-egl` actively *broke* that. It forces Mesa's EGL X11
|
||||
# platform, which offers no window-capable EGLConfig under this Xvfb, so the GPU process logged
|
||||
# ui/gl/gl_surface_egl.cc:262 No suitable EGL configs found.
|
||||
# gave up on GL entirely, and Chromium relaunched it with `--use-gl=disabled` while stamping
|
||||
# `--disable-gpu-compositing` on every renderer — i.e. the flags caused the CPU rendering they
|
||||
# were meant to remove. Measured three times, including at the production 15360x8640 screen:
|
||||
# with the flags the GPU process loads libEGL_mesa and holds 1 fd on the render node; with no
|
||||
# flags it loads libGLX_mesa and holds 8, and no renderer carries --disable-gpu-compositing.
|
||||
#
|
||||
# A standalone ANGLE probe is not evidence for any of this: Claude Desktop's bundled ANGLE
|
||||
# happily creates a hardware context here via its default (GLX) path, which is why the probe
|
||||
# that used to gate these flags passed while the flags themselves disabled the GPU.
|
||||
#
|
||||
# `--use-angle=gl` also works, but it only reproduces what Chromium already chooses by itself,
|
||||
# so it is not passed either.
|
||||
|
||||
# Shared memory.
|
||||
#
|
||||
# --disable-dev-shm-usage exists because Docker's default /dev/shm is 64 MB, which is not
|
||||
# enough for Chromium's renderers and produced a crash loop here (see the 1.3 changelog entry).
|
||||
# Home Assistant ignores the add-on's shm_size, so the size cannot be set from this repo and
|
||||
# genuinely varies between installs — it is 7.7 GB on some hosts and the 64 MB default on
|
||||
# others. Hardcoding either answer is wrong, so ask the kernel: keep the workaround when
|
||||
# /dev/shm is small, and drop it when there is plenty, where it would otherwise push Chromium's
|
||||
# shared memory into ordinary files for no benefit. If the size cannot be determined, keep the
|
||||
# flag — the crash it prevents is worse than the overhead it costs.
|
||||
SHM_FLAGS="--disable-dev-shm-usage"
|
||||
SHM_KB="$(df -k /dev/shm 2> /dev/null | awk 'NR==2 {print $2}')"
|
||||
if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then
|
||||
SHM_FLAGS=""
|
||||
fi
|
||||
|
||||
# SHM_FLAGS must stay unquoted so it expands to a separate argument, or to nothing at all.
|
||||
# shellcheck disable=SC2086
|
||||
exec claude-desktop --no-sandbox --password-store=basic $SHM_FLAGS
|
||||
|
||||
@@ -308,10 +308,13 @@ if bashio::config.true 'install_codex_cli'; then
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
HA_MCP_URL=""
|
||||
HA_MCP_TOKEN=""
|
||||
# Read unconditionally, even when enable_ha_mcp is off. Home Assistant keeps an option's value
|
||||
# when its toggle is disabled, and the reconciliation below needs this URL to recognise the
|
||||
# HTTP entry it previously wrote so that it can be removed — together with the bearer token
|
||||
# inside it — rather than orphaned in ~/.claude.json.
|
||||
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
|
||||
if bashio::config.true 'enable_ha_mcp'; then
|
||||
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
|
||||
if bashio::config.has_value 'ha_mcp_token'; then
|
||||
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
|
||||
fi
|
||||
@@ -325,12 +328,73 @@ if bashio::config.true 'enable_ha_mcp'; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Which of the managed MCP servers each client gets.
|
||||
#
|
||||
# Every stdio MCP server is a separate process *per client*, and Claude Desktop starts another
|
||||
# full set for each Claude Code session it hosts — so a server registered in both clients is
|
||||
# paid for several times over. Measured on a live add-on with three sets running, the private
|
||||
# (non-shared) resident cost was roughly 54 MB per extra `headroom mcp serve`, 45 MB per extra
|
||||
# `mcp-proxy`, and only ~11 MB and ~2 MB for `codex` and `tokensave`, which share most of their
|
||||
# pages. Registering a server only where it is actually used is therefore the cheapest lever
|
||||
# available; these two options expose that choice.
|
||||
#
|
||||
# Defaults keep every enabled server in both clients, i.e. the pre-existing behaviour.
|
||||
MCP_ALL_SERVERS="headroom tokensave homeassistant codex"
|
||||
|
||||
mcp_client_list() {
|
||||
local option="$1"
|
||||
local selected=() entry raw rc=0
|
||||
|
||||
# An option that is absent entirely — i.e. an existing install upgrading from a config that
|
||||
# predates these options — keeps the previous behaviour of registering every enabled server.
|
||||
# bashio distinguishes this from an explicitly empty list: an unset key yields the literal
|
||||
# "null", while `[]` yields an empty string. Those must not be conflated, because an empty
|
||||
# list is a legitimate way to say "no MCP servers in this client" and defaulting it back to
|
||||
# all four would silently ignore the user.
|
||||
if ! bashio::config.exists "$option"; then
|
||||
echo "$MCP_ALL_SERVERS"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Capture first: reading a bashio list straight into `while read` via process substitution
|
||||
# silently yields nothing under this script's errexit. The exit status is kept separately
|
||||
# so that a failed read is not mistaken for a deliberate empty selection.
|
||||
raw="$(bashio::config "$option" 2> /dev/null)" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
bashio::log.warning "Could not read '${option}'; registering every enabled MCP server for this client"
|
||||
echo "$MCP_ALL_SERVERS"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
# Reconciliation deletes any managed server not named here, so an unrecognised value
|
||||
# must never be treated as an authoritative selection.
|
||||
case " $MCP_ALL_SERVERS " in
|
||||
*" $entry "*) selected+=("$entry") ;;
|
||||
*)
|
||||
bashio::log.warning "Ignoring unknown MCP server '${entry}' in '${option}'; registering every enabled server for this client"
|
||||
echo "$MCP_ALL_SERVERS"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done <<< "$raw"
|
||||
|
||||
echo "${selected[@]:-}"
|
||||
}
|
||||
|
||||
MCP_SERVERS_DESKTOP="$(mcp_client_list 'mcp_servers_desktop')"
|
||||
MCP_SERVERS_CODE="$(mcp_client_list 'mcp_servers_code')"
|
||||
bashio::log.info "MCP servers for Claude Desktop: ${MCP_SERVERS_DESKTOP}"
|
||||
bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}"
|
||||
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||
import json
|
||||
@@ -385,6 +449,23 @@ if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
"env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]},
|
||||
}
|
||||
|
||||
# Claude Code speaks Streamable HTTP MCP natively, so pointing it straight at Home Assistant
|
||||
# removes the mcp-proxy bridge process entirely — it exists only to translate stdio to the HTTP
|
||||
# transport Home Assistant already serves. That bridge was the most expensive duplicate
|
||||
# measured (~45 MB of private RSS per copy, one per Claude Code session).
|
||||
#
|
||||
# Claude Desktop keeps the stdio bridge. Its bundled MCP SDK does contain a remote transport,
|
||||
# but the shape `claude_desktop_config.json` accepts for a remote entry — and whether it
|
||||
# persists a static bearer header — could not be confirmed, and a wrong guess would silently
|
||||
# break Home Assistant access in Desktop. Revisit once that schema is verified upstream.
|
||||
HA_MCP_CODE_ENTRY = None
|
||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
HA_MCP_CODE_ENTRY = {
|
||||
"type": "http",
|
||||
"url": os.environ["HA_MCP_URL"],
|
||||
"headers": {"Authorization": "Bearer " + os.environ["HA_MCP_TOKEN"]},
|
||||
}
|
||||
|
||||
# An entry is add-on-managed when its command is one of our binaries living outside the
|
||||
# persistent home. Matching on the basename (rather than the exact path recorded at write
|
||||
# time) keeps entries updatable when a base-image upgrade moves the binary, while commands
|
||||
@@ -392,17 +473,77 @@ if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
HOME_PREFIX = os.path.expanduser("~") + os.sep
|
||||
|
||||
|
||||
def is_managed(name, entry):
|
||||
# Ownership record for the HTTP Home Assistant entry.
|
||||
#
|
||||
# The stdio entries can be recognised on sight, because their `command` points at a binary this
|
||||
# image installs outside $HOME. An HTTP entry has no such tell: it is just a URL plus a bearer
|
||||
# header, and a user who configured `homeassistant` by hand — very plausibly at the same default
|
||||
# http://homeassistant:8123/api/mcp — would be indistinguishable from ours. Inferring ownership
|
||||
# from shape or URL would let this script delete or overwrite that entry, including their token.
|
||||
#
|
||||
# So ownership is recorded rather than guessed: the URL of an entry this script actually wrote is
|
||||
# remembered here, and only an entry matching that record is ever modified or removed. Anything
|
||||
# this script did not write is untouchable, whatever it looks like. The file holds no secrets —
|
||||
# just the endpoint — but is written 0600 to match the configs it describes.
|
||||
STATE_PATH = Path(os.path.expanduser("~")) / ".config" / "claude_desktop_addon" / "managed-mcp.json"
|
||||
|
||||
|
||||
def load_state():
|
||||
try:
|
||||
state = json.loads(STATE_PATH.read_text())
|
||||
return state if isinstance(state, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state):
|
||||
try:
|
||||
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE_PATH.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n")
|
||||
STATE_PATH.chmod(0o600)
|
||||
except Exception:
|
||||
# Losing the record only costs us the ability to clean up later; never fail the boot.
|
||||
pass
|
||||
|
||||
|
||||
def is_managed_http_ha(entry, owned_url):
|
||||
"""True only for an HTTP entry this script previously wrote."""
|
||||
return (
|
||||
bool(owned_url)
|
||||
and entry.get("url") == owned_url
|
||||
and set(entry) == {"type", "url", "headers"}
|
||||
and entry.get("type") == "http"
|
||||
and isinstance(entry.get("headers"), dict)
|
||||
and set(entry["headers"]) == {"Authorization"}
|
||||
)
|
||||
|
||||
|
||||
def is_managed(name, entry, owned_url):
|
||||
if not isinstance(entry, dict):
|
||||
return False
|
||||
command = entry.get("command")
|
||||
if not isinstance(command, str) or command.startswith(HOME_PREFIX):
|
||||
# A commandless entry is ours only when it is an HTTP Home Assistant registration this
|
||||
# script recorded writing. Anything else — including a user's own remote server that
|
||||
# reuses the name, even on the same URL — is left alone.
|
||||
if command is None and name == "homeassistant":
|
||||
return is_managed_http_ha(entry, owned_url)
|
||||
return False
|
||||
return os.path.basename(command) == MANAGED_BASENAMES[name]
|
||||
|
||||
|
||||
SELECTED = {
|
||||
"CLAUDE_DESKTOP_CONFIG": set(os.environ["MCP_SERVERS_DESKTOP"].split()),
|
||||
"CLAUDE_CODE_CONFIG": set(os.environ["MCP_SERVERS_CODE"].split()),
|
||||
}
|
||||
|
||||
state = load_state()
|
||||
state_changed = False
|
||||
|
||||
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
|
||||
path = Path(os.environ[config_var])
|
||||
selected = SELECTED[config_var]
|
||||
owned_url = state.get(str(path), {}).get("homeassistant_http_url", "")
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
if not isinstance(data, dict):
|
||||
@@ -417,17 +558,35 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
changed = False
|
||||
for name in MANAGED_BASENAMES:
|
||||
existing = servers.get(name)
|
||||
if name in desired:
|
||||
entry = dict(desired[name])
|
||||
if stdio_type:
|
||||
entry["type"] = "stdio"
|
||||
if existing is None or is_managed(name, existing):
|
||||
if name in desired and name in selected:
|
||||
if stdio_type and name == "homeassistant" and HA_MCP_CODE_ENTRY is not None:
|
||||
# Claude Code talks to Home Assistant over HTTP directly; no bridge process.
|
||||
entry = dict(HA_MCP_CODE_ENTRY)
|
||||
else:
|
||||
entry = dict(desired[name])
|
||||
if stdio_type:
|
||||
entry["type"] = "stdio"
|
||||
# An entry we did not write is never overwritten, so a user's own HTTP
|
||||
# `homeassistant` survives even when it sits on the configured URL.
|
||||
claimable = existing is None or is_managed(name, existing, owned_url)
|
||||
if claimable:
|
||||
if existing != entry:
|
||||
servers[name] = entry
|
||||
changed = True
|
||||
elif existing is not None and is_managed(name, existing):
|
||||
if name == "homeassistant" and entry.get("type") == "http":
|
||||
if owned_url != entry["url"]:
|
||||
state.setdefault(str(path), {})["homeassistant_http_url"] = entry["url"]
|
||||
owned_url = entry["url"]
|
||||
state_changed = True
|
||||
elif existing is not None and is_managed(name, existing, owned_url):
|
||||
# Covers both "feature disabled" and "deselected for this client".
|
||||
del servers[name]
|
||||
changed = True
|
||||
if name == "homeassistant" and state.get(str(path), {}).pop(
|
||||
"homeassistant_http_url", None
|
||||
):
|
||||
owned_url = ""
|
||||
state_changed = True
|
||||
if changed:
|
||||
if servers:
|
||||
data["mcpServers"] = servers
|
||||
@@ -440,6 +599,9 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
# permissions between merges.
|
||||
if path.exists():
|
||||
path.chmod(0o600)
|
||||
|
||||
if state_changed:
|
||||
save_state(state)
|
||||
PY
|
||||
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,5 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
- Fix persistent data directory: the entrypoint now writes to the `/config` mount (the container-side path of the `app_config` map) instead of an unmounted `/app_configs/cleanuparr` path, so Cleanuparr's data survives container recreation.
|
||||
|
||||
## 2.10.2 (2026-08-01)
|
||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||
|
||||
@@ -63,7 +63,7 @@ COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
|
||||
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
|
||||
|
||||
# Bypass the original /entrypoint.sh (which mounts /config as VOLUME).
|
||||
# Our wrapper symlinks /app/config → HA addon_config and starts ./Cleanuparr directly.
|
||||
# Our wrapper symlinks /app/config → HA app_config and starts ./Cleanuparr directly.
|
||||
ENTRYPOINT ["/ha_entrypoint.sh"]
|
||||
CMD ["./Cleanuparr"]
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ ingress_stream: true
|
||||
init: false
|
||||
panel_icon: mdi:movie-search
|
||||
map:
|
||||
- addon_config:rw
|
||||
- app_config:rw
|
||||
name: Cleanuparr
|
||||
options:
|
||||
env_vars: []
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
###############################################################################
|
||||
# Home Assistant Addon entrypoint for Cleanuparr
|
||||
# The .NET app uses /app/config as its data directory.
|
||||
# We symlink /app/config → /addon_configs/cleanuparr (HA persistent storage)
|
||||
# We symlink /app/config → /config (the HA app_config mount, HA persistent storage)
|
||||
# and start ./Cleanuparr directly, bypassing the original /entrypoint.sh
|
||||
# which would trigger the /config Docker VOLUME mount.
|
||||
###############################################################################
|
||||
@@ -26,7 +26,7 @@ if [ -d /etc/cont-init.d ]; then
|
||||
fi
|
||||
|
||||
# ─── Setup persistent data directory ─────────────────────────────────────────
|
||||
HA_DATA_DIR="/addon_configs/cleanuparr"
|
||||
HA_DATA_DIR="/config"
|
||||
echo "[Cleanuparr] Setting up data directory: $HA_DATA_DIR"
|
||||
mkdir -p "$HA_DATA_DIR"
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 19.19.1 (2026-06-17)
|
||||
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
|
||||
@@ -225,7 +226,7 @@
|
||||
## 16.17.7-3 (2023-11-19)
|
||||
|
||||
- Minor bugs fixed
|
||||
- MAJOR CHANGE : new HA config logic implemented. Files are now located in the addon config file, that can be accessed from the addon_configs folder from my cloudcommander or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- MAJOR CHANGE : new HA config logic implemented. Files are now located in the addon config file, that can be accessed from the app_configs folder from my cloudcommander or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
|
||||
- Homeassistant config accessible in /homeassistant folder ; all addons config in /addons_config ; this addon config in /config
|
||||
|
||||
## 16.17.7-2 (2023-10-30)
|
||||
|
||||
@@ -69,8 +69,8 @@ image: ghcr.io/alexbelgium/cloudcommander-{arch}
|
||||
ingress: true
|
||||
ingress_stream: true
|
||||
map:
|
||||
- addon_config:rw
|
||||
- all_addon_configs:rw
|
||||
- app_config:rw
|
||||
- all_app_configs:rw
|
||||
- homeassistant_config:rw
|
||||
- ssl:rw
|
||||
- share:rw
|
||||
|
||||
@@ -35,7 +35,7 @@ fi
|
||||
# Create symlinks with legacy folders
|
||||
if [ -d /homeassistant/addons_config ]; then
|
||||
ln -s /homeassistant/addons_config /config
|
||||
find /addon_configs/ -maxdepth 1 -mindepth 1 -type d -not -name "*cloudcommander*" -exec ln -s {} /config/addons_config/ \;
|
||||
find /app_configs/ -maxdepth 1 -mindepth 1 -type d -not -name "*cloudcommander*" -exec ln -s {} /config/addons_config/ \;
|
||||
fi
|
||||
if [ -d /homeassistant/addons_autoscripts ]; then
|
||||
ln -s /homeassistant/addons_autoscripts /config
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,4 @@
|
||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||
|
||||
## 2.2.5 (2026-08-02)
|
||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||
|
||||
@@ -91,7 +91,7 @@ cifsdomain: "WORKGROUP"
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
This addon supports custom scripts and environment variables through the `addon_config` mapping:
|
||||
This addon supports custom scripts and environment variables through the `app_config` mapping:
|
||||
|
||||
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
|
||||
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||