modified on local at 2026-07-12 23:13:03
| 1 | 1 | <div class="page-head"> |
| 2 | 2 | <span class="page-eyebrow"><span class="dot"></span> Bookmark</span> |
| 3 | 3 | <h1 class="page-title">Named releases</h1> |
| 4 | 4 | <p class="page-subtitle">Retroactively bookmark a moment. "Before the schema refactor." "Customer verified this worked." "Before I fired the intern." Every blob referenced by a named release is protected from auto-purge, so restore-to-moment always works — scoped portfolio-wide or to a single site.</p> |
| 5 | 5 | </div> |
| 6 | 6 | |
| 7 | 7 | <div class="ds-guide"> |
| 8 | 8 | <div class="dsg-icon">i</div> |
| 9 | 9 | <div> |
| 10 | 10 | <span class="dsg-title">Three ways to pin content:</span> |
| 11 | 11 | (1) <strong>Portfolio-wide</strong>: leave scope as "All monitored sites" — protects every blob captured up to <em>now</em>. (2) <strong>Per-site</strong>: pick a monitor — only that site's blobs are pinned. (3) <strong>Explicit pins</strong><span class="ds-help" data-help-title="Explicit pins" data-help-body="Instead of pinning "everything captured before this moment", explicit pins tie a release to specific <code>NAMED_RELEASE_PINS</code> rows — each pointing at a chosen <code>file_changes_id</code>. Auto-purge always keeps those specific blobs. Use <a href='/pin_hotspots.cgi'>Pin hotspots</a> to create one." data-help-link="/pin_hotspots.cgi" data-help-link-label="Pin hotspots"></span>: use <a href="/pin_hotspots.cgi">Pin hotspots</a> to freeze the current-latest blobs of the churniest files. Set a <strong>Drift alert URL</strong> below and DriftSense will page you when any pinned file's current SHA no longer matches its pinned SHA. |
| 12 | 12 | </div> |
| 13 | 13 | </div> |
| 14 | 14 | |
| 15 | 15 | <div class="module glow-emerald" style="margin-bottom:16px"> |
| 16 | 16 | <div class="module-head"><div class="left"> |
| 17 | 17 | <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" 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></div> |
| 18 | 18 | <div class="module-title">Create a bookmark</div> |
| 19 | 19 | <span class="module-subtitle">Names the current moment. Everything captured up to now becomes restorable from this label forever.</span> |
| 20 | 20 | </div></div> |
| 21 | 21 | <div class="module-body"> |
| 22 | 22 | <form method="post" action="/named_releases.cgi" style="display:flex;flex-direction:column;gap:12px"> |
| 23 | 23 | <input type="hidden" name="save" value="1"> |
| 24 | 24 | |
| 25 | 25 | <div style="display:flex;gap:12px"> |
| 26 | 26 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 27 | 27 | <span class="rb-label">Internal name</span> |
| 28 | 28 | <input type="text" name="name" required placeholder="e.g. before-schema-refactor" |
| 29 | 29 | 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:12.5px"> |
| 30 | 30 | </label> |
| 31 | 31 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 32 | 32 | <span class="rb-label">Version label |
| 33 | 33 | <span class="ds-help" data-help-title="What's a version label?" data-help-body="Display name shown across the UI (e.g. <code>v1.0</code>, <code>working v2.4</code>, <code>stable Nov-2026</code>). If left blank we fall back to the internal name."></span> |
| 34 | 34 | </span> |
| 35 | 35 | <input type="text" name="version_label" placeholder="e.g. v1.0 or Working 2.4" |
| 36 | 36 | 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:12.5px"> |
| 37 | 37 | </label> |
| 38 | 38 | <label style="width:150px;display:flex;flex-direction:column;gap:4px"> |
| 39 | 39 | <span class="rb-label">Tier |
| 40 | 40 | <span class="ds-help" data-help-title="Tier decides how visible this release is" data-help-body="<strong>Draft</strong> = a plain bookmark. <strong>Working</strong> = a milestone you've verified but might revise. <strong>Stable</strong> = production-ready, elevated in the sidebar and dashboard, one-click rollback target."></span> |
| 41 | 41 | </span> |
| 42 | 42 | <select name="tier" style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12.5px"> |
| 43 | 43 | <option value="draft">Draft</option> |
| 44 | 44 | <option value="working">Working</option> |
| 45 | 45 | <option value="stable">Stable</option> |
| 46 | 46 | </select> |
| 47 | 47 | </label> |
| 48 | 48 | <label style="width:140px;display:flex;flex-direction:column;gap:4px"> |
| 49 | 49 | <span class="rb-label">By</span> |
| 50 | 50 | <input type="text" name="released_by" value="Operator" |
| 51 | 51 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12.5px"> |
| 52 | 52 | </label> |
| 53 | 53 | <label style="width:170px;display:flex;flex-direction:column;gap:4px"> |
| 54 | 54 | <span class="rb-label">Auto-promote to stable |
| 55 | 55 | <span class="ds-help" data-help-title="Auto-promote-to-stable" data-help-body="Only meaningful for <strong>Working</strong>-tier releases. When this many days pass with (a) zero new drift on any pinned file AND (b) compile-check still green, the hourly cron promotes this release to Stable automatically. Set 0 to disable."></span> |
| 56 | 56 | </span> |
| 57 | 57 | <input type="number" name="auto_promote_stable_days" min="0" max="365" value="0" |
| 58 | 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:12.5px"> |
| 59 | 59 | </label> |
| 60 | 60 | </div> |
| 61 | 61 | |
| 62 | 62 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 63 | 63 | <span class="rb-label">Description</span> |
| 64 | 64 | <input type="text" name="description" placeholder="Optional context -- what this checkpoint represents" |
| 65 | 65 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12.5px"> |
| 66 | </label> | |
| 67 | ||
| 68 | <label style="display:flex;flex-direction:column;gap:4px"> | |
| 69 | <span class="rb-label">Annotation | |
| 70 | <span class="ds-help" data-help-title="Annotation" data-help-body="Long-form note about this release — known issues, why it was cut, rollback plan, etc. Shown in the release list and via tooltip in the sidebar. Multi-line OK, capped at 4000 chars."></span> | |
| 71 | <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">(optional, up to 4000 chars)</span> | |
| 72 | </span> | |
| 73 | <textarea name="annotation" rows="3" placeholder="Known issues, why cut, rollback plan..." | |
| 74 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--sans);font-size:12.5px;resize:vertical;min-height:64px"></textarea> | |
| 66 | 75 | </label> |
| 67 | 76 | |
| 68 | 77 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 69 | 78 | <span class="rb-label">Scope <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">— primary site (leave "All" for portfolio-wide)</span></span> |
| 70 | 79 | <select name="scope_monitor_id" style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12.5px"> |
| 71 | 80 | <option value="0">All monitored sites (portfolio-wide)</option> |
| 72 | 81 | [loop:@monitors] |
| 73 | 82 | <option value="$mid">$label</option> |
| 74 | 83 | [/loop] |
| 75 | 84 | </select> |
| 76 | 85 | </label> |
| 77 | 86 | |
| 78 | 87 | <label style="display:flex;flex-direction:column;gap:4px"> |
| 79 | 88 | <span class="rb-label">Additional scopes |
| 80 | 89 | <span class="ds-help" data-help-title="Cross-scope releases" data-help-body="A release can cover multiple sites at once. Ctrl+click / Cmd+click to add extra monitors. Great for "stable v1.0 across PTMatrix + AffSoft + ContactForge" without creating three separate releases."></span> |
| 81 | 90 | <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">— Ctrl/Cmd-click to add more (optional)</span> |
| 82 | 91 | </span> |
| 83 | 92 | <select name="extra_monitor_ids" multiple size="4" |
| 84 | 93 | style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12.5px"> |
| 85 | 94 | [loop:@monitors] |
| 86 | 95 | <option value="$mid">$label</option> |
| 87 | 96 | [/loop] |
| 88 | 97 | </select> |
| 89 | 98 | </label> |
| 90 | 99 | |
| 91 | 100 | <div style="display:flex;gap:12px"> |
| 92 | 101 | <label style="flex:1;display:flex;flex-direction:column;gap:4px"> |
| 93 | 102 | <span class="rb-label">Drift alerts <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">(optional)</span></span> |
| 94 | 103 | <select name="alert_delivery_kind" style="background:var(--bg-1);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12.5px"> |
| 95 | 104 | <option value="none">Off</option> |
| 96 | 105 | <option value="generic_webhook">Generic HTTP webhook</option> |
| 97 | 106 | <option value="slack">Slack webhook</option> |
| 98 | 107 | <option value="discord">Discord webhook</option> |
| 99 | 108 | </select> |
| 100 | 109 | </label> |
| 101 | 110 | <label style="flex:2;display:flex;flex-direction:column;gap:4px"> |
| 102 | 111 | <span class="rb-label">Delivery URL <span class="dim" style="text-transform:none;letter-spacing:0;font-size:10.5px">— fires when any pinned file drifts from its pinned SHA</span></span> |
| 103 | 112 | <input type="text" name="alert_delivery_url" placeholder="https://hooks.slack.com/... (leave blank to disable)" |
| 104 | 113 | 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"> |
| 105 | 114 | </label> |
| 106 | 115 | </div> |
| 107 | 116 | |
| 108 | 117 | <div style="display:flex;justify-content:flex-end"> |
| 109 | 118 | <button type="submit" class="tr-chip is-active" style="border:none;padding:8px 18px">Save bookmark</button> |
| 110 | 119 | </div> |
| 111 | 120 | </form> |
| 112 | 121 | </div> |
| 113 | 122 | </div> |
| 114 | 123 | |
| 115 | 124 | <div class="module glow-teal"> |
| 116 | 125 | <div class="module-head"><div class="left"> |
| 117 | 126 | <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg></div> |
| 118 | 127 | <div class="module-title">All bookmarks</div> |
| 119 | 128 | <span class="module-subtitle">$total bookmarks. Protected ones (default) survive auto-purge; unlocked ones are eligible for cleanup after the retention window.</span> |
| 120 | 129 | </div></div> |
| 121 | 130 | <div class="module-body tight"> |
| 122 | 131 | [if:$has_rows] |
| 123 | 132 | <table class="tbl"> |
| 124 | 133 | <thead><tr> |
| 125 | 134 | <th>Version</th> |
| 126 | 135 | <th style="width:90px">Tier</th> |
| 127 | 136 | <th style="width:140px">Scope</th> |
| 128 | 137 | <th style="width:130px">Moment</th> |
| 129 | 138 | <th style="width:260px">Actions</th> |
| 130 | 139 | </tr></thead> |
| 131 | 140 | <tbody> |
| 132 | 141 | [loop:@rows] |
| 133 | 142 | <tr> |
| 134 | 143 | <td> |
| 135 | 144 | <strong>$display_label</strong> |
| 136 | 145 | <div class="dim" style="font-size:11.5px;margin-top:2px">$description</div> |
| 146 | [if:$has_annotation]<details style="margin-top:4px"><summary style="font-size:10.5px;color:var(--accent-hi);cursor:pointer;user-select:none">Annotation</summary><pre style="margin:4px 0 0;padding:8px 10px;background:var(--bg-2);border-radius:6px;font-family:var(--sans);font-size:11.5px;color:var(--text-dim);white-space:pre-wrap;max-width:560px">$annotation</pre></details>[/if] | |
| 137 | 147 | <div class="mono dim" style="font-size:10.5px;margin-top:2px">by $released_by · internal: $name</div> |
| 138 | 148 | </td> |
| 139 | 149 | <td> |
| 140 | 150 | <span class="pill $tier_pill">$tier_lbl</span> |
| 141 | 151 | [if:$has_auto_promote][if:!$is_stable]<div class="dim" style="font-size:10.5px;margin-top:3px" title="auto-promotes to stable after $auto_promote_days days with no drift">[if:$ap_ready]<span style="color:var(--accent-ok)">auto→stable ready</span>[/if][if:!$ap_ready]auto in $ap_days_left d[/if]</div>[/if][/if] |
| 142 | 152 | </td> |
| 143 | 153 | <td> |
| 144 | 154 | <span class="pill $scope_pill" style="font-size:10px">$scope_lbl</span> |
| 145 | 155 | <div class="mono dim" style="font-size:10.5px;margin-top:3px">$scope_h</div> |
| 146 | 156 | </td> |
| 147 | 157 | <td class="mono dim"><span class="ts" data-ts="$released_epoch" data-fmt="datetime">$released_h</span></td> |
| 148 | 158 | <td> |
| 149 | 159 | <a href="$restore_url" class="tr-chip" style="padding:3px 10px;font-size:11px">Restore all</a> |
| 150 | 160 | [if:!$is_stable] |
| 151 | 161 | <form method="post" action="/named_releases.cgi" style="display:inline"> |
| 152 | 162 | <input type="hidden" name="promote" value="1"> |
| 153 | 163 | <input type="hidden" name="release_id" value="$id"> |
| 154 | 164 | <input type="hidden" name="to_tier" value="stable"> |
| 155 | 165 | <button type="submit" class="tr-chip" style="padding:3px 10px;font-size:11px;background:rgba(52,211,153,.14);color:#86efac;border-color:rgba(52,211,153,.35)">↑ Mark stable</button> |
| 156 | 166 | </form> |
| 157 | 167 | [/if] |
| 158 | 168 | <button type="button" onclick="dsTestAlert($id, this)" class="tr-chip" style="padding:3px 10px;font-size:11px" title="Send synthetic drift payload">Test alert</button> |
| 159 | 169 | [if:$is_stable] |
| 160 | 170 | <form method="post" action="/named_releases.cgi" style="display:inline"> |
| 161 | 171 | <input type="hidden" name="promote" value="1"> |
| 162 | 172 | <input type="hidden" name="release_id" value="$id"> |
| 163 | 173 | <input type="hidden" name="to_tier" value="working"> |
| 164 | 174 | <button type="submit" class="tr-chip" style="padding:3px 10px;font-size:11px">↓ Working</button> |
| 165 | 175 | </form> |
| 166 | 176 | [/if] |
| 167 | 177 | </td> |
| 168 | 178 | </tr> |
| 169 | 179 | [/loop] |
| 170 | 180 | </tbody> |
| 171 | 181 | </table> |
| 172 | 182 | [/if] |
| 173 | 183 | [if:!$has_rows] |
| 174 | 184 | <div style="padding:36px 14px;text-align:center;color:var(--text-dim);font-size:13.5px"> |
| 175 | 185 | No bookmarks yet. Save your first one above. |
| 176 | 186 | </div> |
| 177 | 187 | [/if] |
| 178 | 188 | </div> |
| 179 | 189 | </div> |
| 180 | 190 | |
| 181 | 191 | <script> |
| 182 | 192 | window.dsTestAlert = async function (id, btn) { |
| 183 | 193 | var orig = btn.textContent; |
| 184 | 194 | btn.textContent = 'Sending...'; |
| 185 | 195 | btn.disabled = true; |
| 186 | 196 | try { |
| 187 | 197 | var r = await fetch('/named_releases.cgi?op=test_alert&release_id=' + id, { cache: 'no-store' }); |
| 188 | 198 | var j = await r.json(); |
| 189 | 199 | if (j.ok) { |
| 190 | 200 | btn.textContent = 'Sent ✓'; |
| 191 | 201 | btn.style.background = 'rgba(52,211,153,.14)'; |
| 192 | 202 | btn.style.color = '#86efac'; |
| 193 | 203 | btn.style.borderColor = 'rgba(52,211,153,.35)'; |
| 194 | 204 | alert('Test alert delivered\nHTTP ' + j.http_code); |
| 195 | 205 | } else { |
| 196 | 206 | btn.textContent = 'FAIL'; |
| 197 | 207 | btn.style.background = 'rgba(244,63,94,.14)'; |
| 198 | 208 | btn.style.color = '#fda4af'; |
| 199 | 209 | btn.style.borderColor = 'rgba(244,63,94,.35)'; |
| 200 | 210 | alert('Test alert failed:\n' + (j.message || 'unknown')); |
| 201 | 211 | } |
| 202 | 212 | } catch (e) { |
| 203 | 213 | alert('Error: ' + e.message); |
| 204 | 214 | btn.textContent = orig; |
| 205 | 215 | } finally { |
| 206 | 216 | setTimeout(function () { |
| 207 | 217 | btn.disabled = false; |
| 208 | 218 | btn.textContent = orig; |
| 209 | 219 | btn.style.background = ''; |
| 210 | 220 | btn.style.color = ''; |
| 211 | 221 | btn.style.borderColor = ''; |
| 212 | 222 | }, 3000); |
| 213 | 223 | } |
| 214 | 224 | }; |
| 215 | 225 | </script> |