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/MODS/Charts.pm
SiteDriftSense self-monitor on local
Kindlocal
Captured at2026-07-10 23:21:50
Captured SHA4b17b499876983c9866ebc81be6f22699c8b0bd3170b0c661319276e153ebea8
Current state on disk
Live check just now. If SHAs match, the restore is a no-op.
File presentyes
Size10257 bytes
Current SHA59483bc8e7f8
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. +8 additions, -18 deletions, 229 unchanged context lines.
11package MODS::Charts;
22#======================================================================
33# DriftSense -- hand-rolled SVG chart helpers (zero external deps).
44#
55# All charts are inline SVG strings that use CSS variables from
66# drift_sense.css so palette + theme changes propagate automatically.
77#
88# Public functions (call as MODS::Charts::stacked_bars(...), etc.):
99#
1010# stacked_bars(days => \@days, series => { file => \@nums, schema => \@nums })
1111# Wide activity timeline. One bar per day, stacked by series.
1212#
1313# sparkline(values => \@nums, w => 80, h => 20)
1414# Tiny inline SVG summary line. Great next to file / table names.
1515#
1616# simple_donut(size, values => [{v, color, label}], center)
1717# Compact donut for savings / drift / dedup ratios.
1818#======================================================================
1919use strict;
2020use warnings;
2121
2222sub _esc {
2323 my $s = shift; $s //= '';
2424 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
2525 return $s;
2626}
2727
2828#---------------------------------------------------------------------
2929# stacked_bars({ days => [...], series => { file => [...], schema => [...] } })
3030#
3131# @days = arrayref of day labels (oldest -> newest). @series entries
3232# must be same-length arrayrefs of integers. Bars stack file (teal)
3333# below schema (amber). Y-axis auto-scales.
3434#---------------------------------------------------------------------
3535sub stacked_bars {
3636 my %a = @_;
3737 my $days = $a{days} || []; # display labels e.g. "Jul 10"
3838 my $day_keys = $a{day_keys} || []; # ISO YYYY-MM-DD for links/tooltips
3939 my $series = $a{series} || {};
4040 my $w = $a{width} || 900;
4141 my $h = $a{height} || 160;
4242 my $link_file = $a{link_file} || '/file_changes.cgi';
4343 my $link_schema = $a{link_schema} || '/schema_changes.cgi';
4444
4545 my $n = scalar @$days;
4646 return '' unless $n;
4747 # Fall back to display labels for keys if caller didn't pass ISO dates.
4848 my @keys = @$day_keys ? @$day_keys : @$days;
4949
5050 my @file_v = @{ $series->{file} || [] };
5151 my @schema_v = @{ $series->{schema} || [] };
5252
5353 my $max = 1;
5454 for my $i (0..$n-1) {
5555 my $t = ($file_v[$i] || 0) + ($schema_v[$i] || 0);
5656 $max = $t if $t > $max;
5757 }
5858
5959 my $pad_l = 40;
6060 my $pad_b = 22;
6161 my $pad_t = 8;
6262 my $pad_r = 8;
6363
6464 my $plot_w = $w - $pad_l - $pad_r;
6565 my $plot_h = $h - $pad_t - $pad_b;
6666
6767 my $bar_gap = 1;
6868 my $bar_w = ($plot_w - ($n - 1) * $bar_gap) / $n;
6969 $bar_w = 1 if $bar_w < 1;
7070
7171 my $svg = qq~<svg class="ds-timeline" viewBox="0 0 $w $h" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="width:100%;height:${h}px" preserveAspectRatio="none">~;
7272 $svg .= q~<style>
7373 .ds-timeline .ds-day { cursor: pointer; }
7474 .ds-timeline .ds-day:hover .ds-hit { fill: rgba(20,184,166,0.06); }
7575 .ds-timeline .ds-day:hover .ds-bar { opacity: 1; }
7676 .ds-timeline .ds-bar { transition: opacity .15s ease; }
7777 .ds-timeline a { text-decoration: none; }
7878 </style>~;
7979
8080 # Grid lines + Y-axis labels
8181 for my $frac (0, 0.25, 0.5, 0.75, 1) {
8282 my $y = $pad_t + $plot_h * (1 - $frac);
8383 my $lbl = int($max * $frac);
8484 $svg .= qq~<line x1="$pad_l" y1="$y" x2="~ . ($w - $pad_r) . qq~" y2="$y" stroke="#223050" stroke-width="1" stroke-dasharray="2 4" opacity="0.5"/>~;
8585 $svg .= qq~<text x="~ . ($pad_l - 6) . qq~" y="$y" text-anchor="end" dominant-baseline="middle" font-family="var(--mono, monospace)" font-size="9" fill="#6a7a94">$lbl</text>~;
8686 }
8787
8888 # Bars -- wrap each day in a group with data-* attributes so a JS
8989 # tooltip can pick up the counts on hover. Each segment is its own
9090 # <a> so click routes to file_changes or schema_changes.
9191 for my $i (0..$n-1) {
9292 my $fv = $file_v[$i] || 0;
9393 my $sv = $schema_v[$i] || 0;
9494 my $x = $pad_l + $i * ($bar_w + $bar_gap);
9595 my $file_h = ($fv / $max) * $plot_h;
9696 my $schema_h = ($sv / $max) * $plot_h;
9797 my $file_y = $pad_t + $plot_h - $file_h;
9898 my $schema_y = $file_y - $schema_h;
9999 my $key = _esc($keys[$i]);
100100 my $label = _esc($days->[$i]);
101101
102102 # Full-height invisible hit target for tooltip hover on empty days.
103103 # Sits behind the visible bars.
104104 $svg .= sprintf(
105105 qq~<g class="ds-day" data-key="%s" data-label="%s" data-file="%d" data-schema="%d" data-link-file="%s?on=%s" data-link-schema="%s?on=%s">~,
106106 $key, $label, $fv, $sv,
107107 _esc($link_file), $key, _esc($link_schema), $key,
108108 );
109109 $svg .= sprintf(
110110 qq~<rect class="ds-hit" x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="transparent"/>~,
111111 $x, $pad_t, $bar_w, $plot_h,
112112 );
113113 if ($fv > 0) {
114114 $svg .= sprintf(
115115 qq~<a xlink:href="%s?on=%s"><rect class="ds-bar ds-bar-file" x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="#14b8a6" opacity="0.85"/></a>~,
116116 _esc($link_file), $key, $x, $file_y, $bar_w, $file_h,
117117 );
118118 }
119119 if ($sv > 0) {
120120 $svg .= sprintf(
121121 qq~<a xlink:href="%s?on=%s"><rect class="ds-bar ds-bar-schema" x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="#f59e0b" opacity="0.85"/></a>~,
122122 _esc($link_schema), $key, $x, $schema_y, $bar_w, $schema_h,
123123 );
124124 }
125125 $svg .= qq~</g>~;
126126 }
127127
128128 # X-axis: label first, middle, last day
129129 my @tick_idx = (0, int($n/2), $n - 1);
130130 for my $i (@tick_idx) {
131131 my $x = $pad_l + $i * ($bar_w + $bar_gap) + ($bar_w / 2);
132132 my $y = $h - 6;
133133 my $lbl = _esc($days->[$i]);
134134 $svg .= qq~<text x="$x" y="$y" text-anchor="middle" font-family="var(--mono, monospace)" font-size="10" fill="#96a3b8">$lbl</text>~;
135135 }
136136
137 # Legend -- drawn LAST with a solid background rect so bars from
138 # busy days behind it can't visually intrude.
139 my $legend_y = $pad_t + 4;
140 my $legend_w = 172;
141 my $legend_h = 20;
142 my $legend_x = $w - $pad_r - $legend_w;
143 # Backdrop matches the module body bg + a subtle border for polish
144 $svg .= sprintf(
145 qq~<rect x="%.2f" y="%.2f" width="%d" height="%d" fill="#131e30" stroke="#223050" stroke-width="1" rx="6"/>~,
146 $legend_x, $legend_y, $legend_w, $legend_h,
147 );
148 my $lx = $legend_x + 10;
149 my $ly = $legend_y + 5;
137 # Legend
138 my $legend_y = $pad_t + 8;
139 my $legend_x = $w - $pad_r - 160;
150140 $svg .= qq~<g font-family="var(--sans, sans-serif)" font-size="10.5">~;
151 $svg .= qq~<rect x="$lx" y="$ly" width="10" height="10" fill="#14b8a6" rx="1"/>~;
152 $svg .= qq~<text x="~ . ($lx + 14) . qq~" y="~ . ($ly + 9) . qq~" fill="#96a3b8">file changes</text>~;
153 my $lx2 = $lx + 88;
154 $svg .= qq~<rect x="$lx2" y="$ly" width="10" height="10" fill="#f59e0b" rx="1"/>~;
155 $svg .= qq~<text x="~ . ($lx2 + 14) . qq~" y="~ . ($ly + 9) . qq~" fill="#96a3b8">schema</text>~;
141 $svg .= qq~<rect x="$legend_x" y="$legend_y" width="10" height="10" fill="#14b8a6"/>~;
142 $svg .= qq~<text x="~ . ($legend_x + 14) . qq~" y="~ . ($legend_y + 9) . qq~" fill="#96a3b8">file changes</text>~;
143 my $legend_x2 = $legend_x + 90;
144 $svg .= qq~<rect x="$legend_x2" y="$legend_y" width="10" height="10" fill="#f59e0b"/>~;
145 $svg .= qq~<text x="~ . ($legend_x2 + 14) . qq~" y="~ . ($legend_y + 9) . qq~" fill="#96a3b8">schema</text>~;
156146 $svg .= qq~</g>~;
157147
158148 $svg .= qq~</svg>~;
159149 return $svg;
160150}
161151
162152#---------------------------------------------------------------------
163153# sparkline({ values => \@nums, w, h, color })
164154#
165155# Tiny standalone SVG for inline use. Shows a 30-point (or however
166156# many values passed) mini line/bar chart. Great next to a file name
167157# or table name to convey "recent activity rhythm at a glance".
168158#---------------------------------------------------------------------
169159sub sparkline {
170160 my %a = @_;
171161 my $v = $a{values} || [];
172162 my $w = $a{width} || 80;
173163 my $h = $a{height} || 20;
174164 my $color = $a{color} || '#14b8a6';
175165
176166 my $n = scalar @$v;
177167 return '' unless $n;
178168
179169 my $max = 0;
180170 $max = $_ > $max ? $_ : $max for @$v;
181171 $max ||= 1;
182172
183173 my $gap = 1;
184174 my $bar_w = ($w - ($n - 1) * $gap) / $n;
185175 $bar_w = 1 if $bar_w < 1;
186176
187177 my $svg = qq~<svg viewBox="0 0 $w $h" xmlns="http://www.w3.org/2000/svg" style="display:inline-block;vertical-align:middle;width:${w}px;height:${h}px">~;
188178 for my $i (0..$n-1) {
189179 my $val = $v->[$i] || 0;
190180 next if $val == 0;
191181 my $bh = ($val / $max) * $h;
192182 my $x = $i * ($bar_w + $gap);
193183 my $y = $h - $bh;
194184 $svg .= sprintf(qq~<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" opacity="0.85"/>~,
195185 $x, $y, $bar_w, $bh, $color);
196186 }
197187 $svg .= qq~</svg>~;
198188 return $svg;
199189}
200190
201191#---------------------------------------------------------------------
202192# simple_donut({ size, values => [{v, color, label}], center_num, center_lbl })
203193#---------------------------------------------------------------------
204194sub simple_donut {
205195 my %a = @_;
206196 my $size = $a{size} || 140;
207197 my $thickness = $a{thickness} || 16;
208198 my $values = $a{values} || [];
209199 my $center_num = $a{center_num} // '';
210200 my $center_lbl = $a{center_lbl} // '';
211201
212202 my $cx = $size / 2; my $cy = $size / 2;
213203 my $r = ($size / 2) - ($thickness / 2) - 4;
214204 my $total = 0; $total += ($_->{v} || 0) for @$values;
215205 $total = 1 unless $total > 0;
216206
217207 my $svg = qq~<svg viewBox="0 0 $size $size" style="width:${size}px;height:${size}px">~;
218208 # Background ring
219209 $svg .= qq~<circle cx="$cx" cy="$cy" r="$r" stroke="#131e30" stroke-width="$thickness" fill="none"/>~;
220210
221211 my $start_deg = -90;
222212 foreach my $v (@$values) {
223213 my $angle = ($v->{v} / $total) * 360;
224214 next if $angle < 0.5;
225215 my $end_deg = $start_deg + $angle;
226216 my $large = ($angle > 180) ? 1 : 0;
227217 my ($x1, $y1) = _polar($cx, $cy, $r, $start_deg);
228218 my ($x2, $y2) = _polar($cx, $cy, $r, $end_deg);
229219 my $c = $v->{color} || '#14b8a6';
230220 $svg .= qq~<path d="M $x1 $y1 A $r $r 0 $large 1 $x2 $y2" stroke="$c" stroke-width="$thickness" fill="none" stroke-linecap="butt" opacity="0.95"/>~;
231221 $start_deg = $end_deg;
232222 }
233223
234224 my $cnum = _esc($center_num); my $clbl = _esc($center_lbl);
235225 $svg .= qq~<text x="$cx" y="$cy" text-anchor="middle" dy="4" font-family="var(--sans, sans-serif)" font-weight="800" font-size="20" fill="#e5edf5">$cnum</text>~;
236226 $svg .= qq~<text x="$cx" y="$cy" text-anchor="middle" dy="24" font-family="var(--sans, sans-serif)" font-size="10" letter-spacing="1.4" fill="#96a3b8">$clbl</text>~;
237227 $svg .= qq~</svg>~;
238228 return $svg;
239229}
240230
241231sub _polar {
242232 my ($cx, $cy, $r, $deg) = @_;
243233 my $rad = $deg * 3.14159265 / 180;
244234 return ($cx + $r * cos($rad), $cy + $r * sin($rad));
245235}
246236
2472371;
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.