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/user_information.html |
| Site | DriftSense self-monitor on local |
| Kind | local |
| Captured at | 2026-07-11 23:02:35 |
| Captured SHA | 1a03114eaf376b2decc8630b8b7e3b11ee5b473749215034e1c5772808aacfbc |
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
| File present | yes |
| Size | 37455 bytes |
| Current SHA | f21d57b00996 |
| 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.
| 1 | 1 | <!doctype html> |
| 2 | 2 | <html lang="en"> |
| 3 | 3 | <head> |
| 4 | 4 | <meta charset="utf-8"> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | 6 | <title>DriftSense — How your customers use it</title> |
| 7 | 7 | <style> |
| 8 | 8 | :root { |
| 9 | 9 | --bg-0: #030710; |
| 10 | 10 | --bg-1: #0a1220; |
| 11 | 11 | --bg-2: #131e30; |
| 12 | 12 | --bg-3: #1a2740; |
| 13 | 13 | --bg-4: #223050; |
| 14 | 14 | --gray-hi: #cbd5e1; |
| 15 | 15 | --gray: #96a3b8; |
| 16 | 16 | --gray-lo: #6a7a94; |
| 17 | 17 | --border: #223050; |
| 18 | 18 | --border-hi: #2d3d5f; |
| 19 | 19 | --accent: #4c8bf5; /* dark-blue accent, not brand-teal, per Shawn's palette */ |
| 20 | 20 | --accent-hi: #6ea0ff; |
| 21 | 21 | --accent-lo: #1e3a8a; |
| 22 | 22 | --success: #34d399; |
| 23 | 23 | --warn: #f59e0b; |
| 24 | 24 | --danger: #f43f5e; |
| 25 | 25 | --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; |
| 26 | 26 | --sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| 27 | 27 | } |
| 28 | 28 | *,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; } |
| 29 | 29 | html { scroll-behavior: smooth; } |
| 30 | 30 | body { |
| 31 | 31 | background: var(--bg-0); |
| 32 | 32 | background-image: |
| 33 | 33 | radial-gradient(1200px 500px at 20% -10%, rgba(76,139,245,0.10), transparent 60%), |
| 34 | 34 | radial-gradient(900px 400px at 90% 10%, rgba(76,139,245,0.06), transparent 60%); |
| 35 | 35 | color: var(--gray-hi); |
| 36 | 36 | font-family: var(--sans); |
| 37 | 37 | line-height: 1.65; |
| 38 | 38 | -webkit-font-smoothing: antialiased; |
| 39 | 39 | min-height: 100vh; |
| 40 | 40 | } |
| 41 | 41 | .wrap { max-width: 1180px; margin: 0 auto; padding: 48px 32px 96px; } |
| 42 | 42 | |
| 43 | 43 | /* --- Header --------------------------------------------------------- */ |
| 44 | 44 | .brand { |
| 45 | 45 | display: inline-flex; align-items: center; gap: 12px; |
| 46 | 46 | padding: 6px 14px 6px 6px; |
| 47 | 47 | background: linear-gradient(90deg, var(--bg-2), var(--bg-1)); |
| 48 | 48 | border: 1px solid var(--border-hi); |
| 49 | 49 | border-radius: 999px; |
| 50 | 50 | margin-bottom: 24px; |
| 51 | 51 | } |
| 52 | 52 | .brand .logo { |
| 53 | 53 | width: 32px; height: 32px; border-radius: 50%; |
| 54 | 54 | background: linear-gradient(135deg, var(--accent), var(--accent-lo)); |
| 55 | 55 | display: grid; place-items: center; |
| 56 | 56 | font-weight: 800; font-size: 12px; color: #fff; |
| 57 | 57 | letter-spacing: -0.5px; |
| 58 | 58 | box-shadow: 0 0 0 1px rgba(76,139,245,0.35), 0 6px 16px rgba(76,139,245,0.25); |
| 59 | 59 | } |
| 60 | 60 | .brand .name { font-weight: 700; letter-spacing: -0.2px; } |
| 61 | 61 | .brand .name .accent { color: var(--accent-hi); } |
| 62 | 62 | .brand .eyebrow { color: var(--gray); font-size: 11.5px; letter-spacing: 1.4px; text-transform: uppercase; } |
| 63 | 63 | |
| 64 | 64 | h1.hero { |
| 65 | 65 | font-size: clamp(30px, 4.5vw, 48px); |
| 66 | 66 | line-height: 1.15; |
| 67 | 67 | letter-spacing: -0.8px; |
| 68 | 68 | color: #f4f7fb; |
| 69 | 69 | max-width: 20ch; |
| 70 | 70 | margin-bottom: 18px; |
| 71 | 71 | } |
| 72 | 72 | h1.hero em { color: var(--accent-hi); font-style: normal; } |
| 73 | 73 | .lede { |
| 74 | 74 | font-size: 17px; |
| 75 | 75 | color: var(--gray-hi); |
| 76 | 76 | max-width: 62ch; |
| 77 | 77 | margin-bottom: 32px; |
| 78 | 78 | } |
| 79 | 79 | .lede .strong { color: #fff; font-weight: 600; } |
| 80 | 80 | |
| 81 | 81 | /* --- Sections ------------------------------------------------------- */ |
| 82 | 82 | section { margin-top: 56px; } |
| 83 | 83 | .section-eyebrow { |
| 84 | 84 | display: inline-flex; align-items: center; gap: 8px; |
| 85 | 85 | color: var(--accent-hi); |
| 86 | 86 | font-size: 11px; font-weight: 700; |
| 87 | 87 | letter-spacing: 1.6px; text-transform: uppercase; |
| 88 | 88 | margin-bottom: 8px; |
| 89 | 89 | } |
| 90 | 90 | .section-eyebrow .dot { |
| 91 | 91 | width: 8px; height: 8px; border-radius: 50%; |
| 92 | 92 | background: var(--accent); |
| 93 | 93 | box-shadow: 0 0 12px var(--accent); |
| 94 | 94 | } |
| 95 | 95 | h2 { |
| 96 | 96 | font-size: 24px; |
| 97 | 97 | color: #f4f7fb; |
| 98 | 98 | letter-spacing: -0.3px; |
| 99 | 99 | margin-bottom: 20px; |
| 100 | 100 | } |
| 101 | 101 | h3 { font-size: 15px; color: #e2e8f0; letter-spacing: -0.1px; margin: 0 0 6px; } |
| 102 | 102 | p { color: var(--gray-hi); } |
| 103 | 103 | p + p { margin-top: 10px; } |
| 104 | 104 | |
| 105 | 105 | /* --- Cards ---------------------------------------------------------- */ |
| 106 | 106 | .card { |
| 107 | 107 | background: linear-gradient(180deg, var(--bg-2), var(--bg-1)); |
| 108 | 108 | border: 1px solid var(--border); |
| 109 | 109 | border-radius: 14px; |
| 110 | 110 | padding: 22px 24px; |
| 111 | 111 | transition: border-color .15s ease, transform .15s ease; |
| 112 | 112 | } |
| 113 | 113 | .card:hover { border-color: var(--border-hi); } |
| 114 | 114 | .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; } |
| 115 | 115 | .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; } |
| 116 | 116 | @media (max-width: 800px) { |
| 117 | 117 | .grid-2, .grid-3 { grid-template-columns: 1fr; } |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | 120 | /* --- Persona / audience row ---------------------------------------- */ |
| 121 | 121 | .persona { |
| 122 | 122 | display: flex; align-items: center; gap: 12px; |
| 123 | 123 | padding: 10px 14px; |
| 124 | 124 | background: var(--bg-2); |
| 125 | 125 | border: 1px solid var(--border); |
| 126 | 126 | border-radius: 12px; |
| 127 | 127 | font-size: 13.5px; |
| 128 | 128 | } |
| 129 | 129 | .persona .who { |
| 130 | 130 | padding: 3px 10px; |
| 131 | 131 | border-radius: 999px; |
| 132 | 132 | font-weight: 700; |
| 133 | 133 | font-size: 11.5px; |
| 134 | 134 | letter-spacing: 0.8px; |
| 135 | 135 | text-transform: uppercase; |
| 136 | 136 | background: rgba(76,139,245,0.14); |
| 137 | 137 | color: var(--accent-hi); |
| 138 | 138 | border: 1px solid rgba(76,139,245,0.35); |
| 139 | 139 | } |
| 140 | 140 | .persona .who.ops { background: rgba(52,211,153,0.14); color: #86efac; border-color: rgba(52,211,153,0.35); } |
| 141 | 141 | .persona .who.audit { background: rgba(245,158,11,0.14); color: #fcd34d; border-color: rgba(245,158,11,0.35); } |
| 142 | 142 | .persona .who.leader { background: rgba(244,63,94,0.14); color: #fda4af; border-color: rgba(244,63,94,0.35); } |
| 143 | 143 | |
| 144 | 144 | /* --- Steps ---------------------------------------------------------- */ |
| 145 | 145 | .steps { counter-reset: step; } |
| 146 | 146 | .step { |
| 147 | 147 | position: relative; |
| 148 | 148 | padding: 20px 22px 20px 78px; |
| 149 | 149 | background: var(--bg-2); |
| 150 | 150 | border: 1px solid var(--border); |
| 151 | 151 | border-left: 3px solid var(--accent); |
| 152 | 152 | border-radius: 12px; |
| 153 | 153 | margin-top: 14px; |
| 154 | 154 | } |
| 155 | 155 | .step::before { |
| 156 | 156 | counter-increment: step; |
| 157 | 157 | content: counter(step); |
| 158 | 158 | position: absolute; |
| 159 | 159 | left: 18px; top: 20px; |
| 160 | 160 | width: 44px; height: 44px; |
| 161 | 161 | display: grid; place-items: center; |
| 162 | 162 | background: linear-gradient(135deg, var(--accent), var(--accent-lo)); |
| 163 | 163 | color: #fff; font-weight: 800; |
| 164 | 164 | border-radius: 12px; |
| 165 | 165 | box-shadow: 0 4px 14px rgba(76,139,245,0.35); |
| 166 | 166 | font-size: 17px; |
| 167 | 167 | } |
| 168 | 168 | .step h3 { margin-bottom: 8px; } |
| 169 | 169 | .step p { color: var(--gray-hi); font-size: 14px; } |
| 170 | 170 | |
| 171 | 171 | /* --- Code blocks ---------------------------------------------------- */ |
| 172 | 172 | pre, code { |
| 173 | 173 | font-family: var(--mono); |
| 174 | 174 | font-size: 12.5px; |
| 175 | 175 | } |
| 176 | 176 | code.inline { |
| 177 | 177 | background: rgba(76,139,245,0.10); |
| 178 | 178 | color: var(--accent-hi); |
| 179 | 179 | padding: 1px 6px; |
| 180 | 180 | border-radius: 4px; |
| 181 | 181 | border: 1px solid rgba(76,139,245,0.20); |
| 182 | 182 | font-size: 12px; |
| 183 | 183 | } |
| 184 | 184 | pre { |
| 185 | 185 | background: var(--bg-0); |
| 186 | 186 | color: #d1e0f5; |
| 187 | 187 | border: 1px solid var(--border); |
| 188 | 188 | border-radius: 10px; |
| 189 | 189 | padding: 14px 18px; |
| 190 | 190 | overflow-x: auto; |
| 191 | 191 | margin-top: 12px; |
| 192 | 192 | line-height: 1.55; |
| 193 | 193 | } |
| 194 | 194 | pre .cmt { color: var(--gray-lo); } |
| 195 | 195 | pre .kw { color: var(--accent-hi); } |
| 196 | 196 | pre .str { color: #86efac; } |
| 197 | 197 | |
| 198 | 198 | /* --- Pills / meta --------------------------------------------------- */ |
| 199 | 199 | .pill { |
| 200 | 200 | display: inline-block; |
| 201 | 201 | padding: 3px 9px; |
| 202 | 202 | border-radius: 999px; |
| 203 | 203 | font-size: 11px; |
| 204 | 204 | font-weight: 700; |
| 205 | 205 | letter-spacing: 0.4px; |
| 206 | 206 | background: rgba(76,139,245,0.14); |
| 207 | 207 | color: var(--accent-hi); |
| 208 | 208 | border: 1px solid rgba(76,139,245,0.30); |
| 209 | 209 | } |
| 210 | 210 | .pill.ok { background: rgba(52,211,153,0.14); color: #86efac; border-color: rgba(52,211,153,0.30); } |
| 211 | 211 | .pill.warn { background: rgba(245,158,11,0.14); color: #fcd34d; border-color: rgba(245,158,11,0.30); } |
| 212 | 212 | |
| 213 | 213 | /* --- Table --------------------------------------------------------- */ |
| 214 | 214 | table { |
| 215 | 215 | width: 100%; |
| 216 | 216 | border-collapse: separate; |
| 217 | 217 | border-spacing: 0; |
| 218 | 218 | background: var(--bg-2); |
| 219 | 219 | border: 1px solid var(--border); |
| 220 | 220 | border-radius: 12px; |
| 221 | 221 | overflow: hidden; |
| 222 | 222 | font-size: 13.5px; |
| 223 | 223 | } |
| 224 | 224 | th, td { text-align: left; padding: 12px 16px; vertical-align: top; } |
| 225 | 225 | th { |
| 226 | 226 | background: var(--bg-1); |
| 227 | 227 | color: var(--gray); |
| 228 | 228 | font-size: 11.5px; |
| 229 | 229 | letter-spacing: 1.2px; |
| 230 | 230 | text-transform: uppercase; |
| 231 | 231 | font-weight: 700; |
| 232 | 232 | border-bottom: 1px solid var(--border); |
| 233 | 233 | } |
| 234 | 234 | td { border-bottom: 1px solid var(--border); color: var(--gray-hi); } |
| 235 | 235 | tr:last-child td { border-bottom: 0; } |
| 236 | 236 | td strong { color: #e2e8f0; } |
| 237 | 237 | |
| 238 | 238 | /* --- Callout ------------------------------------------------------- */ |
| 239 | 239 | .callout { |
| 240 | 240 | display: flex; gap: 14px; |
| 241 | 241 | background: linear-gradient(135deg, rgba(76,139,245,0.10), rgba(30,58,138,0.06)); |
| 242 | 242 | border: 1px solid rgba(76,139,245,0.30); |
| 243 | 243 | border-radius: 12px; |
| 244 | 244 | padding: 16px 18px; |
| 245 | 245 | margin-top: 18px; |
| 246 | 246 | } |
| 247 | 247 | .callout .ico { |
| 248 | 248 | flex: 0 0 auto; |
| 249 | 249 | width: 32px; height: 32px; |
| 250 | 250 | border-radius: 8px; |
| 251 | 251 | background: rgba(76,139,245,0.20); |
| 252 | 252 | display: grid; place-items: center; |
| 253 | 253 | color: var(--accent-hi); |
| 254 | 254 | font-weight: 800; |
| 255 | 255 | } |
| 256 | 256 | .callout p { color: var(--gray-hi); font-size: 13.5px; } |
| 257 | 257 | .callout .title { color: #fff; font-weight: 700; margin-bottom: 4px; } |
| 258 | 258 | |
| 259 | 259 | /* --- Feature stripe ------------------------------------------------- */ |
| 260 | 260 | .feature-icon { |
| 261 | 261 | width: 40px; height: 40px; |
| 262 | 262 | border-radius: 10px; |
| 263 | 263 | background: linear-gradient(135deg, rgba(76,139,245,0.20), rgba(30,58,138,0.10)); |
| 264 | 264 | border: 1px solid rgba(76,139,245,0.30); |
| 265 | 265 | display: grid; place-items: center; |
| 266 | 266 | color: var(--accent-hi); |
| 267 | 267 | margin-bottom: 12px; |
| 268 | 268 | } |
| 269 | 269 | |
| 270 | 270 | /* --- Nav bar -------------------------------------------------------- */ |
| 271 | 271 | nav.toc { |
| 272 | 272 | position: sticky; top: 12px; z-index: 10; |
| 273 | 273 | display: flex; flex-wrap: wrap; gap: 6px; |
| 274 | 274 | padding: 10px 12px; |
| 275 | 275 | background: rgba(3,7,16,0.75); |
| 276 | 276 | backdrop-filter: blur(10px); |
| 277 | 277 | -webkit-backdrop-filter: blur(10px); |
| 278 | 278 | border: 1px solid var(--border); |
| 279 | 279 | border-radius: 12px; |
| 280 | 280 | margin-top: 32px; |
| 281 | 281 | } |
| 282 | 282 | nav.toc a { |
| 283 | 283 | padding: 5px 11px; |
| 284 | 284 | border-radius: 999px; |
| 285 | 285 | color: var(--gray); |
| 286 | 286 | text-decoration: none; |
| 287 | 287 | font-size: 12px; |
| 288 | 288 | font-weight: 600; |
| 289 | 289 | border: 1px solid transparent; |
| 290 | 290 | transition: all .15s ease; |
| 291 | 291 | } |
| 292 | 292 | nav.toc a:hover { color: #fff; border-color: var(--border-hi); background: var(--bg-2); } |
| 293 | 293 | |
| 294 | 294 | hr.split { |
| 295 | 295 | border: 0; |
| 296 | 296 | height: 1px; |
| 297 | 297 | background: linear-gradient(90deg, transparent, var(--border-hi), transparent); |
| 298 | 298 | margin: 56px 0 0; |
| 299 | 299 | } |
| 300 | 300 | |
| 301 | 301 | footer { |
| 302 | 302 | margin-top: 72px; |
| 303 | 303 | padding-top: 24px; |
| 304 | 304 | border-top: 1px solid var(--border); |
| 305 | 305 | font-size: 12.5px; |
| 306 | 306 | color: var(--gray-lo); |
| 307 | 307 | } |
| 308 | 308 | footer .accent { color: var(--accent-hi); } |
| 309 | 309 | </style> |
| 310 | 310 | </head> |
| 311 | 311 | <body> |
| 312 | 312 | <div class="wrap"> |
| 313 | 313 | |
| 314 | 314 | <!-- ============================ HERO ============================ --> |
| 315 | 315 | <div class="brand"> |
| 316 | 316 | <div class="logo">DS</div> |
| 317 | 317 | <div> |
| 318 | 318 | <div class="name">Drift<span class="accent">Sense</span></div> |
| 319 | 319 | <div class="eyebrow">Customer guide · how it gets used</div> |
| 320 | 320 | </div> |
| 321 | 321 | </div> |
| 322 | 322 | |
| 323 | 323 | <h1 class="hero">The <em>silent safety net</em> for teams without git discipline.</h1> |
| 324 | 324 | <p class="lede"> |
| 325 | 325 | <span class="strong">DriftSense captures every file and schema change on your servers</span>, |
| 326 | 326 | keeps them content-addressable + gzipped in one storage DB, |
| 327 | 327 | lets you diff any two moments in time, and alerts you (Slack/Discord/webhook) |
| 328 | 328 | the second something drifts. It's a <span class="strong">standalone install</span> |
| 329 | 329 | you run inside your own infrastructure — no SaaS, no phone-home, no external |
| 330 | 330 | dependencies beyond core Perl and MariaDB. |
| 331 | 331 | </p> |
| 332 | 332 | |
| 333 | 333 | <nav class="toc"> |
| 334 | 334 | <a href="#what">1. What it is</a> |
| 335 | 335 | <a href="#who">2. Who uses it</a> |
| 336 | 336 | <a href="#getit">3. How they get it</a> |
| 337 | 337 | <a href="#install">4. Install (10 min)</a> |
| 338 | 338 | <a href="#setup">5. First-time setup</a> |
| 339 | 339 | <a href="#dayto">6. Day-to-day use</a> |
| 340 | 340 | <a href="#pricing">7. Delivery model</a> |
| 341 | 341 | <a href="#stealth">8. Stealth deployment</a> |
| 342 | 342 | <a href="#faq">9. FAQ</a> |
| 343 | 343 | </nav> |
| 344 | 344 | |
| 345 | 345 | <!-- ============================ WHAT ============================ --> |
| 346 | 346 | <section id="what"> |
| 347 | 347 | <div class="section-eyebrow"><span class="dot"></span> 01 · the product</div> |
| 348 | 348 | <h2>What DriftSense actually does</h2> |
| 349 | 349 | <div class="grid-3"> |
| 350 | 350 | <div class="card"> |
| 351 | 351 | <div class="feature-icon"> |
| 352 | 352 | <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> |
| 353 | 353 | </div> |
| 354 | 354 | <h3>Silent file capture</h3> |
| 355 | 355 | <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> |
| 356 | 356 | </div> |
| 357 | 357 | <div class="card"> |
| 358 | 358 | <div class="feature-icon"> |
| 359 | 359 | <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> |
| 360 | 360 | </div> |
| 361 | 361 | <h3>Silent schema capture</h3> |
| 362 | 362 | <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> |
| 363 | 363 | </div> |
| 364 | 364 | <div class="card"> |
| 365 | 365 | <div class="feature-icon"> |
| 366 | 366 | <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> |
| 367 | 367 | </div> |
| 368 | 368 | <h3>Diff viewer + timeline</h3> |
| 369 | 369 | <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> |
| 370 | 370 | </div> |
| 371 | 371 | <div class="card"> |
| 372 | 372 | <div class="feature-icon"> |
| 373 | 373 | <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> |
| 374 | 374 | </div> |
| 375 | 375 | <h3>Alert rules & webhooks</h3> |
| 376 | 376 | <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> |
| 377 | 377 | </div> |
| 378 | 378 | <div class="card"> |
| 379 | 379 | <div class="feature-icon"> |
| 380 | 380 | <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> |
| 381 | 381 | </div> |
| 382 | 382 | <h3>Compliance CSV export</h3> |
| 383 | 383 | <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> |
| 384 | 384 | </div> |
| 385 | 385 | <div class="card"> |
| 386 | 386 | <div class="feature-icon"> |
| 387 | 387 | <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> |
| 388 | 388 | </div> |
| 389 | 389 | <h3>Named releases</h3> |
| 390 | 390 | <p>Bookmark a moment ("before the migration", "prod v2.4 cutover") — DriftSense pins those blobs so auto-purge never touches them.</p> |
| 391 | 391 | </div> |
| 392 | 392 | </div> |
| 393 | 393 | </section> |
| 394 | 394 | |
| 395 | 395 | <!-- ============================ WHO ============================ --> |
| 396 | 396 | <section id="who"> |
| 397 | 397 | <div class="section-eyebrow"><span class="dot"></span> 02 · the buyers</div> |
| 398 | 398 | <h2>Who actually reaches for it</h2> |
| 399 | 399 | <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> |
| 400 | 400 | <div class="grid-3"> |
| 401 | 401 | <div class="card"> |
| 402 | 402 | <div class="persona"><span class="who ops">DevOps / SRE</span> Small shop, no git for /etc</div> |
| 403 | 403 | <h3 style="margin-top:14px">"I need to know what changed last night."</h3> |
| 404 | 404 | <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> |
| 405 | 405 | <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> |
| 406 | 406 | </div> |
| 407 | 407 | <div class="card"> |
| 408 | 408 | <div class="persona"><span class="who audit">Compliance / audit</span> Financial or medical</div> |
| 409 | 409 | <h3 style="margin-top:14px">"Prove nothing changed during the window."</h3> |
| 410 | 410 | <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> |
| 411 | 411 | <p><strong style="color:#fff">Primary usage:</strong> Compliance CSV export + Named releases pinning quarter-end snapshots.</p> |
| 412 | 412 | </div> |
| 413 | 413 | <div class="card"> |
| 414 | 414 | <div class="persona"><span class="who leader">Solo founder / dev</span> Ships production alone</div> |
| 415 | 415 | <h3 style="margin-top:14px">"I need a safety net for myself."</h3> |
| 416 | 416 | <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> |
| 417 | 417 | <p><strong style="color:#fff">Primary usage:</strong> File changes list + diff viewer. Occasional Slack alert on deletions.</p> |
| 418 | 418 | </div> |
| 419 | 419 | </div> |
| 420 | 420 | </section> |
| 421 | 421 | |
| 422 | 422 | <!-- ============================ GET IT ============================ --> |
| 423 | 423 | <section id="getit"> |
| 424 | 424 | <div class="section-eyebrow"><span class="dot"></span> 03 · acquisition</div> |
| 425 | 425 | <h2>How a customer gets DriftSense</h2> |
| 426 | 426 | <div class="grid-2"> |
| 427 | 427 | <div class="card"> |
| 428 | 428 | <h3>Buy → download → install</h3> |
| 429 | 429 | <p>Landing page → checkout (Stripe) → 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> |
| 430 | 430 | <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> |
| 431 | 431 | </div> |
| 432 | 432 | <div class="card"> |
| 433 | 433 | <h3>What ships in the tarball</h3> |
| 434 | 434 | <ul style="margin-left:20px;color:var(--gray-hi);font-size:14px;line-height:1.85"> |
| 435 | 435 | <li>All CGIs + Perl modules (~50 files)</li> |
| 436 | 436 | <li><code class="inline">_schema/</code> — v2 schema + alerts migration</li> |
| 437 | 437 | <li><code class="inline">_ops/drift_sense.cron</code> — cron template</li> |
| 438 | 438 | <li><code class="inline">_config/drift_sense.conf.example</code></li> |
| 439 | 439 | <li><code class="inline">assets/</code> — CSS, JS, images</li> |
| 440 | 440 | <li>Installer script (planned: <code class="inline">./install.sh</code>)</li> |
| 441 | 441 | </ul> |
| 442 | 442 | </div> |
| 443 | 443 | </div> |
| 444 | 444 | </section> |
| 445 | 445 | |
| 446 | 446 | <!-- ============================ INSTALL ============================ --> |
| 447 | 447 | <section id="install"> |
| 448 | 448 | <div class="section-eyebrow"><span class="dot"></span> 04 · install</div> |
| 449 | 449 | <h2>The 10-minute install</h2> |
| 450 | 450 | <p style="margin-bottom:12px">Assumes stock CentOS / Debian / Ubuntu / macOS with MariaDB or MySQL already running. Prereqs are boringly minimal on purpose:</p> |
| 451 | 451 | |
| 452 | 452 | <div class="callout"> |
| 453 | 453 | <div class="ico">i</div> |
| 454 | 454 | <div> |
| 455 | 455 | <div class="title">Prereqs (all standard on every mainstream Linux):</div> |
| 456 | 456 | <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> |
| 457 | 457 | </div> |
| 458 | 458 | </div> |
| 459 | 459 | |
| 460 | 460 | <div class="steps"> |
| 461 | 461 | <div class="step"> |
| 462 | 462 | <h3>Untar into a docroot</h3> |
| 463 | 463 | <p>Point the vhost at a fresh directory, drop the tarball, extract.</p> |
| 464 | 464 | <pre><span class="cmt"># pick any docroot</span> |
| 465 | 465 | <span class="kw">cd</span> /var/www/drift_sense |
| 466 | 466 | tar xzf ~/drift_sense-1.0.tar.gz --strip-components=1</pre> |
| 467 | 467 | </div> |
| 468 | 468 | |
| 469 | 469 | <div class="step"> |
| 470 | 470 | <h3>Create the DB + import the schema</h3> |
| 471 | 471 | <p>One DB, one user with all-privileges on it.</p> |
| 472 | 472 | <pre><span class="kw">mysql</span> -u root -p << SQL |
| 473 | 473 | CREATE DATABASE drift_sense CHARACTER SET utf8mb4; |
| 474 | 474 | GRANT ALL PRIVILEGES ON drift_sense.* TO 'drift_sense'@'localhost' |
| 475 | 475 | IDENTIFIED BY <span class="str">'change-me'</span>; |
| 476 | 476 | SQL |
| 477 | 477 | |
| 478 | 478 | <span class="kw">mysql</span> -u drift_sense -p drift_sense < _schema/drift_sense_v2_schema.sql |
| 479 | 479 | <span class="kw">mysql</span> -u drift_sense -p drift_sense < _schema/drift_sense_alerts.sql</pre> |
| 480 | 480 | </div> |
| 481 | 481 | |
| 482 | 482 | <div class="step"> |
| 483 | 483 | <h3>Write /etc/drift_sense/drift_sense.conf</h3> |
| 484 | 484 | <p>Copy the example, fill in DB creds and your public URL.</p> |
| 485 | 485 | <pre>db_engine=mysql |
| 486 | 486 | db_host=localhost |
| 487 | 487 | db_name=drift_sense |
| 488 | 488 | db_user=drift_sense |
| 489 | 489 | db_password=<span class="str">change-me</span> |
| 490 | 490 | public_url=<span class="str">https://drift.example.com</span></pre> |
| 491 | 491 | </div> |
| 492 | 492 | |
| 493 | 493 | <div class="step"> |
| 494 | 494 | <h3>Install the cron entry</h3> |
| 495 | 495 | <p>Two scanners + one dispatcher; cron.d convention keeps them isolated.</p> |
| 496 | 496 | <pre>cp _ops/drift_sense.cron /etc/cron.d/drift_sense |
| 497 | 497 | mkdir -p /var/log/drift_sense</pre> |
| 498 | 498 | </div> |
| 499 | 499 | |
| 500 | 500 | <div class="step"> |
| 501 | 501 | <h3>Point your browser at the vhost</h3> |
| 502 | 502 | <p>The dashboard renders on first visit. Empty because nothing is monitored yet — that's the setup step, next.</p> |
| 503 | 503 | </div> |
| 504 | 504 | </div> |
| 505 | 505 | </section> |
| 506 | 506 | |
| 507 | 507 | <!-- ============================ SETUP ============================ --> |
| 508 | 508 | <section id="setup"> |
| 509 | 509 | <div class="section-eyebrow"><span class="dot"></span> 05 · first-time setup</div> |
| 510 | 510 | <h2>Configure what to watch</h2> |
| 511 | 511 | <p style="margin-bottom:14px">Everything past install happens in the browser. Three configuration screens, all optional depending on what you care about.</p> |
| 512 | 512 | |
| 513 | 513 | <div class="grid-2"> |
| 514 | 514 | <div class="card"> |
| 515 | 515 | <h3>File monitors <span class="pill">/file_monitors.cgi</span></h3> |
| 516 | 516 | <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 — unchanged files skip hashing.</p> |
| 517 | 517 | </div> |
| 518 | 518 | <div class="card"> |
| 519 | 519 | <h3>Database monitors <span class="pill">/databases.cgi</span></h3> |
| 520 | 520 | <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> |
| 521 | 521 | </div> |
| 522 | 522 | <div class="card"> |
| 523 | 523 | <h3>Alert rules <span class="pill">/alerts.cgi</span></h3> |
| 524 | 524 | <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> |
| 525 | 525 | </div> |
| 526 | 526 | <div class="card"> |
| 527 | 527 | <h3>Named releases <span class="pill">/named_releases.cgi</span></h3> |
| 528 | 528 | <p>Bookmark a moment ("v2.4 prod cutover") and DriftSense pins every blob referenced by that moment. Auto-purge never touches pinned blobs — the point-in-time state is restorable forever.</p> |
| 529 | 529 | </div> |
| 530 | 530 | </div> |
| 531 | 531 | </section> |
| 532 | 532 | |
| 533 | 533 | <!-- ============================ DAY TO DAY ============================ --> |
| 534 | 534 | <section id="dayto"> |
| 535 | 535 | <div class="section-eyebrow"><span class="dot"></span> 06 · daily use</div> |
| 536 | 536 | <h2>What the customer actually does</h2> |
| 537 | 537 | <p style="margin-bottom:16px">DriftSense is deliberately low-touch. Most weeks the customer never opens the UI — 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> |
| 538 | 538 | |
| 539 | 539 | <table> |
| 540 | 540 | <thead><tr> |
| 541 | 541 | <th style="width:24%">Page</th> |
| 542 | 542 | <th style="width:34%">What they do there</th> |
| 543 | 543 | <th style="width:22%">How often</th> |
| 544 | 544 | <th>Trigger</th> |
| 545 | 545 | </tr></thead> |
| 546 | 546 | <tbody> |
| 547 | 547 | <tr> |
| 548 | 548 | <td><strong>Dashboard</strong><br><code class="inline">/dashboard.cgi</code></td> |
| 549 | 549 | <td>Glance at the 90-day activity timeline, spot spikes, click a busy day to drill in.</td> |
| 550 | 550 | <td><span class="pill ok">Once a week</span></td> |
| 551 | 551 | <td>Curiosity / weekly triage</td> |
| 552 | 552 | </tr> |
| 553 | 553 | <tr> |
| 554 | 554 | <td><strong>File changes</strong><br><code class="inline">/file_changes.cgi</code></td> |
| 555 | 555 | <td>Filter by date range or path, click a row to see the unified diff of what changed.</td> |
| 556 | 556 | <td><span class="pill">On demand</span></td> |
| 557 | 557 | <td>"Did nginx config change?"</td> |
| 558 | 558 | </tr> |
| 559 | 559 | <tr> |
| 560 | 560 | <td><strong>Diff viewer</strong><br><code class="inline">/diff.cgi</code></td> |
| 561 | 561 | <td>Read the before/after content of a single change. Restore by copy-paste if needed.</td> |
| 562 | 562 | <td><span class="pill">On demand</span></td> |
| 563 | 563 | <td>Slack alert click-through</td> |
| 564 | 564 | </tr> |
| 565 | 565 | <tr> |
| 566 | 566 | <td><strong>Alerts</strong><br><code class="inline">/alerts.cgi</code></td> |
| 567 | 567 | <td>Add or tune rules, check delivery history, verify webhooks are still landing.</td> |
| 568 | 568 | <td><span class="pill warn">Rarely</span></td> |
| 569 | 569 | <td>New watch target</td> |
| 570 | 570 | </tr> |
| 571 | 571 | <tr> |
| 572 | 572 | <td><strong>Compliance export</strong><br><code class="inline">/export.cgi</code></td> |
| 573 | 573 | <td>Pick a date window, download CSV. Send to auditor.</td> |
| 574 | 574 | <td><span class="pill warn">Quarterly</span></td> |
| 575 | 575 | <td>Audit request</td> |
| 576 | 576 | </tr> |
| 577 | 577 | <tr> |
| 578 | 578 | <td><strong>Named releases</strong><br><code class="inline">/named_releases.cgi</code></td> |
| 579 | 579 | <td>Bookmark a state before a risky change; look at old bookmarks after the fact.</td> |
| 580 | 580 | <td><span class="pill warn">Per release</span></td> |
| 581 | 581 | <td>Change management</td> |
| 582 | 582 | </tr> |
| 583 | 583 | </tbody> |
| 584 | 584 | </table> |
| 585 | 585 | </section> |
| 586 | 586 | |
| 587 | 587 | <!-- ============================ PRICING ============================ --> |
| 588 | 588 | <section id="pricing"> |
| 589 | 589 | <div class="section-eyebrow"><span class="dot"></span> 07 · delivery model</div> |
| 590 | 590 | <h2>How it's sold + supported</h2> |
| 591 | 591 | <div class="grid-2"> |
| 592 | 592 | <div class="card"> |
| 593 | 593 | <h3>Ownership: fully theirs</h3> |
| 594 | 594 | <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> — the whole point of a self-hosted product.</p> |
| 595 | 595 | </div> |
| 596 | 596 | <div class="card"> |
| 597 | 597 | <h3>Update model: opt-in</h3> |
| 598 | 598 | <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 — there's no forced upgrade cycle.</p> |
| 599 | 599 | </div> |
| 600 | 600 | <div class="card"> |
| 601 | 601 | <h3>Pricing shape (candidates)</h3> |
| 602 | 602 | <ul style="margin-left:20px;color:var(--gray-hi);font-size:13.5px;line-height:1.9"> |
| 603 | 603 | <li><strong style="color:#fff">One-time</strong> — $199 per server, ships with 1 yr of updates</li> |
| 604 | 604 | <li><strong style="color:#fff">Subscription</strong> — $12/mo per server, updates included forever</li> |
| 605 | 605 | <li><strong style="color:#fff">Portfolio</strong> — $499/yr flat for up to 10 servers</li> |
| 606 | 606 | </ul> |
| 607 | 607 | <p style="margin-top:10px;font-size:12.5px;color:var(--gray)">Placeholder numbers — final pricing lives on the sales page.</p> |
| 608 | 608 | </div> |
| 609 | 609 | <div class="card"> |
| 610 | 610 | <h3>Support</h3> |
| 611 | 611 | <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> |
| 612 | 612 | </div> |
| 613 | 613 | </div> |
| 614 | 614 | </section> |
| 615 | 615 | |
| 616 | 616 | <!-- ============================ STEALTH ============================ --> |
| 617 | 617 | <section id="stealth"> |
| 618 | 618 | <div class="section-eyebrow"><span class="dot"></span> 08 · stealth deployment</div> |
| 619 | 619 | <h2>Running invisibly to the developers</h2> |
| 620 | 620 | <p style="margin-bottom:14px"> |
| 621 | 621 | A common ask from sys-admins in shops without code review: |
| 622 | 622 | "I want DriftSense as a code + config backup, but I don't want the developers messing with it." |
| 623 | 623 | Two flavors, ordered by paranoia level. Both are supported by the same install — just configured differently. |
| 624 | 624 | </p> |
| 625 | 625 | |
| 626 | 626 | <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))"> |
| 627 | 627 | <div class="ico" style="background:rgba(245,158,11,0.20);color:#fcd34d">!</div> |
| 628 | 628 | <div> |
| 629 | 629 | <div class="title">Honest about the threat model</div> |
| 630 | 630 | <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> |
| 631 | 631 | </div> |
| 632 | 632 | </div> |
| 633 | 633 | |
| 634 | 634 | <div class="grid-2" style="margin-top:22px"> |
| 635 | 635 | <div class="card"> |
| 636 | 636 | <h3><span class="pill">Flavor 1</span> Same-server, obscured</h3> |
| 637 | 637 | <p style="margin-top:8px">DriftSense stays on the server it's monitoring, but nothing screams "audit tool":</p> |
| 638 | 638 | <ul style="margin:10px 0 0 20px;color:var(--gray-hi);font-size:13.5px;line-height:1.85"> |
| 639 | 639 | <li>Install to <code class="inline">/opt/sys_audit_log/</code> instead of <code class="inline">/var/www/drift_sense/</code></li> |
| 640 | 640 | <li>UI mounted at a bastion-only URL (not the public domain)</li> |
| 641 | 641 | <li>Apache <code class="inline">Allow from <sysadmin-IP></code> in front of every CGI</li> |
| 642 | 642 | <li>Cron entries in <strong>root's crontab</strong>, not <code class="inline">/etc/cron.d/</code> (less-browsed location)</li> |
| 643 | 643 | <li>Innocuous DB name (<code class="inline">system_metrics</code> instead of <code class="inline">drift_sense</code>)</li> |
| 644 | 644 | <li>Alert deliveries to sysadmin-private Slack workspace or personal email</li> |
| 645 | 645 | <li>Add DriftSense's own dir to every FILE_MONITOR ignore list so it doesn't watch itself</li> |
| 646 | 646 | </ul> |
| 647 | 647 | <p style="margin-top:10px;color:var(--gray);font-size:12.5px">Blocks casual discovery. Doesn't block a determined root actor.</p> |
| 648 | 648 | </div> |
| 649 | 649 | <div class="card"> |
| 650 | 650 | <h3><span class="pill ok">Flavor 2</span> Bastion agent-mode (true stealth)</h3> |
| 651 | 651 | <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> |
| 652 | 652 | <ul style="margin:10px 0 0 20px;color:var(--gray-hi);font-size:13.5px;line-height:1.85"> |
| 653 | 653 | <li>Bastion runs the code, DB, cron, dashboard — all invisible from monitored server</li> |
| 654 | 654 | <li>Scanner SSHes into monitored server with a <em>restricted</em> read-only key</li> |
| 655 | 655 | <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> |
| 656 | 656 | <li>SSH sessions show in <code class="inline">sshd</code> logs but look like ordinary backup connections</li> |
| 657 | 657 | <li>Multiple monitored servers into one central bastion — portfolio view for free</li> |
| 658 | 658 | </ul> |
| 659 | 659 | </div> |
| 660 | 660 | </div> |
| 661 | 661 | |
| 662 | 662 | <h3 style="margin-top:28px;color:#e2e8f0">Apache allowlist snippet (Flavor 1)</h3> |
| 663 | 663 | <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> |
| 664 | 664 | <pre><span class="cmt"># Only sysadmin's IP + office VPN can reach the UI</span> |
| 665 | 665 | <span class="kw"><RequireAll></span> |
| 666 | 666 | <span class="kw">Require</span> ip 203.0.113.42 |
| 667 | 667 | <span class="kw">Require</span> ip 10.20.30.0/24 |
| 668 | 668 | <span class="kw"></RequireAll></span> |
| 669 | 669 | |
| 670 | 670 | <span class="cmt"># Optional -- return 404 (not 403) so scanners see "nothing here" not "protected page"</span> |
| 671 | 671 | ErrorDocument 403 <span class="str">"/404.html"</span></pre> |
| 672 | 672 | |
| 673 | 673 | <h3 style="margin-top:28px;color:#e2e8f0">SSH key restriction (Flavor 2)</h3> |
| 674 | 674 | <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 — the key can only run the specific scan commands, nothing else:</p> |
| 675 | 675 | <pre><span class="cmt"># /root/.ssh/authorized_keys on the monitored server</span> |
| 676 | 676 | command=<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 |
| 677 | 677 | |
| 678 | 678 | <span class="cmt"># /root/.ssh/drift_agent_wrapper.sh -- only allow the scanner's commands</span> |
| 679 | 679 | <span class="kw">#!/bin/bash</span> |
| 680 | 680 | <span class="kw">case</span> <span class="str">"$SSH_ORIGINAL_COMMAND"</span> <span class="kw">in</span> |
| 681 | 681 | <span class="str">"drift-find "*</span>) <span class="kw">exec</span> find ${SSH_ORIGINAL_COMMAND#drift-find } -type f -not -path <span class="str">'*/.*'</span> -printf <span class="str">'%p\t%s\t%T@\n'</span> ;; |
| 682 | 682 | <span class="str">"drift-hash "*</span>) <span class="kw">exec</span> sha256sum ${SSH_ORIGINAL_COMMAND#drift-hash } ;; |
| 683 | 683 | <span class="str">"drift-read "*</span>) <span class="kw">exec</span> cat ${SSH_ORIGINAL_COMMAND#drift-read } ;; |
| 684 | 684 | <span class="str">*</span>) <span class="kw">exit</span> 1 ;; |
| 685 | 685 | <span class="kw">esac</span></pre> |
| 686 | 686 | |
| 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 — 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> | |
| 689 | 689 | <pre><span class="kw">INSERT INTO</span> SERVERS (server_name, kind, ssh_host, ssh_user, ssh_port, ssh_key_path, status) |
| 690 | 690 | <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, |
| 691 | 691 | <span class="str">'/etc/drift_sense/keys/agent_key'</span>, <span class="str">'active'</span>); |
| 692 | 692 | |
| 693 | 693 | <span class="kw">INSERT INTO</span> FILE_MONITOR_SETTINGS |
| 694 | 694 | (server_id, scan_path, ignore_list, file_type_filter, status, scan_name) |
| 695 | 695 | <span class="kw">VALUES</span> |
| 696 | 696 | (LAST_INSERT_ID(), <span class="str">'/etc'</span>, <span class="str">'*.log,*.pid,*.sock'</span>, <span class="str">''</span>, 1, |
| 697 | 697 | <span class="str">'prod-web-1 /etc config drift'</span>);</pre> |
| 698 | 698 | <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> — 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. | |
| 700 | 700 | </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">✓</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> | |
| 709 | 701 | </section> |
| 710 | 702 | |
| 711 | 703 | <!-- ============================ FAQ ============================ --> |
| 712 | 704 | <section id="faq"> |
| 713 | 705 | <div class="section-eyebrow"><span class="dot"></span> 09 · faq</div> |
| 714 | 706 | <h2>What people ask before buying</h2> |
| 715 | 707 | <div class="grid-2"> |
| 716 | 708 | <div class="card"> |
| 717 | 709 | <h3>Isn't this just git?</h3> |
| 718 | 710 | <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 — live edits, cron jobs writing config, someone SSHing in at 3 AM. Both can coexist happily.</p> |
| 719 | 711 | </div> |
| 720 | 712 | <div class="card"> |
| 721 | 713 | <h3>Does it slow the server down?</h3> |
| 722 | 714 | <p>File scanner uses mtime-first, so unchanged files never get hashed — 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> |
| 723 | 715 | </div> |
| 724 | 716 | <div class="card"> |
| 725 | 717 | <h3>How much disk does it use?</h3> |
| 726 | 718 | <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> |
| 727 | 719 | </div> |
| 728 | 720 | <div class="card"> |
| 729 | 721 | <h3>What about containers?</h3> |
| 730 | 722 | <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> |
| 731 | 723 | </div> |
| 732 | 724 | <div class="card"> |
| 733 | 725 | <h3>Multi-server?</h3> |
| 734 | 726 | <p>Central DB, agent binary per server. Central UI shows every host in one view. Included in the product — per-server licensing is on the honor system.</p> |
| 735 | 727 | </div> |
| 736 | 728 | <div class="card"> |
| 737 | 729 | <h3>Backup / restore?</h3> |
| 738 | 730 | <p>Standard <code class="inline">mysqldump</code> works. Or use Named Releases to pin specific moments forever — those blobs never auto-purge.</p> |
| 739 | 731 | </div> |
| 740 | 732 | </div> |
| 741 | 733 | </section> |
| 742 | 734 | |
| 743 | 735 | <hr class="split"> |
| 744 | 736 | |
| 745 | 737 | <footer> |
| 746 | 738 | <p><span class="accent">DriftSense</span> · silent safety net for code + schema drift · <span style="color:var(--gray)">customer guide draft, 2026-07-11</span></p> |
| 747 | 739 | <p style="margin-top:6px">Companion to the in-product README — keep this doc alongside the tarball for the "how do I explain this to a lead?" moment.</p> |
| 748 | 740 | </footer> |
| 749 | 741 | |
| 750 | 742 | </div> |
| 751 | 743 | </body> |
| 752 | 744 | </html> |
Confirm restore
Backs current content to <target>.drift_restore_backup_<epoch>, writes the captured content, verifies SHA post-write.