From c3bde912a157457bb4b0aa303c77660ab0f5262f Mon Sep 17 00:00:00 2001 From: JC Lima Date: Mon, 2 Nov 2026 21:59:51 -0800 Subject: [PATCH 1/6] Accept legacy BOAT_UNITS=nautical on upgrade and prepare 0.6.1 Older setup wizards offered "nautical" and installers before 0.5.7 did not validate BOAT_UNITS, so such configs exist. 0.6.0 refused them with an unhelpful message. Read nautical as metric in the installer, the setup wizard, the Control Panel and Boat Chat, give other unsupported values a message that names the file and the fix, and bump the version to 0.6.1. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 14 +++++ README.md | 70 +++++++++++++++---------- VERSION | 2 +- boat-chat/display_units.py | 2 +- control-panel/app.py | 2 + docs/CONTROL_PANEL_TESTING.md | 18 +++---- docs/DEPLOYMENT.md | 98 +++++++++++++++++------------------ docs/DEVELOPMENT.md | 2 +- docs/GETTING_STARTED.md | 6 +-- docs/INSTALLATION.md | 12 ++--- docs/UPDATING.md | 8 +-- install.sh | 20 ++++++- scripts/get-vesselstack.sh | 4 +- tests/test-clean-install.sh | 22 ++++++++ tests/test_control_panel.py | 14 +++++ tests/test_display_units.py | 8 +++ tests/test_wizard.py | 28 ++++++++-- vesselstack-wizard.py | 9 ++++ 18 files changed, 232 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad6024..55efaac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to VesselStack are documented here. The project follows Semantic Versioning. +## 0.6.1 - 2026-11-03 + +### Fixes + +- Upgrading a boat whose settings say `BOAT_UNITS=nautical` (offered by older setup wizards) no + longer stops the update; it is read as metric, and knots and nautical miles are kept. +- A clearer message, naming the file and the fix, for any other unsupported `BOAT_UNITS` value. + +### Release status + +`v0.6.1` is an SSH-signed tag verified by the release workflow against `.github/allowed_signers`. +It is published first as a pre-release and marked Latest after the maintainer's physical +Raspberry Pi install passes. + ## 0.6.0 - 2026-11-02 ### Highlights diff --git a/README.md b/README.md index bf7d293..62a2477 100644 --- a/README.md +++ b/README.md @@ -157,18 +157,18 @@ session), run the pinned release installer and answer the questions in the same ```bash ssh -t YOUR_USER@raspberrypi.local -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash ``` -The same release is also selected by `VESSELSTACK_VERSION=0.6.0` or `--version 0.6.0`: +The same release is also selected by `VESSELSTACK_VERSION=0.6.1` or `--version 0.6.1`: ```bash -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo VESSELSTACK_VERSION=0.6.0 bash -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash -s -- --version 0.6.0 +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo VESSELSTACK_VERSION=0.6.1 bash +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash -s -- --version 0.6.1 ``` > [!NOTE] -> The pinned URL names the release explicitly, so it installs exactly 0.6.0 whatever +> The pinned URL names the release explicitly, so it installs exactly 0.6.1 whatever > GitHub marks as Latest. The form `https://github.com/jclima/VesselStack/releases/latest/download/get-vesselstack.sh` > points to whichever release GitHub marks Latest, which is not necessarily the newest > one (every 0.5.x release was published as a prerelease, so that URL kept serving @@ -215,10 +215,10 @@ that does not point at the exact commit being built, before building anything. T ```bash # defaults, plus the settings you name (repeat --set; names as in vesselstack.example.env) -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh \ +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh \ | sudo bash -s -- --yes --set BOAT_NAME=Foo --set SOCKETCAN_ENABLE=true # every question's own default answer, nothing asked -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash -s -- --defaults +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash -s -- --defaults # a ready-made vesselstack.env (optionally with --set overrides) sudo bash get-vesselstack.sh --yes --config ./vesselstack.env --set BOAT_NAME=Foo ``` @@ -357,8 +357,8 @@ script is wrapped in functions and ends with a single `main "$@"` call, so a truncated download cannot half-run. If you prefer to read it first (recommended): ```bash -curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh -curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh.sha256 +curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh +curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh.sha256 sha256sum -c get-vesselstack.sh.sha256 less get-vesselstack.sh sudo bash get-vesselstack.sh @@ -380,7 +380,7 @@ from the end-to-end guide: ```bash git clone https://github.com/jclima/VesselStack.git cd VesselStack -git checkout v0.6.0 +git checkout v0.6.1 python3 vesselstack-wizard.py --cli # terminal questions (no browser) # python3 vesselstack-wizard.py # or the optional browser wizard ``` @@ -427,7 +427,25 @@ before carrying an update to a boat without internet access. ## Upgrading to 0.6.0 Read this before updating an installed system. Update from [Updating](#updating) with a backup -on a mounted external drive, not while underway. +on a mounted external drive, not while underway. This section applies to 0.6.0 and 0.6.1. + +**Settings that say `BOAT_UNITS=nautical` (fixed in 0.6.1).** Older setup wizards offered +"nautical" as a units choice, and installers before 0.5.7 did not check it, so such a +`vesselstack.env` may exist. 0.6.0 refused it with `BOAT_UNITS must be us_customary or metric` +and stopped the update before changing anything. From 0.6.1 the installer reads `nautical` as +`metric` (metres for depth; knots and nautical miles are kept in both systems), prints one +`Note:` line, and a real install or update rewrites `vesselstack.env` with `BOAT_UNITS=metric` +(a dry run only prints the note). The setup wizard and the Control Panel also show it as Metric +without changing it. Any other unsupported value is still refused before anything is changed, +now with a message naming the file and the fix. If you are stuck on 0.6.0, change the value +by hand and run the update again: + +```bash +sudo sed -i 's/^BOAT_UNITS=.*/BOAT_UNITS=metric/' /opt/vesselstack/config/vesselstack.env +``` + +Rollback: set `BOAT_UNITS` back to `us_customary` (or `metric`) in the same file. 0.6.1 +needs no other manual step. **What changes for an existing boat** @@ -494,14 +512,14 @@ command under Rollback below, fix the value, and update again. **Verify after updating** ```bash -cat /opt/vesselstack/config/installed-version # 0.6.0 +cat /opt/vesselstack/config/installed-version # 0.6.1 sudo vesselstackctl doctor # RESULT healthy sudo vesselstackctl status ``` Open Boat Chat and the Control Panel, and confirm the dashboards show live values. -**Rollback.** Downgrades are refused (`Downgrade from 0.6.0 to ... is not supported`). Restore the +**Rollback.** Downgrades are refused (`Downgrade from 0.6.1 to ... is not supported`). Restore the verified pre-update backup instead: ```bash @@ -614,26 +632,26 @@ git clone https://github.com/jclima/VesselStack.git cd VesselStack ``` -To work from the 0.6.0 release, check out its tag: +To work from the 0.6.1 release, check out its tag: ```bash git fetch --tags -git checkout v0.6.0 +git checkout v0.6.1 ``` -`v0.6.0` is an SSH-signed tag (Ed25519 release key, verified by the release workflow against +`v0.6.1` is an SSH-signed tag (Ed25519 release key, verified by the release workflow against `.github/allowed_signers`). It is published first as a pre-release and marked Latest after the maintainer's physical Raspberry Pi install passes; see the [CHANGELOG](CHANGELOG.md). Verify the tag yourself (needs `ssh-keygen`, which ships with OpenSSH): ```bash git fetch --tags -scripts/verify-release-tag.sh v0.6.0 +scripts/verify-release-tag.sh v0.6.1 # maintainers and owners: also bind the tag to the commit you recorded when it was built # (the full 40-character SHA from your release notes, not one read back from the tag itself) -scripts/verify-release-tag.sh v0.6.0 .github/allowed_signers "$RECORDED_COMMIT" +scripts/verify-release-tag.sh v0.6.1 .github/allowed_signers "$RECORDED_COMMIT" # equivalent plain git command: -git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag v0.6.0 +git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag v0.6.1 ``` The script prints the signer and key fingerprint, and exits non-zero for an unsigned, @@ -661,7 +679,7 @@ nano vesselstack.env | `BOAT_CHAT_ACCESS_TOKEN` | `GENERATE` (default) creates a random token, preserved on re-install; required for chat, status and data APIs | | `INFLUXDB_CONTAINER_NAME` | Advanced override for existing/custom Compose deployments | | `BOAT_TIMEZONE` | IANA timezone, such as `America/Los_Angeles` | -| `BOAT_UNITS` | `us_customary` or `metric` | +| `BOAT_UNITS` | `us_customary` or `metric` (the legacy value `nautical` is read as `metric`; case and surrounding spaces are ignored) | | `VESSELSTACK_USER` | Existing non-root service account | | `VESSELSTACK_UID`, `VESSELSTACK_GID` | Derived from the service account by the installer | | `VESSELSTACK_ROOT` | Application path; default `/opt/vesselstack` | @@ -1138,7 +1156,7 @@ This is the one place in the README that describes updating and rolling back. 2. **Re-run the pinned release installer.** On the Pi, over `ssh -t`: ```bash - curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash + curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash ``` The installer finds the installed system, never overwrites it silently, and offers @@ -1148,7 +1166,7 @@ This is the one place in the README that describes updating and rolling back. `install.sh --start`. Answer **n** to leave the system untouched; the installer prints the exact command to run later. Services restart briefly, so do not update while underway. Plain `sudo vesselstackctl update` with no argument fails: the command needs the release - directory, for example `sudo vesselstackctl update /opt/vesselstack-release/0.6.0`. + directory, for example `sudo vesselstackctl update /opt/vesselstack-release/0.6.1`. Without internet, use `--offline --bundle ...` as described under [Offline install](#offline-install-no-internet-on-the-boat). @@ -1165,13 +1183,13 @@ This is the one place in the README that describes updating and rolling back. commands at the same time. Transient updater files under `/var/lib/vesselstack` are not backed up; configuration and data are. -| Installed version | Update to 0.6.0 | +| Installed version | Update to 0.6.1 | |---|---| -| 0.5.7 | Control Panel or the pinned installer. 0.6.0 is published first as a pre-release (in the 0.5.x panel, enable **Include lab prereleases** on the update card to see it; the 0.6.0 panel calls it **Include early test releases**) and is marked Latest after the maintainer's physical Raspberry Pi install passes | +| 0.5.7 | Control Panel or the pinned installer. 0.6.1 is published first as a pre-release (in the 0.5.x panel, enable **Include lab prereleases** on the update card to see it; the 0.6.1 panel calls it **Include early test releases**) and is marked Latest after the maintainer's physical Raspberry Pi install passes | | 0.5.5, 0.5.6 | Same as 0.5.7: the browser updater was introduced in 0.5.5 | | 0.5.4 or earlier, including 0.4.x | Run the pinned installer from the CLI once (these versions cannot update from the browser); read [Upgrading to 0.6.0](#upgrading-to-060) first | -A checksum shows a download is intact; it is not signed provenance. The signature on the `v0.6.0` tag (see [Download VesselStack](#3-download-vesselstack)) is the provenance check, and the installer does not perform it for you. +A checksum shows a download is intact; it is not signed provenance. The signature on the `v0.6.1` tag (see [Download VesselStack](#3-download-vesselstack)) is the provenance check, and the installer does not perform it for you. **Rollback.** Downgrades are refused. If an update fails, the verified pre-update backup is kept. Check it and restore it during a maintenance window: @@ -1187,7 +1205,7 @@ There is no automatic rollback. To update from a reviewed source checkout with ### Offline install (no internet on the boat) Prepare a bundle on a connected machine, carry it to the boat, and install with -`sudo bash get-vesselstack.sh --offline --bundle /path/to/offline-bundle --tarball /path/to/vesselstack-0.6.0.tar.gz`. +`sudo bash get-vesselstack.sh --offline --bundle /path/to/offline-bundle --tarball /path/to/vesselstack-0.6.1.tar.gz`. The bundle must be root-owned and not group/world-writable. Steps: [Updating and offline install](docs/UPDATING.md#offline-install-no-internet-on-the-boat). ## Stopping or removing services diff --git a/VERSION b/VERSION index a918a2a..ee6cdce 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0 +0.6.1 diff --git a/boat-chat/display_units.py b/boat-chat/display_units.py index 2500eaf..297c99b 100644 --- a/boat-chat/display_units.py +++ b/boat-chat/display_units.py @@ -34,7 +34,7 @@ def normalize_preference(value: str | None) -> str: - return "metric" if str(value or "").strip().lower() in {"metric", "si", "metre", "meters"} else "us_customary" + return "metric" if str(value or "").strip().lower() in {"metric", "si", "metre", "meters", "nautical"} else "us_customary" def preference(setting_getter: Callable[[str, str], str] | None = None) -> str: diff --git a/control-panel/app.py b/control-panel/app.py index 327b5c0..a3f5c6b 100644 --- a/control-panel/app.py +++ b/control-panel/app.py @@ -521,6 +521,8 @@ def public_configuration() -> dict[str, Any]: for spec in FIELDS: source = chat if spec["source"] == "chat" else vessel value = source.get(spec["key"], "") + if spec["key"] == "BOAT_UNITS" and value.strip().lower() == "nautical": + value = "metric" # legacy value from older setup wizards: show Metric, not an unknown choice if spec["key"] in {"CONTROL_PANEL_HOST", "CONTROL_PANEL_PORT"}: value = panel.get(spec["key"], value) result[spec["key"]] = {"configured": bool(value)} if spec["kind"] == "secret" else value diff --git a/docs/CONTROL_PANEL_TESTING.md b/docs/CONTROL_PANEL_TESTING.md index 08b1633..4306cee 100644 --- a/docs/CONTROL_PANEL_TESTING.md +++ b/docs/CONTROL_PANEL_TESTING.md @@ -1,6 +1,6 @@ # Control Panel component lifecycle lab test -This is the release 0.6.0 lab recipe. It is a bounded acceptance test, not +This is the release 0.6.1 lab recipe. It is a bounded acceptance test, not evidence of a boat or physical Raspberry Pi test. Use a scratch lab Pi or an isolated VM with Docker and a mounted data disk. Keep real vessel identity, positions, MMSIs, tokens, logs, and backups out of this repository. @@ -8,19 +8,19 @@ positions, MMSIs, tokens, logs, and backups out of this repository. ## Fetch and verify the pinned prerelease Download the pinned bootstrap and the CI-generated checksum from the same -`v0.6.0` GitHub release. Verify before reading or running it: +`v0.6.1` GitHub release. Verify before reading or running it: ```bash -curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh -curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh.sha256 +curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh +curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh.sha256 sha256sum -c get-vesselstack.sh.sha256 less get-vesselstack.sh sudo bash get-vesselstack.sh --help ``` -0.6.0 release status: `v0.6.0` is an SSH-signed tag, published first as a pre-release and +0.6.1 release status: `v0.6.1` is an SSH-signed tag, published first as a pre-release and marked Latest after the maintainer's physical Raspberry Pi install passes (see the CHANGELOG). -Check the tag with `scripts/verify-release-tag.sh v0.6.0`. The checksum catches a corrupt or +Check the tag with `scripts/verify-release-tag.sh v0.6.1`. The checksum catches a corrupt or truncated asset; it does not replace that signature check. ## Exercise the browser updater @@ -28,13 +28,13 @@ truncated asset; it does not replace that signature check. Open the authenticated panel's **Updates** tab and press **Check for updates**. Before you press it, confirm the tab says nothing has been contacted. Confirm the catalog is `jclima/VesselStack`, stable releases are the default, and **Include early test -releases** is opt-in. When the installed `0.6.0` release is current, the panel +releases** is opt-in. When the installed `0.6.1` release is current, the panel correctly reports "You're up to date"; do not downgrade, retag, or force the same version. Exercise **Back up & install** only when a newer public release -is available. A fresh `0.6.0` install is a CLI install; an existing `0.5.5`, +is available. A fresh `0.6.1` install is a CLI install; an existing `0.5.5`, `0.5.6` or `0.5.7` install may use the panel upgrade path, after enabling **Include early test releases** (named **Include lab prereleases** in the 0.5.x panel) if -0.6.0 is published as a prerelease. CI-controlled fixtures exercise the install +0.6.1 is published as a prerelease. CI-controlled fixtures exercise the install orchestration, and the actual systemd restart is covered by the backend verifier. Full physical-Pi upgrade proof remains pending physical testing. Mount the configured external backup device first. Confirm the dialog and the Home tab diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 8b22993..6a2c8a9 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,6 +1,6 @@ -# Deployment runbook: VesselStack 0.6.0 to a home-lab Pi and the boat Pi +# Deployment runbook: VesselStack 0.6.1 to a home-lab Pi and the boat Pi -This is an operator runbook for taking release 0.6.0 through two targets: +This is an operator runbook for taking release 0.6.1 through two targets: 1. **Home lab**: Raspberry Pi 4B, internet-connected, used for staging, a 48 h+ soak and rehearsals (CAN replay through `vcan0`). @@ -23,7 +23,7 @@ the scripts in this repository; where a thing does not exist the text says so. Related: [INSTALLATION.md](INSTALLATION.md) (hardware and first install), [README.md](../README.md) (reference for every variable and command). -0.6.0 release status: `v0.6.0` is an SSH-signed tag (verified by the release workflow against +0.6.1 release status: `v0.6.1` is an SSH-signed tag (verified by the release workflow against `.github/allowed_signers`), published first as a pre-release and marked Latest after the maintainer's physical Raspberry Pi install passes (recorded in the CHANGELOG). Physical Raspberry Pi acceptance is a separate gate (section 7). @@ -42,10 +42,10 @@ carrying bundles, rollback and the acceptance checklist. **Home lab (internet-connected Pi).** On the Pi: ```bash -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash ``` -(`.../releases/latest/download/get-vesselstack.sh` points to whichever release GitHub marks Latest, which may not be 0.6.0; use the pinned URL above.) +(`.../releases/latest/download/get-vesselstack.sh` points to whichever release GitHub marks Latest, which may not be 0.6.1; use the pinned URL above.) Do it over plain SSH (`ssh -t YOUR_USER@LAB_PI`): no screen, browser or tunnel is needed. It installs prerequisites, asks before installing Docker, verifies the release sha256 (compare it with the value recorded in section 2), asks the configuration @@ -61,7 +61,7 @@ read it, then `sudo bash get-vesselstack.sh`. Without questions (scripts, no ter browser wizard instead of the terminal questions. **Boat (offline Pi).** The boat has no internet, so carry the files in (section 4): -`get-vesselstack.sh`, `vesselstack-0.6.0.tar.gz` with its `.sha256`, and the +`get-vesselstack.sh`, `vesselstack-0.6.1.tar.gz` with its `.sha256`, and the offline bundle (copy the bundle root-owned, `chmod -R go-w`). Install Docker and the apt prerequisites while the Pi still has internet (the script cannot install packages offline and tells you what is missing), then: @@ -72,7 +72,7 @@ offline plugin or bridge-virtualenv installation. ```bash sudo bash get-vesselstack.sh --offline --bundle /path/to/offline-bundle \ - --tarball /path/to/vesselstack-0.6.0.tar.gz + --tarball /path/to/vesselstack-0.6.1.tar.gz ``` It verifies the tarball checksum and the bundle (`prepare-offline-bundle.sh @@ -234,22 +234,22 @@ hatch; never set it on the boat. The boat has no route to `origin`, so `vesselstack-update` is not the boat's update path; leave it unconfigured there unless you later add a connection you trust. -Once `v0.6.0` is published, do not recreate or move the tag. On the maintainer +Once `v0.6.1` is published, do not recreate or move the tag. On the maintainer machine, verify that the published tag points at the intended merged commit; do not force-fetch or overwrite a published tag: ```bash git fetch origin main --tags -git show --no-patch --decorate v0.6.0 -git rev-parse v0.6.0^{commit} -git merge-base --is-ancestor v0.6.0^{commit} origin/main +git show --no-patch --decorate v0.6.1 +git rev-parse v0.6.1^{commit} +git merge-base --is-ancestor v0.6.1^{commit} origin/main ``` -Then verify the signature with `scripts/verify-release-tag.sh v0.6.0` and claim success only +Then verify the signature with `scripts/verify-release-tag.sh v0.6.1` and claim success only for a tag that passes it. When you have the commit recorded from the build output in section 2 (`commit `, kept in your notes), bind the tag to it: -`scripts/verify-release-tag.sh v0.6.0 .github/allowed_signers "$RECORDED_COMMIT"` (full SHA; -abbreviations are refused). Do not take the value from `git rev-parse v0.6.0^{commit}`, which +`scripts/verify-release-tag.sh v0.6.1 .github/allowed_signers "$RECORDED_COMMIT"` (full SHA; +abbreviations are refused). Do not take the value from `git rev-parse v0.6.1^{commit}`, which would only compare the tag with itself. Follow the tag-creation and signing gates in [DEVELOPMENT.md](DEVELOPMENT.md#release-checklist). @@ -266,29 +266,29 @@ From a clean clone, so no stray files can matter (`build-package.sh` packages ```bash git clone https://github.com/jclima/VesselStack.git vs-release && cd vs-release -git checkout v0.6.0 -cat VERSION # 0.6.0 +git checkout v0.6.1 +cat VERSION # 0.6.1 VESSELSTACK_BUILD_REQUIRE_CLEAN=true ./build-package.sh ``` -It writes `generated/vesselstack-0.6.0.tar.gz` and -`generated/vesselstack-0.6.0.tar.gz.sha256`, and prints the sha256 and +It writes `generated/vesselstack-0.6.1.tar.gz` and +`generated/vesselstack-0.6.1.tar.gz.sha256`, and prints the sha256 and `commit `. **Record both values** in your release notes (out of band from the tarball): they are the boat's acceptance check. ```bash -git rev-parse v0.6.0^{commit} # must equal the printed commit -tar -tzf generated/vesselstack-0.6.0.tar.gz | head -20 -tar -xzOf generated/vesselstack-0.6.0.tar.gz vesselstack-0.6.0/COMMIT -tar -xzOf generated/vesselstack-0.6.0.tar.gz vesselstack-0.6.0/BUILD_INFO -tar -tzf generated/vesselstack-0.6.0.tar.gz | grep -E '\.env$|boat_facts|\.venv|memory/|data/' \ +git rev-parse v0.6.1^{commit} # must equal the printed commit +tar -tzf generated/vesselstack-0.6.1.tar.gz | head -20 +tar -xzOf generated/vesselstack-0.6.1.tar.gz vesselstack-0.6.1/COMMIT +tar -xzOf generated/vesselstack-0.6.1.tar.gz vesselstack-0.6.1/BUILD_INFO +tar -tzf generated/vesselstack-0.6.1.tar.gz | grep -E '\.env$|boat_facts|\.venv|memory/|data/' \ || echo "no private artifacts" python3 scripts/check-public-tree.py ``` (`vesselstack.example.env` is expected in the listing; a real `*.env` is not.) `scripts/update-requirements-lock.sh` is for maintainers when -`boat-chat/requirements.txt` changes; for an unchanged 0.6.0 the lock ships in +`boat-chat/requirements.txt` changes; for an unchanged 0.6.1 the lock ships in the tarball and you do not run it. ## 3. Home lab (Pi 4B, connected) @@ -298,14 +298,14 @@ the tarball and you do not run it. Copy the tarball to the lab Pi, check it, unpack it root-owned: ```bash -scp generated/vesselstack-0.6.0.tar.gz* YOUR_USER@LAB_PI:~/ +scp generated/vesselstack-0.6.1.tar.gz* YOUR_USER@LAB_PI:~/ # on LAB_PI: -sha256sum -c vesselstack-0.6.0.tar.gz.sha256 # [read-only] +sha256sum -c vesselstack-0.6.1.tar.gz.sha256 # [read-only] sudo install -d -m 0755 /var/lib/vesselstack/releases # [MUTATES HOST] -sudo tar -xzf vesselstack-0.6.0.tar.gz -C /var/lib/vesselstack/releases --no-same-owner -sudo chown -R root:root /var/lib/vesselstack/releases/vesselstack-0.6.0 -sudo chmod -R go-w /var/lib/vesselstack/releases/vesselstack-0.6.0 -cd /var/lib/vesselstack/releases/vesselstack-0.6.0 +sudo tar -xzf vesselstack-0.6.1.tar.gz -C /var/lib/vesselstack/releases --no-same-owner +sudo chown -R root:root /var/lib/vesselstack/releases/vesselstack-0.6.1 +sudo chmod -R go-w /var/lib/vesselstack/releases/vesselstack-0.6.1 +cd /var/lib/vesselstack/releases/vesselstack-0.6.1 ``` Configuration: either the terminal wizard (`python3 vesselstack-wizard.py --cli --output ~/vesselstack-lab.env`, over plain @@ -343,7 +343,7 @@ sudo ./install.sh --config ~/vesselstack-lab.env # [MUTATES HOST] re sudo ./install.sh --config ~/vesselstack-lab.env --start # [MUTATES HOST] pulls images, starts stack sudo vesselstackctl doctor sudo vesselstackctl urls -cat /opt/vesselstack/config/installed-version # 0.6.0 +cat /opt/vesselstack/config/installed-version # 0.6.1 ``` ### 3.2 Replay a private CAN log @@ -401,11 +401,11 @@ values, and record the `doctor` result and data-directory size each day. 1. **Update from the previous version.** On a spare lab install (or before the soak), install the prior tag the normal way (`git checkout v0.3.0` in a clean - clone, `sudo ./install.sh --config ... --start`), then update with 0.6.0 from + clone, `sudo ./install.sh --config ... --start`), then update with 0.6.1 from the unpacked release directory: ```bash - sudo vesselstackctl update /var/lib/vesselstack/releases/vesselstack-0.6.0 # [MUTATES HOST] + sudo vesselstackctl update /var/lib/vesselstack/releases/vesselstack-0.6.1 # [MUTATES HOST] ``` It runs `install.sh --dry-run`, takes a verified backup into @@ -443,7 +443,7 @@ values, and record the `doctor` result and data-directory size each day. sudo chown -R root:root offline-bundle && sudo chmod -R go-w offline-bundle sudo scripts/prepare-offline-bundle.sh --load /path/to/offline-bundle # docker load sudo VESSELSTACK_OFFLINE=true VESSELSTACK_WHEELHOUSE=/path/to/offline-bundle/wheelhouse \ - vesselstackctl update /var/lib/vesselstack/releases/vesselstack-0.6.0 + vesselstackctl update /var/lib/vesselstack/releases/vesselstack-0.6.1 ``` The bundle path and **every parent** must be root-owned and not @@ -471,8 +471,8 @@ Assemble one directory on the connected machine; transfer it as a unit: | File | Source | | --- | --- | -| `vesselstack-0.6.0.tar.gz` | `./build-package.sh` | -| `vesselstack-0.6.0.tar.gz.sha256` | same run | +| `vesselstack-0.6.1.tar.gz` | `./build-package.sh` | +| `vesselstack-0.6.1.tar.gz.sha256` | same run | | `offline-bundle/` (`wheelhouse/`, `images/`, `BUNDLE_INFO`, `MANIFEST.sha256`) | `scripts/prepare-offline-bundle.sh --output ./offline-bundle` | | Release notes with the recorded sha256 + commit | your notes, **kept separately** | @@ -519,19 +519,19 @@ backup, the recorded sha256 and a console/Ethernet path ready. your notes, not with the `.sha256` file that travelled with it): ```bash - sha256sum ~/carry/vesselstack-0.6.0.tar.gz # must equal the recorded value + sha256sum ~/carry/vesselstack-0.6.1.tar.gz # must equal the recorded value ``` 3. **Stage root-owned [MUTATES HOST].** ```bash sudo install -d -m 0755 /var/lib/vesselstack/releases - sudo tar -xzf ~/carry/vesselstack-0.6.0.tar.gz -C /var/lib/vesselstack/releases --no-same-owner + sudo tar -xzf ~/carry/vesselstack-0.6.1.tar.gz -C /var/lib/vesselstack/releases --no-same-owner sudo cp -a ~/carry/offline-bundle /var/lib/vesselstack/releases/ - sudo chown -R root:root /var/lib/vesselstack/releases/vesselstack-0.6.0 /var/lib/vesselstack/releases/offline-bundle - sudo chmod -R go-w /var/lib/vesselstack/releases/vesselstack-0.6.0 /var/lib/vesselstack/releases/offline-bundle - cat /var/lib/vesselstack/releases/vesselstack-0.6.0/COMMIT # matches recorded commit - sudo /var/lib/vesselstack/releases/vesselstack-0.6.0/scripts/prepare-offline-bundle.sh \ + sudo chown -R root:root /var/lib/vesselstack/releases/vesselstack-0.6.1 /var/lib/vesselstack/releases/offline-bundle + sudo chmod -R go-w /var/lib/vesselstack/releases/vesselstack-0.6.1 /var/lib/vesselstack/releases/offline-bundle + cat /var/lib/vesselstack/releases/vesselstack-0.6.1/COMMIT # matches recorded commit + sudo /var/lib/vesselstack/releases/vesselstack-0.6.1/scripts/prepare-offline-bundle.sh \ --verify /var/lib/vesselstack/releases/offline-bundle # [read-only] ``` @@ -540,16 +540,16 @@ backup, the recorded sha256 and a console/Ethernet path ready. boat uses: ```bash - sudo /var/lib/vesselstack/releases/vesselstack-0.6.0/scripts/prepare-offline-bundle.sh \ + sudo /var/lib/vesselstack/releases/vesselstack-0.6.1/scripts/prepare-offline-bundle.sh \ --load /var/lib/vesselstack/releases/offline-bundle # verifies, then docker load sudo VESSELSTACK_OFFLINE=true VESSELSTACK_WHEELHOUSE=/var/lib/vesselstack/releases/offline-bundle/wheelhouse \ - vesselstackctl update /var/lib/vesselstack/releases/vesselstack-0.6.0 + vesselstackctl update /var/lib/vesselstack/releases/vesselstack-0.6.1 ``` 5. **Verify.** ```bash - cat /opt/vesselstack/config/installed-version # 0.6.0 + cat /opt/vesselstack/config/installed-version # 0.6.1 sudo vesselstackctl doctor # RESULT healthy sudo reboot # after reconnecting: @@ -618,12 +618,12 @@ secrets. `vesselstack-update --rollback BACKUP_DIRECTORY` is only for code-only ## 7. Acceptance checklist -- [ ] Release `v0.6.0`: tag signature verified (`scripts/verify-release-tag.sh v0.6.0 .github/allowed_signers "$RECORDED_COMMIT"`) and Latest status recorded in the CHANGELOG; CI asset sha256 and commit recorded. +- [ ] Release `v0.6.1`: tag signature verified (`scripts/verify-release-tag.sh v0.6.1 .github/allowed_signers "$RECORDED_COMMIT"`) and Latest status recorded in the CHANGELOG; CI asset sha256 and commit recorded. - [ ] Lab: 48 h+ soak with daily `doctor` healthy; replay data visible in Grafana. - [ ] Lab: update from 0.3.0, backup/verify/restore, and offline install rehearsed. - [ ] Boat: sha256 of the carried tarball equals the recorded value. - [ ] Boat: verified backup exists on separate media and a copy is off the Pi. -- [ ] Boat: `installed-version` is `0.6.0`; `doctor` healthy before and after reboot. +- [ ] Boat: `installed-version` is `0.6.1`; `doctor` healthy before and after reboot. - [ ] Boat: `can0` RX counter increases, error/dropped counters flat. - [ ] Boat: tokens are per-host; no lab secrets copied; control panel reached by tunnel only. @@ -660,12 +660,12 @@ machine only. - **Real SSH signature verification** on the Pi (`vesselstack-update` against its allowed-signers file) has only been tested with fixtures; do the first real run on the lab Pi. The release workflow's tag check (`scripts/verify-release-tag.sh`) is covered by - `tests/test-verify-release-tag.sh` with throwaway keys; its first real run is the `v0.6.0` + `tests/test-verify-release-tag.sh` with throwaway keys; its first real run is the `v0.6.1` tag push. - The **arm64 offline bundle** (wheels for `manylinux*_aarch64`, `linux/arm64` images) has not been built and installed on a real Pi; section 3.5.3 is that rehearsal. Image tarballs are manifest-checked but not digest-pinned. - Scoped read-only InfluxDB tokens for Boat Chat and Grafana are **deferred** - (see README and CHANGELOG); they are not part of 0.6.0. + (see README and CHANGELOG); they are not part of 0.6.1. - The tarball is integrity-checked by sha256 only on the boat (no signature check in `install.sh`/`vesselstackctl update`). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index fb9e3d4..f302a3a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -60,7 +60,7 @@ Do not build and upload files by hand. the workflow reads the copy at the tagged commit. The `v0.5.7` lab prerelease was an annotated unsigned tag because no SSH/GPG key was available; it is not signed and would now be refused. The `v0.5.1` tag is untouched and was not published because its release pipeline - caught the bridge startup-status race. `v0.6.0` is an SSH-signed tag, published first as a + caught the bridge startup-status race. `v0.6.1` is an SSH-signed tag, published first as a pre-release and marked Latest after the maintainer's physical Raspberry Pi install passes (see the CHANGELOG). 4. `.github/workflows/release.yml` then (a) reruns the whole CI workflow on the diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 6b25338..17b02fd 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -41,11 +41,11 @@ installer. `-t` gives the session a terminal so the installer can ask questions: ```bash ssh -t YOUR_USER@raspberrypi.local -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash ``` -The URL pins release `v0.6.0`, so it installs that release whatever GitHub marks as Latest -(`--version 0.6.0` selects the same release). The form `.../releases/latest/download/get-vesselstack.sh` +The URL pins release `v0.6.1`, so it installs that release whatever GitHub marks as Latest +(`--version 0.6.1` selects the same release). The form `.../releases/latest/download/get-vesselstack.sh` points to whichever release GitHub marks Latest, which is not necessarily the newest one. For a lab Pi and an offline boat, follow [DEPLOYMENT.md](DEPLOYMENT.md). diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 4e3365c..4310afe 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -63,7 +63,7 @@ terminal. The Pi needs no screen, keyboard, or browser, and nothing has to be opened or tunnelled: ```bash -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash ``` The terminal questions come in short sections (boat, Signal K, NMEA 2000 / CAN, @@ -110,7 +110,7 @@ changed the panel port in Settings, use that number. See the **Without questions** (scripts, repeatable installs, or when no terminal is available): ```bash -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh \ +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh \ | sudo bash -s -- --yes --set BOAT_NAME=Foo --set SOCKETCAN_ENABLE=true ``` @@ -123,7 +123,7 @@ installer stops before changing anything and lists these options. `--web-wizard` optional browser wizard (loopback only; needs a browser on the Pi or an `ssh -L` tunnel). The pinned URL above works for any published release, including a prerelease; add -`--version 0.6.0` (`| sudo bash -s -- --version 0.6.0`) or set `VESSELSTACK_VERSION=0.6.0` for +`--version 0.6.1` (`| sudo bash -s -- --version 0.6.1`) or set `VESSELSTACK_VERSION=0.6.1` for the same effect. The form `https://github.com/jclima/VesselStack/releases/latest/download/get-vesselstack.sh` points to whichever release GitHub marks Latest, which is not necessarily the newest release (every 0.5.x release was a prerelease), so it is a secondary option. The script installs the Debian @@ -142,8 +142,8 @@ page, or pass `--sha256 HASH`. To read the script first (recommended for the fir install): ```bash -curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh -curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh.sha256 +curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh +curl -fsSLO https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh.sha256 sha256sum -c get-vesselstack.sh.sha256 less get-vesselstack.sh sudo bash get-vesselstack.sh @@ -251,7 +251,7 @@ Skip sections 5 and 6 if you used the one-line installer. ```bash git clone https://github.com/jclima/VesselStack.git cd VesselStack -git checkout v0.6.0 +git checkout v0.6.1 python3 vesselstack-wizard.py --cli # questions in the terminal, works over plain SSH # python3 vesselstack-wizard.py # the optional browser wizard (below) ``` diff --git a/docs/UPDATING.md b/docs/UPDATING.md index 05cc981..cbbe8bd 100644 --- a/docs/UPDATING.md +++ b/docs/UPDATING.md @@ -15,7 +15,7 @@ Re-run the pinned release installer over `ssh -t`. It finds the installed system installs and starts the release: ```bash -curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash +curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash ``` The command itself needs the unpacked release directory, so plain `vesselstackctl update` @@ -23,7 +23,7 @@ with no argument fails. To run it by hand once a release is unpacked (the instal `/opt/vesselstack-release/`): ```bash -sudo vesselstackctl update /opt/vesselstack-release/0.6.0 +sudo vesselstackctl update /opt/vesselstack-release/0.6.1 ``` ## From the Control Panel @@ -100,7 +100,7 @@ sudo vesselstackctl restore /path/to/vesselstack-TIMESTAMP.tar.gz --yes Prepare a bundle on the home lab or any connected machine, carry it to the boat, then install without any network access. The guided installer does the checks, `--verify`, `--load` and install for you: -`sudo bash get-vesselstack.sh --offline --bundle /path/to/offline-bundle --tarball /path/to/vesselstack-0.6.0.tar.gz` +`sudo bash get-vesselstack.sh --offline --bundle /path/to/offline-bundle --tarball /path/to/vesselstack-0.6.1.tar.gz` (copy `get-vesselstack.sh`, the tarball with its `.sha256` and the bundle on the carry media; the bundle must be root-owned and not group/world-writable, and packages and Docker must already be installed). The manual steps follow: @@ -188,7 +188,7 @@ and `ssh-keygen` (part of OpenSSH): ```bash git fetch --tags -scripts/verify-release-tag.sh v0.6.0 +scripts/verify-release-tag.sh v0.6.1 ``` The script takes `TAG [ALLOWED_SIGNERS_FILE [EXPECTED_COMMIT]]`. It checks that the tag is diff --git a/install.sh b/install.sh index b4c74be..17ef5fd 100755 --- a/install.sh +++ b/install.sh @@ -82,6 +82,18 @@ BOAT_CHAT_HOST_FROM_CONFIG=${BOAT_CHAT_HOST+x} : "${TELEGRAM_ENABLE:=false}" : "${TELEMETRY_INDEXER_ENABLE:=true}" : "${BOAT_UNITS:=us_customary}" +# Older setup wizards offered "nautical" and installers before 0.5.7 did not check it, so +# such configs exist. It is read as metric (knots and nautical miles are kept in both +# systems), and the rewritten vesselstack.env persists "metric". Surrounding whitespace and +# letter case are tolerated for the two supported values. +BOAT_UNITS_INPUT=$BOAT_UNITS +BOAT_UNITS=${BOAT_UNITS#"${BOAT_UNITS%%[![:space:]]*}"} +BOAT_UNITS=${BOAT_UNITS%"${BOAT_UNITS##*[![:space:]]}"} +BOAT_UNITS=${BOAT_UNITS,,} +if [ "$BOAT_UNITS" = nautical ]; then + echo "Note: BOAT_UNITS=nautical is no longer a separate option; using metric (knots and nautical miles are kept)." >&2 + BOAT_UNITS=metric +fi : "${NODE_EXPORTER_ENABLE:=false}" : "${DISPLAYS_ENABLE:=false}" : "${DISPLAYS_BRIDGE_MQTT_USERNAME:=vesselstack-bridge}" @@ -160,7 +172,13 @@ for key in SOCKETCAN_ENABLE VESSELSTACK_CAN_CREATE_VCAN AIS_ENABLE VESSELSTACK_F *) echo "$key must be true or false" >&2; exit 1 ;; esac done -case "$BOAT_UNITS" in us_customary|metric) ;; *) echo "BOAT_UNITS must be us_customary or metric" >&2; exit 1 ;; esac +case "$BOAT_UNITS" in + us_customary|metric) ;; + *) + echo "BOAT_UNITS=$BOAT_UNITS_INPUT in $CONFIG_FILE is not supported. Set it to us_customary or metric, for example: sudo sed -i 's/^BOAT_UNITS=.*/BOAT_UNITS=metric/' $CONFIG_FILE" >&2 + exit 1 + ;; +esac # MQTT account names are written into the Mosquitto password and ACL files. for key in MQTT_USERNAME DISPLAYS_BRIDGE_MQTT_USERNAME; do if ! [[ "${!key}" =~ ^[A-Za-z0-9_.-]{1,64}$ ]]; then diff --git a/scripts/get-vesselstack.sh b/scripts/get-vesselstack.sh index b9aca42..66d2830 100755 --- a/scripts/get-vesselstack.sh +++ b/scripts/get-vesselstack.sh @@ -2,7 +2,7 @@ # VesselStack guided installer (bootstrap). # # curl -fsSL https://github.com/jclima/VesselStack/releases/latest/download/get-vesselstack.sh | sudo bash -# curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.0/get-vesselstack.sh | sudo bash +# curl -fsSL https://github.com/jclima/VesselStack/releases/download/v0.6.1/get-vesselstack.sh | sudo bash # # Cautious alternative: download it, read it, then run it. # curl -fsSLO https://github.com/jclima/VesselStack/releases/latest/download/get-vesselstack.sh @@ -24,7 +24,7 @@ set -euo pipefail umask 022 # Baked from VERSION by build-package.sh (and checked by tests/test-distribution.sh). -DEFAULT_VERSION="0.6.0" +DEFAULT_VERSION="0.6.1" GITHUB_RELEASES="https://github.com/jclima/VesselStack/releases/download" APT_PACKAGES=(ca-certificates curl jq openssl python3 python3-venv rsync tar gzip) diff --git a/tests/test-clean-install.sh b/tests/test-clean-install.sh index ac3d897..d6ed369 100755 --- a/tests/test-clean-install.sh +++ b/tests/test-clean-install.sh @@ -725,6 +725,28 @@ HOME_ASSISTANT_TOKEN="it's fine" CFG xinstall "$X_APP/installer/install.sh" --config "$GOODVAL" --dry-run | grep -q 'Preflight passed' +# 1b-bis. Legacy BOAT_UNITS=nautical (offered by older wizards, unchecked before 0.5.7) is read as +# metric: the dry run prints one note and changes nothing; a real run persists metric. Any other +# unsupported value is refused before anything changes, with a message naming the file and fix. +NAUTICAL="$TEST_ROOT/nautical.env" +grep -v '^BOAT_UNITS=' "$XENV" > "$NAUTICAL" +printf 'BOAT_UNITS=nautical\n' >> "$NAUTICAL" +env_before=$(sha256sum "$XENV") +xinstall "$X_APP/installer/install.sh" --config "$NAUTICAL" --dry-run >"$TEST_ROOT/nautical.out" 2>&1 +grep -q 'Preflight passed' "$TEST_ROOT/nautical.out" +test "$(grep -c 'BOAT_UNITS=nautical is no longer a separate option; using metric (knots and nautical miles are kept)' "$TEST_ROOT/nautical.out")" -eq 1 +test "$env_before" = "$(sha256sum "$XENV")" +IMPERIAL="$TEST_ROOT/imperial.env" +grep -v '^BOAT_UNITS=' "$XENV" > "$IMPERIAL" +printf 'BOAT_UNITS=imperial\n' >> "$IMPERIAL" +expect_unchanged_failure "dry-run with unsupported units" xinstall "$X_APP/installer/install.sh" --config "$IMPERIAL" --dry-run +grep -qF "BOAT_UNITS=imperial in $IMPERIAL is not supported. Set it to us_customary or metric, for example: sudo sed -i 's/^BOAT_UNITS=.*/BOAT_UNITS=metric/' $IMPERIAL" "$TEST_ROOT/failure.out" +expect_unchanged_failure "real run with unsupported units" xinstall "$X_APP/installer/install.sh" --config "$IMPERIAL" +grep -q 'BOAT_UNITS=imperial' "$TEST_ROOT/failure.out" +xinstall "$X_APP/installer/install.sh" --config "$NAUTICAL" >/dev/null 2>&1 +grep -q "^BOAT_UNITS=metric$" "$XENV" +if grep -q nautical "$XENV"; then echo "the installed config still says nautical" >&2; exit 1; fi + # 1c. Stale env temp files (exact mktemp names) are removed by a real run; a dry run # leaves them, and similarly named files are never touched. for stale in .vesselstack.env.AbC123 .boat-chat.env.xYz789 .control-panel.env.000000; do : > "$X_APP/config/$stale"; done diff --git a/tests/test_control_panel.py b/tests/test_control_panel.py index c815e89..1b2b25e 100644 --- a/tests/test_control_panel.py +++ b/tests/test_control_panel.py @@ -32,6 +32,20 @@ def test_round_trip_supports_spaces_and_empty_values(self) -> None: ) self.assertEqual(path.stat().st_mode & 0o777, 0o600) + def test_legacy_nautical_units_display_as_metric_without_rewriting_the_file(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + vessel = root / "vessel.env" + vessel.write_text("BOAT_NAME=Old Boat\nBOAT_UNITS=nautical\n") + before = vessel.read_bytes() + with mock.patch.multiple(panel, VESSEL_ENV=vessel, CHAT_ENV=root / "chat.env", PANEL_ENV=root / "control.env"): + values = panel.public_configuration()["values"] + self.assertEqual(values["BOAT_UNITS"], "metric") + self.assertEqual(vessel.read_bytes(), before) + self.assertIn(values["BOAT_UNITS"], panel.FIELD_BY_KEY["BOAT_UNITS"]["choices"]) + with self.assertRaises(ValueError): + panel.validate_value(panel.FIELD_BY_KEY["BOAT_UNITS"], "imperial") + def test_public_configuration_redacts_secrets(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) diff --git a/tests/test_display_units.py b/tests/test_display_units.py index fcddc4e..30c9fd2 100644 --- a/tests/test_display_units.py +++ b/tests/test_display_units.py @@ -18,6 +18,14 @@ class DisplayUnitsTests(unittest.TestCase): def setUpClass(cls): cls.units = load_units() + def test_legacy_nautical_preference_is_metric(self): + u = self.units + self.assertEqual(u.normalize_preference("nautical"), "metric") + self.assertEqual(u.normalize_preference(" Nautical "), "metric") + self.assertEqual(u.preference(lambda key, default: "nautical"), "metric") + self.assertEqual(u.normalize_preference("us_customary"), "us_customary") + self.assertEqual(u.unit_for_measurement("depth", "nautical", "ft"), "m") + def test_known_conversions(self): u = self.units self.assertAlmostEqual(u.convert(32, "F", "C"), 0.0, places=6) diff --git a/tests/test_wizard.py b/tests/test_wizard.py index 5f0055b..7293eb0 100644 --- a/tests/test_wizard.py +++ b/tests/test_wizard.py @@ -28,11 +28,15 @@ def test_units_are_us_customary_or_metric_only_and_default_matches_the_installer self.assertIn(': "${BOAT_UNITS:=us_customary}"', installer) for good in ("us_customary", "metric"): self.assertEqual(wizard.validate({"BOAT_UNITS": good})["BOAT_UNITS"], good) - for bad in ("nautical", "imperial", "", "Metric"): + for bad in ("imperial", "", "metrics"): with self.subTest(bad=bad), self.assertRaisesRegex(ValueError, "BOAT_UNITS"): wizard.validate({"BOAT_UNITS": bad}) + # Legacy "nautical" (offered by older wizards) is read as metric, with a note; case and spaces are tolerated. + for legacy, expected in (("nautical", "metric"), ("Nautical", "metric"), (" metric", "metric"), ("Metric", "metric"), ("US_Customary", "us_customary")): + with self.subTest(legacy=legacy): + self.assertEqual(wizard.validate({"BOAT_UNITS": legacy})["BOAT_UNITS"], expected) self.assertEqual([name for name, _ in wizard.FIELD_INDEX["BOAT_UNITS"]["choices"]], ["us_customary", "metric"]) - with self.assertRaises(ValueError): wizard.apply_settings(wizard.DEFAULTS.copy(), ["BOAT_UNITS=nautical"]) + with self.assertRaises(ValueError): wizard.apply_settings(wizard.DEFAULTS.copy(), ["BOAT_UNITS=imperial"]) app = (ROOT / "wizard" / "app.js").read_text() self.assertNotIn("nautical'", app.lower().replace("nautical miles", "")) self.assertNotIn("value=\"nautical", app) @@ -195,12 +199,28 @@ def test_invalid_lan_access_is_rejected_without_writing(self): self.assertEqual(status, 400) self.assertFalse(self.output.exists()) - def test_web_save_rejects_nautical_units_without_writing(self): - status, body = self.request("POST", "/api/save", {"values": {"BOAT_UNITS": "nautical"}}, self.good()) + def test_web_save_rejects_unknown_units_without_writing(self): + status, body = self.request("POST", "/api/save", {"values": {"BOAT_UNITS": "imperial"}}, self.good()) self.assertEqual(status, 400) self.assertIn(b"BOAT_UNITS", body) self.assertFalse(self.output.exists()) + def test_legacy_nautical_units_in_an_existing_config_load_as_metric_with_a_note(self): + import contextlib, io + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) / "vesselstack.env" + base.write_text('BOAT_NAME="Old Boat"\nBOAT_UNITS=nautical\n') + err = io.StringIO() + with contextlib.redirect_stderr(err): + values = wizard.validate({**wizard.DEFAULTS, **wizard.load_env(base)}) + self.assertEqual(wizard.validate(wizard.load_env(base))["BOAT_UNITS"], "metric") + self.assertEqual((values["BOAT_NAME"], values["BOAT_UNITS"]), ("Old Boat", "metric")) + self.assertIn("BOAT_UNITS=nautical is no longer a separate option; using metric", err.getvalue()) + result = subprocess.run([sys.executable, str(ROOT / "vesselstack-wizard.py"), "--validate", str(base)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("valid", result.stdout) + self.assertIn("using metric", result.stderr) + def test_wizard_page_asks_the_lan_question_with_the_all_warning(self): source = (ROOT / "wizard" / "app.js").read_text() self.assertIn("Who should be able to reach the dashboards?", source) diff --git a/vesselstack-wizard.py b/vesselstack-wizard.py index 1840e07..bce6119 100755 --- a/vesselstack-wizard.py +++ b/vesselstack-wizard.py @@ -44,6 +44,14 @@ def system_report(): interfaces=sorted(p.name for p in Path("/sys/class/net").glob("*") if p.name.startswith(("eth","en","wlan","wl","can"))) return {"architecture":platform.machine(),"os":platform.platform(),"docker":bool(shutil.which("docker")),"compose":compose_available(),"interfaces":interfaces,"can0":"can0" in interfaces,"memory_gib":round(os.sysconf("SC_PAGE_SIZE")*os.sysconf("SC_PHYS_PAGES")/2**30,1),"output":str(OUTPUT)} +def normalize_units(value): + """Older wizards offered "nautical"; it is read as metric (knots and nautical miles are kept anyway).""" + text=value.strip().lower() if isinstance(value,str) else value + if text=="nautical": + print("Note: BOAT_UNITS=nautical is no longer a separate option; using metric (knots and nautical miles are kept).",file=sys.stderr) + return "metric" + return text if text in {"us_customary","metric"} else value + def validate(values): unknown=set(values)-set(DEFAULTS) if unknown: raise ValueError("Unknown settings: "+", ".join(sorted(unknown))) @@ -53,6 +61,7 @@ def validate(values): result[key]=value result["VESSELSTACK_DISABLED_COMPONENTS"] = normalize_disabled_components(result["VESSELSTACK_DISABLED_COMPONENTS"]) if result["SIGNALK_MODE"] not in {"existing","docker","native"}: raise ValueError("Invalid SignalK mode") + result["BOAT_UNITS"]=normalize_units(result["BOAT_UNITS"]) if result["BOAT_UNITS"] not in {"us_customary","metric"}: raise ValueError("Invalid BOAT_UNITS: use us_customary or metric") if _any_ip(result["CONTROL_PANEL_HOST"]): raise ValueError("Invalid CONTROL_PANEL_HOST: enter an IP address") for key in ("SIGNALK_VESSELTWIN_PRELOAD","SOCKETCAN_ENABLE","VESSELSTACK_CAN_CREATE_VCAN","AIS_ENABLE","TELEGRAM_ENABLE","TELEMETRY_INDEXER_ENABLE","NODE_EXPORTER_ENABLE","DISPLAYS_ENABLE","VESSELSTACK_FIREWALL_ENABLE","VESSELSTACK_FIREWALL_STRICT"): From 65369027f580e8cda5c53c8ba4f83533e7670f5c Mon Sep 17 00:00:00 2001 From: JC Lima Date: Mon, 2 Nov 2026 23:06:46 -0800 Subject: [PATCH 2/6] Offer interactive fixes for unsupported units and unsafe saved values Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + CHANGELOG.md | 4 + README.md | 39 +++++++ docs/UPDATING.md | 7 ++ install.sh | 207 +++++++++++++++++++++++++++++++++- scripts/lib/remediate.sh | 129 +++++++++++++++++++++ tests/test-clean-install.sh | 190 ++++++++++++++++++++++++++++++- tests/test-distribution.sh | 6 +- tests/test-offline-install.sh | 3 + 9 files changed, 574 insertions(+), 12 deletions(-) create mode 100644 scripts/lib/remediate.sh diff --git a/.gitignore b/.gitignore index 571cdcf..4b5c56d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ *.env !*.example.env *.secret +*.env.before-fix-* *.sqlite *.sqlite3 *.sqlite-wal diff --git a/CHANGELOG.md b/CHANGELOG.md index 55efaac..d13fba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Semantic Versioning. ## 0.6.1 - 2026-11-03 +### Highlights + +- Upgrades and installs now offer to fix an unsupported units setting or an unsafe saved value at the prompt, instead of stopping; unattended runs still stop safely and print the exact fix. Each fix keeps a root-only `*.before-fix-*` copy of the previous settings file, which you can delete once the install works. + ### Fixes - Upgrading a boat whose settings say `BOAT_UNITS=nautical` (offered by older setup wizards) no diff --git a/README.md b/README.md index 62a2477..732c87e 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,36 @@ sudo sed -i 's/^BOAT_UNITS=.*/BOAT_UNITS=metric/' /opt/vesselstack/config/vessel Rollback: set `BOAT_UNITS` back to `us_customary` (or `metric`) in the same file. 0.6.1 needs no other manual step. +**Fix it at the prompt (0.6.1).** When you run the installer, an update or the pinned +`get-vesselstack.sh` from a terminal (for example over `ssh -t`), an unsupported `BOAT_UNITS` +value, or a setting in `vesselstack.env` that cannot be written safely (see below), no longer +just stops the run. The installer asks what to do before changing anything else: + +```text +BOAT_UNITS in /opt/vesselstack/config/vesselstack.env is "imperial", which is not supported. + [1] US customary (feet, °F, psi, US gallons) + [2] Metric (metres, °C, kPa, litres) + [c] Cancel (nothing is changed) +Choice: +``` + +An unsafe value (a token or other setting with an apostrophe plus a backslash, double quote, +dollar sign or backtick, a line break, or a control character) is offered as +`[1] Enter a new value for `; what you type for a token, password or key is hidden and is +never printed or logged, and a replacement that is still unsafe is refused. Your answer is saved +in the settings file, the installer prints `Changed in (previous copy: .before-fix-