Restore

O Operator
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/user_information.html
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-11 23:02:35
Captured SHA1a03114eaf376b2decc8630b8b7e3b11ee5b473749215034e1c5772808aacfbc
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size37455 bytes
Current SHAf21d57b00996
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. +3 additions, -11 deletions, 741 unchanged context lines.
11<!doctype html>
22<html lang="en">
33<head>
44<meta charset="utf-8">
55<meta name="viewport" content="width=device-width, initial-scale=1">
66<title>DriftSense — How your customers use it</title>
77<style>
88 :root {
99 --bg-0: #030710;
1010 --bg-1: #0a1220;
1111 --bg-2: #131e30;
1212 --bg-3: #1a2740;
1313 --bg-4: #223050;
1414 --gray-hi: #cbd5e1;
1515 --gray: #96a3b8;
1616 --gray-lo: #6a7a94;
1717 --border: #223050;
1818 --border-hi: #2d3d5f;
1919 --accent: #4c8bf5; /* dark-blue accent, not brand-teal, per Shawn's palette */
2020 --accent-hi: #6ea0ff;
2121 --accent-lo: #1e3a8a;
2222 --success: #34d399;
2323 --warn: #f59e0b;
2424 --danger: #f43f5e;
2525 --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
2626 --sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2727 }
2828 *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
2929 html { scroll-behavior: smooth; }
3030 body {
3131 background: var(--bg-0);
3232 background-image:
3333 radial-gradient(1200px 500px at 20% -10%, rgba(76,139,245,0.10), transparent 60%),
3434 radial-gradient(900px 400px at 90% 10%, rgba(76,139,245,0.06), transparent 60%);
3535 color: var(--gray-hi);
3636 font-family: var(--sans);
3737 line-height: 1.65;
3838 -webkit-font-smoothing: antialiased;
3939 min-height: 100vh;
4040 }
4141 .wrap { max-width: 1180px; margin: 0 auto; padding: 48px 32px 96px; }
4242
4343 /* --- Header --------------------------------------------------------- */
4444 .brand {
4545 display: inline-flex; align-items: center; gap: 12px;
4646 padding: 6px 14px 6px 6px;
4747 background: linear-gradient(90deg, var(--bg-2), var(--bg-1));
4848 border: 1px solid var(--border-hi);
4949 border-radius: 999px;
5050 margin-bottom: 24px;
5151 }
5252 .brand .logo {
5353 width: 32px; height: 32px; border-radius: 50%;
5454 background: linear-gradient(135deg, var(--accent), var(--accent-lo));
5555 display: grid; place-items: center;
5656 font-weight: 800; font-size: 12px; color: #fff;
5757 letter-spacing: -0.5px;
5858 box-shadow: 0 0 0 1px rgba(76,139,245,0.35), 0 6px 16px rgba(76,139,245,0.25);
5959 }
6060 .brand .name { font-weight: 700; letter-spacing: -0.2px; }
6161 .brand .name .accent { color: var(--accent-hi); }
6262 .brand .eyebrow { color: var(--gray); font-size: 11.5px; letter-spacing: 1.4px; text-transform: uppercase; }
6363
6464 h1.hero {
6565 font-size: clamp(30px, 4.5vw, 48px);
6666 line-height: 1.15;
6767 letter-spacing: -0.8px;
6868 color: #f4f7fb;
6969 max-width: 20ch;
7070 margin-bottom: 18px;
7171 }
7272 h1.hero em { color: var(--accent-hi); font-style: normal; }
7373 .lede {
7474 font-size: 17px;
7575 color: var(--gray-hi);
7676 max-width: 62ch;
7777 margin-bottom: 32px;
7878 }
7979 .lede .strong { color: #fff; font-weight: 600; }
8080
8181 /* --- Sections ------------------------------------------------------- */
8282 section { margin-top: 56px; }
8383 .section-eyebrow {
8484 display: inline-flex; align-items: center; gap: 8px;
8585 color: var(--accent-hi);
8686 font-size: 11px; font-weight: 700;
8787 letter-spacing: 1.6px; text-transform: uppercase;
8888 margin-bottom: 8px;
8989 }
9090 .section-eyebrow .dot {
9191 width: 8px; height: 8px; border-radius: 50%;
9292 background: var(--accent);
9393 box-shadow: 0 0 12px var(--accent);
9494 }
9595 h2 {
9696 font-size: 24px;
9797 color: #f4f7fb;
9898 letter-spacing: -0.3px;
9999 margin-bottom: 20px;
100100 }
101101 h3 { font-size: 15px; color: #e2e8f0; letter-spacing: -0.1px; margin: 0 0 6px; }
102102 p { color: var(--gray-hi); }
103103 p + p { margin-top: 10px; }
104104
105105 /* --- Cards ---------------------------------------------------------- */
106106 .card {
107107 background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
108108 border: 1px solid var(--border);
109109 border-radius: 14px;
110110 padding: 22px 24px;
111111 transition: border-color .15s ease, transform .15s ease;
112112 }
113113 .card:hover { border-color: var(--border-hi); }
114114 .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }
115115 .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
116116 @media (max-width: 800px) {
117117 .grid-2, .grid-3 { grid-template-columns: 1fr; }
118118 }
119119
120120 /* --- Persona / audience row ---------------------------------------- */
121121 .persona {
122122 display: flex; align-items: center; gap: 12px;
123123 padding: 10px 14px;
124124 background: var(--bg-2);
125125 border: 1px solid var(--border);
126126 border-radius: 12px;
127127 font-size: 13.5px;
128128 }
129129 .persona .who {
130130 padding: 3px 10px;
131131 border-radius: 999px;
132132 font-weight: 700;
133133 font-size: 11.5px;
134134 letter-spacing: 0.8px;
135135 text-transform: uppercase;
136136 background: rgba(76,139,245,0.14);
137137 color: var(--accent-hi);
138138 border: 1px solid rgba(76,139,245,0.35);
139139 }
140140 .persona .who.ops { background: rgba(52,211,153,0.14); color: #86efac; border-color: rgba(52,211,153,0.35); }
141141 .persona .who.audit { background: rgba(245,158,11,0.14); color: #fcd34d; border-color: rgba(245,158,11,0.35); }
142142 .persona .who.leader { background: rgba(244,63,94,0.14); color: #fda4af; border-color: rgba(244,63,94,0.35); }
143143
144144 /* --- Steps ---------------------------------------------------------- */
145145 .steps { counter-reset: step; }
146146 .step {
147147 position: relative;
148148 padding: 20px 22px 20px 78px;
149149 background: var(--bg-2);
150150 border: 1px solid var(--border);
151151 border-left: 3px solid var(--accent);
152152 border-radius: 12px;
153153 margin-top: 14px;
154154 }
155155 .step::before {
156156 counter-increment: step;
157157 content: counter(step);
158158 position: absolute;
159159 left: 18px; top: 20px;
160160 width: 44px; height: 44px;
161161 display: grid; place-items: center;
162162 background: linear-gradient(135deg, var(--accent), var(--accent-lo));
163163 color: #fff; font-weight: 800;
164164 border-radius: 12px;
165165 box-shadow: 0 4px 14px rgba(76,139,245,0.35);
166166 font-size: 17px;
167167 }
168168 .step h3 { margin-bottom: 8px; }
169169 .step p { color: var(--gray-hi); font-size: 14px; }
170170
171171 /* --- Code blocks ---------------------------------------------------- */
172172 pre, code {
173173 font-family: var(--mono);
174174 font-size: 12.5px;
175175 }
176176 code.inline {
177177 background: rgba(76,139,245,0.10);
178178 color: var(--accent-hi);
179179 padding: 1px 6px;
180180 border-radius: 4px;
181181 border: 1px solid rgba(76,139,245,0.20);
182182 font-size: 12px;
183183 }
184184 pre {
185185 background: var(--bg-0);
186186 color: #d1e0f5;
187187 border: 1px solid var(--border);
188188 border-radius: 10px;
189189 padding: 14px 18px;
190190 overflow-x: auto;
191191 margin-top: 12px;
192192 line-height: 1.55;
193193 }
194194 pre .cmt { color: var(--gray-lo); }
195195 pre .kw { color: var(--accent-hi); }
196196 pre .str { color: #86efac; }
197197
198198 /* --- Pills / meta --------------------------------------------------- */
199199 .pill {
200200 display: inline-block;
201201 padding: 3px 9px;
202202 border-radius: 999px;
203203 font-size: 11px;
204204 font-weight: 700;
205205 letter-spacing: 0.4px;
206206 background: rgba(76,139,245,0.14);
207207 color: var(--accent-hi);
208208 border: 1px solid rgba(76,139,245,0.30);
209209 }
210210 .pill.ok { background: rgba(52,211,153,0.14); color: #86efac; border-color: rgba(52,211,153,0.30); }
211211 .pill.warn { background: rgba(245,158,11,0.14); color: #fcd34d; border-color: rgba(245,158,11,0.30); }
212212
213213 /* --- Table --------------------------------------------------------- */
214214 table {
215215 width: 100%;
216216 border-collapse: separate;
217217 border-spacing: 0;
218218 background: var(--bg-2);
219219 border: 1px solid var(--border);
220220 border-radius: 12px;
221221 overflow: hidden;
222222 font-size: 13.5px;
223223 }
224224 th, td { text-align: left; padding: 12px 16px; vertical-align: top; }
225225 th {
226226 background: var(--bg-1);
227227 color: var(--gray);
228228 font-size: 11.5px;
229229 letter-spacing: 1.2px;
230230 text-transform: uppercase;
231231 font-weight: 700;
232232 border-bottom: 1px solid var(--border);
233233 }
234234 td { border-bottom: 1px solid var(--border); color: var(--gray-hi); }
235235 tr:last-child td { border-bottom: 0; }
236236 td strong { color: #e2e8f0; }
237237
238238 /* --- Callout ------------------------------------------------------- */
239239 .callout {
240240 display: flex; gap: 14px;
241241 background: linear-gradient(135deg, rgba(76,139,245,0.10), rgba(30,58,138,0.06));
242242 border: 1px solid rgba(76,139,245,0.30);
243243 border-radius: 12px;
244244 padding: 16px 18px;
245245 margin-top: 18px;
246246 }
247247 .callout .ico {
248248 flex: 0 0 auto;
249249 width: 32px; height: 32px;
250250 border-radius: 8px;
251251 background: rgba(76,139,245,0.20);
252252 display: grid; place-items: center;
253253 color: var(--accent-hi);
254254 font-weight: 800;
255255 }
256256 .callout p { color: var(--gray-hi); font-size: 13.5px; }
257257 .callout .title { color: #fff; font-weight: 700; margin-bottom: 4px; }
258258
259259 /* --- Feature stripe ------------------------------------------------- */
260260 .feature-icon {
261261 width: 40px; height: 40px;
262262 border-radius: 10px;
263263 background: linear-gradient(135deg, rgba(76,139,245,0.20), rgba(30,58,138,0.10));
264264 border: 1px solid rgba(76,139,245,0.30);
265265 display: grid; place-items: center;
266266 color: var(--accent-hi);
267267 margin-bottom: 12px;
268268 }
269269
270270 /* --- Nav bar -------------------------------------------------------- */
271271 nav.toc {
272272 position: sticky; top: 12px; z-index: 10;
273273 display: flex; flex-wrap: wrap; gap: 6px;
274274 padding: 10px 12px;
275275 background: rgba(3,7,16,0.75);
276276 backdrop-filter: blur(10px);
277277 -webkit-backdrop-filter: blur(10px);
278278 border: 1px solid var(--border);
279279 border-radius: 12px;
280280 margin-top: 32px;
281281 }
282282 nav.toc a {
283283 padding: 5px 11px;
284284 border-radius: 999px;
285285 color: var(--gray);
286286 text-decoration: none;
287287 font-size: 12px;
288288 font-weight: 600;
289289 border: 1px solid transparent;
290290 transition: all .15s ease;
291291 }
292292 nav.toc a:hover { color: #fff; border-color: var(--border-hi); background: var(--bg-2); }
293293
294294 hr.split {
295295 border: 0;
296296 height: 1px;
297297 background: linear-gradient(90deg, transparent, var(--border-hi), transparent);
298298 margin: 56px 0 0;
299299 }
300300
301301 footer {
302302 margin-top: 72px;
303303 padding-top: 24px;
304304 border-top: 1px solid var(--border);
305305 font-size: 12.5px;
306306 color: var(--gray-lo);
307307 }
308308 footer .accent { color: var(--accent-hi); }
309309</style>
310310</head>
311311<body>
312312<div class="wrap">
313313
314314 <!-- ============================ HERO ============================ -->
315315 <div class="brand">
316316 <div class="logo">DS</div>
317317 <div>
318318 <div class="name">Drift<span class="accent">Sense</span></div>
319319 <div class="eyebrow">Customer guide &middot; how it gets used</div>
320320 </div>
321321 </div>
322322
323323 <h1 class="hero">The <em>silent safety net</em> for teams without git discipline.</h1>
324324 <p class="lede">
325325 <span class="strong">DriftSense captures every file and schema change on your servers</span>,
326326 keeps them content-addressable + gzipped in one storage DB,
327327 lets you diff any two moments in time, and alerts you (Slack/Discord/webhook)
328328 the second something drifts. It's a <span class="strong">standalone install</span>
329329 you run inside your own infrastructure &mdash; no SaaS, no phone-home, no external
330330 dependencies beyond core Perl and MariaDB.
331331 </p>
332332
333333 <nav class="toc">
334334 <a href="#what">1. What it is</a>
335335 <a href="#who">2. Who uses it</a>
336336 <a href="#getit">3. How they get it</a>
337337 <a href="#install">4. Install (10 min)</a>
338338 <a href="#setup">5. First-time setup</a>
339339 <a href="#dayto">6. Day-to-day use</a>
340340 <a href="#pricing">7. Delivery model</a>
341341 <a href="#stealth">8. Stealth deployment</a>
342342 <a href="#faq">9. FAQ</a>
343343 </nav>
344344
345345 <!-- ============================ WHAT ============================ -->
346346 <section id="what">
347347 <div class="section-eyebrow"><span class="dot"></span> 01 &middot; the product</div>
348348 <h2>What DriftSense actually does</h2>
349349 <div class="grid-3">
350350 <div class="card">
351351 <div class="feature-icon">
352352 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
353353 </div>
354354 <h3>Silent file capture</h3>
355355 <p>Every add / modify / delete across configured paths is captured. Content is stored SHA-256-addressed + gzipped, so 100 versions of the same file take one copy plus a ref-count.</p>
356356 </div>
357357 <div class="card">
358358 <div class="feature-icon">
359359 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 11v6c0 1.7 4 3 9 3s9-1.3 9-3v-6"/></svg>
360360 </div>
361361 <h3>Silent schema capture</h3>
362362 <p>MariaDB / MySQL DDL is snapshotted every 5 minutes. Someone adds a column at 2 AM? You'll see the exact <code class="inline">ALTER</code> in the diff viewer the next morning.</p>
363363 </div>
364364 <div class="card">
365365 <div class="feature-icon">
366366 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><polyline points="21 15 16 20 12 16 3 25"/><polyline points="21 15 21 9"/></svg>
367367 </div>
368368 <h3>Diff viewer + timeline</h3>
369369 <p>Click any change to see a unified diff of the before / after content. The 90-day activity chart shows drift rhythms per file / per table.</p>
370370 </div>
371371 <div class="card">
372372 <div class="feature-icon">
373373 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
374374 </div>
375375 <h3>Alert rules &amp; webhooks</h3>
376376 <p>Rule engine matches on path glob + status + kind, then POSTs to Slack / Discord / any HTTP endpoint. Rate limits, coalescing, full audit trail per delivery.</p>
377377 </div>
378378 <div class="card">
379379 <div class="feature-icon">
380380 <svg viewBox="0 0 24 24" width="18" height="18" 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="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
381381 </div>
382382 <h3>Compliance CSV export</h3>
383383 <p>Auditor asks "prove nothing changed in Q3": pick a range, hit download. Rows include SHA-256 blob pointers so auditors can independently verify content.</p>
384384 </div>
385385 <div class="card">
386386 <div class="feature-icon">
387387 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 13V7a2 2 0 0 0-2-2h-4l-2-3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h5"/><path d="M14 19l2 2 4-4"/></svg>
388388 </div>
389389 <h3>Named releases</h3>
390390 <p>Bookmark a moment ("before the migration", "prod v2.4 cutover") &mdash; DriftSense pins those blobs so auto-purge never touches them.</p>
391391 </div>
392392 </div>
393393 </section>
394394
395395 <!-- ============================ WHO ============================ -->
396396 <section id="who">
397397 <div class="section-eyebrow"><span class="dot"></span> 02 &middot; the buyers</div>
398398 <h2>Who actually reaches for it</h2>
399399 <p style="margin-bottom:18px">Three personas end up on the pricing page most often. Each gets a different kind of value out of the same install.</p>
400400 <div class="grid-3">
401401 <div class="card">
402402 <div class="persona"><span class="who ops">DevOps / SRE</span> Small shop, no git for /etc</div>
403403 <h3 style="margin-top:14px">"I need to know what changed last night."</h3>
404404 <p>They run a handful of servers where /etc, nginx configs, and cron files evolve by hand. When something breaks, "did anyone touch nginx?" is the first question. DriftSense answers it in five seconds.</p>
405405 <p><strong style="color:#fff">Primary usage:</strong> Dashboard + diff viewer. Alert rules on <code class="inline">/etc/nginx/**</code> and <code class="inline">/etc/cron.d/**</code>.</p>
406406 </div>
407407 <div class="card">
408408 <div class="persona"><span class="who audit">Compliance / audit</span> Financial or medical</div>
409409 <h3 style="margin-top:14px">"Prove nothing changed during the window."</h3>
410410 <p>Auditors want an immutable record of file + schema state over time. DriftSense's content-addressable blob store gives them SHA-256 receipts they can verify independently.</p>
411411 <p><strong style="color:#fff">Primary usage:</strong> Compliance CSV export + Named releases pinning quarter-end snapshots.</p>
412412 </div>
413413 <div class="card">
414414 <div class="persona"><span class="who leader">Solo founder / dev</span> Ships production alone</div>
415415 <h3 style="margin-top:14px">"I need a safety net for myself."</h3>
416416 <p>One-person team, no code-review process. Modifies configs live. DriftSense lets them undo any mistake by clicking through history — no discipline required, no workflow to adopt.</p>
417417 <p><strong style="color:#fff">Primary usage:</strong> File changes list + diff viewer. Occasional Slack alert on deletions.</p>
418418 </div>
419419 </div>
420420 </section>
421421
422422 <!-- ============================ GET IT ============================ -->
423423 <section id="getit">
424424 <div class="section-eyebrow"><span class="dot"></span> 03 &middot; acquisition</div>
425425 <h2>How a customer gets DriftSense</h2>
426426 <div class="grid-2">
427427 <div class="card">
428428 <h3>Buy → download → install</h3>
429429 <p>Landing page &rarr; checkout (Stripe) &rarr; email with a signed download link + license key. The tarball is a single <code class="inline">drift_sense-x.y.tar.gz</code> plus a short PDF quickstart.</p>
430430 <p style="margin-top:10px"><span class="pill">Self-hosted</span> <span class="pill ok">No phone-home</span> <span class="pill warn">One-time or subscription</span></p>
431431 </div>
432432 <div class="card">
433433 <h3>What ships in the tarball</h3>
434434 <ul style="margin-left:20px;color:var(--gray-hi);font-size:14px;line-height:1.85">
435435 <li>All CGIs + Perl modules (~50 files)</li>
436436 <li><code class="inline">_schema/</code> — v2 schema + alerts migration</li>
437437 <li><code class="inline">_ops/drift_sense.cron</code> — cron template</li>
438438 <li><code class="inline">_config/drift_sense.conf.example</code></li>
439439 <li><code class="inline">assets/</code> — CSS, JS, images</li>
440440 <li>Installer script (planned: <code class="inline">./install.sh</code>)</li>
441441 </ul>
442442 </div>
443443 </div>
444444 </section>
445445
446446 <!-- ============================ INSTALL ============================ -->
447447 <section id="install">
448448 <div class="section-eyebrow"><span class="dot"></span> 04 &middot; install</div>
449449 <h2>The 10-minute install</h2>
450450 <p style="margin-bottom:12px">Assumes stock CentOS / Debian / Ubuntu / macOS with MariaDB or MySQL already running. Prereqs are boringly minimal on purpose:</p>
451451
452452 <div class="callout">
453453 <div class="ico">i</div>
454454 <div>
455455 <div class="title">Prereqs (all standard on every mainstream Linux):</div>
456456 <p>Perl 5.10+ · MariaDB / MySQL 5.5+ · Apache or nginx with CGI enabled · <code class="inline">curl</code> binary. That's it. Zero CPAN installs required.</p>
457457 </div>
458458 </div>
459459
460460 <div class="steps">
461461 <div class="step">
462462 <h3>Untar into a docroot</h3>
463463 <p>Point the vhost at a fresh directory, drop the tarball, extract.</p>
464464<pre><span class="cmt"># pick any docroot</span>
465465<span class="kw">cd</span> /var/www/drift_sense
466466tar xzf ~/drift_sense-1.0.tar.gz --strip-components=1</pre>
467467 </div>
468468
469469 <div class="step">
470470 <h3>Create the DB + import the schema</h3>
471471 <p>One DB, one user with all-privileges on it.</p>
472472<pre><span class="kw">mysql</span> -u root -p &lt;&lt; SQL
473473CREATE DATABASE drift_sense CHARACTER SET utf8mb4;
474474GRANT ALL PRIVILEGES ON drift_sense.* TO 'drift_sense'@'localhost'
475475 IDENTIFIED BY <span class="str">'change-me'</span>;
476476SQL
477477
478478<span class="kw">mysql</span> -u drift_sense -p drift_sense &lt; _schema/drift_sense_v2_schema.sql
479479<span class="kw">mysql</span> -u drift_sense -p drift_sense &lt; _schema/drift_sense_alerts.sql</pre>
480480 </div>
481481
482482 <div class="step">
483483 <h3>Write /etc/drift_sense/drift_sense.conf</h3>
484484 <p>Copy the example, fill in DB creds and your public URL.</p>
485485<pre>db_engine=mysql
486486db_host=localhost
487487db_name=drift_sense
488488db_user=drift_sense
489489db_password=<span class="str">change-me</span>
490490public_url=<span class="str">https://drift.example.com</span></pre>
491491 </div>
492492
493493 <div class="step">
494494 <h3>Install the cron entry</h3>
495495 <p>Two scanners + one dispatcher; cron.d convention keeps them isolated.</p>
496496<pre>cp _ops/drift_sense.cron /etc/cron.d/drift_sense
497497mkdir -p /var/log/drift_sense</pre>
498498 </div>
499499
500500 <div class="step">
501501 <h3>Point your browser at the vhost</h3>
502502 <p>The dashboard renders on first visit. Empty because nothing is monitored yet &mdash; that's the setup step, next.</p>
503503 </div>
504504 </div>
505505 </section>
506506
507507 <!-- ============================ SETUP ============================ -->
508508 <section id="setup">
509509 <div class="section-eyebrow"><span class="dot"></span> 05 &middot; first-time setup</div>
510510 <h2>Configure what to watch</h2>
511511 <p style="margin-bottom:14px">Everything past install happens in the browser. Three configuration screens, all optional depending on what you care about.</p>
512512
513513 <div class="grid-2">
514514 <div class="card">
515515 <h3>File monitors <span class="pill">/file_monitors.cgi</span></h3>
516516 <p>Add one row per directory tree you want captured. Give it a scan path, an ignore-glob list (git, node_modules, logs), an optional file-type filter, and a max file size. Scanner runs every 2 minutes with mtime-first optimization &mdash; unchanged files skip hashing.</p>
517517 </div>
518518 <div class="card">
519519 <h3>Database monitors <span class="pill">/databases.cgi</span></h3>
520520 <p>Add one row per DB to snapshot. Just needs a read-only user with <code class="inline">SELECT</code> on that DB and <code class="inline">information_schema</code>. Uses <code class="inline">UPDATE_TIME</code> for an early-exit when nothing has changed.</p>
521521 </div>
522522 <div class="card">
523523 <h3>Alert rules <span class="pill">/alerts.cgi</span></h3>
524524 <p>Optional. Rules match on kind (file / schema / either) + path glob + status filter, then POST to Slack, Discord, or any HTTP endpoint. Add rate limits and coalescing to keep from paging yourself into oblivion.</p>
525525 </div>
526526 <div class="card">
527527 <h3>Named releases <span class="pill">/named_releases.cgi</span></h3>
528528 <p>Bookmark a moment ("v2.4 prod cutover") and DriftSense pins every blob referenced by that moment. Auto-purge never touches pinned blobs &mdash; the point-in-time state is restorable forever.</p>
529529 </div>
530530 </div>
531531 </section>
532532
533533 <!-- ============================ DAY TO DAY ============================ -->
534534 <section id="dayto">
535535 <div class="section-eyebrow"><span class="dot"></span> 06 &middot; daily use</div>
536536 <h2>What the customer actually does</h2>
537537 <p style="margin-bottom:16px">DriftSense is deliberately low-touch. Most weeks the customer never opens the UI &mdash; they only visit when Slack pings them or something goes wrong. When they do visit, here are the tools they reach for, in order of frequency.</p>
538538
539539 <table>
540540 <thead><tr>
541541 <th style="width:24%">Page</th>
542542 <th style="width:34%">What they do there</th>
543543 <th style="width:22%">How often</th>
544544 <th>Trigger</th>
545545 </tr></thead>
546546 <tbody>
547547 <tr>
548548 <td><strong>Dashboard</strong><br><code class="inline">/dashboard.cgi</code></td>
549549 <td>Glance at the 90-day activity timeline, spot spikes, click a busy day to drill in.</td>
550550 <td><span class="pill ok">Once a week</span></td>
551551 <td>Curiosity / weekly triage</td>
552552 </tr>
553553 <tr>
554554 <td><strong>File changes</strong><br><code class="inline">/file_changes.cgi</code></td>
555555 <td>Filter by date range or path, click a row to see the unified diff of what changed.</td>
556556 <td><span class="pill">On demand</span></td>
557557 <td>"Did nginx config change?"</td>
558558 </tr>
559559 <tr>
560560 <td><strong>Diff viewer</strong><br><code class="inline">/diff.cgi</code></td>
561561 <td>Read the before/after content of a single change. Restore by copy-paste if needed.</td>
562562 <td><span class="pill">On demand</span></td>
563563 <td>Slack alert click-through</td>
564564 </tr>
565565 <tr>
566566 <td><strong>Alerts</strong><br><code class="inline">/alerts.cgi</code></td>
567567 <td>Add or tune rules, check delivery history, verify webhooks are still landing.</td>
568568 <td><span class="pill warn">Rarely</span></td>
569569 <td>New watch target</td>
570570 </tr>
571571 <tr>
572572 <td><strong>Compliance export</strong><br><code class="inline">/export.cgi</code></td>
573573 <td>Pick a date window, download CSV. Send to auditor.</td>
574574 <td><span class="pill warn">Quarterly</span></td>
575575 <td>Audit request</td>
576576 </tr>
577577 <tr>
578578 <td><strong>Named releases</strong><br><code class="inline">/named_releases.cgi</code></td>
579579 <td>Bookmark a state before a risky change; look at old bookmarks after the fact.</td>
580580 <td><span class="pill warn">Per release</span></td>
581581 <td>Change management</td>
582582 </tr>
583583 </tbody>
584584 </table>
585585 </section>
586586
587587 <!-- ============================ PRICING ============================ -->
588588 <section id="pricing">
589589 <div class="section-eyebrow"><span class="dot"></span> 07 &middot; delivery model</div>
590590 <h2>How it's sold + supported</h2>
591591 <div class="grid-2">
592592 <div class="card">
593593 <h3>Ownership: fully theirs</h3>
594594 <p>DriftSense installs on their server, their DB, their infrastructure. No phone-home. No SaaS backend. If your company disappears tomorrow, the customer's install keeps working forever. That's the <em style="color:var(--accent-hi);font-style:normal">ownership principle</em> &mdash; the whole point of a self-hosted product.</p>
595595 </div>
596596 <div class="card">
597597 <h3>Update model: opt-in</h3>
598598 <p>Version 1.0 today, 1.1 later. Customer receives an update tarball; runs a schema-migration script + swaps in the new code. Old versions keep working &mdash; there's no forced upgrade cycle.</p>
599599 </div>
600600 <div class="card">
601601 <h3>Pricing shape (candidates)</h3>
602602 <ul style="margin-left:20px;color:var(--gray-hi);font-size:13.5px;line-height:1.9">
603603 <li><strong style="color:#fff">One-time</strong> &mdash; $199 per server, ships with 1 yr of updates</li>
604604 <li><strong style="color:#fff">Subscription</strong> &mdash; $12/mo per server, updates included forever</li>
605605 <li><strong style="color:#fff">Portfolio</strong> &mdash; $499/yr flat for up to 10 servers</li>
606606 </ul>
607607 <p style="margin-top:10px;font-size:12.5px;color:var(--gray)">Placeholder numbers &mdash; final pricing lives on the sales page.</p>
608608 </div>
609609 <div class="card">
610610 <h3>Support</h3>
611611 <p>Email support included for subscribers. Community forum for the one-time tier. Priority incident response as an add-on for enterprise customers who need SLA-backed help.</p>
612612 </div>
613613 </div>
614614 </section>
615615
616616 <!-- ============================ STEALTH ============================ -->
617617 <section id="stealth">
618618 <div class="section-eyebrow"><span class="dot"></span> 08 &middot; stealth deployment</div>
619619 <h2>Running invisibly to the developers</h2>
620620 <p style="margin-bottom:14px">
621621 A common ask from sys-admins in shops without code review:
622622 "I want DriftSense as a code + config backup, but I don't want the developers messing with it."
623623 Two flavors, ordered by paranoia level. Both are supported by the same install &mdash; just configured differently.
624624 </p>
625625
626626 <div class="callout" style="border-color:rgba(245,158,11,0.30);background:linear-gradient(135deg,rgba(245,158,11,0.08),rgba(120,53,15,0.04))">
627627 <div class="ico" style="background:rgba(245,158,11,0.20);color:#fcd34d">!</div>
628628 <div>
629629 <div class="title">Honest about the threat model</div>
630630 <p>Neither flavor stops a root-privileged actor determined to hide. This is <em>deterrence + evidence-preservation</em> against casual discovery and accidental tampering, not adversarial security. Sysadmin needs an out-of-band alert path (personal email, SMS) so a dev deactivating alerts doesn't blind them.</p>
631631 </div>
632632 </div>
633633
634634 <div class="grid-2" style="margin-top:22px">
635635 <div class="card">
636636 <h3><span class="pill">Flavor 1</span> Same-server, obscured</h3>
637637 <p style="margin-top:8px">DriftSense stays on the server it's monitoring, but nothing screams "audit tool":</p>
638638 <ul style="margin:10px 0 0 20px;color:var(--gray-hi);font-size:13.5px;line-height:1.85">
639639 <li>Install to <code class="inline">/opt/sys_audit_log/</code> instead of <code class="inline">/var/www/drift_sense/</code></li>
640640 <li>UI mounted at a bastion-only URL (not the public domain)</li>
641641 <li>Apache <code class="inline">Allow from &lt;sysadmin-IP&gt;</code> in front of every CGI</li>
642642 <li>Cron entries in <strong>root's crontab</strong>, not <code class="inline">/etc/cron.d/</code> (less-browsed location)</li>
643643 <li>Innocuous DB name (<code class="inline">system_metrics</code> instead of <code class="inline">drift_sense</code>)</li>
644644 <li>Alert deliveries to sysadmin-private Slack workspace or personal email</li>
645645 <li>Add DriftSense's own dir to every FILE_MONITOR ignore list so it doesn't watch itself</li>
646646 </ul>
647647 <p style="margin-top:10px;color:var(--gray);font-size:12.5px">Blocks casual discovery. Doesn't block a determined root actor.</p>
648648 </div>
649649 <div class="card">
650650 <h3><span class="pill ok">Flavor 2</span> Bastion agent-mode (true stealth)</h3>
651651 <p style="margin-top:8px">DriftSense lives on a separate server developers don't touch. Monitored servers have <strong>zero DriftSense footprint</strong> beyond one SSH key entry.</p>
652652 <ul style="margin:10px 0 0 20px;color:var(--gray-hi);font-size:13.5px;line-height:1.85">
653653 <li>Bastion runs the code, DB, cron, dashboard &mdash; all invisible from monitored server</li>
654654 <li>Scanner SSHes into monitored server with a <em>restricted</em> read-only key</li>
655655 <li>Only footprint on monitored server: one line in <code class="inline">/root/.ssh/authorized_keys</code>, name it something like <code class="inline">backup</code></li>
656656 <li>SSH sessions show in <code class="inline">sshd</code> logs but look like ordinary backup connections</li>
657657 <li>Multiple monitored servers into one central bastion &mdash; portfolio view for free</li>
658658 </ul>
659659 </div>
660660 </div>
661661
662662 <h3 style="margin-top:28px;color:#e2e8f0">Apache allowlist snippet (Flavor 1)</h3>
663663 <p style="font-size:13.5px">Restrict the UI to the sysadmin's static IP or VPN CIDR. Drop this in the vhost's <code class="inline">.htaccess</code> or Apache conf:</p>
664664<pre><span class="cmt"># Only sysadmin's IP + office VPN can reach the UI</span>
665665<span class="kw">&lt;RequireAll&gt;</span>
666666 <span class="kw">Require</span> ip 203.0.113.42
667667 <span class="kw">Require</span> ip 10.20.30.0/24
668668<span class="kw">&lt;/RequireAll&gt;</span>
669669
670670<span class="cmt"># Optional -- return 404 (not 403) so scanners see "nothing here" not "protected page"</span>
671671ErrorDocument 403 <span class="str">"/404.html"</span></pre>
672672
673673 <h3 style="margin-top:28px;color:#e2e8f0">SSH key restriction (Flavor 2)</h3>
674674 <p style="font-size:13.5px">On the monitored server, add the bastion's public key to <code class="inline">/root/.ssh/authorized_keys</code> with these restrictions &mdash; the key can only run the specific scan commands, nothing else:</p>
675675<pre><span class="cmt"># /root/.ssh/authorized_keys on the monitored server</span>
676676command=<span class="str">"/root/.ssh/drift_agent_wrapper.sh"</span>,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty <span class="kw">ssh-ed25519</span> AAAA...bastion-key backup
677677
678678<span class="cmt"># /root/.ssh/drift_agent_wrapper.sh -- only allow the scanner's commands</span>
679679<span class="kw">#!/bin/bash</span>
680680<span class="kw">case</span> <span class="str">"$SSH_ORIGINAL_COMMAND"</span> <span class="kw">in</span>
681681 <span class="str">"drift-find "*</span>) <span class="kw">exec</span> find $&#123;SSH_ORIGINAL_COMMAND#drift-find } -type f -not -path <span class="str">'*/.*'</span> -printf <span class="str">'%p\t%s\t%T@\n'</span> ;;
682682 <span class="str">"drift-hash "*</span>) <span class="kw">exec</span> sha256sum $&#123;SSH_ORIGINAL_COMMAND#drift-hash } ;;
683683 <span class="str">"drift-read "*</span>) <span class="kw">exec</span> cat $&#123;SSH_ORIGINAL_COMMAND#drift-read } ;;
684684 <span class="str">*</span>) <span class="kw">exit</span> 1 ;;
685685<span class="kw">esac</span></pre>
686686
687 <h3 style="margin-top:28px;color:#e2e8f0"><span class="pill ok">Shipped 2026-07-11</span> Add an ssh_agent server via SQL</h3>
688 <p style="font-size:13.5px">The dashboard doesn't yet expose ssh_agent servers in the UI (that's the next roadmap item &mdash; today you configure via SQL). The <code class="inline">_agent_scan.pl</code> daemon runs every 5 minutes via cron and picks up any new <code class="inline">ssh_agent</code> row automatically:</p>
687 <h3 style="margin-top:28px;color:#e2e8f0">Add an ssh_agent server via SQL</h3>
688 <p style="font-size:13.5px">The dashboard doesn't yet expose ssh_agent servers in the UI (roadmap). For now, insert directly:</p>
689689<pre><span class="kw">INSERT INTO</span> SERVERS (server_name, kind, ssh_host, ssh_user, ssh_port, ssh_key_path, status)
690690<span class="kw">VALUES</span> (<span class="str">'prod-web-1'</span>, <span class="str">'ssh_agent'</span>, <span class="str">'prod-web-1.internal'</span>, <span class="str">'root'</span>, 22,
691691 <span class="str">'/etc/drift_sense/keys/agent_key'</span>, <span class="str">'active'</span>);
692692
693693<span class="kw">INSERT INTO</span> FILE_MONITOR_SETTINGS
694694 (server_id, scan_path, ignore_list, file_type_filter, status, scan_name)
695695<span class="kw">VALUES</span>
696696 (LAST_INSERT_ID(), <span class="str">'/etc'</span>, <span class="str">'*.log,*.pid,*.sock'</span>, <span class="str">''</span>, 1,
697697 <span class="str">'prod-web-1 /etc config drift'</span>);</pre>
698698 <p style="font-size:13.5px;color:var(--gray);margin-top:12px">
699 Same FILE_CHANGES table as local mode, keyed to a different <code class="inline">server_id</code> &mdash; the dashboard, file changes list, diff viewer, alerts, and CSV export all show remote captures uniformly next to local ones.
699 The <code class="inline">_agent_scan.pl</code> daemon picks up any <code class="inline">ssh_agent</code> row on its next tick and starts collecting. Same FILE_CHANGES table as local mode, so the UI shows everything uniformly.
700700 </p>
701
702 <div class="callout" style="margin-top:22px;border-color:rgba(52,211,153,0.30);background:linear-gradient(135deg,rgba(52,211,153,0.10),rgba(6,95,70,0.05))">
703 <div class="ico" style="background:rgba(52,211,153,0.20);color:#86efac">&#10003;</div>
704 <div>
705 <div class="title">Smoke-tested end-to-end</div>
706 <p>Loopback SSH agent (127.0.0.1) monitoring <code class="inline">/etc/cron.d</code> captured 10 files on first tick (content pulled over SSH into central BLOB_STORE), detected a modification on the next tick, and the diff viewer rendered add/delete/context lines identically to local mode. Full uniform experience.</p>
707 </div>
708 </div>
709701 </section>
710702
711703 <!-- ============================ FAQ ============================ -->
712704 <section id="faq">
713705 <div class="section-eyebrow"><span class="dot"></span> 09 &middot; faq</div>
714706 <h2>What people ask before buying</h2>
715707 <div class="grid-2">
716708 <div class="card">
717709 <h3>Isn't this just git?</h3>
718710 <p>Git works when your team commits. DriftSense works when they don't. It's a safety net for the drift that happens outside your workflow &mdash; live edits, cron jobs writing config, someone SSHing in at 3 AM. Both can coexist happily.</p>
719711 </div>
720712 <div class="card">
721713 <h3>Does it slow the server down?</h3>
722714 <p>File scanner uses mtime-first, so unchanged files never get hashed &mdash; a 10 GB codebase scans in seconds. Schema scanner uses <code class="inline">information_schema.TABLES.UPDATE_TIME</code> as an early-exit. Baseline load is negligible.</p>
723715 </div>
724716 <div class="card">
725717 <h3>How much disk does it use?</h3>
726718 <p>Content is SHA-256-addressed + gzipped in BLOB_STORE. Duplicates cost one row plus a ref-count bump. A 10 GB codebase with normal churn typically sits under 500 MB after a month.</p>
727719 </div>
728720 <div class="card">
729721 <h3>What about containers?</h3>
730722 <p>Roadmap: host-side OverlayFS reader captures container drift without needing an in-container agent. Ship separately once the base product is battle-tested.</p>
731723 </div>
732724 <div class="card">
733725 <h3>Multi-server?</h3>
734726 <p>Central DB, agent binary per server. Central UI shows every host in one view. Included in the product &mdash; per-server licensing is on the honor system.</p>
735727 </div>
736728 <div class="card">
737729 <h3>Backup / restore?</h3>
738730 <p>Standard <code class="inline">mysqldump</code> works. Or use Named Releases to pin specific moments forever &mdash; those blobs never auto-purge.</p>
739731 </div>
740732 </div>
741733 </section>
742734
743735 <hr class="split">
744736
745737 <footer>
746738 <p><span class="accent">DriftSense</span> &middot; silent safety net for code + schema drift &middot; <span style="color:var(--gray)">customer guide draft, 2026-07-11</span></p>
747739 <p style="margin-top:6px">Companion to the in-product README &mdash; keep this doc alongside the tarball for the "how do I explain this to a lead?" moment.</p>
748740 </footer>
749741
750742</div>
751743</body>
752744</html>
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.
Cancel Logged to RESTORE_LOG regardless of outcome.