Showing 2 changed files with 129 additions and 10 deletions
+11 -0
scripts/deploy_to_jumper.sh
@@ -79,6 +79,13 @@ ssh "$TARGET_HOST" "command -v rsync >/dev/null 2>&1" || {
79 79
 }
80 80
 
81 81
 perl -c scripts/host_manager.pl >/dev/null
82
+BUILD_REVISION="$(git rev-parse --short=12 HEAD)"
83
+BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
84
+BUILD_DIRTY=0
85
+if [[ -n "$(git status --porcelain)" ]]; then
86
+    BUILD_DIRTY=1
87
+fi
88
+BUILD_DEPLOYED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
82 89
 
83 90
 rsync_args=(-az --delete)
84 91
 if [[ "$DRY_RUN" -eq 1 ]]; then
@@ -100,6 +107,10 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
100 107
     exit 0
101 108
 fi
102 109
 
110
+printf 'revision=%s\nbranch=%s\ndirty=%s\ndeployed_at=%s\n' \
111
+    "$BUILD_REVISION" "$BUILD_BRANCH" "$BUILD_DIRTY" "$BUILD_DEPLOYED_AT" |
112
+    ssh "$TARGET_HOST" "cat > '$TARGET_DIR/BUILD'"
113
+
103 114
 ssh "$TARGET_HOST" "cd '$TARGET_DIR' && perl -c scripts/host_manager.pl >/dev/null"
104 115
 
105 116
 if [[ "$RESTART" -eq 1 ]]; then
+118 -10
scripts/host_manager.pl
@@ -1125,13 +1125,84 @@ sub iso_now {
1125 1125
     return strftime('%Y-%m-%dT%H:%M:%SZ', gmtime);
1126 1126
 }
1127 1127
 
1128
+sub build_info {
1129
+    my %info = (
1130
+        revision => '',
1131
+        branch => '',
1132
+        built_at => '',
1133
+        deployed_at => '',
1134
+        dirty => '',
1135
+    );
1136
+
1137
+    if ($ENV{HOST_MANAGER_BUILD}) {
1138
+        $info{revision} = clean_scalar($ENV{HOST_MANAGER_BUILD});
1139
+        return \%info;
1140
+    }
1141
+
1142
+    my $build_file = "$project_dir/BUILD";
1143
+    if (-f $build_file) {
1144
+        for my $line (split /\n/, read_file($build_file)) {
1145
+            next unless $line =~ /\A([A-Za-z0-9_.-]+)=(.*)\z/;
1146
+            $info{$1} = clean_scalar($2);
1147
+        }
1148
+        return \%info if $info{revision} || $info{built_at};
1149
+    }
1150
+
1151
+    my $revision = git_value('rev-parse --short=12 HEAD');
1152
+    my $branch = git_value('rev-parse --abbrev-ref HEAD');
1153
+    $info{revision} = $revision if $revision;
1154
+    $info{branch} = $branch if $branch && $branch ne 'HEAD';
1155
+    return \%info;
1156
+}
1157
+
1158
+sub git_value {
1159
+    my ($args) = @_;
1160
+    return '' unless -d "$project_dir/.git";
1161
+    open my $fh, '-|', "git -C '$project_dir' $args 2>/dev/null" or return '';
1162
+    my $value = <$fh> || '';
1163
+    close $fh;
1164
+    chomp $value;
1165
+    return clean_scalar($value);
1166
+}
1167
+
1168
+sub build_label {
1169
+    my $info = build_info();
1170
+    my $revision = $info->{revision} || 'unknown';
1171
+    my $branch = $info->{branch} || '';
1172
+    $branch = '' if $branch eq 'HEAD';
1173
+    my $label = $branch ? "$branch $revision" : $revision;
1174
+    $label .= '+dirty' if ($info->{dirty} || '') eq '1';
1175
+    return $label;
1176
+}
1177
+
1178
+sub build_title {
1179
+    my $info = build_info();
1180
+    my $label = build_label();
1181
+    my $stamp = $info->{deployed_at} || $info->{built_at} || '';
1182
+    return $stamp ? "$label deployed $stamp" : $label;
1183
+}
1184
+
1185
+sub html_escape {
1186
+    my ($value) = @_;
1187
+    $value = '' unless defined $value;
1188
+    $value =~ s/&/&amp;/g;
1189
+    $value =~ s/</&lt;/g;
1190
+    $value =~ s/>/&gt;/g;
1191
+    $value =~ s/"/&quot;/g;
1192
+    $value =~ s/'/&#039;/g;
1193
+    return $value;
1194
+}
1195
+
1128 1196
 sub app_html {
1129
-    return <<'HTML';
1197
+    my $build = html_escape(build_label());
1198
+    my $build_title = html_escape(build_title());
1199
+    my $html = <<'HTML';
1130 1200
 <!doctype html>
1131 1201
 <html lang="ro">
1132 1202
 <head>
1133 1203
   <meta charset="utf-8">
1134 1204
   <meta name="viewport" content="width=device-width, initial-scale=1">
1205
+  <meta name="xdev-build" content="__HOST_MANAGER_BUILD_TITLE__">
1135 1206
   <title>Madagascar Local Authority</title>
1136 1207
   <style>
1137 1208
     :root {
@@ -1165,10 +1236,10 @@ sub app_html {
1165 1236
       --login-form-width: calc((var(--otp-size) * 6) + (var(--otp-gap) * 5));
1166 1237
       background: #fff;
1167 1238
       border-radius: 16px;
1168
-      padding: 54px 64px 48px;
1239
+      padding: 54px 64px 34px;
1169 1240
       width: 100%;
1170 1241
       max-width: 680px;
1171
-      min-height: 440px;
1242
+      min-height: 360px;
1172 1243
       display: grid;
1173 1244
       align-content: start;
1174 1245
       justify-items: center;
@@ -1286,8 +1357,36 @@ sub app_html {
1286 1357
     .span2 { grid-column: 1 / -1; }
1287 1358
     label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; font-weight: 650; }
1288 1359
     .muted { color: var(--muted); }
1360
+    .build-badge {
1361
+      position: fixed;
1362
+      right: 10px;
1363
+      bottom: 8px;
1364
+      z-index: 5;
1365
+      color: rgba(255,255,255,.46);
1366
+      background: rgba(19,24,42,.28);
1367
+      border: 1px solid rgba(255,255,255,.08);
1368
+      border-radius: 4px;
1369
+      padding: 2px 5px;
1370
+      font-size: 10px;
1371
+      line-height: 1.2;
1372
+      pointer-events: none;
1373
+      user-select: none;
1374
+    }
1375
+    body.is-app .build-badge {
1376
+      color: rgba(100,112,132,.58);
1377
+      background: rgba(255,255,255,.72);
1378
+      border-color: rgba(216,222,232,.72);
1379
+    }
1289 1380
     .problems { padding: 10px 14px; display: grid; gap: 8px; }
1290 1381
     .problem { border-left: 3px solid var(--warn); padding: 7px 9px; background: #fffaf0; }
1382
+    .work-order-card { display: grid; gap: 8px; min-width: 0; }
1383
+    .work-order-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
1384
+    .work-order-title { color: var(--ink); font-size: 14px; font-weight: 650; }
1385
+    .work-order-checklist, .work-order-actions { display: grid; gap: 6px; min-width: 0; }
1386
+    .work-order-actions { gap: 4px; }
1387
+    .work-order-checkitem { display: flex; align-items: flex-start; gap: 8px; min-width: 0; color: var(--ink); font-size: 13px; font-weight: 400; }
1388
+    .work-order-checkitem input[type="checkbox"] { width: auto; flex: 0 0 auto; margin: 2px 0 0; }
1389
+    .work-order-checkitem span { min-width: 0; overflow-wrap: anywhere; }
1291 1390
     @media (max-width: 760px) {
1292 1391
       .grid { grid-template-columns: 1fr; }
1293 1392
       table { min-width: 760px; }
@@ -1295,7 +1394,7 @@ sub app_html {
1295 1394
     }
1296 1395
   </style>
1297 1396
 </head>
1298
-<body>
1397
+<body class="is-login">
1299 1398
 
1300 1399
   <!-- ── Login screen ── -->
1301 1400
   <div id="login-screen">
@@ -1422,6 +1521,8 @@ sub app_html {
1422 1521
     </main>
1423 1522
   </div>
1424 1523
 
1524
+  <div class="build-badge" title="Running build __HOST_MANAGER_BUILD_TITLE__">build __HOST_MANAGER_BUILD__</div>
1525
+
1425 1526
   <script>
1426 1527
     let state = { hosts: [], problems: [], workOrders: [], authenticated: false };
1427 1528
 
@@ -1436,6 +1537,8 @@ sub app_html {
1436 1537
     }
1437 1538
 
1438 1539
     function showLogin(errorText) {
1540
+      document.body.classList.remove('is-app');
1541
+      document.body.classList.add('is-login');
1439 1542
       $('app').style.display = 'none';
1440 1543
       $('login-screen').style.display = 'flex';
1441 1544
       $('login-error').textContent = errorText || '';
@@ -1443,6 +1546,8 @@ sub app_html {
1443 1546
     }
1444 1547
 
1445 1548
     function showApp() {
1549
+      document.body.classList.remove('is-login');
1550
+      document.body.classList.add('is-app');
1446 1551
       $('login-screen').style.display = 'none';
1447 1552
       $('app').style.display = 'block';
1448 1553
     }
@@ -1515,7 +1620,7 @@ sub app_html {
1515 1620
           const checklistComplete = checklist.length === 0 || doneItems === checklist.length;
1516 1621
           const checklistHtml = checklist.map(item => {
1517 1622
             const checked = (item.status || 'pending') === 'done' ? 'checked' : '';
1518
-            return `<label style="display:flex;align-items:flex-start;gap:8px">
1623
+            return `<label class="work-order-checkitem">
1519 1624
               <input type="checkbox" data-wo-checklist="${escapeHtml(wo.id)}" data-item-id="${escapeHtml(item.id || '')}" ${checked}>
1520 1625
               <span><strong>${escapeHtml(item.id || '')}</strong> ${escapeHtml(item.text || '')}</span>
1521 1626
             </label>`;
@@ -1528,15 +1633,15 @@ sub app_html {
1528 1633
           const button = (wo.status || 'pending') === 'pending'
1529 1634
             ? `<button type="button" class="primary" data-confirm-wo="${escapeHtml(wo.id)}" ${checklistComplete ? '' : 'disabled'}>Confirm</button>`
1530 1635
             : '';
1531
-          return `<div class="problem" style="display:grid;gap:8px">
1532
-            <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
1636
+          return `<div class="problem work-order-card">
1637
+            <div class="work-order-head">
1533 1638
               <div><strong>${escapeHtml(wo.id || '')}</strong> <span class="pill ${statusClass}">${escapeHtml(wo.status || 'pending')}</span> <span class="pill">${doneItems}/${checklist.length} done</span></div>
1534 1639
               ${button}
1535 1640
             </div>
1536
-            <div>${escapeHtml(wo.title || '')}</div>
1641
+            <div class="work-order-title">${escapeHtml(wo.title || '')}</div>
1537 1642
             <div class="muted">${escapeHtml(wo.reason || '')}</div>
1538
-            <div style="display:grid;gap:6px">${checklistHtml}</div>
1539
-            <div style="display:grid;gap:4px">${actions}</div>
1643
+            <div class="work-order-checklist">${checklistHtml}</div>
1644
+            <div class="work-order-actions">${actions}</div>
1540 1645
             ${wo.confirmed_at ? `<div class="muted">confirmed ${escapeHtml(wo.confirmed_at)}</div>` : ''}
1541 1646
           </div>`;
1542 1647
         }).join('');
@@ -1759,4 +1864,7 @@ sub app_html {
1759 1864
 </body>
1760 1865
 </html>
1761 1866
 HTML
1867
+    $html =~ s/__HOST_MANAGER_BUILD_TITLE__/$build_title/g;
1868
+    $html =~ s/__HOST_MANAGER_BUILD__/$build/g;
1869
+    return $html;
1762 1870
 }