plex32-scripts/KingDezigns-Summary-2026-06-21.html
2026-07-26 21:35:57 -04:00

498 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KingDezigns — PLEX32 Timezone Fix & Remote Reboot Script Summary</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f7f6f2;
--surface: #ffffff;
--surface-alt: #f1f0eb;
--border: #e2e0d8;
--border-strong: #c8c5ba;
--text-primary: #1a1917;
--text-secondary: #6b6960;
--text-muted: #9b9890;
--green-bg: #eaf3de;
--green-border: #97c459;
--green-text: #2d5a0e;
--red-bg: #fcebeb;
--red-border: #f09595;
--red-text: #7a1f1f;
--amber-bg: #faeeda;
--amber-border: #fac775;
--amber-text: #633806;
--coral-bg: #faece7;
--coral-border: #f0997b;
--coral-text: #4a1b0c;
--mono: 'IBM Plex Mono', monospace;
--sans: 'IBM Plex Sans', sans-serif;
}
body {
font-family: var(--sans);
font-size: 14px;
background: var(--bg);
color: var(--text-primary);
line-height: 1.7;
padding: 2.5rem 1.5rem;
}
.page { max-width: 860px; margin: 0 auto; }
header {
border-bottom: 1px solid var(--border-strong);
padding-bottom: 1.25rem;
margin-bottom: 2rem;
}
header h1 {
font-family: var(--mono);
font-size: 18px;
font-weight: 500;
letter-spacing: -0.02em;
margin-bottom: 6px;
}
.meta {
font-size: 12px;
color: var(--text-muted);
font-family: var(--mono);
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.section { margin-bottom: 2.25rem; }
.section-title {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 10px;
}
.section-title::before {
content: attr(data-num);
background: var(--text-primary);
color: var(--bg);
font-size: 10px;
width: 18px;
height: 18px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.section-title::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
p { margin-bottom: 0.75rem; color: var(--text-primary); }
p:last-child { margin-bottom: 0; }
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 1rem; }
.badge {
font-family: var(--mono);
font-size: 11px;
padding: 3px 10px;
border-radius: 3px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
}
.badge.red { background: var(--red-bg); color: var(--red-text); border-color: var(--red-border); }
.badge.green { background: var(--green-bg); color: var(--green-text); border-color: var(--green-border); }
.badge.amber { background: var(--amber-bg); color: var(--amber-text); border-color: var(--amber-border); }
.step {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.25rem;
margin-bottom: 0.6rem;
}
.step-header {
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.6rem;
display: flex;
align-items: center;
gap: 8px;
}
.step-num {
background: var(--surface-alt);
border: 1px solid var(--border);
border-radius: 50%;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
flex-shrink: 0;
color: var(--text-muted);
}
pre {
background: var(--surface-alt);
border: 1px solid var(--border);
border-radius: 4px;
padding: 10px 12px;
font-family: var(--mono);
font-size: 12px;
overflow-x: auto;
margin: 0.5rem 0;
white-space: pre;
line-height: 1.6;
}
code {
font-family: var(--mono);
font-size: 12px;
background: var(--surface-alt);
padding: 1px 5px;
border-radius: 3px;
border: 1px solid var(--border);
}
.why {
font-size: 12px;
color: var(--text-muted);
margin-top: 0.4rem;
padding-left: 2px;
}
.why::before { content: '↳ why: '; font-family: var(--mono); }
.result { font-size: 12px; margin-top: 0.35rem; padding-left: 2px; }
.result.pass { color: var(--green-text); }
.result.pass::before { content: '✓ '; font-family: var(--mono); }
.result.fail { color: var(--red-text); }
.result.fail::before { content: '✗ '; font-family: var(--mono); }
.result.neutral { color: var(--text-secondary); }
.result.neutral::before { content: '→ '; font-family: var(--mono); }
.callout { border-radius: 6px; padding: 1rem 1.25rem; margin-bottom: 0.75rem; }
.callout.coral { background: var(--coral-bg); border: 1px solid var(--coral-border); }
.callout.coral p { color: var(--coral-text); }
.callout.coral pre { background: #fdf0eb; border-color: var(--coral-border); }
.callout.amber { background: var(--amber-bg); border: 1px solid var(--amber-border); }
.callout.amber p { color: var(--amber-text); }
.callout.amber pre { background: #fdf5e6; border-color: var(--amber-border); }
.callout.amber code { background: #fdf5e6; border-color: var(--amber-border); color: var(--amber-text); }
.callout.green { background: var(--green-bg); border: 1px solid var(--green-border); }
.callout.green p { color: var(--green-text); }
.callout.green pre { background: #d8efc0; border-color: var(--green-border); }
.clue-label {
font-family: var(--mono);
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--amber-text);
margin-bottom: 8px;
}
.script-box { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.script-row { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); }
.script-row:last-child { border-bottom: none; }
.script-label { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); margin-bottom: 6px; }
.script-row pre { margin: 0; }
table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
th { text-align: left; padding: 8px 14px; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); background: var(--surface-alt); border-bottom: 1px solid var(--border); font-weight: 500; }
td { padding: 9px 14px; border-bottom: 1px solid var(--border); vertical-align: top; line-height: 1.5; }
tr:last-child td { border-bottom: none; }
.footer {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
margin-top: 2.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
</style>
</head>
<body>
<div class="page">
<header>
<h1>KingDezigns — PLEX32 Timezone Fix &amp; Remote Reboot Script summary</h1>
<div class="meta">
<span>📅 2026-06-21</span>
<span>🖥 PLEX32 / NAS08</span>
<span>⚙️ Debian / OpenMediaVault</span>
<span>🐳 Docker Compose</span>
<span>☁️ Plex Media Server</span>
</div>
</header>
<!-- SECTION 1 — THE PROBLEM -->
<div class="section">
<div class="section-title" data-num="1">The problem</div>
<p>The PLEX32 backup script ran at 10:00 PM instead of the scheduled 2:00 AM, indicating a scheduling problem. A separate gap was also identified: there was no way to remotely reboot PLEX32 when away from home if it became unresponsive.</p>
<div class="badge-row">
<span class="badge red">Backup ran at 10 PM instead of 2 AM</span>
<span class="badge amber">No remote reboot capability for PLEX32</span>
</div>
<p>The crontab entry on PLEX32 was correct (<code>0 2 */3 * *</code>), but the system timezone was set to <code>Etc/UTC</code> instead of <code>America/New_York</code>, causing all cron jobs to fire 4 hours early during EDT (UTC-4).</p>
</div>
<!-- SECTION 2 — DIAGNOSTIC COMMANDS -->
<div class="section">
<div class="section-title" data-num="2">Diagnostic commands run &amp; why</div>
<div class="step">
<div class="step-header"><span class="step-num">1</span> Checked backup log timestamps</div>
<pre>ls -lt /var/log/plex32-backup/</pre>
<p class="why">Confirm the 10 PM run actually happened and check if it was recurring.</p>
<p class="result neutral">Showed Jun 19 ran at 23:17, Jun 20 at 02:03, Jun 22 at 02:09 — pattern inconsistent, not a simple crontab typo.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">2</span> Verified crontab entry on PLEX32</div>
<pre>sudo crontab -l</pre>
<p class="why">Rule out a bad cron expression as the cause.</p>
<p class="result pass">Crontab was correct: <code>0 2 */3 * *</code> — not the problem.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">3</span> Checked system timezone</div>
<pre>timedatectl</pre>
<p class="why">A 4-hour offset is the classic symptom of UTC vs EDT mismatch.</p>
<p class="result fail">Time zone: Etc/UTC — confirmed root cause. Cron was firing at 2 AM UTC = 10 PM EDT.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">4</span> Checked timezones on all other VLAN 50 servers</div>
<pre>ssh rufusking@192.168.150.35 "timedatectl | grep 'Time zone'" # NAS08
ssh rufusking@192.168.150.40 "timedatectl | grep 'Time zone'" # NAS16
ssh rufusking@192.168.150.30 "timedatectl | grep 'Time zone'" # HAS</pre>
<p class="why">If PLEX32 had the wrong timezone, other servers might too.</p>
<p class="result pass">All other servers were already on America/New_York — PLEX32 was the only one affected.</p>
</div>
</div>
<!-- SECTION 3 — ROOT CAUSE (TIMEZONE) -->
<div class="section">
<div class="section-title" data-num="3">Root cause — cron firing at wrong time</div>
<div class="callout coral">
<p>PLEX32's system timezone was set to <code>Etc/UTC</code> (the Debian default). All cron jobs fired at the correct UTC time, which is 4 hours ahead of Eastern Daylight Time, causing the 2 AM backup to run at 10 PM EDT.</p>
</div>
<div class="callout amber">
<div class="clue-label">⚠ Key clue</div>
<p style="margin-bottom:6px;">The backup log on Jun 19 showed 23:17 and Jun 20 showed 02:03 — the jump from late-night to early-morning suggested the timezone was corrected mid-cycle, but <code>timedatectl</code> showed it was still UTC. The log timestamps themselves are in UTC, so both runs were actually at 2 AM UTC.</p>
<pre>-rw-r--r-- 1 root root 1559 Jun 22 02:09 backup-20260622.log
-rw-rw-r-- 1 rufusking rufusking 3120 Jun 20 02:03 backup-20260620.log
-rw-rw-r-- 1 rufusking rufusking 5879 Jun 19 23:17 backup-20260619.log</pre>
<p style="margin-top:8px;">The Jun 19 timestamp difference was due to the backup running at a slightly different interval that cycle — not a timezone change.</p>
</div>
<div class="callout green" style="margin-top:0.75rem;">
<p><strong>Resolution:</strong> Set the timezone to <code>America/New_York</code> with <code>sudo timedatectl set-timezone America/New_York</code>. No crontab changes needed — cron reads system time automatically.</p>
</div>
</div>
<!-- SECTION 4 — REMOTE REBOOT SCRIPT -->
<div class="section">
<div class="section-title" data-num="4">New capability — PLEX32 remote reboot script</div>
<p>A remote reboot script was created on NAS08 to provide a way to reboot PLEX32 from outside the home network via OMV's Scheduled Jobs interface. The script was iteratively debugged through several test runs.</p>
<div class="badge-row" style="margin-top:0.75rem;">
<span class="badge red"><code>nc</code> not installed on NAS08 — port check always failed</span>
<span class="badge amber">docker compose down clears restart policy — containers didn't auto-start</span>
<span class="badge amber">flat <code>sleep 60</code> caused unnecessary delay in Plex detection</span>
</div>
<div class="step">
<div class="step-header"><span class="step-num">1</span> Verified SSH key from NAS08 root → PLEX32 already existed</div>
<pre>sudo ssh-copy-id -i /root/.ssh/id_ed25519.pub rufusking@192.168.150.45
sudo ssh -i /root/.ssh/id_ed25519 rufusking@192.168.150.45 "echo OK"</pre>
<p class="result pass">Key was already trusted — ssh-copy-id skipped it, echo OK confirmed passwordless access.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">2</span> Added reboot to PLEX32 sudoers</div>
<pre>sudo visudo -f /etc/sudoers.d/rufusking-nopasswd
# Appended: /sbin/reboot</pre>
<p class="why">The script runs <code>sudo reboot</code> over SSH — must be passwordless or it hangs.</p>
<p class="result pass">Full sudoers line: apt-get update, fail2ban-client status, fail2ban-client status sshd, /sbin/reboot</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">3</span> First test — containers didn't restart after reboot</div>
<pre>docker ps
# Result: empty — no containers running</pre>
<p class="why">The script ran <code>docker compose down</code> before rebooting, which clears the <code>unless-stopped</code> restart policy. Nothing brought the containers back up.</p>
<p class="result fail">Plex was down after reboot — had to manually run <code>docker compose up -d</code>.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">4</span> Fixed — added docker compose up -d after PLEX32 comes back online</div>
<pre>ssh ... "cd ~/docker/plex && docker compose up -d"</pre>
<p class="result pass">Containers started cleanly after subsequent reboots.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">5</span> Port check always reported failure despite Plex being up</div>
<pre>nc -z -w 5 192.168.150.45 32400; echo "nc exit: $?"
# Result: nc exit: 127 (command not found)</pre>
<p class="why"><code>nc</code> (netcat) is not installed on NAS08 — the check always returned failure regardless of Plex state.</p>
<p class="result fail">Switched to <code>curl</code> HTTP check — confirmed Plex returns HTTP 302 on port 32400.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">6</span> Replaced flat sleep 60 with polling loop</div>
<pre>while [ $PLEX_ELAPSED -lt 90 ]; do
HTTP_CODE=$(curl ... http://192.168.150.45:32400/web)
[ "$HTTP_CODE" = "302" ] && break
sleep 5; PLEX_ELAPSED=$((PLEX_ELAPSED + 5))
done</pre>
<p class="why">Plex was up within ~10s of containers starting but the script was waiting a full 60s before checking.</p>
<p class="result pass">Plex confirmed responding after 10s on final test run.</p>
</div>
</div>
<!-- SECTION 5 — SCRIPT -->
<div class="section">
<div class="section-title" data-num="5">Script created</div>
<div class="script-box">
<div class="script-row">
<div class="script-label">Location (NAS08)</div>
<pre>/usr/scripts/nas/plex32_reboot.sh</pre>
</div>
<div class="script-row">
<div class="script-label">Log directory</div>
<pre>/var/log/plex32-reboot/reboot-YYYYMMDD-HHMMSS.log (90 day retention)</pre>
</div>
<div class="script-row">
<div class="script-label">What it does</div>
<pre>1. SSH to PLEX32 → docker compose down (graceful container stop)
2. SSH to PLEX32 → sudo reboot
3. Wait 30s then poll ping every 10s (max 3 min) until PLEX32 responds
4. SSH to PLEX32 → docker compose up -d
5. Poll curl http://192.168.150.45:32400/web every 5s (max 90s)
6. Log success (HTTP 302/200/301) or warning if timeout</pre>
</div>
<div class="script-row">
<div class="script-label">OMV Scheduled Job</div>
<pre>Tag: PLEX32 Remote Reboot
Command: /usr/scripts/nas/plex32_reboot.sh
Enabled: OFF — enable manually, run once, disable again</pre>
</div>
<div class="script-row">
<div class="script-label">Final test run result</div>
<pre>[23:40:39] PLEX32 Remote Reboot — Initiated
[23:40:45] Docker containers stopped cleanly.
[23:40:45] Reboot command sent. PLEX32 is going down.
[23:41:27] PLEX32 is back online (ping responded after ~40s).
[23:41:30] Docker containers started.
[23:41:40] Plex is responding (HTTP 302) after 10s — reboot successful.</pre>
</div>
</div>
</div>
<!-- SECTION 6 — LESSONS LEARNED -->
<div class="section">
<div class="section-title" data-num="6">Lessons learned</div>
<table>
<thead>
<tr><th>Lesson</th><th>Detail</th></tr>
</thead>
<tbody>
<tr>
<td>Always set timezone on new installs</td>
<td>Debian/Ubuntu default is <code>Etc/UTC</code>. Run <code>sudo timedatectl set-timezone America/New_York</code> immediately after setup. Verify with <code>timedatectl | grep "Time zone"</code>. A 4-hour cron offset is the telltale symptom.</td>
</tr>
<tr>
<td><code>docker compose down</code> clears restart policy</td>
<td>Unlike a system reboot (which respects <code>restart: unless-stopped</code>), an explicit <code>compose down</code> removes containers entirely. Always follow a pre-reboot <code>compose down</code> with a post-boot <code>compose up -d</code>.</td>
</tr>
<tr>
<td><code>nc</code> is not installed on NAS08</td>
<td>Use <code>curl -s -o /dev/null -w "%{http_code}"</code> for HTTP service checks from NAS08. Plex returns HTTP 302 on port 32400 — treat 200, 301, and 302 all as success.</td>
</tr>
<tr>
<td>Flat sleep before health checks wastes time</td>
<td>Replace <code>sleep N</code> before a readiness check with a polling loop. Check every 5 seconds, break on success, cap with a max timeout. Services often come up well before the sleep expires.</td>
</tr>
<tr>
<td>Verify deployed scripts after editing</td>
<td>Use <code>grep -n "key_string" /path/to/script</code> to confirm the correct version is on disk before testing. Copy errors and stale files caused multiple confusing test failures in this session.</td>
</tr>
</tbody>
</table>
</div>
<!-- SECTION 7 — DOCS UPDATED -->
<div class="section">
<div class="section-title" data-num="7">Documentation updated</div>
<table>
<thead>
<tr><th>File</th><th>What changed</th></tr>
</thead>
<tbody>
<tr>
<td><code>server_plex32.md</code></td>
<td>Added timezone section (was UTC, corrected to America/New_York). Updated sudoers entry to include <code>/sbin/reboot</code>. Updated AI summary with timezone note and NAS08→PLEX32 SSH key entry.</td>
</tr>
<tr>
<td><code>server_nas08.md</code></td>
<td>Added <code>plex32_reboot.sh</code> to OMV Scheduled Jobs table (disabled/manual). Added NAS08 root → PLEX32 SSH key note. Updated AI summary with remote reboot script details.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">
<span>KingDezigns Home Network</span>
<span>Generated 2026-06-21</span>
</div>
</div>
</body>
</html>