Skip to content

Create persistent notification also (not just log warning) when key expiration is detected#605

Draft
lmagyar wants to merge 11 commits into
hassio-addons:mainfrom
lmagyar:pr-key-expiry-notification
Draft

Create persistent notification also (not just log warning) when key expiration is detected#605
lmagyar wants to merge 11 commits into
hassio-addons:mainfrom
lmagyar:pr-key-expiry-notification

Conversation

@lmagyar

@lmagyar lmagyar commented Jan 10, 2026

Copy link
Copy Markdown
Collaborator

Proposed Changes

If we thought that this is the only warning we should highlight in the docs, it worths a notification.

But this will be created only once after install.

Note to myself: notification is also useful for invalid networking DNS config check, see: lmagyar@6669e24

Image

Related Issues

fixes #603

Summary by CodeRabbit

  • New Features

    • Enabled Home Assistant API integration via a new top-level flag.
    • Added a one-time persistent notification when a Tailscale key expiration is detected.
  • Improvements

    • Reformatted key-expiration timestamps to "YYYY-MM-DD HH:MM UTC".
    • Introduced an API helper for supervisor REST calls with clearer logging, robust HTTP status handling, optional silent mode, and idempotent notification guarding.

@lmagyar lmagyar added the enhancement Enhancement of the code, not introducing new features. label Jan 10, 2026
@coderabbitai

coderabbitai Bot commented Jan 10, 2026

Copy link
Copy Markdown

Walkthrough

Adds a Home Assistant REST API Bash library and a one-time persistent notification flow in post-tailscaled/run to notify on Tailscale auth key expiry (guarded by a file). Also adds a top-level config flag homeassistant_api: true.

Changes

Cohort / File(s) Summary
Configuration
tailscale/config.yaml
Added top-level boolean homeassistant_api: true.
API Integration Library
tailscale/rootfs/usr/lib/api.core.sh
New Bash library exposing api.core() (wrapper for Home Assistant Supervisor API with auth header, request/response handling, optional jq filtering, options -s and -w) and log.error_or_warning().
Startup Notification Flow
tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
Sources api.core.sh; adds declare attributes; reformats key expiry timestamp to "YYYY-MM-DD HH:MM UTC"; builds JSON payload and calls api.core to POST a one-time persistent notification on key expiry; creates guard file /data/key_expiry_notification_is_done to ensure idempotence.

Sequence Diagram(s)

sequenceDiagram
    participant Tailscale as Tailscale Service
    participant Script as post-tailscaled/run
    participant API as api.core.sh
    participant HA as Home Assistant Supervisor API
    participant UI as Persistent Notification Service

    Tailscale->>Script: start / provide key expiry info
    Script->>Script: check expiry & /data/key_expiry_notification_is_done
    alt key expired AND no guard file
        Script->>API: api.core POST services/persistent_notification/create (JSON)
        API->>HA: HTTP request with Authorization
        HA-->>API: 200 OK / response
        API-->>Script: success
        Script->>Script: create /data/key_expiry_notification_is_done
        HA->>UI: display persistent notification
    else guard exists OR not expired
        Script->>Script: skip notification
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nibbled at the config line,

A tiny flag, a tidy sign.
I whispered through a bashy plea,
“Notify them once — then let me be.”
Hop — a bell for keys that time. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: adding a persistent Home Assistant notification when Tailscale key expiration is detected, in addition to existing logging.
Linked Issues check ✅ Passed The PR implements the core requirement from issue #603: creating a Home Assistant notification for authentication key expiry, enabling users to be promptly alerted.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the persistent notification feature for key expiry. The new API library and configuration flag support this objective without introducing unrelated functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lmagyar lmagyar added the no-stale This issue or PR is exempted from the stable bot. label Jan 10, 2026
@lmagyar lmagyar marked this pull request as draft January 10, 2026 22:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @tailscale/rootfs/usr/lib/core.sh:
- Around line 4-11: The function log.error_or_warning incorrectly forwards
arguments using unquoted $* which breaks when arguments contain whitespace;
change both bashio::log.error $* and bashio::log.warning $* to forward the
original arguments as "$@" so argument boundaries and spaces are preserved when
calling bashio::log.error and bashio::log.warning from log.error_or_warning.
- Around line 38-49: The local variable declarations are syntactically wrong and
declare an unused variable; in the scope where getopts is used (the while
getopts "sw" o; do ... done block in core.sh) replace "local OPTIND o a" with
"local OPTIND o" (remove unused 'a'), and replace "local silent= local
warning_only=" with proper initializations like "local silent= warning_only=" or
"local silent=0 warning_only=0" depending on expected truthiness; also add a
default *) case inside the case statement to handle invalid flags (e.g., print
an error or return/non-zero) so the getopts loop robustly handles unexpected
options.
🧹 Nitpick comments (1)
tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run (1)

167-178: Consider improving error handling for notification creation.

The notification logic correctly prevents duplicates, but has potential error handling concerns:

  1. Line 176: The -w flag (warning only) combined with > /dev/null suppresses all output and downgrades errors to warnings. If the API call fails, users may not notice, though the log warnings still provide visibility.

  2. Line 177: The marker file is created regardless of whether the API call succeeded. If the notification creation fails, the marker prevents future retry attempts.

Consider checking the exit status of core.api before creating the marker file to allow retry on failure.

♻️ Proposed improvement for error handling
   # Create persistent notification also, but only once
   if ! bashio::fs.file_exists "/data/key_expiry_notification_is_done"; then
     attributes=$(bashio::var.json \
         title "Tailscale Add-on" \
         message "$(printf "%s\n\n" \
           "**The connection's key will expire on: ${keyexpiry}!**" \
           "Consider disabling key expiry to avoid losing connection to your Home Assistant device." \
           "You can use the [Machines page](https://login.tailscale.com/admin/machines) of Tailscale's admin console to disable key expiry." \
           "Please check your configuration based on the add-on's documentation under the \"Configuration\" section.")")
-    core.api POST services/persistent_notification/create "${attributes}" -w > /dev/null
-    touch "/data/key_expiry_notification_is_done"
+    if core.api POST services/persistent_notification/create "${attributes}" -w > /dev/null; then
+      touch "/data/key_expiry_notification_is_done"
+    fi
   fi

This ensures the marker file is only created when the notification is successfully created.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37feb10 and f26e3f7.

📒 Files selected for processing (3)
  • tailscale/config.yaml
  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
  • tailscale/rootfs/usr/lib/core.sh
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:29-30
Timestamp: 2025-11-23T00:06:06.013Z
Learning: In the Home Assistant Tailscale add-on (hassio-addons/addon-tailscale), userspace_networking defaults to true because TUN networking can cause problems and is not needed for basic Home Assistant accessibility. This is an intentional deviation from stock Tailscale's default kernel/TUN networking.
Learnt from: mikz
Repo: hassio-addons/addon-tailscale PR: 407
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run:24-29
Timestamp: 2024-10-09T10:32:14.110Z
Learning: In the Tailscale add-on for Home Assistant, the `tailscale/rootfs` directory is copied into the container's root directory via the Dockerfile, ensuring that files like `/etc/nginx/templates/homeassistant.gtpl` are present at runtime.
📚 Learning: 2025-11-06T10:55:55.062Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 566
File: tailscale/rootfs/usr/bin/subnet-routes:1-2
Timestamp: 2025-11-06T10:55:55.062Z
Learning: In the hassio-addons/addon-tailscale project, utility scripts in `/usr/bin/` (like `subnet-routes`, `protect-subnet-routes`, `unprotect-subnet-routes`) are invoked from s6-overlay service scripts or NetworkManager dispatchers that already use `#!/command/with-contenv bashio`. These utilities inherit the environment from their parent processes and therefore use `#!/usr/bin/env bashio` rather than `#!/command/with-contenv bashio`.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
📚 Learning: 2024-11-15T17:38:56.576Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 419
File: tailscale/rootfs/command/with-contenv-merge:8-12
Timestamp: 2024-11-15T17:38:56.576Z
Learning: The script `with-contenv-merge` in `tailscale/rootfs/command/` is directly copied from s6 and should not be modified.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
📚 Learning: 2025-11-23T00:06:06.013Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:29-30
Timestamp: 2025-11-23T00:06:06.013Z
Learning: In the Home Assistant Tailscale add-on (hassio-addons/addon-tailscale), userspace_networking defaults to true because TUN networking can cause problems and is not needed for basic Home Assistant accessibility. This is an intentional deviation from stock Tailscale's default kernel/TUN networking.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
  • tailscale/config.yaml
📚 Learning: 2024-10-09T10:32:14.110Z
Learnt from: mikz
Repo: hassio-addons/addon-tailscale PR: 407
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run:24-29
Timestamp: 2024-10-09T10:32:14.110Z
Learning: In the Tailscale add-on for Home Assistant, the `tailscale/rootfs` directory is copied into the container's root directory via the Dockerfile, ensuring that files like `/etc/nginx/templates/homeassistant.gtpl` are present at runtime.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
  • tailscale/config.yaml
📚 Learning: 2025-09-17T23:53:19.467Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 509
File: tailscale/config.yaml:0-0
Timestamp: 2025-09-17T23:53:19.467Z
Learning: In the Tailscale add-on config.yaml, there is an intentional naming mismatch between map entries and schema field names: map entries use actual Home Assistant directory names (like `homeassistant_config`, `all_addon_configs`) while schema fields use user-friendly names (like `config`, `addon_configs`). This allows correct directory mapping while presenting cleaner option names in the UI.

Applied to files:

  • tailscale/config.yaml
🪛 Shellcheck (0.11.0)
tailscale/rootfs/usr/lib/core.sh

[warning] 7-7: Use "$@" (with quotes) to prevent whitespace problems.

(SC2048)


[warning] 9-9: Use "$@" (with quotes) to prevent whitespace problems.

(SC2048)


[warning] 38-38: a appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 39-39: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[error] 39-39: This applies local to the variable named local, which is probably not what you want. Use a separate command or the appropriate declare options instead.

(SC2316)


[warning] 39-39: local appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 41-48: Invalid flags are not handled. Add a *) case.

(SC2220)

🔇 Additional comments (9)
tailscale/config.yaml (1)

17-17: LGTM! Configuration change enables Home Assistant API access.

The addition of homeassistant_api: true correctly enables API access needed for the persistent notification feature.

tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run (3)

8-8: LGTM! Sourcing the new utility library.

The script correctly sources the new core library that provides the API wrapper functions.


18-18: LGTM! Variable declaration for notification attributes.

The declaration of attributes is appropriate for storing the JSON payload used in the notification logic.


161-165: LGTM! Date formatting and improved warning messages.

The key expiry date formatting and updated warning text correctly guide users to the configuration documentation.

tailscale/rootfs/usr/lib/core.sh (5)

26-36: LGTM! Function signature and argument parsing logic.

The function correctly handles method, resource, optional POST data, and optional jq filter arguments.


51-71: LGTM! Authorization and HTTP request logic.

The function correctly handles authorization headers and executes the curl request with appropriate error handling.


73-80: LGTM! Response parsing and debug logging.

The code correctly extracts the HTTP status code and response body, with appropriate debug logging.


82-112: LGTM! HTTP status code handling.

The function provides comprehensive error handling for common HTTP status codes (400, 401, 403, 404, 405) and handles unknown errors appropriately.


114-121: LGTM! Optional jq filtering and response output.

The function correctly applies optional jq filtering and returns the response with appropriate exit codes.

Comment thread tailscale/rootfs/usr/lib/api.core.sh
Comment thread tailscale/rootfs/usr/lib/core.sh Outdated
@lmagyar lmagyar marked this pull request as ready for review January 10, 2026 22:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @tailscale/rootfs/usr/lib/core.sh:
- Around line 117-124: If bashio::jq fails when applying the filter, do not echo
partial/empty output or return __BASHIO_EXIT_OK; instead capture the exit status
of bashio::jq after calling it with response and filter, and on non-zero status
call bashio::log.error with a descriptive message (including the filter and any
stderr if available) and return __BASHIO_EXIT_NOK; only echo "${response}" and
return __BASHIO_EXIT_OK when bashio::jq succeeds or when no filter was provided.
- Around line 76-115: Strip any trailing newlines from the extracted status
reliably and guard all numeric comparisons by first validating status is
numeric: after extracting status, remove newlines (e.g.,
status=${status//$'\n'/} and response=${response%$'\n'}), then check with a
regex like [[ "${status}" =~ ^[0-9]+$ ]] and if it fails call
log.error_or_warning "${warning_only}" "Unexpected non-numeric HTTP status:
${status}" and return "${__BASHIO_EXIT_NOK}"; only perform the existing [[
"${status}" -eq ... ]] checks when the numeric validation passes.
- Around line 26-53: The core.api function misuses getopts without resetting
OPTIND and does blind shifts that can fail if callers pass too few args; before
any shifts validate that required arguments (method and resource) exist using
parameter checks like "${1:-}" and return or error early if missing, then safely
assign method and resource, and before invoking getopts reset OPTIND=1 so option
parsing is deterministic; ensure you only shift when you confirmed an argument
exists (use guarded shifts) and keep option variables (silent, warning_only) as
before.
- Around line 54-84: The function currently always sends -d "${data}" and
doesn't capture curl stderr or set timeouts and logs raw request bodies; update
the curl invocation in the API call to: omit the -d option when "${method}" is
GET (only include -d when method != GET), redirect stderr into the response
capture (append 2>&1 to the subshell command) and add connection/read timeouts
(e.g. --connect-timeout and --max-time flags), and remove or redact logging of
the raw request body (avoid bashio::log.debug "Request data: ${data}" or only
log a masked/summary value when method != GET) while keeping existing variables
(method, data, response, status, __BASHIO_SUPERVISOR_TOKEN,
__BASHIO_SUPERVISOR_API) and same error path (log.error_or_warning and return
__BASHIO_EXIT_NOK).
🧹 Nitpick comments (1)
tailscale/rootfs/usr/lib/core.sh (1)

4-11: Make warning_only a boolean flag (don’t overload “has value”).
Right now any non-empty first arg flips to warning, which is easy to misuse (e.g., passing a message accidentally as first arg). Consider explicit 0/1 semantics and clearer call sites.

Proposed tweak
 function log.error_or_warning() {
-    local warning_only="$1"; shift
-    if ! bashio::var.has_value "${warning_only}"; then
+    local warning_only="${1:-0}"; shift || true
+    if [[ "${warning_only}" == "0" ]]; then
         bashio::log.error "$@"
     else
         bashio::log.warning "$@"
     fi
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f26e3f7 and 2a123f7.

📒 Files selected for processing (1)
  • tailscale/rootfs/usr/lib/core.sh
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:29-30
Timestamp: 2025-11-23T00:06:06.013Z
Learning: In the Home Assistant Tailscale add-on (hassio-addons/addon-tailscale), userspace_networking defaults to true because TUN networking can cause problems and is not needed for basic Home Assistant accessibility. This is an intentional deviation from stock Tailscale's default kernel/TUN networking.
Learnt from: mikz
Repo: hassio-addons/addon-tailscale PR: 407
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run:24-29
Timestamp: 2024-10-09T10:32:14.110Z
Learning: In the Tailscale add-on for Home Assistant, the `tailscale/rootfs` directory is copied into the container's root directory via the Dockerfile, ensuring that files like `/etc/nginx/templates/homeassistant.gtpl` are present at runtime.
📚 Learning: 2025-10-29T20:47:02.818Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 562
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run:120-128
Timestamp: 2025-10-29T20:47:02.818Z
Learning: In the hassio-addons/addon-tailscale project, when checking for IPv6 addresses in bash scripts, use the regex format `if [[ "${variable}" =~ .*:.* ]]; then` for consistency with other IPv6 tests in the codebase.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-11-06T10:55:55.062Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 566
File: tailscale/rootfs/usr/bin/subnet-routes:1-2
Timestamp: 2025-11-06T10:55:55.062Z
Learning: In the hassio-addons/addon-tailscale project, utility scripts in `/usr/bin/` (like `subnet-routes`, `protect-subnet-routes`, `unprotect-subnet-routes`) are invoked from s6-overlay service scripts or NetworkManager dispatchers that already use `#!/command/with-contenv bashio`. These utilities inherit the environment from their parent processes and therefore use `#!/usr/bin/env bashio` rather than `#!/command/with-contenv bashio`.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-09-16T23:47:20.987Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:30-37
Timestamp: 2025-09-16T23:47:20.987Z
Learning: In the hassio-addons/addon-tailscale project, the advertise_routes configuration uses `local_subnets` (with underscore) as the default value, and the runtime regex patterns use `local.subnets` (with dot) which correctly matches the underscore variant because '.' in regex matches any single character including '_'.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2024-11-15T17:38:56.576Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 419
File: tailscale/rootfs/command/with-contenv-merge:8-12
Timestamp: 2024-11-15T17:38:56.576Z
Learning: The script `with-contenv-merge` in `tailscale/rootfs/command/` is directly copied from s6 and should not be modified.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh

Comment thread tailscale/rootfs/usr/lib/core.sh Outdated
Comment thread tailscale/rootfs/usr/lib/api.core.sh
Comment thread tailscale/rootfs/usr/lib/api.core.sh
Comment thread tailscale/rootfs/usr/lib/api.core.sh

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run:
- Around line 167-179: The current one-time guard using the file
/data/key_expiry_notification_is_done prevents re-notification when a new key
expiry appears; change the guard logic around the keyexpiry check so you store
the expiry value in the marker instead of just touching it: read the marker if
present and compare its contents to the current keyexpiry, and only call
core.api POST services/persistent_notification/create with attributes and then
update/write the marker with the current keyexpiry when they differ (replace the
touch "/data/key_expiry_notification_is_done" behavior with writing keyexpiry),
or alternatively remove the guard entirely if you want notifications on every
restart; update the logic surrounding attributes, keyexpiry and the marker file
accordingly.
- Around line 169-175: The notification currently hardcodes the admin URL;
update the attributes construction in the s6 run script to use the configured
login_server variable (e.g., use "${login_server%/}/admin/machines" or similar
to avoid double slashes) instead of
"https://login.tailscale.com/admin/machines", and ensure you fall back to
"https://login.tailscale.com" when login_server is empty or unset (login_server
is declared earlier around lines 78-82) so custom login servers produce a
correct admin console link.
🧹 Nitpick comments (2)
tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run (2)

161-161: Add format validation for robust keyexpiry parsing.

The substring extraction assumes ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ). If Tailscale changes the KeyExpiry format in the future, this will silently produce malformed output.

🛡️ Proposed validation and error handling
+# Validate and format keyexpiry
+if [[ ! "${keyexpiry}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ]]; then
+  bashio::log.warning "Unexpected KeyExpiry format: ${keyexpiry}"
+  keyexpiry_display="${keyexpiry}"
+else
-  keyexpiry="${keyexpiry:0:10} ${keyexpiry:11:8} UTC"
+  keyexpiry_display="${keyexpiry:0:10} ${keyexpiry:11:8} UTC"
+fi

-  bashio::log.warning "The connection's key will expire on: ${keyexpiry}"
+  bashio::log.warning "The connection's key will expire on: ${keyexpiry_display}"

Alternatively, consider using date for more robust parsing:

keyexpiry_display=$(date -d "${keyexpiry}" "+%Y-%m-%d %H:%M:%S UTC" 2>/dev/null) || keyexpiry_display="${keyexpiry}"

167-168: Consider logging when notification is skipped.

When the guard file exists, the notification creation is silently skipped. Adding a debug log message would help administrators understand why they're not seeing a notification on subsequent restarts.

📝 Suggested logging enhancement
   # Create persistent notification also, but only once
-  if ! bashio::fs.file_exists "/data/key_expiry_notification_is_done"; then
+  if ! bashio::fs.file_exists "/data/key_expiry_notification_is_done"; then
     attributes=$(bashio::var.json \
         title "Tailscale Add-on" \
         message "$(printf "%s\n\n" \
           "**The connection's key will expire on: ${keyexpiry}!**" \
           "Consider disabling key expiry to avoid losing connection to your Home Assistant device." \
           "You can use the [Machines page](https://login.tailscale.com/admin/machines) of Tailscale's admin console to disable key expiry." \
           "Please check your configuration based on the add-on's documentation under the \"Configuration\" section.")")
     if core.api POST services/persistent_notification/create "${attributes}" -w > /dev/null; then
       touch "/data/key_expiry_notification_is_done"
     fi
+  else
+    bashio::log.debug "Key expiry notification already sent; skipping (guard file exists)"
   fi
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a123f7 and fcdcdf4.

📒 Files selected for processing (1)
  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: mikz
Repo: hassio-addons/addon-tailscale PR: 407
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run:24-29
Timestamp: 2024-10-09T10:32:14.110Z
Learning: In the Tailscale add-on for Home Assistant, the `tailscale/rootfs` directory is copied into the container's root directory via the Dockerfile, ensuring that files like `/etc/nginx/templates/homeassistant.gtpl` are present at runtime.
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:29-30
Timestamp: 2025-11-23T00:06:06.013Z
Learning: In the Home Assistant Tailscale add-on (hassio-addons/addon-tailscale), userspace_networking defaults to true because TUN networking can cause problems and is not needed for basic Home Assistant accessibility. This is an intentional deviation from stock Tailscale's default kernel/TUN networking.
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 566
File: tailscale/rootfs/usr/bin/subnet-routes:1-2
Timestamp: 2025-11-06T10:55:55.062Z
Learning: In the hassio-addons/addon-tailscale project, utility scripts in `/usr/bin/` (like `subnet-routes`, `protect-subnet-routes`, `unprotect-subnet-routes`) are invoked from s6-overlay service scripts or NetworkManager dispatchers that already use `#!/command/with-contenv bashio`. These utilities inherit the environment from their parent processes and therefore use `#!/usr/bin/env bashio` rather than `#!/command/with-contenv bashio`.
📚 Learning: 2024-11-15T17:38:56.576Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 419
File: tailscale/rootfs/command/with-contenv-merge:8-12
Timestamp: 2024-11-15T17:38:56.576Z
Learning: The script `with-contenv-merge` in `tailscale/rootfs/command/` is directly copied from s6 and should not be modified.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
📚 Learning: 2025-11-06T10:55:55.062Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 566
File: tailscale/rootfs/usr/bin/subnet-routes:1-2
Timestamp: 2025-11-06T10:55:55.062Z
Learning: In the hassio-addons/addon-tailscale project, utility scripts in `/usr/bin/` (like `subnet-routes`, `protect-subnet-routes`, `unprotect-subnet-routes`) are invoked from s6-overlay service scripts or NetworkManager dispatchers that already use `#!/command/with-contenv bashio`. These utilities inherit the environment from their parent processes and therefore use `#!/usr/bin/env bashio` rather than `#!/command/with-contenv bashio`.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
📚 Learning: 2025-11-23T00:06:06.013Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:29-30
Timestamp: 2025-11-23T00:06:06.013Z
Learning: In the Home Assistant Tailscale add-on (hassio-addons/addon-tailscale), userspace_networking defaults to true because TUN networking can cause problems and is not needed for basic Home Assistant accessibility. This is an intentional deviation from stock Tailscale's default kernel/TUN networking.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
📚 Learning: 2024-10-09T10:32:14.110Z
Learnt from: mikz
Repo: hassio-addons/addon-tailscale PR: 407
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run:24-29
Timestamp: 2024-10-09T10:32:14.110Z
Learning: In the Tailscale add-on for Home Assistant, the `tailscale/rootfs` directory is copied into the container's root directory via the Dockerfile, ensuring that files like `/etc/nginx/templates/homeassistant.gtpl` are present at runtime.

Applied to files:

  • tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run

Comment thread tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
Comment thread tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @tailscale/rootfs/usr/lib/core.sh:
- Around line 41-52: The getopts loop can fail on subsequent calls because
OPTIND is not reset; before the while getopts "sw" o; do block (which sets
silent and warning_only), reset OPTIND (e.g. OPTIND=1 or declare a local OPTIND)
so option parsing starts fresh each call, ensuring getopts processes options
correctly across multiple invocations.
🧹 Nitpick comments (2)
tailscale/rootfs/usr/lib/core.sh (2)

34-34: Consider using -n for improved readability.

The condition ! -z works correctly but -n is more idiomatic for checking if a variable has a value.

♻️ Suggested refactor
-    if [[ ! -z "${1:-}" && "${1::1}" != "-" ]]; then
+    if [[ -n "${1:-}" && "${1::1}" != "-" ]]; then

54-74: Consider adding validation for missing authentication token.

If __BASHIO_SUPERVISOR_TOKEN is not set, the Authorization header will be Authorization: Bearer without a token value (Line 54), which will likely result in a 401 authentication error. While this error would be handled by the status code checks later, it might be more defensive to detect and log this condition upfront.

Consider adding a check after Line 62:

if [[ "${auth_header}" == "Authorization: Bearer" ]]; then
    bashio::log.warning "Supervisor token not available, API call may fail authentication"
fi

This would make it clearer when authentication credentials are missing, rather than relying on interpreting HTTP 401 responses.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fcdcdf4 and 9ff0dc5.

📒 Files selected for processing (1)
  • tailscale/rootfs/usr/lib/core.sh
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 605
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run:167-179
Timestamp: 2026-01-10T23:19:59.890Z
Learning: In the Tailscale add-on, the key expiry notification in `tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run` is intentionally designed to be shown only once per device lifecycle using the guard file `/data/key_expiry_notification_is_done`. This one-time behavior is the desired design.
Learnt from: mikz
Repo: hassio-addons/addon-tailscale PR: 407
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run:24-29
Timestamp: 2024-10-09T10:32:14.110Z
Learning: In the Tailscale add-on for Home Assistant, the `tailscale/rootfs` directory is copied into the container's root directory via the Dockerfile, ensuring that files like `/etc/nginx/templates/homeassistant.gtpl` are present at runtime.
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:29-30
Timestamp: 2025-11-23T00:06:06.013Z
Learning: In the Home Assistant Tailscale add-on (hassio-addons/addon-tailscale), userspace_networking defaults to true because TUN networking can cause problems and is not needed for basic Home Assistant accessibility. This is an intentional deviation from stock Tailscale's default kernel/TUN networking.
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 566
File: tailscale/rootfs/usr/bin/subnet-routes:1-2
Timestamp: 2025-11-06T10:55:55.062Z
Learning: In the hassio-addons/addon-tailscale project, utility scripts in `/usr/bin/` (like `subnet-routes`, `protect-subnet-routes`, `unprotect-subnet-routes`) are invoked from s6-overlay service scripts or NetworkManager dispatchers that already use `#!/command/with-contenv bashio`. These utilities inherit the environment from their parent processes and therefore use `#!/usr/bin/env bashio` rather than `#!/command/with-contenv bashio`.
📚 Learning: 2026-01-10T23:18:22.775Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 605
File: tailscale/rootfs/usr/lib/core.sh:26-53
Timestamp: 2026-01-10T23:18:22.775Z
Learning: In tailscale/rootfs/usr/lib/core.sh, the core.api() function uses a design pattern where options (-s, -w) are placed AFTER all positional arguments (method, resource, optional POST data, optional jq filter), as documented in the function's comment block.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-10-29T20:47:02.818Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 562
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run:120-128
Timestamp: 2025-10-29T20:47:02.818Z
Learning: In the hassio-addons/addon-tailscale project, when checking for IPv6 addresses in bash scripts, use the regex format `if [[ "${variable}" =~ .*:.* ]]; then` for consistency with other IPv6 tests in the codebase.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-11-06T10:55:55.062Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 566
File: tailscale/rootfs/usr/bin/subnet-routes:1-2
Timestamp: 2025-11-06T10:55:55.062Z
Learning: In the hassio-addons/addon-tailscale project, utility scripts in `/usr/bin/` (like `subnet-routes`, `protect-subnet-routes`, `unprotect-subnet-routes`) are invoked from s6-overlay service scripts or NetworkManager dispatchers that already use `#!/command/with-contenv bashio`. These utilities inherit the environment from their parent processes and therefore use `#!/usr/bin/env bashio` rather than `#!/command/with-contenv bashio`.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-09-16T23:47:20.987Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:30-37
Timestamp: 2025-09-16T23:47:20.987Z
Learning: In the hassio-addons/addon-tailscale project, the advertise_routes configuration uses `local_subnets` (with underscore) as the default value, and the runtime regex patterns use `local.subnets` (with dot) which correctly matches the underscore variant because '.' in regex matches any single character including '_'.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2024-11-15T17:38:56.576Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 419
File: tailscale/rootfs/command/with-contenv-merge:8-12
Timestamp: 2024-11-15T17:38:56.576Z
Learning: The script `with-contenv-merge` in `tailscale/rootfs/command/` is directly copied from s6 and should not be modified.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-12-26T14:17:39.622Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 455
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/run:73-75
Timestamp: 2025-12-26T14:17:39.622Z
Learning: In the hassio-addons projects using bashio, the bashio library automatically sets `set -o errexit`, `set -o errtrace`, `set -o nounset`, and `set -o pipefail` by default (see https://github.com/hassio-addons/bashio/blob/main/lib/bashio lines 10-13). Scripts using `#!/command/with-contenv bashio` or `#!/usr/bin/env bashio` inherit this behavior, so explicit error handling is not required for commands that exit with non-zero status codes.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-09-16T23:47:20.987Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/config.yaml:30-37
Timestamp: 2025-09-16T23:47:20.987Z
Learning: In regex patterns, '.' matches any single character, so `local.subnets` in a regex would match `local_subnets`, `localXsubnets`, etc. The user lmagyar correctly pointed this out when I mistakenly identified it as a literal string mismatch.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2025-09-17T21:52:47.107Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 541
File: tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh:68-72
Timestamp: 2025-09-17T21:52:47.107Z
Learning: I made a critical regex error by claiming that the pattern "local.subnets" wouldn't match "local_subnets". In regex, '.' matches any single character including '_', so this pattern works correctly. The user lmagyar had to correct this same basic regex misunderstanding twice.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
📚 Learning: 2026-01-10T23:19:59.890Z
Learnt from: lmagyar
Repo: hassio-addons/addon-tailscale PR: 605
File: tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run:167-179
Timestamp: 2026-01-10T23:19:59.890Z
Learning: In the Tailscale add-on, the key expiry notification in `tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run` is intentionally designed to be shown only once per device lifecycle using the guard file `/data/key_expiry_notification_is_done`. This one-time behavior is the desired design.

Applied to files:

  • tailscale/rootfs/usr/lib/core.sh
🔇 Additional comments (3)
tailscale/rootfs/usr/lib/core.sh (3)

1-3: LGTM!

The shebang and shellcheck directive are appropriate for a bash library file that will be sourced by other scripts.


4-11: LGTM!

The function correctly handles conditional error/warning logging based on the first parameter value.


76-124: LGTM!

The response parsing, HTTP status code handling, and error reporting are well-structured. The function correctly:

  • Extracts the HTTP status code from curl output
  • Handles all common HTTP error codes with appropriate messages
  • Respects the -s (silent) and -w (warning-only) flags
  • Applies optional jq filtering when specified
  • Returns appropriate exit codes

Comment thread tailscale/rootfs/usr/lib/api.core.sh Outdated
@lmagyar lmagyar force-pushed the pr-key-expiry-notification branch from 45b3280 to 2019d1b Compare January 30, 2026 22:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tailscale/rootfs/usr/lib/api.core.sh`:
- Around line 26-36: The POST argument shifting currently unconditionally
consumes the next positional arg; update the api.core function so data is only
consumed and shifted when a POST payload is actually provided: check the next
argument (after resource) for existence and that its first character is not "-"
(e.g., [[ -n "${1:-}" && "${1::1}" != "-" ]]) before assigning data="${1}" and
doing the shift; keep data='{}' otherwise so subsequent option parsing
(filter/getopts) sees flags like -w intact. Ensure this logic references the
existing locals method, resource, data, and filter in api.core.

Comment thread tailscale/rootfs/usr/lib/api.core.sh

@frenck frenck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persistent notifications are user space. This shouldn't be used by integrations (and also thus not add-ons).

../Frenck

                       

Blogging my personal ramblings at frenck.dev

@lmagyar lmagyar force-pushed the pr-key-expiry-notification branch from 5ab62c4 to 5764317 Compare February 5, 2026 20:04
@lmagyar

lmagyar commented Feb 5, 2026

Copy link
Copy Markdown
Collaborator Author

Integrations can use the repair/issue system, but that is not available for an add-on/app.

Persistent notifications are the only way to create a message for the user about an important situation that can cause losing access to the device. Like we get failed login attempt notifications from the http/auth integrations.

I think it is useful, but it's your decision, I can live with it.

@frenck

frenck commented Feb 5, 2026

Copy link
Copy Markdown
Member

Integrations can use the repair/issue system, but that is not available for an add-on/app.

That doesn't make this PR a good thing. Persistent notification are a forbidden area in Home Assistant in that regard. Additionally, these are also visible for secondary users, who can absolutely do nothing with it.

I rather explore the ability for apps to raise repairs (which I really think it discussable)

@lmagyar

lmagyar commented Feb 5, 2026

Copy link
Copy Markdown
Collaborator Author

OK, I mark it as draft, until a SU API is available.

Note: Issue fields that are hard to set for an add-on:

  • domain (maybe the add-on slug?)
  • translation_key (though add-ons have a translation folder)

@lmagyar lmagyar marked this pull request as draft February 5, 2026 21:41
@lmagyar lmagyar force-pushed the pr-key-expiry-notification branch from 5764317 to f5ce9bc Compare February 6, 2026 21:28
@lmagyar lmagyar mentioned this pull request Apr 6, 2026
@lmagyar lmagyar added new-feature New features or options. and removed enhancement Enhancement of the code, not introducing new features. labels Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-feature New features or options. no-stale This issue or PR is exempted from the stable bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authentication expiry should give some notification

2 participants