From 4ca2625d627d0620334b7f8f35fe3c06b62c1030 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 3 May 2026 18:34:01 +0100 Subject: [PATCH 1/4] feat(reboot-tracker): add reboot-tracker.mjs (TS-stripped Deno script) --- reboot-tracker.mjs | 131 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 reboot-tracker.mjs diff --git a/reboot-tracker.mjs b/reboot-tracker.mjs new file mode 100644 index 0000000..6ba1e33 --- /dev/null +++ b/reboot-tracker.mjs @@ -0,0 +1,131 @@ +// scripts/reboot-tracker.mjs +import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; +import { parseArgs } from "https://deno.land/std@0.224.0/cli/parse_args.ts"; + +const REASONS = [ + "Planned Maintenance", + "Security Update", + "Hardware Issue", + "OS Update", + "Unexpected Crash", + "Software Bug", + "Migration", + "Other" +]; + +const BASE_DIR = "monitoring/reboot-tracker/logs"; +const LOG_FILE = join(BASE_DIR, "reboot-reasons.json"); +const SNAPSHOT_DIR = join(BASE_DIR, "snapshots"); + +async function captureLogs(timestamp) { + const filename = `snapshot-${timestamp.replace(/[:.]/g, "-")}.log`; + const filepath = join(SNAPSHOT_DIR, filename); + + console.log(`\nCapturing system logs to ${filename}...`); + + try { + await Deno.mkdir(SNAPSHOT_DIR, { recursive: true }); + + // Capture journalctl and dmesg + const journalCmd = new Deno.Command("sudo", { + args: ["journalctl", "-n", "200", "--no-pager"], + }); + const dmesgCmd = new Deno.Command("sudo", { + args: ["dmesg", "-T"], // -T for human readable timestamps + }); + + const journalResult = await journalCmd.output(); + const dmesgResult = await dmesgCmd.output(); + + const decoder = new TextDecoder(); + const logContent = [ + "=== SYSTEM LOG SNAPSHOT ===", + `Captured at: ${timestamp}`, + "", + "--- journalctl (last 200 lines) ---", + decoder.decode(journalResult.stdout), + "", + "--- dmesg (tail) ---", + decoder.decode(dmesgResult.stdout).split("\n").slice(-100).join("\n"), + ].join("\n"); + + await Deno.writeTextFile(filepath, logContent); + return filename; + } catch (err) { + console.error(`Warning: Failed to capture system logs: ${err.message}`); + return null; + } +} + +async function main() { + const args = parseArgs(Deno.args); + const isShutdown = args.shutdown === true; + const actionName = isShutdown ? "SHUTDOWN" : "REBOOT"; + + console.log("--------------------------------------------------"); + console.log(` SYSTEM ${actionName} TRACKER (Server Reason Prompt) `); + console.log("--------------------------------------------------"); + console.log(`\nPlease select a reason for the ${actionName}:`); + REASONS.forEach((reason, i) => { + console.log(` [${i + 1}] ${reason}`); + }); + + let selection = ""; + while (true) { + const input = prompt("\nEnter your choice [1-8]:"); + if (input && Number(input) >= 1 && Number(input) <= REASONS.length) { + selection = REASONS[Number(input) - 1]; + break; + } + console.log("Invalid selection. Please try again."); + } + + const details = prompt("\nProvide details/comments (optional):") || "No details provided."; + + const timestamp = new Date().toISOString(); + const snapshotFile = await captureLogs(timestamp); + + const logEntry = { + timestamp: timestamp, + user: Deno.env.get("USER") || "unknown", + action: actionName, + reason: selection, + details: details, + hostname: Deno.hostname(), + snapshot: snapshotFile, + }; + + try { + let logs = []; + try { + const content = await Deno.readTextFile(LOG_FILE); + logs = JSON.parse(content); + } catch { + // File doesn't exist or is empty + } + + logs.push(logEntry); + await Deno.writeTextFile(LOG_FILE, JSON.stringify(logs, null, 2)); + console.log(`\nReason and log snapshot recorded in ${BASE_DIR}`); + } catch (err) { + console.error(`Error logging reason: ${err.message}`); + const retry = prompt(`\nContinue with ${actionName} anyway? (y/N):`); + if (retry?.toLowerCase() !== 'y') { + console.log("Aborted."); + Deno.exit(1); + } + } + + const confirm = prompt(`\nAre you sure you want to ${actionName} now? (y/N):`); + if (confirm?.toLowerCase() === 'y') { + console.log(`Initiating ${actionName}...`); + const cmd = new Deno.Command("sudo", { + args: [isShutdown ? "shutdown" : "reboot"], + }); + await cmd.spawn(); + } else { + console.log(`${actionName} cancelled. Reason and logs have been recorded.`); + } +} + +main(); From 50586fd056542a185a7365d68137382d331f11af Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 3 May 2026 18:34:02 +0100 Subject: [PATCH 2/4] chore(reboot.sh): point at reboot-tracker.mjs --- reboot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reboot.sh b/reboot.sh index 75de9dc..61cadfa 100755 --- a/reboot.sh +++ b/reboot.sh @@ -5,7 +5,7 @@ # alias shutdown='bash /var$REPOS_DIR/scripts/reboot.sh --shutdown' DENO_BIN="/home/hyper/.deno/bin/deno" -TRACKER_TS="/var$REPOS_DIR/scripts/reboot-tracker.ts" +TRACKER_TS="/var$REPOS_DIR/scripts/reboot-tracker.mjs" # Parse args IS_SHUTDOWN=false From a48b65bafd758c97dd8ca73a6887605063ba9713 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 3 May 2026 18:34:03 +0100 Subject: [PATCH 3/4] =?UTF-8?q?docs(readme):=20update=20reboot-tracker.ts?= =?UTF-8?q?=20=E2=86=92=20.mjs=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 5c30ba7..8743242 100644 --- a/README.adoc +++ b/README.adoc @@ -100,7 +100,7 @@ This repository contains scripts for: |`check-changes.sh` |Git status checker for CI/CD pipelines -|`reboot-tracker.ts` +|`reboot-tracker.mjs` |Deno script to track system reboot reasons with journalctl/dmesg capture |=== From 154304205f3caa77baac40c36c166b60574910ac Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 3 May 2026 18:34:04 +0100 Subject: [PATCH 4/4] chore: remove reboot-tracker.ts (replaced by .mjs) --- reboot-tracker.ts | 131 ---------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100755 reboot-tracker.ts diff --git a/reboot-tracker.ts b/reboot-tracker.ts deleted file mode 100755 index d300af5..0000000 --- a/reboot-tracker.ts +++ /dev/null @@ -1,131 +0,0 @@ -// scripts/reboot-tracker.ts -import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; -import { parseArgs } from "https://deno.land/std@0.224.0/cli/parse_args.ts"; - -const REASONS = [ - "Planned Maintenance", - "Security Update", - "Hardware Issue", - "OS Update", - "Unexpected Crash", - "Software Bug", - "Migration", - "Other" -]; - -const BASE_DIR = "monitoring/reboot-tracker/logs"; -const LOG_FILE = join(BASE_DIR, "reboot-reasons.json"); -const SNAPSHOT_DIR = join(BASE_DIR, "snapshots"); - -async function captureLogs(timestamp: string): Promise { - const filename = `snapshot-${timestamp.replace(/[:.]/g, "-")}.log`; - const filepath = join(SNAPSHOT_DIR, filename); - - console.log(`\nCapturing system logs to ${filename}...`); - - try { - await Deno.mkdir(SNAPSHOT_DIR, { recursive: true }); - - // Capture journalctl and dmesg - const journalCmd = new Deno.Command("sudo", { - args: ["journalctl", "-n", "200", "--no-pager"], - }); - const dmesgCmd = new Deno.Command("sudo", { - args: ["dmesg", "-T"], // -T for human readable timestamps - }); - - const journalResult = await journalCmd.output(); - const dmesgResult = await dmesgCmd.output(); - - const decoder = new TextDecoder(); - const logContent = [ - "=== SYSTEM LOG SNAPSHOT ===", - `Captured at: ${timestamp}`, - "", - "--- journalctl (last 200 lines) ---", - decoder.decode(journalResult.stdout), - "", - "--- dmesg (tail) ---", - decoder.decode(dmesgResult.stdout).split("\n").slice(-100).join("\n"), - ].join("\n"); - - await Deno.writeTextFile(filepath, logContent); - return filename; - } catch (err) { - console.error(`Warning: Failed to capture system logs: ${err.message}`); - return null; - } -} - -async function main() { - const args = parseArgs(Deno.args); - const isShutdown = args.shutdown === true; - const actionName = isShutdown ? "SHUTDOWN" : "REBOOT"; - - console.log("--------------------------------------------------"); - console.log(` SYSTEM ${actionName} TRACKER (Server Reason Prompt) `); - console.log("--------------------------------------------------"); - console.log(`\nPlease select a reason for the ${actionName}:`); - REASONS.forEach((reason, i) => { - console.log(` [${i + 1}] ${reason}`); - }); - - let selection = ""; - while (true) { - const input = prompt("\nEnter your choice [1-8]:"); - if (input && Number(input) >= 1 && Number(input) <= REASONS.length) { - selection = REASONS[Number(input) - 1]; - break; - } - console.log("Invalid selection. Please try again."); - } - - const details = prompt("\nProvide details/comments (optional):") || "No details provided."; - - const timestamp = new Date().toISOString(); - const snapshotFile = await captureLogs(timestamp); - - const logEntry = { - timestamp: timestamp, - user: Deno.env.get("USER") || "unknown", - action: actionName, - reason: selection, - details: details, - hostname: Deno.hostname(), - snapshot: snapshotFile, - }; - - try { - let logs = []; - try { - const content = await Deno.readTextFile(LOG_FILE); - logs = JSON.parse(content); - } catch { - // File doesn't exist or is empty - } - - logs.push(logEntry); - await Deno.writeTextFile(LOG_FILE, JSON.stringify(logs, null, 2)); - console.log(`\nReason and log snapshot recorded in ${BASE_DIR}`); - } catch (err) { - console.error(`Error logging reason: ${err.message}`); - const retry = prompt(`\nContinue with ${actionName} anyway? (y/N):`); - if (retry?.toLowerCase() !== 'y') { - console.log("Aborted."); - Deno.exit(1); - } - } - - const confirm = prompt(`\nAre you sure you want to ${actionName} now? (y/N):`); - if (confirm?.toLowerCase() === 'y') { - console.log(`Initiating ${actionName}...`); - const cmd = new Deno.Command("sudo", { - args: [isShutdown ? "shutdown" : "reboot"], - }); - await cmd.spawn(); - } else { - console.log(`${actionName} cancelled. Reason and logs have been recorded.`); - } -} - -main();