Showing 1 changed files with 79 additions and 9 deletions
+79 -9
scripts/host_manager.pl
@@ -1197,6 +1197,26 @@ sub build_title {
1197 1197
     return $stamp ? "$label deployed $stamp" : $label;
1198 1198
 }
1199 1199
 
1200
+sub build_revision {
1201
+    my $info = build_info();
1202
+    return $info->{revision} || 'unknown';
1203
+}
1204
+
1205
+sub build_details {
1206
+    my $info = build_info();
1207
+    my %details = (
1208
+        app => 'Madagascar Local Authority',
1209
+        revision => $info->{revision} || 'unknown',
1210
+        branch => $info->{branch} || '',
1211
+        dirty => ($info->{dirty} || '') eq '1' ? json_bool(1) : json_bool(0),
1212
+        built_at => $info->{built_at} || '',
1213
+        deployed_at => $info->{deployed_at} || '',
1214
+        label => build_label(),
1215
+        title => build_title(),
1216
+    );
1217
+    return json_encode(\%details);
1218
+}
1219
+
1200 1220
 sub html_escape {
1201 1221
     my ($value) = @_;
1202 1222
     $value = '' unless defined $value;
@@ -1209,8 +1229,9 @@ sub html_escape {
1209 1229
 }
1210 1230
 
1211 1231
 sub app_html {
1212
-    my $build = html_escape(build_label());
1232
+    my $build = html_escape(build_revision());
1213 1233
     my $build_title = html_escape(build_title());
1234
+    my $build_details = html_escape(build_details());
1214 1235
     my $html = <<'HTML';
1215 1236
 <!doctype html>
1216 1237
 <html lang="ro">
@@ -1313,7 +1334,7 @@ sub app_html {
1313 1334
     .otp-row input.filled { border-color: #b3c6f0; background: #fff; }
1314 1335
     #login-error {
1315 1336
       color: var(--bad); font-size: 13px; text-align: center;
1316
-      min-height: 18px; margin-top: -52px;
1337
+      min-height: 18px; margin-top: -8px;
1317 1338
     }
1318 1339
     @media (max-width: 760px) {
1319 1340
       .login-card {
@@ -1325,7 +1346,6 @@ sub app_html {
1325 1346
       .login-card .brand h1 { font-size: 24px; }
1326 1347
       .login-card .brand p { font-size: 14px; }
1327 1348
       .login-card form { padding-bottom: 0; }
1328
-      #login-error { margin-top: -24px; }
1329 1349
     }
1330 1350
     @media (max-width: 430px) {
1331 1351
       #login-screen { padding: 24px 16px 120px; }
@@ -1341,7 +1361,6 @@ sub app_html {
1341 1361
       #login-screen { padding-top: 28px; padding-bottom: 96px; }
1342 1362
       .login-card { padding-top: 34px; padding-bottom: 34px; gap: 20px; }
1343 1363
       .login-card form { padding-bottom: 0; }
1344
-      #login-error { margin-top: -32px; }
1345 1364
     }
1346 1365
 
1347 1366
     /* ── App shell (hidden until authenticated) ── */
@@ -1389,27 +1408,46 @@ sub app_html {
1389 1408
     .ca-detail { display: grid; gap: 6px; min-width: 0; }
1390 1409
     .ca-fingerprint { overflow-wrap: anywhere; }
1391 1410
     .ca-empty { padding: 12px 14px; }
1392
-    .build-badge {
1411
+    .build-control {
1393 1412
       position: fixed;
1394 1413
       right: 10px;
1395 1414
       bottom: 8px;
1396 1415
       z-index: 5;
1416
+      display: inline-flex;
1417
+      align-items: center;
1418
+      gap: 4px;
1419
+    }
1420
+    .build-badge, .build-copy {
1397 1421
       color: rgba(255,255,255,.46);
1398 1422
       background: rgba(19,24,42,.28);
1399 1423
       border: 1px solid rgba(255,255,255,.08);
1400 1424
       border-radius: 4px;
1401
-      padding: 2px 5px;
1402 1425
       font-size: 10px;
1403 1426
       line-height: 1.2;
1427
+    }
1428
+    .build-badge {
1429
+      padding: 2px 5px;
1404 1430
       cursor: text;
1405
-      pointer-events: auto;
1406 1431
       user-select: text;
1407 1432
     }
1408
-    body.is-app .build-badge {
1433
+    .build-copy {
1434
+      min-height: 0;
1435
+      padding: 2px 5px;
1436
+      cursor: pointer;
1437
+    }
1438
+    .build-copy:hover {
1439
+      color: rgba(255,255,255,.72);
1440
+      border-color: rgba(255,255,255,.24);
1441
+    }
1442
+    body.is-app .build-badge, body.is-app .build-copy {
1409 1443
       color: rgba(100,112,132,.58);
1410 1444
       background: rgba(255,255,255,.72);
1411 1445
       border-color: rgba(216,222,232,.72);
1412 1446
     }
1447
+    body.is-app .build-copy:hover {
1448
+      color: rgba(21,32,51,.78);
1449
+      border-color: rgba(100,112,132,.42);
1450
+    }
1413 1451
     .problems { padding: 10px 14px; display: grid; gap: 8px; }
1414 1452
     .problem { border-left: 3px solid var(--warn); padding: 7px 9px; background: #fffaf0; }
1415 1453
     .work-order-card { display: grid; gap: 8px; min-width: 0; }
@@ -1646,7 +1684,10 @@ sub app_html {
1646 1684
     </div>
1647 1685
   </div>
1648 1686
 
1649
-  <div class="build-badge" title="Running build __HOST_MANAGER_BUILD_TITLE__">build __HOST_MANAGER_BUILD__</div>
1687
+  <div class="build-control" title="Running build __HOST_MANAGER_BUILD_TITLE__">
1688
+    <span class="build-badge">__HOST_MANAGER_BUILD__</span>
1689
+    <button type="button" class="build-copy" id="copy-build" data-build-details="__HOST_MANAGER_BUILD_DETAILS__" aria-label="Copy build details">Copy</button>
1690
+  </div>
1650 1691
 
1651 1692
   <script>
1652 1693
     let state = { hosts: [], problems: [], workOrders: [], authenticated: false };
@@ -2067,6 +2108,7 @@ sub app_html {
2067 2108
 
2068 2109
     otpDigits.forEach((input, idx) => {
2069 2110
       input.addEventListener('input', () => {
2111
+        $('login-error').textContent = '';
2070 2112
         // A single box may receive several digits at once (autofill / typing fast).
2071 2113
         if (input.value.replace(/\D/g, '').length > 1) {
2072 2114
           fillOtp(input.value, idx);
@@ -2080,6 +2122,7 @@ sub app_html {
2080 2122
 
2081 2123
       input.addEventListener('paste', (e) => {
2082 2124
         e.preventDefault();
2125
+        $('login-error').textContent = '';
2083 2126
         const text = (e.clipboardData || window.clipboardData).getData('text');
2084 2127
         fillOtp(text, idx);
2085 2128
       });
@@ -2087,6 +2130,7 @@ sub app_html {
2087 2130
       input.addEventListener('keydown', (e) => {
2088 2131
         if (e.key === 'Backspace') {
2089 2132
           e.preventDefault();
2133
+          $('login-error').textContent = '';
2090 2134
           if (input.value) { setOtpDigit(idx, ''); }
2091 2135
           else if (idx > 0) { setOtpDigit(idx - 1, ''); otpDigits[idx - 1].focus(); }
2092 2136
           syncOtpFields();
@@ -2117,6 +2161,31 @@ sub app_html {
2117 2161
 
2118 2162
     window.addEventListener('popstate', () => showPage(currentPage()));
2119 2163
 
2164
+    async function copyText(text) {
2165
+      if (navigator.clipboard && window.isSecureContext) {
2166
+        await navigator.clipboard.writeText(text);
2167
+        return;
2168
+      }
2169
+      const input = document.createElement('textarea');
2170
+      input.value = text;
2171
+      input.setAttribute('readonly', '');
2172
+      input.style.position = 'fixed';
2173
+      input.style.left = '-10000px';
2174
+      document.body.appendChild(input);
2175
+      input.select();
2176
+      document.execCommand('copy');
2177
+      document.body.removeChild(input);
2178
+    }
2179
+
2180
+    $('copy-build').addEventListener('click', async () => {
2181
+      try {
2182
+        await copyText($('copy-build').dataset.buildDetails || '');
2183
+        if (state.authenticated) msg('build details copied');
2184
+      } catch (e) {
2185
+        if (state.authenticated) msg('copy failed');
2186
+      }
2187
+    });
2188
+
2120 2189
     $('login-form').addEventListener('submit', async (event) => {
2121 2190
       event.preventDefault();
2122 2191
       if (!otpReady() || $('login-form').classList.contains('busy')) return;
@@ -2209,5 +2278,6 @@ sub app_html {
2209 2278
 HTML
2210 2279
     $html =~ s/__HOST_MANAGER_BUILD_TITLE__/$build_title/g;
2211 2280
     $html =~ s/__HOST_MANAGER_BUILD__/$build/g;
2281
+    $html =~ s/__HOST_MANAGER_BUILD_DETAILS__/$build_details/g;
2212 2282
     return $html;
2213 2283
 }