Restore
Restore
Restore file to captured state
Every captured change carries its SHA-256-addressed content in BLOB_STORE. Restoring writes that content back to the original path — current file gets backed up to <path>.drift_restore_backup_<epoch> before overwrite.
What will change
Preview -- nothing has been written yet.
| Target file | /var/www/vhosts/3dshawn.com/site1/TEMPLATES/servers.html |
| Site | DriftSense self-monitor on local |
| Kind | local |
| Captured at | 2026-07-11 23:15:14 |
| Captured SHA | b85517f9ee2ee4a3b2596cfcdf36c89e45612fa3b3a5037ff9c95224669b111d |
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
| File present | yes |
| Size | 17969 bytes |
| Current SHA | 66b67331501d |
| Same as captured? | no -- restore will change it |
What restore will change — live diff
Left column shows current on-disk content; right shows what restore will write. +0 additions, -8 deletions, 278 unchanged context lines.
| 1 | 1 | <div class="page-head"> |
| 2 | 2 | <span class="page-eyebrow"><span class="dot"></span> Configure</span> |
| 3 | 3 | <h1 class="page-title">Servers & agents</h1> |
| 4 | 4 | <p class="page-subtitle">The nodes DriftSense knows about — local (this box) and SSH agents (remote servers scanned over a restricted-key SSH connection). Add one at a time or bulk-import a list of hostnames sharing a single key.</p> |
| 5 | </div> | |
| 6 | ||
| 7 | <div class="ds-guide"> | |
| 8 | <div class="dsg-icon">i</div> | |
| 9 | <div> | |
| 10 | <span class="dsg-title">Two flavors:</span> | |
| 11 | <strong>Local</strong> = scanner runs on the same box as the file being watched (fast, zero-network). <strong>SSH agent</strong> = scanner SSHes into the target box using a restricted key; only footprint on that box is one <code>authorized_keys</code> entry. Great for <em>stealth deployment</em> where you don't want developers on the monitored box to see DriftSense installed. Paste a PEM key below to store it <span class="ds-help" data-help-title="Encrypted at rest" data-help-body="PEM keys pasted here are encrypted with an HMAC-SHA-256 CTR construction using a per-install master key at <code>/etc/drift_sense/master.key</code> (mode 0640, group psacln). Neither the DB nor a filesystem snapshot exposes plaintext keys."></span> in the DB (encrypted). Test any row with the "Test" button. | |
| 12 | </div> | |
| 13 | 5 | </div> |
| 14 | 6 | |
| 15 | 7 | [if:$has_imported] |
| 16 | 8 | <div class="banner banner-ok" style="margin:0 0 16px;padding:10px 14px;border-radius:10px;background:rgba(52,211,153,.10);border:1px solid rgba(52,211,153,.35);color:#a7f3d0;font-size:13px"> |
| 17 | 9 | Bulk import created $imported server(s). They'll be scanned on the next agent tick (within 5 minutes). |
| 18 | 10 | </div> |
| 19 | 11 | [/if] |
| 20 | 12 | |
| 21 | 13 | [if:!$crypto_ready] |
| 22 | 14 | <div class="banner banner-warn" style="margin:0 0 16px;padding:10px 14px;border-radius:10px;background:rgba(245,158,11,.10);border:1px solid rgba(245,158,11,.35);color:#fcd34d;font-size:13px"> |
| 23 | 15 | <strong>Master encryption key missing.</strong> Encrypted key storage requires <code>/etc/drift_sense/master.key</code>. Run on the server: <code>sudo perl -e 'use lib "/var/www/vhosts/3dshawn.com/site1"; use MODS::Crypto; MODS::Crypto::ensure_master()'</code>. Until then, encrypted-blob storage is unavailable and only on-disk <code>ssh_key_path</code> works. |
| 24 | 16 | </div> |
| 25 | 17 | [/if] |
| 26 | 18 | |
| 27 | 19 | <!-- ============================ TOP TABS ============================ --> |
| 28 | 20 | <div style="display:flex;gap:8px;margin-bottom:16px"> |
| 29 | 21 | <a class="tr-chip[if:!$bulk_mode] is-active[/if]" href="/servers.cgi">Single-server form</a> |
| 30 | 22 | <a class="tr-chip[if:$bulk_mode] is-active[/if]" href="/servers.cgi?bulk=1">Bulk import</a> |
| 31 | 23 | </div> |
| 32 | 24 | |
| 33 | 25 | <div class="dash-grid" style="grid-template-columns:1fr 1fr;margin-bottom:20px"> |
| 34 | 26 | <!-- ============================ SINGLE-SERVER FORM ============================ --> |
| 35 | 27 | [if:!$bulk_mode] |
| 36 | 28 | <div class="module glow-teal"> |
| 37 | 29 | <div class="module-head"><div class="left"> |
| 38 | 30 | <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/></svg></div> |
| 39 | 31 | <div class="module-title">[if:$is_edit]Edit server #$server_id[/if][if:!$is_edit]Add server[/if]</div> |
| 40 | 32 | <span class="module-subtitle">Pick <code>local</code> for the DriftSense host itself, or <code>ssh_agent</code> for anything remote.</span> |
| 41 | 33 | </div></div> |
| 42 | 34 | <div class="module-body"> |
| 43 | 35 | <form method="post" action="/servers.cgi" style="display:flex;flex-direction:column;gap:12px"> |
| 44 | 36 | <input type="hidden" name="op" value="$form_op"> |
| 45 | 37 | [if:$is_edit]<input type="hidden" name="server_id" value="$server_id">[/if] |
| 46 | 38 | |
| 47 | 39 | <div style="display:flex;gap:12px"> |
| 48 | 40 | <label style="flex:2;display:flex;flex-direction:column;gap:4px"> |
| 49 | 41 | <span class="rb-label">Server name</span> |
| 50 | 42 | <input type="text" name="server_name" value="$server_name" required placeholder="e.g. prod-web-1" |
| 51 | 43 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:inherit"> |
| 52 | 44 | </label> |
| 53 | 45 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 54 | 46 | <span class="rb-label">Kind</span> |
| 55 | 47 | <select name="kind" style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px"> |
| 56 | 48 | <option value="local" $k_local_sel>local</option> |
| 57 | 49 | <option value="ssh_agent" $k_ssh_sel>ssh_agent</option> |
| 58 | 50 | </select> |
| 59 | 51 | </label> |
| 60 | 52 | </div> |
| 61 | 53 | |
| 62 | 54 | <div style="display:flex;gap:12px"> |
| 63 | 55 | <label style="flex:2;display:flex;flex-direction:column;gap:4px"> |
| 64 | 56 | <span class="rb-label">SSH host</span> |
| 65 | 57 | <input type="text" name="ssh_host" value="$ssh_host" placeholder="e.g. prod-web-1.internal or 10.0.0.42" |
| 66 | 58 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:12px"> |
| 67 | 59 | </label> |
| 68 | 60 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 69 | 61 | <span class="rb-label">User</span> |
| 70 | 62 | <input type="text" name="ssh_user" value="$ssh_user" |
| 71 | 63 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:12px"> |
| 72 | 64 | </label> |
| 73 | 65 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 74 | 66 | <span class="rb-label">Port</span> |
| 75 | 67 | <input type="number" name="ssh_port" value="$ssh_port" min="1" max="65535" |
| 76 | 68 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px"> |
| 77 | 69 | </label> |
| 78 | 70 | </div> |
| 79 | 71 | |
| 80 | 72 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 81 | 73 | <span class="rb-label">SSH key path <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">(on-disk fallback if you don't paste a key below)</span></span> |
| 82 | 74 | <input type="text" name="ssh_key_path" value="$ssh_key_path" placeholder="/etc/drift_sense/keys/agent_key" |
| 83 | 75 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:12px"> |
| 84 | 76 | </label> |
| 85 | 77 | |
| 86 | 78 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 87 | 79 | <span class="rb-label"> |
| 88 | 80 | Paste PEM key (encrypted at rest with master key) |
| 89 | 81 | [if:$has_key_blob]<span class="pill" style="background:rgba(52,211,153,.14);color:#86efac;border-color:rgba(52,211,153,.35);margin-left:6px">key already stored</span>[/if] |
| 90 | 82 | </span> |
| 91 | 83 | <textarea name="ssh_key_blob_plain" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----" |
| 92 | 84 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--mono);font-size:11.5px;resize:vertical"></textarea> |
| 93 | 85 | </label> |
| 94 | 86 | |
| 95 | 87 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 96 | 88 | <span class="rb-label">Extra SSH options <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">(optional, space-separated <code>-o key=value</code> pairs)</span></span> |
| 97 | 89 | <input type="text" name="ssh_options" value="$ssh_options" placeholder="e.g. IdentitiesOnly=yes ProxyJump=bastion.internal" |
| 98 | 90 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:11.5px"> |
| 99 | 91 | </label> |
| 100 | 92 | |
| 101 | 93 | <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px"> |
| 102 | 94 | [if:$is_edit]<a href="/servers.cgi" class="tr-chip">Cancel</a>[/if] |
| 103 | 95 | <button type="submit" class="tr-chip is-active" style="border:none">[if:$is_edit]Save[/if][if:!$is_edit]Add server[/if]</button> |
| 104 | 96 | </div> |
| 105 | 97 | </form> |
| 106 | 98 | </div> |
| 107 | 99 | </div> |
| 108 | 100 | [/if] |
| 109 | 101 | |
| 110 | 102 | <!-- ============================ BULK IMPORT ============================ --> |
| 111 | 103 | [if:$bulk_mode] |
| 112 | 104 | <div class="module glow-emerald" style="grid-column:1 / span 2"> |
| 113 | 105 | <div class="module-head"><div class="left"> |
| 114 | 106 | <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div> |
| 115 | 107 | <div class="module-title">Bulk import</div> |
| 116 | 108 | <span class="module-subtitle">Paste one hostname per line + one shared SSH key + a base scan config. DriftSense fans out.</span> |
| 117 | 109 | </div></div> |
| 118 | 110 | <div class="module-body"> |
| 119 | 111 | <form method="post" action="/servers.cgi" style="display:flex;flex-direction:column;gap:14px"> |
| 120 | 112 | <input type="hidden" name="op" value="bulk_import"> |
| 121 | 113 | |
| 122 | 114 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 123 | 115 | <span class="rb-label">Hostnames — one per line, optional <code>label|host</code> or <code>label=host</code> format</span> |
| 124 | 116 | <textarea name="hosts" rows="7" placeholder="prod-web-1.internal prod-web-2 = 10.0.0.11 webstls | webstls.com staging | staging.internal" |
| 125 | 117 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--mono);font-size:12.5px;resize:vertical" required></textarea> |
| 126 | 118 | </label> |
| 127 | 119 | |
| 128 | 120 | <div style="display:flex;gap:12px"> |
| 129 | 121 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 130 | 122 | <span class="rb-label">User</span> |
| 131 | 123 | <input type="text" name="ssh_user" value="root" |
| 132 | 124 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:12px"> |
| 133 | 125 | </label> |
| 134 | 126 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 135 | 127 | <span class="rb-label">Port</span> |
| 136 | 128 | <input type="number" name="ssh_port" value="22" |
| 137 | 129 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px"> |
| 138 | 130 | </label> |
| 139 | 131 | </div> |
| 140 | 132 | |
| 141 | 133 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 142 | 134 | <span class="rb-label">Shared PEM key (encrypted at rest) — optional if all hosts use <code>ssh_key_path</code></span> |
| 143 | 135 | <textarea name="ssh_key_blob_plain" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----" |
| 144 | 136 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--mono);font-size:11.5px;resize:vertical"></textarea> |
| 145 | 137 | </label> |
| 146 | 138 | |
| 147 | 139 | <hr style="border:none;border-top:1px solid var(--border);margin:4px 0"> |
| 148 | 140 | |
| 149 | 141 | <div class="rb-label" style="margin-bottom:-6px">Base scan config — applied to every host</div> |
| 150 | 142 | |
| 151 | 143 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 152 | 144 | <span class="rb-label">Scan path</span> |
| 153 | 145 | <input type="text" name="scan_path" value="/etc" placeholder="/etc or /var/www" |
| 154 | 146 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:12px" required> |
| 155 | 147 | </label> |
| 156 | 148 | |
| 157 | 149 | <div style="display:flex;gap:12px"> |
| 158 | 150 | <label style="flex:2;display:flex;flex-direction:column;gap:4px"> |
| 159 | 151 | <span class="rb-label">Ignore globs (comma-separated)</span> |
| 160 | 152 | <input type="text" name="ignore_list" value="*.log,*.pid,*.sock,*.tmp,tmp/*,cache/*,logs/*,.git/*,node_modules/*" |
| 161 | 153 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:11.5px"> |
| 162 | 154 | </label> |
| 163 | 155 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 164 | 156 | <span class="rb-label">File-type filter <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">(blank = all)</span></span> |
| 165 | 157 | <input type="text" name="file_type_filter" value="" placeholder="cgi,pl,pm,html,css,js,md,sql,conf" |
| 166 | 158 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--mono);font-size:11.5px"> |
| 167 | 159 | </label> |
| 168 | 160 | </div> |
| 169 | 161 | |
| 170 | 162 | <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px"> |
| 171 | 163 | <a href="/servers.cgi" class="tr-chip">Cancel</a> |
| 172 | 164 | <button type="submit" class="tr-chip is-active" style="border:none">Import all</button> |
| 173 | 165 | </div> |
| 174 | 166 | </form> |
| 175 | 167 | </div> |
| 176 | 168 | </div> |
| 177 | 169 | [/if] |
| 178 | 170 | |
| 179 | 171 | <!-- ============================ QUICK HELP CARD ============================ --> |
| 180 | 172 | [if:!$bulk_mode] |
| 181 | 173 | <div class="module glow-sky"> |
| 182 | 174 | <div class="module-head"><div class="left"> |
| 183 | 175 | <div class="module-title">Quick help</div> |
| 184 | 176 | <span class="module-subtitle">The three most common setup shapes.</span> |
| 185 | 177 | </div></div> |
| 186 | 178 | <div class="module-body" style="font-size:13.5px;color:var(--text-dim);line-height:1.75"> |
| 187 | 179 | <p style="margin-bottom:10px"><strong style="color:var(--text)">Same server</strong> — pick <code>local</code>, skip all the SSH fields. This is the default install monitoring itself.</p> |
| 188 | 180 | <p style="margin-bottom:10px"><strong style="color:var(--text)">Remote via SSH</strong> — pick <code>ssh_agent</code>, fill <code>ssh_host</code> + <code>ssh_user</code>, and either paste the PEM key below (encrypted at rest with the master key) OR give a path to a keyfile on disk.</p> |
| 189 | 181 | <p style="margin-bottom:10px"><strong style="color:var(--text)">Test the connection</strong> — after saving, hit the "Test" button on the server row. Runs an SSH probe and reports back inline.</p> |
| 190 | 182 | <p><strong style="color:var(--text)">Bulk import 20 hosts at once</strong> — switch to the "Bulk import" tab. Paste hostnames, one shared key, one base scan config. DriftSense fans out and starts scanning within 5 minutes.</p> |
| 191 | 183 | </div> |
| 192 | 184 | </div> |
| 193 | 185 | [/if] |
| 194 | 186 | </div> |
| 195 | 187 | |
| 196 | 188 | <!-- ============================ SERVER LIST ============================ --> |
| 197 | 189 | <div class="module glow-sky"> |
| 198 | 190 | <div class="module-head"><div class="left"> |
| 199 | 191 | <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/></svg></div> |
| 200 | 192 | <div class="module-title">Registered servers</div> |
| 201 | 193 | <span class="module-subtitle">$total total.</span> |
| 202 | 194 | </div></div> |
| 203 | 195 | <div class="module-body tight"> |
| 204 | 196 | [if:$has_rows] |
| 205 | 197 | <table class="tbl"> |
| 206 | 198 | <thead><tr> |
| 207 | 199 | <th style="width:140px">Kind / status</th> |
| 208 | 200 | <th>Name</th> |
| 209 | 201 | <th style="width:180px">Reach</th> |
| 210 | 202 | <th style="width:130px">Key</th> |
| 211 | 203 | <th style="width:150px">Last scan</th> |
| 212 | 204 | <th style="width:220px">Actions</th> |
| 213 | 205 | </tr></thead> |
| 214 | 206 | <tbody> |
| 215 | 207 | [loop:@rows] |
| 216 | 208 | <tr> |
| 217 | 209 | <td> |
| 218 | 210 | <span class="pill $kind_pill">$kind</span> |
| 219 | 211 | <span class="pill $status_pill" style="margin-left:4px">$status</span> |
| 220 | 212 | </td> |
| 221 | 213 | <td> |
| 222 | 214 | <a href="$edit_url" style="color:var(--accent-hi);text-decoration:none"><strong>$server_name</strong></a> |
| 223 | 215 | [if:$err_short]<div class="dim" style="font-size:11px;color:var(--accent-danger)">$err_short</div>[/if] |
| 224 | 216 | </td> |
| 225 | 217 | <td class="mono dim" style="font-size:12px">$ssh_user@$host_display:$ssh_port</td> |
| 226 | 218 | <td class="mono dim" style="font-size:11.5px">$key_display</td> |
| 227 | 219 | <td class="mono dim"><span class="ts" data-ts="$last_scan_epoch" data-fmt="relative">$last_scan_h</span></td> |
| 228 | 220 | <td> |
| 229 | 221 | <button type="button" class="tr-chip" onclick="dsTestConn($server_id, this)">Test</button> |
| 230 | 222 | <a href="$edit_url" class="tr-chip">Edit</a> |
| 231 | 223 | <form method="post" action="/servers.cgi" style="display:inline" onsubmit="return confirm('Disable this server?')"> |
| 232 | 224 | <input type="hidden" name="op" value="delete"> |
| 233 | 225 | <input type="hidden" name="server_id" value="$server_id"> |
| 234 | 226 | <button type="submit" class="tr-chip" title="Disable">×</button> |
| 235 | 227 | </form> |
| 236 | 228 | </td> |
| 237 | 229 | </tr> |
| 238 | 230 | [/loop] |
| 239 | 231 | </tbody> |
| 240 | 232 | </table> |
| 241 | 233 | [/if] |
| 242 | 234 | [if:!$has_rows] |
| 243 | 235 | <div style="padding:36px 14px;text-align:center;color:var(--text-dim);font-size:13.5px"> |
| 244 | 236 | No servers registered. Use the form above to add one, or switch to Bulk Import. |
| 245 | 237 | </div> |
| 246 | 238 | [/if] |
| 247 | 239 | </div> |
| 248 | 240 | </div> |
| 249 | 241 | |
| 250 | 242 | <script> |
| 251 | 243 | (function () { |
| 252 | 244 | window.dsTestConn = async function (id, btn) { |
| 253 | 245 | var orig = btn.textContent; |
| 254 | 246 | btn.textContent = 'Testing...'; |
| 255 | 247 | btn.disabled = true; |
| 256 | 248 | try { |
| 257 | 249 | var r = await fetch('/servers.cgi?op=test&id=' + id + '&nc=' + Date.now(), { cache: 'no-store' }); |
| 258 | 250 | var j = await r.json(); |
| 259 | 251 | if (j.ok) { |
| 260 | 252 | btn.textContent = 'OK'; |
| 261 | 253 | btn.style.background = '#34d399'; |
| 262 | 254 | btn.style.color = '#0a1220'; |
| 263 | 255 | btn.style.borderColor = '#34d399'; |
| 264 | 256 | alert('SSH probe OK\n\n' + (j.message || '')); |
| 265 | 257 | } else { |
| 266 | 258 | btn.textContent = 'FAIL'; |
| 267 | 259 | btn.style.background = '#f43f5e'; |
| 268 | 260 | btn.style.color = '#fff'; |
| 269 | 261 | btn.style.borderColor = '#f43f5e'; |
| 270 | 262 | alert('SSH probe failed\n\n' + (j.message || 'unknown error')); |
| 271 | 263 | } |
| 272 | 264 | } catch (e) { |
| 273 | 265 | alert('Probe error: ' + e.message); |
| 274 | 266 | btn.textContent = orig; |
| 275 | 267 | } finally { |
| 276 | 268 | setTimeout(function () { |
| 277 | 269 | btn.disabled = false; |
| 278 | 270 | btn.textContent = orig; |
| 279 | 271 | btn.style.background = ''; |
| 280 | 272 | btn.style.color = ''; |
| 281 | 273 | btn.style.borderColor = ''; |
| 282 | 274 | }, 3500); |
| 283 | 275 | } |
| 284 | 276 | }; |
| 285 | 277 | })(); |
| 286 | 278 | </script> |
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.