Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d46d036c0 | ||
|
|
9ae4e34fe0 | ||
|
|
d1e226a687 | ||
|
|
49e41eeea9 | ||
|
|
88be5b99dd |
@@ -908,11 +908,68 @@ def _get_remote_rev(branch=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version(text):
|
||||||
|
"""Return a (major, minor, patch) tuple from a VERSION string, or None."""
|
||||||
|
try:
|
||||||
|
match = re.search(r"(\d+)\.(\d+)\.(\d+)", str(text))
|
||||||
|
if match:
|
||||||
|
return tuple(int(g) for g in match.groups())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_remote_version(branch=None):
|
||||||
|
"""Read VERSION on the tracked branch, e.g. the stable release version."""
|
||||||
|
try:
|
||||||
|
ref = branch or "stable"
|
||||||
|
url = (
|
||||||
|
"https://git.sovransystems.com/api/v1/repos/"
|
||||||
|
"Sovran_Systems/Sovran_SystemsOS/raw/VERSION?ref="
|
||||||
|
+ urllib.parse.quote(ref)
|
||||||
|
)
|
||||||
|
req = urllib.request.Request(url, method="GET")
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
|
return _parse_version(resp.read().decode())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates() -> bool | None:
|
def check_for_updates() -> bool | None:
|
||||||
|
"""Whether an update is available.
|
||||||
|
|
||||||
|
Primary signal: the flake lock's pinned Sovran_Systems rev differs from
|
||||||
|
the remote branch head. BUT a failed update rewrites ``flake.lock`` (the
|
||||||
|
``nix flake update`` step) without staging a generation (the
|
||||||
|
``nixos-rebuild boot`` step failed), so after a failure the lock and the
|
||||||
|
remote agree while the *running* system is still on the old version. In
|
||||||
|
that case the rev comparison alone reports a false "up to date" and hides
|
||||||
|
the failed update from the dashboard.
|
||||||
|
|
||||||
|
Backstop: compare the running Hub version against the branch VERSION.
|
||||||
|
A newer released version while running an older one means the update did
|
||||||
|
not apply (build failed, reboot skipped, generation rolled back) and must
|
||||||
|
be offered again.
|
||||||
|
"""
|
||||||
locked_rev, branch = _get_locked_info()
|
locked_rev, branch = _get_locked_info()
|
||||||
remote_rev = _get_remote_rev(branch)
|
remote_rev = _get_remote_rev(branch)
|
||||||
if locked_rev and remote_rev:
|
if locked_rev and remote_rev:
|
||||||
return locked_rev != remote_rev
|
rev_differs = locked_rev != remote_rev
|
||||||
|
if rev_differs:
|
||||||
|
return True
|
||||||
|
# Revs match — make sure the pinned (failed) rev isn't masking an
|
||||||
|
# older *running* system.
|
||||||
|
running_ver = _parse_version(_get_sovran_version())
|
||||||
|
remote_ver = _get_remote_version(branch)
|
||||||
|
if running_ver and remote_ver and remote_ver > running_ver:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
# Couldn't compare revs — fall back to the version backstop.
|
||||||
|
running_ver = _parse_version(_get_sovran_version())
|
||||||
|
remote_ver = _get_remote_version(branch)
|
||||||
|
if running_ver and remote_ver:
|
||||||
|
return remote_ver > running_ver
|
||||||
return None # inconclusive — couldn't read lock or reach remote
|
return None # inconclusive — couldn't read lock or reach remote
|
||||||
|
|
||||||
|
|
||||||
@@ -3885,6 +3942,11 @@ async def api_updates_check():
|
|||||||
# Avoid a slow remote update check when there is already an operation
|
# Avoid a slow remote update check when there is already an operation
|
||||||
# the dashboard needs to surface.
|
# the dashboard needs to surface.
|
||||||
return {"available": True, "status": status.lower()}
|
return {"available": True, "status": status.lower()}
|
||||||
|
if status == "FAILED":
|
||||||
|
# The last update did not complete (build failed). Keep offering the
|
||||||
|
# update so the user can re-run it rather than silently landing on a
|
||||||
|
# false "up to date".
|
||||||
|
return {"available": True, "status": "failed"}
|
||||||
|
|
||||||
available = await loop.run_in_executor(None, check_for_updates)
|
available = await loop.run_in_executor(None, check_for_updates)
|
||||||
# None means inconclusive (check failed) — report as available so the UI doesn't block
|
# None means inconclusive (check failed) — report as available so the UI doesn't block
|
||||||
@@ -3962,8 +4024,15 @@ async def api_updates_run():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Re-read status: a prior failed update leaves flake.lock advanced even
|
||||||
|
# though no generation was staged, so the rev-based check below can say
|
||||||
|
# "no updates" even though the system is still old. A failed update must
|
||||||
|
# always be re-runnable to recover.
|
||||||
|
persisted_status = await loop.run_in_executor(None, _read_update_status)
|
||||||
|
last_failed = persisted_status == "FAILED"
|
||||||
|
|
||||||
available = await loop.run_in_executor(None, check_for_updates)
|
available = await loop.run_in_executor(None, check_for_updates)
|
||||||
if available is False: # only block when positively confirmed no updates
|
if available is False and not last_failed: # only block when positively confirmed no updates
|
||||||
# Clear stale status/log so they don't contaminate future modal opens.
|
# Clear stale status/log so they don't contaminate future modal opens.
|
||||||
_write_update_status("IDLE")
|
_write_update_status("IDLE")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
|
|||||||
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
|
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
|
||||||
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
|
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
|
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
|
||||||
|
if ($btnRetryRun) $btnRetryRun.addEventListener("click", retryUpdateRun);
|
||||||
|
|
||||||
// Browser timers and requests may be suspended while an RDP session/tab is in
|
// Browser timers and requests may be suspended while an RDP session/tab is in
|
||||||
// the background. Reconcile immediately when the user returns instead of
|
// the background. Reconcile immediately when the user returns instead of
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function renderPortForwardGuideHtml(ports, opts) {
|
|||||||
var noteClass = opts.noteClass || "port-req-hint";
|
var noteClass = opts.noteClass || "port-req-hint";
|
||||||
var ipHtml = opts.internalIp
|
var ipHtml = opts.internalIp
|
||||||
? '<code class="port-req-internal-ip">' + escHtml(opts.internalIp) + '</code>'
|
? '<code class="port-req-internal-ip">' + escHtml(opts.internalIp) + '</code>'
|
||||||
: 'this computer’s <strong>internal IP</strong> (shown as “Internal IP” at the top of the Hub dashboard)';
|
: 'this computer’s <strong>internal IP</strong>';
|
||||||
|
|
||||||
var rows = (ports || []).map(function(p) {
|
var rows = (ports || []).map(function(p) {
|
||||||
return '<tr>' +
|
return '<tr>' +
|
||||||
@@ -78,26 +78,17 @@ function renderPortForwardGuideHtml(ports, opts) {
|
|||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
var forWhat = opts.serviceName
|
var forWhat = opts.serviceName
|
||||||
? 'For <strong>' + escHtml(opts.serviceName) + '</strong> to be reachable from outside your home network, open'
|
? 'To make <strong>' + escHtml(opts.serviceName) + '</strong> reachable from outside your home, forward these ports to ' + ipHtml + ':'
|
||||||
: 'Open';
|
: 'Forward these ports to ' + ipHtml + ':';
|
||||||
|
|
||||||
return '<p class="' + introClass + '">' +
|
return '<p class="' + introClass + '">' + forWhat + '</p>' +
|
||||||
forWhat + ' the ports below in your router’s <strong>port forwarding</strong> settings ' +
|
'<p class="port-req-steps" style="margin-top:6px;margin-bottom:10px;font-size:0.92em;color:#555;">' +
|
||||||
'and point them at ' + ipHtml + '.' +
|
'Set the internal and external port to the <strong>same number</strong>. Match <strong>TCP</strong> or <strong>UDP</strong> exactly. For ranges like <strong>40000-40099</strong>, use your router’s range fields (start 40000, end 40099).' +
|
||||||
'</p>' +
|
'</p>' +
|
||||||
'<ul class="port-req-steps">' +
|
|
||||||
'<li>Set the <strong>internal (private) port</strong> and the <strong>external (public) port</strong> to the <strong>same number</strong>.</li>' +
|
|
||||||
'<li>Match the <strong>protocol</strong> exactly — a rule set to TCP will not pass UDP traffic. Where the table says <strong>TCP + UDP</strong>, create both rules (or pick “Both”/“TCP/UDP” if your router offers it).</li>' +
|
|
||||||
'<li>For a range such as <strong>40000-40099</strong>, use your router’s port-range fields — start 40000, end 40099 — rather than one rule per port.</li>' +
|
|
||||||
'</ul>' +
|
|
||||||
'<table class="' + tableClass + '">' +
|
'<table class="' + tableClass + '">' +
|
||||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Used for</th></tr></thead>' +
|
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Used for</th></tr></thead>' +
|
||||||
'<tbody>' + rows + '</tbody>' +
|
'<tbody>' + rows + '</tbody>' +
|
||||||
'</table>' +
|
'</table>';
|
||||||
'<p class="' + noteClass + '">' +
|
|
||||||
'📱 <strong>How to confirm it worked:</strong> forwarding happens on your router, so it can only be verified from outside your network. ' +
|
|
||||||
'Turn Wi-Fi off on your phone and open the service over mobile data — if it loads, your ports are open.' +
|
|
||||||
'</p>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(seconds) {
|
function formatDuration(seconds) {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const $modalLog = document.getElementById("modal-log");
|
|||||||
const $btnReboot = document.getElementById("btn-reboot");
|
const $btnReboot = document.getElementById("btn-reboot");
|
||||||
const $btnSave = document.getElementById("btn-save-report");
|
const $btnSave = document.getElementById("btn-save-report");
|
||||||
const $btnRetryUpdate = document.getElementById("btn-retry-update-status");
|
const $btnRetryUpdate = document.getElementById("btn-retry-update-status");
|
||||||
|
const $btnRetryRun = document.getElementById("btn-retry-update");
|
||||||
const $btnCloseModal = document.getElementById("btn-close-modal");
|
const $btnCloseModal = document.getElementById("btn-close-modal");
|
||||||
|
|
||||||
const $rebootOverlay = document.getElementById("reboot-overlay");
|
const $rebootOverlay = document.getElementById("reboot-overlay");
|
||||||
|
|||||||
@@ -275,7 +275,13 @@ async function checkUpdates() {
|
|||||||
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
||||||
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
||||||
if (sidebarUpdateBtn) {
|
if (sidebarUpdateBtn) {
|
||||||
if (updateStatus === "reboot_required") {
|
if (updateStatus === "failed") {
|
||||||
|
// Last update errored and did not apply — surface it as a persistent
|
||||||
|
// red banner that re-opens the failed run with a "Retry Update" action.
|
||||||
|
sidebarUpdateBtn.style.borderColor = "#e01b24";
|
||||||
|
sidebarUpdateBtn.style.backgroundColor = "rgba(224, 27, 36, 0.10)";
|
||||||
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry";
|
||||||
|
} else if (updateStatus === "reboot_required") {
|
||||||
sidebarUpdateBtn.style.borderColor = "#e5a50a";
|
sidebarUpdateBtn.style.borderColor = "#e5a50a";
|
||||||
sidebarUpdateBtn.style.backgroundColor = "rgba(229, 165, 10, 0.10)";
|
sidebarUpdateBtn.style.backgroundColor = "rgba(229, 165, 10, 0.10)";
|
||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required";
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required";
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ async function openUpdateModal() {
|
|||||||
{ cache: "no-store" },
|
{ cache: "no-store" },
|
||||||
STATUS_POLL_FETCH_TIMEOUT
|
STATUS_POLL_FETCH_TIMEOUT
|
||||||
);
|
);
|
||||||
if (current.running || current.result === "reboot_required") {
|
if (current.running || current.result === "reboot_required" || current.result === "failed") {
|
||||||
|
// An in-progress update, a staged update awaiting reboot, or a prior
|
||||||
|
// failed update — reattach to the persisted systemd/log state instead of
|
||||||
|
// starting over or wrongly reporting "up to date".
|
||||||
showExistingUpdate(current);
|
showExistingUpdate(current);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,7 @@ async function openUpdateModal() {
|
|||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
if ($btnSave) $btnSave.style.display = "none";
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
||||||
|
if ($btnRetryRun) $btnRetryRun.style.display = "none";
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
||||||
$modal.classList.add("open");
|
$modal.classList.add("open");
|
||||||
return;
|
return;
|
||||||
@@ -69,6 +73,7 @@ function prepareUpdateModal() {
|
|||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
if ($btnSave) $btnSave.style.display = "none";
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
||||||
|
if ($btnRetryRun) $btnRetryRun.style.display = "none";
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = true;
|
if ($btnCloseModal) $btnCloseModal.disabled = true;
|
||||||
$modal.classList.add("open");
|
$modal.classList.add("open");
|
||||||
}
|
}
|
||||||
@@ -154,6 +159,7 @@ function startUpdate() {
|
|||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
if ($btnSave) $btnSave.style.display = "none";
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
||||||
|
if ($btnRetryRun) $btnRetryRun.style.display = "none";
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
||||||
_updateFinished = true;
|
_updateFinished = true;
|
||||||
return;
|
return;
|
||||||
@@ -283,6 +289,16 @@ function retryUpdateStatus() {
|
|||||||
startUpdatePoll();
|
startUpdatePoll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-run a failed (or never-applied) update from scratch. The backend always
|
||||||
|
// allows this after a FAILED attempt even though flake.lock may already be
|
||||||
|
// advanced (the previous build never staged a bootable generation).
|
||||||
|
function retryUpdateRun() {
|
||||||
|
if ($btnRetryRun) $btnRetryRun.style.display = "none";
|
||||||
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
|
_doOpenUpdateModal();
|
||||||
|
}
|
||||||
|
|
||||||
function resumeUpdateStatusAfterInterruption() {
|
function resumeUpdateStatusAfterInterruption() {
|
||||||
if (!$modal || !$modal.classList.contains("open")) return;
|
if (!$modal || !$modal.classList.contains("open")) return;
|
||||||
if (_updateStatusUnavailable) {
|
if (_updateStatusUnavailable) {
|
||||||
@@ -304,9 +320,10 @@ function onUpdateDone(result) {
|
|||||||
if ($modalStatus) $modalStatus.textContent = "✓ Update complete — restart required";
|
if ($modalStatus) $modalStatus.textContent = "✓ Update complete — restart required";
|
||||||
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
||||||
} else {
|
} else {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✗ Update failed";
|
if ($modalStatus) $modalStatus.textContent = "✗ Update failed — your system was not changed. Run the update again or save the error report for support.";
|
||||||
|
if ($btnRetryRun) $btnRetryRun.style.display = "inline-flex";
|
||||||
if ($btnSave) $btnSave.style.display = "inline-flex";
|
if ($btnSave) $btnSave.style.display = "inline-flex";
|
||||||
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button>
|
<button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button>
|
||||||
<button class="btn btn-save" id="btn-retry-update-status" style="display:none">Retry Status</button>
|
<button class="btn btn-save" id="btn-retry-update-status" style="display:none">Retry Status</button>
|
||||||
|
<button class="btn btn-reboot" id="btn-retry-update" style="display:none">Retry Update</button>
|
||||||
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
|
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
|
||||||
<button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button>
|
<button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -147,7 +147,7 @@
|
|||||||
hunspell hunspellDicts.en_US
|
hunspell hunspellDicts.en_US
|
||||||
synadm brave-origin dua
|
synadm brave-origin dua
|
||||||
gparted pv unzip parted screen zenity
|
gparted pv unzip parted screen zenity
|
||||||
libargon2 gnome-terminal libreoffice-fresh
|
libargon2 gnome-terminal libreoffice-stable
|
||||||
dig firefox wp-cli axel
|
dig firefox wp-cli axel
|
||||||
lk-jwt-service livekit-libwebrtc livekit
|
lk-jwt-service livekit-libwebrtc livekit
|
||||||
matrix-synapse age onlyoffice-desktopeditors
|
matrix-synapse age onlyoffice-desktopeditors
|
||||||
|
|||||||
Generated
+15
-15
@@ -5,11 +5,11 @@
|
|||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1787862680,
|
"lastModified": 1788527183,
|
||||||
"narHash": "sha256-mv+W62cI1Bjtkz9k8N8M7+7VSauv0/WmBrDYjcy5sE0=",
|
"narHash": "sha256-Jvuh4VWR8Ep+UlX1siyiMWJAn9fPNZKN5IxzVOnGAec=",
|
||||||
"owner": "emmanuelrosa",
|
"owner": "emmanuelrosa",
|
||||||
"repo": "btc-clients-nix",
|
"repo": "btc-clients-nix",
|
||||||
"rev": "f00585b12e751ac738392e2cccfbe305d077e2d4",
|
"rev": "761b147fd7038b210251b16cb2b4e3fe5e9b603b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -41,11 +41,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785590095,
|
"lastModified": 1788179970,
|
||||||
"narHash": "sha256-CNO2szJbdLjVN/Hi1BML9MSALz1GM2fIdwnzs404QO8=",
|
"narHash": "sha256-r5LmxzIhsu5+oDybatN/HJ8roYOKjb2Apa5xI6v46VU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e568f3b19d54b08f48bfae9b12b3e124d1a28002",
|
"rev": "1db62ab7d2ccf1916bbf7deb61fc9d16f1c4ab49",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788297115,
|
"lastModified": 1788690626,
|
||||||
"narHash": "sha256-Z+vUNbfd2FIKkWOTkcT7RYlh3oFCnig/d2eXD1SWf2E=",
|
"narHash": "sha256-+v4I4LawmRD/mVxO7QIAerRrCkElp3YImzWkkUnvOTg=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a3116115851d68b8952a2a4221cc25a84e56b532",
|
"rev": "c25784012c9982bca5b3e0de87e90bbdac8927d3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -88,11 +88,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788179007,
|
"lastModified": 1788614874,
|
||||||
"narHash": "sha256-hn1oU2rue2SYK8dAr8+WNZWtbsz1S2W5mnHlSEuh3bo=",
|
"narHash": "sha256-7QYjT2vHLuX9Z1pdxHXDKCbh1CR3D/2rywB9Tx0MPRg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "34ab99075ac4f7e40cf037eef32cb1c360bb85e9",
|
"rev": "c043004d1c6985732bcc1cbc5a9c9aecbbb4e0f0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -169,11 +169,11 @@
|
|||||||
"nixpkgs-stable": "nixpkgs-stable_2"
|
"nixpkgs-stable": "nixpkgs-stable_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788378694,
|
"lastModified": 1788796120,
|
||||||
"narHash": "sha256-s2yvGC1IHrQq2jP4KSbV2TA9Gh1T/YkcS+9neS/WcVI=",
|
"narHash": "sha256-SLxKMbMPf0MN4tVLAURr6p5mNsieXH8dzmMZisqljmM=",
|
||||||
"owner": "naturallaw777",
|
"owner": "naturallaw777",
|
||||||
"repo": "Sovran_Bitcoin",
|
"repo": "Sovran_Bitcoin",
|
||||||
"rev": "b4678fcc712da9dd01c167f62275192183490085",
|
"rev": "17cc04f5229563cb06f7e1843ccb8994af1df7f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
|
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
|
||||||
./iso/common.nix
|
./iso/common.nix
|
||||||
sovran-bitcoin.nixosModules.default
|
sovran-bitcoin.nixosModules.default
|
||||||
./modules/sovran-bitcoin-integration.nix
|
|
||||||
nixvim.nixosModules.nixvim
|
nixvim.nixosModules.nixvim
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
+79
-18
@@ -150,6 +150,61 @@ let
|
|||||||
"haven-relay.service" = if pkgs ? haven-relay then pkgs.haven-relay.version else (if pkgs ? haven then pkgs.haven.version else "0.1.0");
|
"haven-relay.service" = if pkgs ? haven-relay then pkgs.haven-relay.version else (if pkgs ? haven then pkgs.haven.version else "0.1.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
# Shared shell prelude used by both the update and rebuild wrapper scripts.
|
||||||
|
# A flake/package fetch that is interrupted (network blip, reboot
|
||||||
|
# mid-download, disk filled, hiccup on the remote) can leave a truncated
|
||||||
|
# tarball or partial git clone in Nix's download caches. Nix then reuses the
|
||||||
|
# corrupt archive on every retry and dies with "cannot read file from
|
||||||
|
# tarball: Truncated tar archive detected" — a failure that is NOT fixed by
|
||||||
|
# simply re-running, but IS fixed by clearing the fetch caches. run_step runs
|
||||||
|
# a command and, on the first failure that matches a download/cache
|
||||||
|
# signature, clears the caches and retries once. Real config errors never
|
||||||
|
# match, so they still fail loudly. Each sourcing script must define $LOG.
|
||||||
|
nix-self-heal-prelude = ''
|
||||||
|
transient_failure() {
|
||||||
|
grep -Eqi 'truncated tar|unexpected end of (file|archive)|unexpected eof|corrupt(ed)? (archive|nar|download|file)|could not (fetch|download)|download.*(failed|interrupted)|timed out|timeout|connection (reset|refused|timed out)|network is unreachable|temporary failure in name resolution|checksum mismatch|hash mismatch|nar hash|unable to download|store path.*is not valid|cannot read file from tarball|into the git cache' "$LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_fetch_caches() {
|
||||||
|
echo "[SELF-HEAL] Clearing stale Nix download caches and verifying the Nix store…"
|
||||||
|
# Re-fetchable caches only; /nix/store generations and the running system
|
||||||
|
# are never touched here.
|
||||||
|
rm -rf /root/.cache/nix/tarballs /root/.cache/nix/vcs-cache /root/.cache/nix/git* /root/.cache/nix/flakes 2>/dev/null || true
|
||||||
|
# Fast closure-level repair only. A full --check-contents scan hashes
|
||||||
|
# every store path and can take tens of minutes on a big node; the cache
|
||||||
|
# clear above is the actual fix for truncated/corrupt downloads.
|
||||||
|
nix-store --verify --repair >/dev/null 2>&1 || true
|
||||||
|
echo "[SELF-HEAL] Caches cleared; retrying…"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_step LABEL CMD [ARGS...] — run a build step; on a transient
|
||||||
|
# fetch/cache failure, heal once and retry. Returns the command exit code
|
||||||
|
# but leaves error messaging to the caller.
|
||||||
|
run_step() {
|
||||||
|
label="$1"; shift
|
||||||
|
rc=1
|
||||||
|
for try in 1 2; do
|
||||||
|
if [ "$try" -eq 2 ]; then
|
||||||
|
echo "── $label — retry after cache repair ──"
|
||||||
|
fi
|
||||||
|
"$@"
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$try" -eq 1 ] && transient_failure; then
|
||||||
|
echo ""
|
||||||
|
echo "[SELF-HEAL] $label failed on a download/cache error (see above)."
|
||||||
|
clear_fetch_caches
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
return "$rc"
|
||||||
|
done
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
# ── Update wrapper script ──────────────────────────────────────
|
# ── Update wrapper script ──────────────────────────────────────
|
||||||
update-script = pkgs.writeShellScript "sovran-hub-update.sh" ''
|
update-script = pkgs.writeShellScript "sovran-hub-update.sh" ''
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
@@ -171,12 +226,14 @@ let
|
|||||||
|
|
||||||
RC=0
|
RC=0
|
||||||
|
|
||||||
|
${nix-self-heal-prelude}
|
||||||
|
|
||||||
echo "── Step 1/3: nix flake update ────────────────────"
|
echo "── Step 1/3: nix flake update ────────────────────"
|
||||||
if ! nix flake update --flake /etc/nixos --print-build-logs \
|
if ! run_step "nix flake update" nix flake update --flake /etc/nixos --print-build-logs \
|
||||||
--option connect-timeout 10 \
|
--option connect-timeout 10 \
|
||||||
--option stalled-download-timeout 90 \
|
--option stalled-download-timeout 90 \
|
||||||
--option download-attempts 7 \
|
--option download-attempts 7 \
|
||||||
--option fallback true 2>&1; then
|
--option fallback true; then
|
||||||
echo "[ERROR] nix flake update failed"
|
echo "[ERROR] nix flake update failed"
|
||||||
RC=1
|
RC=1
|
||||||
fi
|
fi
|
||||||
@@ -186,21 +243,21 @@ let
|
|||||||
echo "── Step 2/3: nixos-rebuild boot (stage next reboot) ──"
|
echo "── Step 2/3: nixos-rebuild boot (stage next reboot) ──"
|
||||||
# Stream output straight into $LOG (see rebuild-script) so the Hub UI
|
# Stream output straight into $LOG (see rebuild-script) so the Hub UI
|
||||||
# shows live progress instead of an empty log during long builds.
|
# shows live progress instead of an empty log during long builds.
|
||||||
nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
if run_step "nixos-rebuild boot" nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
--option connect-timeout 10 \
|
--option connect-timeout 10 \
|
||||||
--option stalled-download-timeout 90 \
|
--option stalled-download-timeout 90 \
|
||||||
--option download-attempts 7 \
|
--option download-attempts 7 \
|
||||||
--option fallback true
|
--option fallback true; then
|
||||||
BOOT_RC=$?
|
if ! readlink -f /nix/var/nix/profiles/system > "$GENERATION"; then
|
||||||
if [ "$BOOT_RC" -ne 0 ]; then
|
# The marker is informational only. The Hub derives pending-reboot
|
||||||
|
# state from the NixOS system profile itself, so failing to record
|
||||||
|
# the marker must not fail an otherwise successful update.
|
||||||
|
echo "[WARNING] update succeeded but its staged generation could not be recorded"
|
||||||
|
rm -f "$GENERATION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
echo "[ERROR] nixos-rebuild boot failed"
|
echo "[ERROR] nixos-rebuild boot failed"
|
||||||
RC=1
|
RC=1
|
||||||
elif ! readlink -f /nix/var/nix/profiles/system > "$GENERATION"; then
|
|
||||||
# The marker is informational only. The Hub derives pending-reboot
|
|
||||||
# state from the NixOS system profile itself, so failing to record
|
|
||||||
# the marker must not fail an otherwise successful update.
|
|
||||||
echo "[WARNING] update succeeded but its staged generation could not be recorded"
|
|
||||||
rm -f "$GENERATION"
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
@@ -245,12 +302,15 @@ let
|
|||||||
echo " Sovran_SystemsOS Rebuild — $(date)"
|
echo " Sovran_SystemsOS Rebuild — $(date)"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
${nix-self-heal-prelude}
|
||||||
|
|
||||||
echo "── Rebuilding system configuration ──────────────"
|
echo "── Rebuilding system configuration ──────────────"
|
||||||
# Stream output straight into $LOG (tee'd by the exec redirect above) so
|
# Stream output straight into $LOG (tee'd by the exec redirect above) so
|
||||||
# the Hub UI shows live progress. Capturing the output in a variable
|
# the Hub UI shows live progress. Capturing the output in a variable
|
||||||
# kept the log empty for the entire build+activation, which made long
|
# kept the log empty for the entire build+activation, which made long
|
||||||
# rebuilds can otherwise look like a hang.
|
# rebuilds can otherwise look like a hang.
|
||||||
nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
run_step "nixos-rebuild switch" nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
||||||
--option connect-timeout 10 \
|
--option connect-timeout 10 \
|
||||||
--option stalled-download-timeout 90 \
|
--option stalled-download-timeout 90 \
|
||||||
--option download-attempts 7 \
|
--option download-attempts 7 \
|
||||||
@@ -266,11 +326,11 @@ let
|
|||||||
echo ""
|
echo ""
|
||||||
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
|
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
|
||||||
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
||||||
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
if run_step "nixos-rebuild boot" nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
--option connect-timeout 10 \
|
--option connect-timeout 10 \
|
||||||
--option stalled-download-timeout 90 \
|
--option stalled-download-timeout 90 \
|
||||||
--option download-attempts 7 \
|
--option download-attempts 7 \
|
||||||
--option fallback true 2>&1; then
|
--option fallback true; then
|
||||||
echo "REBOOT_REQUIRED" > "$STATUS"
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
||||||
else
|
else
|
||||||
echo "[ERROR] nixos-rebuild boot also failed"
|
echo "[ERROR] nixos-rebuild boot also failed"
|
||||||
@@ -278,6 +338,7 @@ let
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
echo "[ERROR] nixos-rebuild switch failed"
|
||||||
echo ""
|
echo ""
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✗ Rebuild failed — see errors above"
|
echo " ✗ Rebuild failed — see errors above"
|
||||||
|
|||||||
Reference in New Issue
Block a user