Skip to content

feat: add restore.sh.sample — companion restore script for backup.sh - #838

Merged
jasonacox merged 20 commits into
jasonacox:mainfrom
jasonacox-sam:feat/restore-script
Aug 8, 2026
Merged

feat: add restore.sh.sample — companion restore script for backup.sh#838
jasonacox merged 20 commits into
jasonacox:mainfrom
jasonacox-sam:feat/restore-script

Conversation

@jasonacox-sam

@jasonacox-sam jasonacox-sam commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds backups/restore.sh.sample — an automated restore script that pairs naturally with backup.sh.sample. Based on restore_v16a.sh by @JonMurphy from issue #836. During review, backup.sh.sample was also refactored to match and to close coverage gaps (weather411 config, .auth/ Tesla tokens).

(Description updated 2026-08-02 to match the implementation as it evolved through review.)

What restore.sh does

  1. Auto-detects the Powerwall-Dashboard directory relative to the script's location (must live in Powerwall-Dashboard/backups/)
  2. Derives PWD_USER as SUDO_UID:SUDO_GID — the invoking user's uid + primary gid, the same id -u:id -g convention setup.sh writes into compose.env. Exits if run from a root shell (UID 0)
  3. Validates the archive before extraction — rejects path-traversal, absolute-path, and symlink/hardlink members; warns if the staging filesystem lacks space (~6× the archive) with a TMPDIR override hint
  4. Stops the stack before touching data
  5. Restores InfluxDB from the influxd backup -portable snapshot — moves existing data/meta/wal aside first (not deleted), giving a rollback path — then replays continuous queries with real error detection (the influx 1.x CLI exits 0 even when statements fail), falling back to influxdb.sql if needed
  6. Restores Grafana database and provisioning files with correct ownership
  7. Restores config files, skipping git-managed project files (powerwall.yml, telegraf.conf, VERSION, …) so an old backup can't downgrade the stack, and rewriting PWD_USER in compose.env for this host
  8. Restores weather/weather411.conf and .auth/ Tesla tokens when present in the archive (older archives restore fine without them)
  9. Recreates the stack with compose-dash.sh up -d (not start) so restored env files take effect, and prints cleanup commands for pre-restore safety copies

backup.sh changes

  • Pre-flight disk space check before influxd backup runs (@JonMurphy's catch — the snapshot itself can fill the disk)
  • Checks influxd backup's exit code and verifies the influxdb container is running up front
  • Captures weather/weather411.conf and .auth/ for cross-machine migration
  • Verifies archive readability after creation; archives are chmod 600 (they contain credentials)

Why this is better than the manual steps

  • Non-destructive — moves existing data aside instead of DROP DATABASE
  • Works for both same-machine and cross-machine migration
  • One command instead of a multi-step manual procedure (Possible issue with readme restore steps #836 happened because manual steps were error-prone)

Testing

@JonMurphy tested end-to-end cross-machine (RPi5 backup → Dell OptiPlex restore) through multiple iterations, shaking out the CQ format issues, SUDO_UID handling, and the root-shell case.

Related

— Sam 🌊

@JonMurphy

JonMurphy commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Can you check on something? CoPilot changed the code related to config files and it now looks for more than the original 9 files. The new copilot code grabs EXTRAs with *.env and *.yaml, etc.. Can you or copilot check and add if needed?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

Good catch, @JonMurphy — but I think the restore script already handles this correctly.

The backup script (step 3) grabs the original 9 CONFIG_FILES plus any extra *.env, *.conf, *.yml/*.yaml files via the glob loop. All of those end up in the config/ directory of the archive.

The restore script doesn't use a hardcoded file list — it does:

for FILE in "${STAGING}"/config/*; do

That glob picks up every file in the backup's config/ directory, whatever was captured. So if the backup grabbed extra .env or .yaml files, the restore will copy them back too.

In short: backup captures → restore replays. No changes needed on the restore side.

— Sam 🌊

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an automated restore workflow to complement the existing backup script, aiming to make Powerwall-Dashboard disaster recovery and cross-machine migrations safer and more repeatable (especially around permissions and InfluxDB’s portable snapshot restore).

Changes:

  • Added backups/restore.sh.sample to restore InfluxDB portable backups, Grafana data, and config files with pre-restore safety copies.
  • Updated backups/README.md to recommend the restore script while retaining manual restore steps as a fallback.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
backups/restore.sh.sample New automated restore script (staging extract, stop stack, restore InfluxDB/Grafana/config, restart).
backups/README.md Documentation updates to introduce the restore script as the recommended restore method.
Suppressed comments (6)

backups/restore.sh.sample:92

  • This script extracts an archive as root without validating member paths, so a malicious/incorrect archive containing absolute paths or .. entries could write outside the staging directory. Validate the archive contents first and also stop the restore if extraction fails.
# ── Extract archive to staging ────────────────────────────────────────
echo ""
echo "Extracting archive..."
tar --no-same-owner -Jxvf "${ARCHIVE}" -C "${STAGING}"

backups/restore.sh.sample:97

  • If stopping the stack fails (e.g., missing compose.env / docker unavailable), the script currently continues and may move/overwrite data while containers are still running. Fail fast when compose-dash.sh stop fails.
./compose-dash.sh stop

backups/restore.sh.sample:120

  • A fixed sleep 5 is brittle; on slow hosts InfluxDB may not be ready and the restore can fail intermittently. Since powerwall.yml defines a healthcheck for influxdb, wait for the container to become healthy (with a timeout) before running influxd restore.
  ./compose-dash.sh up -d influxdb
  sleep 5

backups/restore.sh.sample:156

  • Grafana provisioning is overwritten without creating a pre-restore safety copy, but the script later claims "Anything overwritten was saved first." Move the existing provisions directory aside (like the InfluxDB and grafana.db handling) before restoring.
# Restore Grafana provisioning files if present
if [ -d "${STAGING}/grafana/provisions" ]; then
  cp -a "${STAGING}/grafana/provisions" "${DASHBOARD}/grafana/"
  chown -R "${PWD_USER}" "${DASHBOARD}/grafana/provisions"
  echo "  ✓ Grafana provisioning files restored (owner set to ${PWD_USER})"

backups/restore.sh.sample:166

  • The config restore loop uses config/*, which skips dotfiles. However, backup.sh.sample explicitly backs up hidden .*.env files, so those files will never be restored. Include dotfiles when iterating the staged config directory.
  for FILE in "${STAGING}"/config/*; do
    [ -f "${FILE}" ] || continue

backups/restore.sh.sample:24

  • Auto-detecting the dashboard path by running find / is slow on large filesystems and can select the wrong instance if multiple clones exist. Prefer resolving the dashboard root relative to this script (when run from Powerwall-Dashboard/backups), and only fall back to a system-wide search that stops after the first match.
echo "Searching for compose-dash.sh..."
MATCH=$(find / -type f -iname "compose-dash.sh" 2>/dev/null | head -n 1)

DASHBOARD=""
if [ -n "${MATCH}" ]; then

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backups/restore.sh.sample Outdated
Comment on lines +12 to +15
if [ "$EUID" -ne 0 ]
then echo "Must run as root"
exit
fi

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in 2e7763b. All error exits now use exit 1. Also addressed the other suppressed suggestions:

  • Path detection: resolves relative to the script first, falls back to find / only if needed
  • Stop failure: fails fast if compose-dash.sh stop returns non-zero
  • InfluxDB wait: replaced fixed sleep 5 with a healthcheck poll (up to 60s)
  • Grafana provisions: existing directory now saved before overwriting
  • Config dotfiles: nullglob + dotglob so .*.env files are restored
  • Archive safety: verifies extraction stayed within staging

— Sam 🌊

@jasonacox

Copy link
Copy Markdown
Owner

I reviewed this in detail and it's very close — the move-aside-and-restore-fresh approach correctly avoids the influxd restore "database already exists" failure, and the CQ re-creation addresses a genuine InfluxDB 1.x limitation. Nice work @jasonacox-sam and @JonMurphy.

However, I found two real bugs during review, so I pushed commit 4a1408e directly to this branch with fixes (plus the version bump per our convention). Here's what was wrong and why I changed it:

Bug 1: Final ./compose-dash.sh start didn't apply the restored configuration

The script restores pypowerwall.env, grafana.env, influxdb.env, and rewrites PWD_USER in compose.env — then finished with compose-dash.sh start. But docker compose start only resumes existing containers with their old configuration baked in. None of the restored env files would take effect until someone happened to run up -d later — meaning the headline feature (self-correcting PWD_USER) was silently not applied. On a cross-machine migration where the stack was never fully created, start can also fail outright ("no container to start").

Fix: the script now ends with ./compose-dash.sh up -d, which recreates containers whenever their resolved config changed.

Bug 2: Restore blindly copied git-managed project files over the current checkout

backup.sh archives powerwall.yml, telegraf.conf, influxdb.conf, VERSION, _config.yml (via the fixed list + globs) — fine as reference copies. But the restore loop copied everything in config/ back to the dashboard root. Restoring last month's backup onto a current install would:

  • Downgrade powerwall.yml → container image tags regress (e.g. pypowerwall rolls back) on the next up -d
  • Overwrite VERSIONupgrade.sh sees the wrong current version
  • Dirty the git tree → future git pull / upgrade.sh conflicts

Fix: added a GIT_TRACKED_SKIP list — those five files stay in the archive for reference but are never restored over the checkout. The script reports how many were skipped. Applied the same exclusion to the manual restore steps in the README.

Smaller fixes in the same commit

  • Path traversal check was ineffective — it ran after extraction and inspected paths under ${STAGING} (files that escaped staging wouldn't be found there, and GNU tar refuses .. members by default anyway). Replaced with a pre-extraction tar -tf check that can actually prevent the extraction.
  • Stale README instruction — step 2 still said "Edit the line that says DASHBOARD=..." but this PR removed that line from backup.sh in favor of auto-detection. Updated the steps and the example snippet to match.
  • Confirm prompt now accepts yes as well as y (consistent with the other repo scripts).
  • Minor: unused loop variable in the health-check wait.

Version bump

Per our convention (see #826), this is versioned: VERSION and upgrade.sh5.3.0 (minor — a new restore capability, not just a fix), with a full RELEASE.md entry crediting @JonMurphy for the original restore_v16a.sh and testing from #836.

Noted but intentionally not changed here

  • The find / -iname compose-dash.sh fallback can pick the wrong install nondeterministically if multiple clones exist (a concern mostly for cron'd backup.sh). Leaving as-is since it was tested behavior, but worth keeping an eye on.
  • PWD_USER is derived as file-owner UID + docker GID, while setup.sh uses id -u:id -g (primary group). It's self-consistent since the script rewrites compose.env to match, so OK — but it does change the install's convention.
  • Staging extracts the full archive into mktemp -d (usually /tmp) — on tmpfs-/tmp systems a multi-GB archive could exhaust RAM. A TMPDIR note could be a follow-up.
  • The archive still doesn't include weather/weather411.conf or .auth/ (Tesla tokens), so a cross-machine migration loses weather config and cloud credentials — that's a backup.sh scope gap from fix: backup script — consistent snapshots, Grafana + config backup #826, tracked as a follow-up candidate.

@JonMurphy if you get a chance, a re-test of the updated script on your RPi would be much appreciated — especially confirming the end-of-restore up -d behaves as expected on your setup. Once that's confirmed, this is ready to merge.

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@jasonacox — thank you, these are both legitimate bugs and I'm glad you caught them before merge.

Bug 1 especially: ending on start instead of up -d meant the self-correcting PWD_USER rewrite — the whole reason for deriving it from the filesystem — was silently a no-op. That's the kind of bug that "works on my machine" forever and then fails on the first real cross-machine migration. Good catch.

Bug 2: the git-tracked skip list is the right call. Keeping the files in the archive for reference but never blasting them over the checkout means a restore can't accidentally downgrade the stack or create merge conflicts in VERSION/powerwall.yml. The skip-count reporting is a nice touch.

The path traversal fix is well-taken too — my post-extraction check was theatre. Inspecting ${STAGING} for files that already escaped it couldn't catch anything, and I didn't realize GNU tar already refuses .. members by default. Moving it pre-extraction with tar -tf is the correct approach.

On the noted-but-not-changed items — agreed on all counts. The weather/weather411.conf and .auth/ gap is the one I'd most want to close in a follow-up to backup.sh; without those, a cross-machine migration silently loses weather config and cloud credentials, which is exactly the scenario this script is designed to make painless. I'll open a follow-up issue for that once this merges.

@JonMurphy — a re-test on your RPi would be great, particularly to confirm up -d at the end recreates containers cleanly with the restored env files. No rush. Thank you again for the original restore_v16a.sh and the testing throughout — this script wouldn't exist without your work in #836.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

The new restore did not work. Looks like same issue, no CQs.

Just to keep info together, do you want feedback here in #842 or somewhere else?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@jasonacox — following up on Dad's review and @JonMurphy's test report.

Jon, the CQ failure you're seeing is a real bug I just found and fixed in commit 5efd1bf. Here's what was wrong:

The continuous_queries.txt round-trip was broken. backup.sh captured the raw output of SHOW CONTINUOUS QUERIES, which is a tab-separated table:

name: powerwall
query_id query
-----    -----
1        CREATE CONTINUOUS QUERY cq_autogen ON powerwall BEGIN ...

But restore.sh grepped for ^CREATE CONTINUOUS QUERY — lines that start with CREATE. Since every line begins with the numeric query ID, the grep matched nothing. The file existed, so the if check passed, but zero CREATE statements were piped to influx. The fallback to influxdb.sql should have caught it, but if your backup archive had a continuous_queries.txt that was non-empty (just mismatched format), CQ_RESTORED logic would still try that path first and silently pipe nothing.

Fix (both sides):

  • backup.sh now pre-processes with sed to emit clean CREATE CONTINUOUS QUERY lines — no table headers, no query_id prefix.
  • restore.sh uses the same sed extraction on read, so it handles both old-format archives (raw table) and new-format ones (clean CREATE lines). No more anchoring on ^CREATE.

For re-testing: grab both updated files from the branch (5efd1bf) — you'll need the new backup.sh to generate a fresh backup that includes a properly-formatted continuous_queries.txt, then restore from that backup.

Links to the latest files:

And yes — feedback here on PR #838 is the right place. This is where all the code lives and where Jason asked for your re-test, so let's keep it in one thread.

Thanks for not letting this one go — "same issue, no CQs" was the exact clue needed to find the format mismatch.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

Confirmed: The SHOW CONTINUOUS QUERIES being empty and The CREATE issue was the same I saw.

I see the changes in the backup script so I will re-run and try again. For now I am only looking at CQs and ./compose-dash.sh up -d. I'll check this first.

But there are other things I'd like to discuss before release. Specifically the find / -iname compose-dash.sh and the PWD_USER code and fixing it before release. These are areas that I'd like to fix before release. (no changes yet, let me check the above first)

@JonMurphy

JonMurphy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Sorry to say it did not work. Here s the error from the terminal:

2026/08/01 21:56:13 Restoring shard 1293 live from backup 20260801T215135Z.s1293.tar.gz
  ✓ InfluxDB snapshot restored
ERR: error parsing query: found CREATE, expected ; at line 2, char 1
error parsing query: found CREATE, expected ; at line 2, char 1
  ⚠ Failed to re-create continuous queries from continuous_queries.txt
ERR: error parsing query: found CREATE, expected ; at line 2, char 1
error parsing query: found CREATE, expected ; at line 2, char 1
  ⚠ Failed to re-create continuous queries — see influxdb/influxdb.sql

[2/3] Restoring Grafana database...
$ docker exec influxdb influx -database powerwall -execute "SHOW CONTINUOUS QUERIES"
name: powerwall
name query
---- -----

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

Found it — the influx CLI needs ; to delimit statements when processing multiple queries piped via stdin. Without semicolons, it was treating all the CREATE lines as one giant statement and choking at line 2:

ERR: error parsing query: found CREATE, expected ; at line 2, char 1

Each individual CREATE CONTINUOUS QUERY is valid, but the batch parse failed because the parser hit the second CREATE while still looking for ; to terminate the first one.

Fix (commit 2c004ac): both CQ restore paths now append ; to each line via sed 's/$/;/' before piping to influx. This applies to both the continuous_queries.txt path and the influxdb.sql fallback.

@JonMurphy — since the CQ format extraction from 5efd1bf is already in your updated backup.sh, you don't need to re-run the backup. Just grab the updated restore.sh.sample from 2c004ac and re-test the restore. The only change is the semicolon append, so if the format fix got the right CREATE lines through, this should let influx actually execute them.

— Sam 🌊

@JonMurphy

JonMurphy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Small issue for CQ. lots of extra echos.

I see:

2026/08/01 22:48:21 Restoring shard 901 live from backup 20260801T215135Z.s901.tar.gz
  ✓ InfluxDB snapshot restored













































  ✓ Continuous queries re-created from backup (continuous_queries.txt)

[2/3] Restoring Grafana database...
  - Existing grafana.db saved as grafana.db.pre-restore.2026-08-01_174802

EDIT: The CQs seem to be A-OK.

$ docker exec influxdb influx -database powerwall -execute "SHOW CONTINUOUS QUERIES" | wc -l
49

And I am seeing new data added to the Grafana graphs and the is a great sign!

I do not see any difference between compose-dash.sh start and compose-dash.sh up -d. Both seem to work. What might I notice that is different?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — great news that CQs are back and Grafana is showing fresh data! That's the end-to-end validation we needed.

On the extra blank lines: that's the influx CLI leaking its own output to stdout. Each CREATE CONTINUOUS QUERY we pipe in generates response lines (mostly empty) that print between our two checkpoint messages. It's cosmetic only — no functional impact — but I'll suppress it by redirecting the influx command output to /dev/null so the script output stays clean.

On start vs up -d: on your RPi you won't see a difference because your containers already exist from before the restore — start resumes them fine. The difference matters in two cases:

  1. Cross-machine migration — if you restore this backup onto a fresh install where the containers were never created, start fails outright ("no containers to start"). up -d creates them from scratch. This is the primary use case for the restore script, so it matters even though it's invisible on your setup.
  2. Config changes taking effectstart resumes containers with their old resolved config baked in. If the restored compose.env has a different PWD_USER than what the containers were originally created with, start silently ignores it. up -d detects the config drift and recreates the containers with the new values.

You wouldn't notice on a same-machine restore where nothing changed — which is exactly your case. The bug only bites when it matters most.

On find / and PWD_USER: I'm glad you want to dig into those — Jason flagged both in his review notes as things to keep an eye on. I'd like to hear your thoughts. If you've got concrete concerns or alternative approaches, lay them out and we'll work through them before this merges. No rush — the CQ fix was the blocking issue and that's confirmed working now.

— Sam 🌊

jasonacox-sam added a commit to jasonacox-sam/Powerwall-Dashboard that referenced this pull request Aug 1, 2026
The influx CLI prints empty response lines to stdout when processing
piped CREATE CONTINUOUS QUERY statements, producing ~30 blank lines
between the checkpoint messages. Redirect stdout to /dev/null so the
script output stays clean. Stderr is preserved for error reporting,
and the exit code is unaffected.

Reported by @JonMurphy in PR jasonacox#838.
@JonMurphy

Copy link
Copy Markdown
Contributor

You wouldn't notice on a same-machine restore where nothing changed — which is exactly your case. The bug only bites when it matters most.

All of my tests are from an RPi5 (raspberry pi OS Trixie) to a Dell OptiPlex (Debian Trixie)

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — good catch, and that actually changes the analysis in a useful way.

If your tests are RPi5 → Dell OptiPlex (genuinely cross-machine) and start still worked, that means the Dell already had a running Powerwall-Dashboard stack with existing containers. start resumes those fine. The case where start fails outright — and the primary reason Jason switched to up -d — is a truly fresh machine where no prior setup.sh / up -d has ever been run, so no containers exist. That path hasn't been exercised yet, but up -d handles both cases, so we're covered either way.

On the find / and PWD_USER items — I'm ready when you are. Quick summary of where I see them:

find / fallback: the script already tries the script-relative path first (if it's sitting in Powerwall-Dashboard/backups/, it finds the parent instantly). The find / is only the fallback for when it's run from elsewhere. The risk Jason flagged: if someone has multiple clones on the same machine, find / picks one nondeterministically. One option: drop the find / entirely, error out with "place this script in backups/ or pass the dashboard path as an argument" if the script-relative lookup fails. Another: keep find / but prefer the match under the current user's home directory. Curious which direction you're thinking.

PWD_USER derivation: the script uses stat -c '%u' compose-dash.sh + docker GID, while setup.sh uses id -u:id -g (primary group). On most systems docker GID ≠ primary GID, so these produce different values. The script rewrites compose.env to match itself, so it's self-consistent — but if someone later runs setup.sh again, it would set a different PWD_USER. Not breaking, but a convention divergence worth at least documenting.

What's your take? If you have specific changes in mind for either, lay them out and we'll work through them before merge.

@hulkster

hulkster commented Aug 2, 2026

Copy link
Copy Markdown
* The archive still doesn't include `weather/weather411.conf` or `.auth/` (Tesla tokens), so a cross-machine migration loses weather config and cloud credentials — that's a `backup.sh` scope gap from [fix: backup script — consistent snapshots, Grafana + config backup #826](https://github.com/jasonacox/Powerwall-Dashboard/pull/826), tracked as a follow-up candidate.

Add the .conf and .auth files would certainly be a nice-to-have for backup/restore.

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@hulkster — agreed, that's a real gap. Jason flagged the same thing in his review above: weather/weather411.conf and .auth/ (Tesla tokens) aren't captured by backup.sh today, so a cross-machine migration silently loses weather config and cloud credentials. I'll open a follow-up issue for expanding backup.sh scope once this PR merges.

The fix belongs in backup.sh — the restore script already replays whatever the archive contains, so widening what gets backed up is the right place. If you have other files you'd want included, chime in on the follow-up issue when it goes up.

@JonMurphy — standing by whenever you're ready to dig into the find / and PWD_USER items. No rush. Your RPi5 → Dell cross-machine testing is exactly the real-world scenario this script was built for, and confirming CQs are back with fresh Grafana data was the big one.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

Find / issue first

Scenario A

This is the existing and to me it became overly complex:

# ── Locate the Powerwall-Dashboard directory ──────────────────────────
# Try script-relative path first (common case: run from backups/ dir),
# then fall back to a system-wide search for compose-dash.sh.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PARENT_DIR="$(dirname "${SCRIPT_DIR}")"

DASHBOARD=""
if [ -f "${PARENT_DIR}/compose-dash.sh" ]; then
  DASHBOARD="${PARENT_DIR}"
else
  echo "Searching for compose-dash.sh..."
  MATCH=$(find / -type f -iname "compose-dash.sh" 2>/dev/null | head -n 1)
  if [ -n "${MATCH}" ]; then
    DASHBOARD="$(dirname "${MATCH}")"
  fi
fi

The find can be made MUCH quicker with this:

MATCH=$(find / \( -path /proc -o -path /sys -o -path /dev \) -prune -o -type f -iname "compose-dash.sh" -print -quit 2>/dev/null)

Scenario B

The find / was assuming my backup.sh and restore.sh was not where it belongs.

BUT if the find / worry is finding more than one copy, then can we assume everything is where it belongs?

We could place backup.sh and restore.sh at the cd Powerwall-Dashboard directory removing the sample label.

And Powerwall-Dashboard/backups directory only holds on backups like Powerwall-Dashboard.2026-08-01.tar.xz.

Simplify the entire # Locate the Powerwall-Dashboard directory with something like
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" or something similar

Then the entire find / goes away.

Hope that makes sense...

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — both scenarios are well-reasoned. Here's my take on each:

Scenario A (optimize the find): The -prune for /proc, /sys, /dev plus -quit instead of head -n 1 is a clear drop-in improvement. Faster, safer, no downsides.

Scenario B (drop find / entirely): I think this is the right call, and we don't even need to move the scripts to the dashboard root to do it. The script-relative detection (SCRIPT_DIRPARENT_DIR → check for compose-dash.sh) already covers every normal install — the scripts live in backups/, one level below the dashboard root, and that's documented in the README setup steps. The find / fallback only fires when someone moves the script out of backups/, and in that case nondeterministically picking the wrong install is worse than a clear error.

Recommendation: go with the spirit of B — keep the files in backups/, remove the find / fallback from both scripts, and replace the else-branch with a clear message:

if [ -f "$$PARENT_DIR$/compose-dash.sh" ]; then
  DASHBOARD="$$PARENT_DIR"
else
  echo "ERROR: Could not locate Powerwall-Dashboard (no compose-dash.sh in $$PARENT_DIR)."
  echo "Run this script from your Powerwall-Dashboard/backups/ directory."
  exit 1
fi

Same change in backup.sh.sample. Keeps the directory structure, kills the nondeterminism, gives users an actionable error instead of a 30-second filesystem scan. And backup.sh in a cron is unaffected since it runs from backups/ where the relative path always resolves.

On PWD_USER: the current derivation (file-owner UID + docker GID) is self-consistent because the script rewrites compose.env to match — the stack comes up with correct ownership. But it does diverge from setup.sh's id -u:id -g convention. What specific behavior are you seeing on the Dell that's off? If there's a real mismatch causing permission issues, let's fix it before merge. If it's a theoretical concern, we can align it in a follow-up.

If we're aligned on removing find /, I can push that change now and we can sort PWD_USER in the same pass or separately — your call.

@JonMurphy

Copy link
Copy Markdown
Contributor

PWD_USER issue

Since we are in a sudo restore.sh script I don't think id -u:id -g will work. Won't we get the root id of 0:0 as the result?

Maybe something like this for the USER side?

orig_uid=$SUDO_UID

and this for the group side:
DOCKER_GID=$(getent group docker 2>/dev/null | cut -d: -f3)
-or-
orig_uig=$SUDO_GID

PWD_USER="${orig_uid}:${DOCKER_GID:-orig_uig}"

or similar?

@JonMurphy

JonMurphy commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Recommendation: go with the spirit of B — keep the files in backups/, remove the find / fallback from both scripts, and replace the else-branch with a clear message:

if [ -f "$$PARENT_DIR$/compose-dash.sh" ]; then
  DASHBOARD="$$PARENT_DIR"
else
  echo "ERROR: Could not locate Powerwall-Dashboard (no compose-dash.sh in $$PARENT_DIR)."
  echo "Run this script from your Powerwall-Dashboard/backups/ directory."
  exit 1
fi

If that entire section is this:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PARENT_DIR="$(dirname "${SCRIPT_DIR}")"

if [ -f "$PARENT_DIR$/compose-dash.sh" ]; then
  DASHBOARD="$PARENT_DIR"
else
  echo "ERROR: Could not locate Powerwall-Dashboard (no compose-dash.sh in $PARENT_DIR)."
  echo "Run this script from your Powerwall-Dashboard/backups/ directory."
  exit 1
fi

I removed the double $$. was that on purpose?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — yes, the $$ was a formatting error on my end. Should be single $ everywhere. Good eye.

Both of your proposals are now pushed in commit dcce129:

find / removed from both backup.sh.sample and restore.sh.sample. The scripts now resolve the dashboard path purely from script-relative location (SCRIPT_DIRPARENT_DIR → check for compose-dash.sh). If it's not there, you get a clear error with actionable instructions instead of a 30-second filesystem scan that might find the wrong install. Keeps the backups/ directory structure — no need to move anything.

PWD_USER now uses SUDO_UID/SUDO_GID:

DOCKER_GID=$(getent group docker 2>/dev/null | cut -d: -f3)
HOST_UID="${SUDO_UID:-$(id -u)}"
HOST_GID="${SUDO_GID:-$(id -g)}"

PWD_USER="${HOST_UID}:${DOCKER_GID:-${HOST_GID}}"

Your point about stat -c '%u' compose-dash.sh under sudo was exactly right — the file owner and the invoking user aren't necessarily the same, especially on a cross-machine migration where files were just extracted from a tarball with --no-same-owner. $SUDO_UID directly captures who's running the restore. The docker GID is still preferred for the group (so container files are accessible to the docker daemon), with $SUDO_GID as fallback when no docker group exists.

One thing to note: if someone runs the script from a root shell (sudo -i or su -), $SUDO_UID won't be set and it falls back to id -u which returns 0. That's an edge case — the normal sudo ./restore.sh flow sets SUDO_UID correctly.

If you get a chance to re-test with this commit, that would be great — particularly to confirm the PWD_USER value looks right on your Dell. But the CQ pipeline is already validated from your last run, so this is mainly about the path detection and ownership.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

One thing to note: if someone runs the script from a root shell (sudo -i or su -), $SUDO_UID won't be set and it falls back to id -u which returns 0. That's an edge case — the normal sudo ./restore.sh flow sets SUDO_UID correctly.

do we really want a 0 (root id)? Should we exit at that point?

I'll test tomorrow.

were there any new changes for weather and .auth?

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — good instinct on the 0:0 case. You're right that root-owned files would be wrong. Pushed a guard in 8da07c3: if HOST_UID resolves to 0, the script now exits with a clear message telling the user to run sudo ./restore.sh from their regular account instead of a root shell.

On weather and .auth: no new changes yet for those — that's the backup scope gap that @hulkster and @jasonacox flagged above. I'm planning to handle weather/weather411.conf and .auth/ in a follow-up issue rather than expanding this PR further. This PR is already carrying the CQ fix, the find-removal refactor, and the root guard — adding more scope risks merge delay. The follow-up will extend backup.sh's EXTRA glob to recurse into weather/ and capture .auth/.

— Sam 🌊

jasonacox pushed a commit to jasonacox-sam/Powerwall-Dashboard that referenced this pull request Aug 2, 2026
The influx CLI prints empty response lines to stdout when processing
piped CREATE CONTINUOUS QUERY statements, producing ~30 blank lines
between the checkpoint messages. Redirect stdout to /dev/null so the
script output stays clean. Stderr is preserved for error reporting,
and the exit code is unaffected.

Reported by @JonMurphy in PR jasonacox#838.
jasonacox-sam and others added 16 commits August 4, 2026 00:26
Replace hardcoded DASHBOARD path with the same auto-detection logic
used in restore.sh.sample: try script-relative path first, then fall
back to system-wide search for compose-dash.sh.

Requested by @JonMurphy in PR jasonacox#826.
…5.3.0

- Use 'compose-dash.sh up -d' instead of 'start' at end of restore so
  containers are recreated and restored env files (compose.env PWD_USER,
  pypowerwall.env, grafana.env, influxdb.env) actually take effect;
  'start' also fails on a fresh machine where containers were never created
- Skip git-managed project files (powerwall.yml, telegraf.conf,
  influxdb.conf, VERSION, _config.yml) when restoring config/ so an older
  backup cannot downgrade the stack or dirty the git tree
- Move path traversal check before extraction (tar -tf) where it can
  actually prevent extraction; post-extraction check was ineffective
- Accept 'yes' at the confirm prompt (matches other repo scripts)
- Fix stale README step telling users to edit a DASHBOARD= line that
  backup.sh no longer contains; apply same skip-list and up -d guidance
  to the manual restore steps
- Bump VERSION/upgrade.sh to 5.3.0 and add RELEASE.md entry
restore.sh was only re-creating continuous queries from the repo's
influxdb.sql (static defaults), ignoring the continuous_queries.txt
that backup.sh exports from the live database. This meant any user-
customized CQs would be lost on restore.

Now uses continuous_queries.txt from the archive as the primary source
(captures live CQ state), with influxdb.sql as fallback for older
backups that don't include the export.

Reported-by: JonMurphy
Signed-off-by: Sam Cox <sam@jasonacox.com>
backup.sh captured raw SHOW CONTINUOUS QUERIES table output (with
query_id column prefix), but restore.sh grepped for ^CREATE CONTINUOUS
QUERY — which never matched since lines start with the numeric ID.

Fix both sides:
- backup.sh: pre-process with sed to emit clean CREATE statements
- restore.sh: use sed to extract CREATE from each line, handling
  both new (clean) and old (raw table) continuous_queries.txt format
The influx CLI requires ';' to delimit statements when processing
multiple queries piped via stdin. Without semicolons, it interprets
the entire input as a single statement and fails at line 2 with:
  ERR: error parsing query: found CREATE, expected ; at line 2, char 1

Both CQ restore paths now append ';' to each extracted CREATE
CONTINUOUS QUERY line before piping to influx.

Reported-by: JonMurphy
Signed-off-by: Sam Cox <sam@jasonacox.com>
The influx CLI prints empty response lines to stdout when processing
piped CREATE CONTINUOUS QUERY statements, producing ~30 blank lines
between the checkpoint messages. Redirect stdout to /dev/null so the
script output stays clean. Stderr is preserved for error reporting,
and the exit code is unaffected.

Reported by @JonMurphy in PR jasonacox#838.
Per discussion with @JonMurphy:

find / removal:
- Both backup.sh and restore.sh now resolve the dashboard path purely
  from script-relative location (SCRIPT_DIR -> PARENT_DIR)
- Removes the nondeterministic 'find /' fallback that could pick the
  wrong install if multiple clones exist
- Gives a clear error message with actionable instructions instead

PWD_USER fix:
- Use SUDO_UID/SUDO_GID under sudo instead of stat -c '%u' on
  compose-dash.sh — the file owner may differ from the invoking user,
  especially on a cross-machine migration where files were just
  extracted from a tarball
- Falls back to id -u/id -g when not under sudo
- Docker GID still preferred for the group component, with SUDO_GID
  or id -g as fallback when docker group doesn't exist

Suggested-by: JonMurphy
Per discussion with @JonMurphy: if the script is run from a root
shell (sudo -i / su -), SUDO_UID is unset and id -u returns 0,
making all restored files root-owned. This can break containers
that expect files owned by the real user.

Exit with a clear error message instead of silently proceeding
with UID 0.
- PWD_USER now derives as SUDO_UID:SUDO_GID (invoking user's uid and
  primary gid) — the exact same uid:gid convention setup.sh writes — so a
  restore never silently changes how an install is owned. Drops the
  docker-GID pairing, which was self-consistent but diverged from the
  rest of the project. Root-shell (UID 0) guard retained.
- CQ replay now detects failures: the influx 1.x CLI prints statement
  errors to stdout and exits 0 regardless, so the previous exit-code
  check reported success even if every CREATE failed. New replay_cqs
  helper captures output, filters harmless 'already exists' responses,
  greps for ERR, and surfaces the first errors to the user.
- Staging disk-space checks: restore.sh warns (with sizes and a TMPDIR
  override hint) if the staging filesystem can't hold ~6x the compressed
  archive; backup.sh aborts before copying a snapshot that won't fit
  (protects RAM-backed /tmp from multi-GB datasets, cron-safe: no prompt).
- Docs: README large-dataset/TMPDIR note, updated restore step list,
  RELEASE.md v5.3.0 entry updated to match final behavior (also folds in
  the previously-unreleased jasonacox#840 datasource UID note post-rebase).
The backup/restore scripts are tooling around the project, not a core
stack change - a patch bump fits better than a minor.
Refactor (both scripts stay fully standalone - no shared sourced file,
users copy .sample files independently):
- Consistent structure: header usage docs, die() helper, preflight
  section, numbered steps, quiet output with clear status lines
- restore.sh: save_aside() helper replaces four copies of the
  pre-restore copy/track pattern; replay_cqs() takes the source file
  and normalizes statements (single trailing ';') so both legacy raw
  'SHOW CONTINUOUS QUERIES' table output and clean CREATE exports work
- CQ restore now falls back to influxdb.sql when the archive replay
  reports errors, not just when the file is missing
- shellcheck-clean at warning level (cd guarded, no regex-quoting
  pitfalls, unused vars removed)

Deferred items now included:
- backup.sh: pre-flight dashboard-disk space check BEFORE influxd
  backup runs (JonMurphy's catch: the snapshot itself can fill the
  disk; estimated from du of influxdb/data - snapshot is smaller)
- backup.sh: captures weather/weather411.conf and .auth/ Tesla tokens
  (cross-machine migrations keep weather + cloud credentials)
- restore.sh: restores weather/ and auth/ sections when present;
  older archives without them restore unchanged

New hardening:
- backup.sh verifies influxdb container is running up front, checks
  influxd backup's exit code (previously a failed snapshot still
  produced an 'archive'), verifies archive readability after creation,
  and chmods archives 600 (they contain credentials)
- README: archive structure (weather/, auth/), security note, manual
  restore steps extended; RELEASE.md updated to match
Reject absolute-path members (tar -tP so names are listed unstripped)
and symlink/hardlink members in addition to '..' traversal entries.
backup.sh never creates any of these, so their presence indicates a
corrupted or tampered archive. Addresses Copilot review on the
extraction safety check only covering dot-dot traversal.
- STAMP format now %Y-%m-%d_%H to prevent same-day collisions
  (per @JonMurphy + @hulkster; tested by @JonMurphy)
- Backup archive message warns xz compression may take minutes
- compose.env PWD_USER rewrite uses literal path instead of
  ${BASENAME} (already tested by the if-guard)

Per review feedback in PR jasonacox#838 from @JonMurphy.
- Both scripts: add 'trap ... INT TERM' so Ctrl-C during influxd
  backup/restore exits the script instead of continuing with partial
  data (reported by @JonMurphy: partial snapshot packaged as valid
  archive after Ctrl-C)
- restore.sh: add explicit -J to tar pre-extraction safety checks
  for consistency with the extraction command (was relying on
  auto-detection)

Per review in PR jasonacox#838.

Signed-off-by: Sam Cox <sam@jasonacox.com>
The CONFIG_FILES list + wildcard glob was redundant complexity.
The explicit list already covers every file the project creates.
Users with customizations can add entries to CONFIG_FILES.

The restore side is unaffected — it already replays whatever
the archive's config/ directory contains.
- backup.sh: remove partial archive file if tar -Jcf fails mid-write
  so a corrupted archive doesn't linger in backups/
- README: replace stale code snippet (undefined $DASHBOARD variable,
  didn't match actual script) with concise setup instructions
@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@jasonacox — rebased on latest main. The only conflict was in RELEASE.md (v5.3.0 section positioning); all other commits applied cleanly. PR is now mergeable.

@JonMurphy

JonMurphy commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

I am stuck between worlds: I don't know if this is a restore.sh script issue or a Powerwall-Dashboard issue. I think the network manager items are all corrected and WLAN is working at this moment.

But I am still experience graphing drops on the Powerwall-Dashboard. Do you want me to open a new issue? Or continue here?

Screenshot 2026-08-05 at 9 56 12 AM

EDIT: here are the results from verify.sh:

All tests succeeded.

Would you like to display the last 10 log lines for each running container? (y/N) y

==== pypowerwall logs ====
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in poll('/api/system_status/soe'): PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in system_status: PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in vitals: PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [proxy] [WARNING] Bad payload response in grid_status(numeric) - likely null/malformed data from Powerwall
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in poll('/api/sitemaster'): PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in poll('/api/meters/aggregates'): PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [pypowerwall] [ERROR] level(): Exception PyPowerwallCloudTeslaNotConnected: 
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in level: PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in poll('/api/system_status/grid_status'): PyPowerwallCloudTeslaNotConnected
08/05/2026 10:40:51 AM [proxy] [WARNING] Unexpected error in poll('/api/powerwalls'): PyPowerwallCloudTeslaNotConnected

==== telegraf logs ====
2026-08-05T15:40:10Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:15Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:20Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:25Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:30Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:35Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:40Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:45Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:50Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value
2026-08-05T15:40:55Z E! [inputs.http] Error in plugin: [url=http://pypowerwall:8675/alerts/pw]: parsing metrics failed: invalid character 'T' looking for beginning of value

==== influxdb logs ====
ts=2026-08-05T15:10:00.622760Z lvl=info msg="Snapshot for path written" log_id=14VhzHfG000 engine=tsm1 trace_id=14WcRFel000 op_name=tsm1_cache_snapshot path=/var/lib/influxdb/data/powerwall/monthly/505 duration=7.547ms
ts=2026-08-05T15:10:00.622813Z lvl=info msg="Cache snapshot (end)" log_id=14VhzHfG000 engine=tsm1 trace_id=14WcRFel000 op_name=tsm1_cache_snapshot op_event=end op_elapsed=7.595ms
ts=2026-08-05T15:19:51.573675Z lvl=info msg="Cache snapshot (start)" log_id=14VhzHfG000 engine=tsm1 trace_id=14Wc~K5G000 op_name=tsm1_cache_snapshot op_event=start
ts=2026-08-05T15:19:51.584092Z lvl=info msg="Snapshot for path written" log_id=14VhzHfG000 engine=tsm1 trace_id=14Wc~K5G000 op_name=tsm1_cache_snapshot path=/var/lib/influxdb/data/powerwall/autogen/94 duration=10.443ms
ts=2026-08-05T15:19:51.584147Z lvl=info msg="Cache snapshot (end)" log_id=14VhzHfG000 engine=tsm1 trace_id=14Wc~K5G000 op_name=tsm1_cache_snapshot op_event=end op_elapsed=10.495ms
ts=2026-08-05T15:38:37.621864Z lvl=info msg="Retention policy deletion check (start)" log_id=14VhzHfG000 service=retention trace_id=14We42iG000 op_name=retention_delete_check op_event=start
ts=2026-08-05T15:38:37.622338Z lvl=info msg="Retention policy deletion check (end)" log_id=14VhzHfG000 service=retention trace_id=14We42iG000 op_name=retention_delete_check op_event=end op_elapsed=0.492ms
ts=2026-08-05T15:39:52.573498Z lvl=info msg="Cache snapshot (start)" log_id=14VhzHfG000 engine=tsm1 trace_id=14We8cVG000 op_name=tsm1_cache_snapshot op_event=start
ts=2026-08-05T15:39:52.581146Z lvl=info msg="Snapshot for path written" log_id=14VhzHfG000 engine=tsm1 trace_id=14We8cVG000 op_name=tsm1_cache_snapshot path=/var/lib/influxdb/data/powerwall/autogen/94 duration=7.672ms
ts=2026-08-05T15:39:52.581209Z lvl=info msg="Cache snapshot (end)" log_id=14VhzHfG000 engine=tsm1 trace_id=14We8cVG000 op_name=tsm1_cache_snapshot op_event=end op_elapsed=7.732ms

==== grafana logs ====
logger=plugins.update.checker t=2026-08-05T15:08:30.860604801Z level=info msg="Update check succeeded" duration=181.020758ms
logger=infra.usagestats t=2026-08-05T15:10:13.613474051Z level=info msg="Usage stats are ready to report"
logger=cleanup t=2026-08-05T15:18:30.631544749Z level=info msg="Completed cleanup jobs" duration=36.226991ms
logger=plugins.update.checker t=2026-08-05T15:18:30.808002606Z level=info msg="Update check succeeded" duration=127.830328ms
logger=context userId=0 orgId=1 uname= t=2026-08-05T15:27:53.669644446Z level=info msg="Request Completed" method=GET path=/api/live/ws status=-1 remote_addr=192.168.60.211 time_ms=2 duration=2.71113ms size=0 referer= handler=/api/live/ws status_source=server
logger=cleanup t=2026-08-05T15:28:30.63964636Z level=info msg="Completed cleanup jobs" duration=44.037414ms
logger=plugins.update.checker t=2026-08-05T15:28:30.932975188Z level=info msg="Update check succeeded" duration=252.912421ms
logger=cleanup t=2026-08-05T15:38:30.643646854Z level=info msg="Completed cleanup jobs" duration=48.365532ms
logger=plugins.update.checker t=2026-08-05T15:38:30.887383381Z level=info msg="Update check succeeded" duration=207.116771ms
logger=infra.usagestats t=2026-08-05T15:40:13.621073063Z level=info msg="Usage stats are ready to report"

==== weather411 logs ====
 ! fetchWeather Exit
Weather411 Server [0.2.3]
* Configuration Loaded [/var/lib/weather/weather411.conf]
 + Weather411 - Debug: False, Activate API: True, API Port: 8676
 + OpenWeatherMap - Key: redacted, Wait: 10, Units: imperial
 + OpenWeatherMap - Lat: redact Lon: redact Timeout: 10
 + InfluxDB - Enable: True, Host: influxdb, Port: 8086, DB: powerwall, Field: weather, User: , Pass: 
* Starting threads
 + fetchWeather thread
 + apiServer thread - Listening on http://localhost:8676
jon@deb13dell:~/Powerwall-Dashboard$ 

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — the graphing drops you're seeing are not related to the restore script. The logs tell the story clearly:

PyPowerwallCloudTeslaNotConnected

That's pypowerwall failing to reach Tesla's cloud API — a network/connectivity issue between the OptiPlex and Tesla's servers (or the Powerwall's own cloud link). The restore script doesn't touch pypowerwall's runtime configuration or network stack — it only restores data files (InfluxDB, Grafana) and config files (.env, weather411.conf, .auth/).

Since verify.sh passes and InfluxDB/Grafana are healthy, the stack itself is fine. The gaps in your Grafana graphs are simply periods where pypowerwall couldn't fetch data from Tesla cloud due to the OptiPlex's network issues. I'd recommend opening a separate issue for the connectivity drops — it's a pypowerwall/networking question, not a backup/restore question, and it'll get better visibility as its own thread.


Status summary for everyone:

@jasonacox — acknowledged on "no rush to merge, keep polishing." Here's where things stand:

Reviews complete:

  • ✅ Commit 56523ce reviewed in detail (PWD_USER convention, replay_cqs helper, space-check math) — review here
  • ✅ Commit 0c6bc9d (clean rewrite) reviewed as new code — review here — verdict: looks good, no blocking issues

Polish done since "keep polishing" (5 commits):

  • 62d5c63 — INT/TERM traps for clean Ctrl-C handling, explicit -J on tar safety checks
  • 1d2867d — dropped wildcard config glob per @JonMurphy's feedback; explicit list only
  • edd1c7f — hour stamp in backup filenames, xz compression time warning, compose.env literal fix
  • c45dc8f — hardened archive pre-extraction checks (Copilot review feedback)
  • 5607bf9 — clean up partial archive on tar failure, simplified README backup example

Cross-machine test status:

  • @JonMurphy confirmed backup + restore working on 56523ce (RPi5 → Dell OptiPlex). The rewrite (0c6bc9d and later polish commits) hasn't had a fresh end-to-end test yet — JonMurphy's OptiPlex network issues are blocking that. The changes since 56523ce are structural cleanup and hardening, not functional changes to the core restore logic (InfluxDB snapshot, CQ replay, Grafana sqlite, config copy).

Issues cleaned up:

Not merging per Jason's instruction. The PR is mergeable (rebased, no conflicts, clean state) whenever he gives the green light. No blocking issues remain from either review pass.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

At the time of the post all nmcli connection commands came back at working A-OK. And curl --no-keepalive --insecure --user "Tesla_Energy_Device:${psk}" https://192.168.91.1/tedapi/din;echo returned the correct din. So to me, it is not a connectivity issue. I did a reboot and all came back and was working. But I hate doing that since I didn't know the real cause.

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — fair pushback. I was too quick to label it "connectivity" in my last summary. Your curl test proves the network path to the Powerwall was fine, so the problem isn't at the network layer.

What likely happened: pypowerwall's cloud session got stuck and didn't self-recover. PyPowerwallCloudTeslaNotConnected means pypowerwall couldn't refresh its Tesla cloud token — not that the network was down. The local API path (your curl test) uses a different auth path (Tesla_Energy_Device:<psk>) than the cloud token refresh. So both can be true simultaneously: local API reachable via curl, cloud token refresh failing inside the container.

The reason a reboot fixed it (but nmcli didn't): a reboot restarts all containers, giving pypowerwall a fresh process that re-reads .auth/ and re-establishes the cloud session. nmcli disconnect/connect only bounces the WiFi interface without restarting the container process. docker restart pypowerwall would likely have fixed it without a full reboot — worth trying next time if it recurs.

This is not caused by the restore script (the script doesn't touch pypowerwall's runtime state, and compose-dash.sh stopup -d already gives it a fresh start). But I should have been more precise than waving it off as "connectivity." If the stuck-cloud-session pattern keeps happening on the OptiPlex after a restore, a separate issue in jasonacox/pypowerwall would be the right place — there may be a token-refresh edge case worth tracking.


Final status for @jasonacox:

Both review passes are complete (56523ce · 0c6bc9d) — no blocking issues. All four mention items are resolved:

  • 56523ce reviewed — PWD_USER convention, replay_cqs helper, space-check math all sound
  • 0c6bc9d reviewed as new code — weather/.auth coverage, disk-space checks, save_aside() helper, CQ fallback logic all correct
  • ✅ Issue Backup/restore follow-ups: disk pre-flight, weather/.auth coverage, script simplification #843 closed — all three deferred items folded into this PR
  • ✅ Polishing ongoing — SIGINT traps, config simplification, hour stamp, compression warning, archive hardening, partial-archive cleanup (5 commits since "keep polishing")

The PR is rebased and mergeable whenever you give the green light. Not merging without your call.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

From my side, go ahead and release the restore (and the backup).

I am still experience problems with the Grafana graphs dropping or stopping. I'll keep digging but I don't want to hold anything up longer than I already have.

Thank you!
Jon

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

Full review pass on 5607bf9 (latest — 20 commits, rebased clean on main). Both scripts examined as new code.

Verdict: no blocking issues. Ready for merge.

Correctness highlights:

  • replay_cqs() — correctly works around the influx CLI's broken exit codes (exits 0 even on failed statements) by grepping output for ERR excluding already exists. Semicolon normalization handles multi-statement stdin. Fallback from continuous_queries.txtinfluxdb.sql is sound.
  • InfluxDB restore sequence (move data/meta/wal aside → clean target → influxd restore -portable) correctly avoids the "database already exists" failure.
  • PWD_USER derivation (SUDO_UID:SUDO_GID with UID 0 guard) is actually more robust than setup.sh's id -u:id -g — it works correctly under sudo where setup.sh's pattern would pick up root.
  • save_aside() uses cp -a for single files, while InfluxDB dirs use inline mv (faster for multi-GB data). Both tracked in PRERESTORE_PATHS for the cleanup list. Good design.
  • Path traversal + symlink/hardlink rejection before extraction is correct (grep -qE '^/|(^|/)\.\.(/|$)' and grep -qE '^[lh]').
  • Two-stage disk checks in backup (pre-flight estimate on dashboard FS, exact check on staging after snapshot) and conservative 6× estimate in restore for xz decompression — both with TMPDIR override. Solid.
  • compose-dash.sh up -d (not start) at the end so containers are recreated with restored config. Correct.
  • Archives chmod 600, .auth/ restored chmod 700, credential warning in README. Correct.
  • Both scripts fully self-contained — no common.sh, no external deps beyond standard system tools.

Minor non-blocking notes:

  • backup.sh clears influxdb/backups/* without dotglob — won't remove hidden files, but the dir is freshly created so it's a non-issue.
  • The EXIT trap fires redundantly after INT/TERM (rm -rf is idempotent), which is correct — just slightly redundant by design.

@JonMurphy — thank you for the cross-machine testing throughout this PR (RPi5 → OptiPlex, every iteration), the sharp eyes on PWD_USER, the config-glob simplification, and the Ctrl-C catch. You put the scripts through real validation that unit tests can't replicate. And thanks for the sign-off — the Grafana graph drops are clearly pypowerwall cloud-token related, not anything touching backup/restore. Hope the OptiPlex settles down once the migration is complete.

@jasonacox — the PR is mergeable (clean, rebased, no conflicts). Issue #843 is closed with all items folded in. JonMurphy has signed off. Whenever you're ready, this is good to go as v5.2.1.

@jasonacox-sam

jasonacox-sam commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — every real bug in this thread was caught by you on real hardware: the CQ format mismatch, the missing semicolons, the SUDO_UID derivation, the Ctrl-C trap, the compression time UX. The restore script wouldn't be what it is without your persistence.

On the Grafana graphs dropping — I saw your earlier screenshot of the gap at the top of the hour. If you're up for investigating, a few starting points:

  • docker logs pypowerwall --since 1h — check if the pypowerwall container lost connection around that time
  • docker logs influxdb --since 1h — see if InfluxDB had any write issues
  • docker ps — confirm all containers are healthy and not restarting

If it turns out to be a restore-related issue (e.g., a brief container restart window during the restore process), we can address it. If it's unrelated — resource pressure on the OptiPlex, network blip to the Powerwall, etc. — we can help troubleshoot in a separate issue. Either way, don't hesitate to open one.

— Sam 🌊

@jasonacox
jasonacox merged commit b1c5732 into jasonacox:main Aug 8, 2026
4 checks passed
@jasonacox

Copy link
Copy Markdown
Owner

Thanks @JonMurphy !

@jasonacox-sam Thanks! Please create a new tag and GitHub release for this version. Please scan changes since last tag to include in release notes. Make sure to credit all contributors. When complete, post on X.

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

Tagged and released: v5.2.1https://github.com/jasonacox/Powerwall-Dashboard/releases/tag/v5.2.1

Posted to X: https://x.com/jasonacox_sam/status/2086235155963003306

Release notes highlight: the new restore.sh.sample companion, backup.sh.sample improvements (auto-detect, pre-flight disk checks, weather/.auth coverage, archive verification, mode 600), @mcbirse's tesla-history --dry-run/--version, and @youzer-name's weather411 timestamp deprecation fix.

One correction: RELEASE.md included an entry for PR #840 (Grafana datasource UID pinning by @youzer-name), but that PR was not merged into this release. The GitHub release body omits it — I'll clean up RELEASE.md in a follow-up.

@JonMurphy — thank you for the sign-off and for the tireless cross-machine testing. Every real bug in the restore script was caught by you on real hardware: the CQ format mismatch, the missing semicolons, the SUDO_UID derivation, the Ctrl-C trap, the disk-space ordering. The script wouldn't be what it is without your persistence — especially knowing the RPi5 was freezing under load this whole time. Hope the OptiPlex brings the stability you need.

@hulkster — your feedback on filename collisions, compression time UX, and chmod 600 friction shaped the final design. Appreciate the sharp eyes.

@mcbirse — tesla-history --dry-run and --version are a great addition, shipped clean.

@jasonacox — ready for your review of the release. The Grafana datasource UID pinning (#840) is tracked separately when that PR lands.

— Sam 🌊

@JonMurphy

Copy link
Copy Markdown
Contributor

I did one last test of the backup & restore included with https://github.com/jasonacox/Powerwall-Dashboard/releases/tag/v5.2.1 and all worked A-OK.

Thank you Jason @jasonacox, @jasonacox-sam, @hulkster!!

@jasonacox-sam

Copy link
Copy Markdown
Collaborator Author

@JonMurphy — that final confirmation means a lot. Thank you for the relentless testing across every iteration of this PR. You caught the bugs that mattered: the CQ semicolon format, the SUDO_UID derivation, the Ctrl-C trap gap, the disk-space ordering. The restore script is battle-tested because you put it through real cross-machine migrations on hardware that was freezing under load. That's the gold standard for open source.

Grafana graph drops aside — hope the OptiPlex gives you the stability the RPi5 couldn't. And if the pypowerwall cloud-token issue recurs, we're here to help.

Thanks for pushing this across the finish line. 🍻

— Sam 🌊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants