1 2<!doctype html> 3<html lang="en" data-theme="light"> 4<head> 5 <meta charset="UTF-8"> 6 <!-- Theme persistence â runs before CSS to prevent flash --> 7
7<script> 8 (function() { 9 var t = localStorage.getItem('theme'); 10 if (t) document.documentElement.setAttribute('data-theme', t); 11 if (localStorage.getItem('comfortMode') === 'true') document.documentElement.setAttribute('data-comfort-mode', 'on'); 12 if (localStorage.getItem('colorblindMode') === 'true') document.documentElement.setAttribute('data-cb-mode', 'on'); 13 var fm = localStorage.getItem('sb_mobile_mode'); 14 document.documentElement.setAttribute('data-mobile-mode', fm === 'field' ? 'field' : 'office'); 15 })(); 16 </script>
16 17 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> 18 <meta name="description" content="SmartBuild smrtBuild - Construction Project Management"> 19 <meta name="theme-color" content="#006998"> 20 <title>smrtBuild</title> 21 22 <!-- Favicon --> 23 <link rel="icon" type="image/svg+xml" href="/icons/favicon.svg"> 24 <link rel="icon" type="image/png" href="/icons/favicon.png"> 25 26 <!-- PWA Manifest --> 27 <link rel="manifest" href="/manifest.json"> 28 <link rel="apple-touch-icon" href="/icons/app-icon-192.png"> 29 30 <!-- iOS PWA --> 31 <meta name="apple-mobile-web-app-capable" content="yes"> 32 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 33 <meta name="apple-mobile-web-app-title" content="smrtBuild"> 34 <link rel="mask-icon" href="/icons/favicon.svg" color="#006998"> 35 36 <!-- Google Fonts - Montserrat (Design Guide: 300-700 weights) --> 37 <link rel="preconnect" href="https://fonts.googleapis.com"> 38 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 39 <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 40 41 <!-- Cascade layer order. `vendor` is a WINDOW between `base` and `components`, and both 42 sides are load-bearing, because layer order sorts BEFORE specificity (CSS Cascade 5). 43 AFTER `base`: @layer base is Tailwind preflight, and its first rule zeroes margin, 44 padding and border on `*`. Below base, that blanket reset would outrank every 45 margin/padding/border in all four vendored sheets no matter how specific their 46 selectors are -- it erased Quill toolbar spacing, list indents and tooltip chrome, 47 and Leaflet popup-tip centering and control margins (167 measured clobbers). 48 BEFORE `components`: the app's authored theme overrides live in @layer components 49 and must beat the library defaults they exist to override -- the MCF-0461 defect, 50 where the sheets were unlayered and so outranked every app layer. 51 The non-vendor names mirror the generated tailwind-output.css block order. --> 52 <style>@layer properties, theme, base, vendor, components, utilities;</style> 53 54 <!-- Tailwind CSS 4.1 - Built output --> 55 <link rel="stylesheet" href="/css/tailwind-output.css"> 56 57 <!-- Lucide Icons (local) --> 58
58<script src="/lib/lucide/dist/umd/lucide.min.js"></script>
58 59 60 <!-- Vendored stylesheets (local): Leaflet, MarkerCluster, Quill. 61 Imported into @layer vendor rather than linked, so library defaults lose to the 62 app's theme overrides (@layer components) while still keeping their own geometry 63 out of reach of the preflight reset in @layer base. Same URLs, same fetches, same 64 (network-only) caching posture as the links they replace. --> 65 <style> 66 @import url("/lib/leaflet/leaflet.min.css") layer(vendor); 67 @import url("/lib/leaflet-markercluster/MarkerCluster.css") layer(vendor); 68 @import url("/lib/leaflet-markercluster/MarkerCluster.Default.css") layer(vendor); 69 @import url("/lib/quill/dist/quill.snow.css") layer(vendor); 70 </style> 71</head> 72<body class="bg-theme-primary font-sans text-theme-primary"> 73 <!-- App Container --> 74 <div id="app"> 75 <!-- Header (loaded from templates/components/header.html) --> 76 <div id="app-header"></div> 77 78 <!-- Sidebar (loaded from templates/components/sidebar.html) --> 79 <div id="app-sidebar"></div> 80 81 <!-- Main Content Area --> 82 <main id="content" class="min-h-screen pt-topnav lg:pl-60 transition-all duration-300 bg-theme-secondary"> 83 <!-- Page content will be injected here --> 84 <div id="app-content" class="h-app-content"> 85 <!-- Loading State --> 86 <div id="page-loading" class="flex items-center justify-center min-h-96"> 87 <div class="text-center"> 88 <div class="animate-spin w-10 h-10 border-4 border-st-primary border-t-transparent rounded-full mx-auto"></div> 89 <p class="mt-4 text-theme-secondary">Loading...</p> 90 </div> 91 </div> 92 </div> 93 </main> 94 </div> 95 96 <!-- Modals Container --> 97 <div id="app-modals"></div> 98 99 100 <!-- ============================================ --> 101 <!-- EXTERNAL SCRIPTS --> 102 <!-- ============================================ --> 103 104 <!-- jQuery (local) --> 105
105<script src="/lib/jquery/jquery.min.js"></script>
105 106 107 <!-- JsRender (local) --> 108
108<script src="/lib/jsrender/jsrender.min.js"></script>
108 109 110 <!-- kjua â client-side QR code generator for smrtField photo upload codes --> 111
111<script src="/lib/kjua/kjua.min.js"></script>
111 112 113 <!-- Page.js Router (local) --> 114
114<script src="/lib/pagejs/page.js"></script>
114 115 116 <!-- Dexie.js - IndexedDB wrapper for offline storage (local) --> 117
117<script src="/lib/dexie/dexie.min.js"></script>
117 118 119 <!-- Chart.js (local) --> 120
120<script src="/lib/chartjs/chart.umd.min.js"></script>
120 121 122 <!-- Leaflet + MarkerCluster (local) --> 123
123<script src="/lib/leaflet/leaflet.min.js"></script>
123 124
124<script src="/lib/leaflet-markercluster/leaflet.markercluster.min.js"></script>
124 125 126 <!-- SignalR - for real-time updates (local) --> 127
127<script src="/lib/signalr/dist/browser/signalr.min.js"></script>
127 128 129 <!-- SortableJS - for drag-and-drop reordering (local) --> 130
130<script src="/lib/sortablejs/Sortable.min.js"></script>
130 131 132 <!-- Browser Image Compression - client-side photo optimization (local) --> 133
133<script src="/lib/browser-image-compression/dist/browser-image-compression.js"></script>
133 134 135 <!-- FullCalendar v6 (local) --> 136
136<script src="/lib/fullcalendar/index.global.min.js"></script>
136 137 138 <!-- Quill Rich Text Editor (local) --> 139
139<script src="/lib/quill/dist/quill.js"></script>
139 140 141 <!-- Microsoft Authentication Library (MSAL) for Microsoft 365 integration --> 142
142<script src="/lib/msal-browser/msal-browser.min.js"></script>
142 143 144 <!-- Initialize Lucide Icons --> 145
145<script> 146 document.addEventListener('DOMContentLoaded', function() { 147 lucide.createIcons(); 148 }); 149 </script>
149 150 151 <!-- Native-shell environment (MOB-001). Generated by the mobile repo's sync-web.mjs; on the 152 plain web this file does not exist and the 404 is harmless â config.js falls back to hostname. --> 153
153<script defer src="/env.js"></script>
153 154 155 <!-- Toast Notifications --> 156
156<script type="module" src="/js/toast.js"></script>
156 157 158 <!-- Application JavaScript (ES6 Module) --> 159
159<script type="module" src="/js/app.js"></script>
159 160 161 <!-- Service Worker Registration --> 162 <!-- 163 User-prompted update UX: 164 1. New SW installs and enters the "waiting" state (sw-src.js install 165 handler does NOT call skipWaiting). 166 2. We detect that via reg.waiting at register time OR via the 167 updatefound -> statechange === 'installed' flow. 168 3. We show a sticky banner with "Update Now" and "Later". The banner is 169 NOT auto-dismissing â a silent reload mid-task loses unsaved work. 170 4. On "Update Now", we post SKIP_WAITING to the WAITING worker (not the 171 controller â the controller is the old SW), set userRequestedUpdate, 172 and wait for controllerchange to reload. 173 5. controllerchange ONLY reloads when userRequestedUpdate is true, so a 174 SKIP_WAITING from another tab does NOT yank this tab. 175 176 Sentinels: 177 hadController â distinguishes upgrade from first install 178 refreshing â guards against reload loops 179 userRequestedUpdate â guards against unsolicited reloads 180 (e.g. another tab triggered SKIP_WAITING) 181 182 MUST run before ES modules so hadController is captured before any 183 controllerchange events can fire. 184 --> 185
185<script> 186 if ('serviceWorker' in navigator) { 187 var hadController = !!navigator.serviceWorker.controller; 188 var refreshing = false; 189 var userRequestedUpdate = false; 190 191 function _swDismissBanner() { 192 var el = document.getElementById('sw-update-banner'); 193 if (el) el.remove(); 194 } 195 196 function _swTriggerUpdate() { 197 // Immediate feedback â the SKIP_WAITING -> controllerchange -> 198 // reload round-trip isn't instant, and a dead-looking button 199 // reads as "nothing happened." 200 var btn = document.getElementById('sw-update-now-btn'); 201 if (btn) { btn.disabled = true; btn.textContent = 'Updatingâ¦'; } 202 203 userRequestedUpdate = true; 204 try { sessionStorage.setItem('sb_sw_updated', '1'); } catch (e) {} 205 206 // Always re-resolve the waiting worker fresh. A cached ref can 207 // be stale: a sibling tab may have already activated this update 208 // (making it our controller), or it may have gone redundant 209 // behind a newer SW. A stale ref is exactly why "Update Now" 210 // sometimes did nothing and the banner never closed. 211 navigator.serviceWorker.getRegistration().then(function (reg) { 212 if (reg && reg.waiting) { 213 // New SW is genuinely waiting -> tell it to activate. 214 // controllerchange fires next and reloads us. 215 reg.waiting.postMessage({ type: 'SKIP_WAITING' }); 216 } else { 217 // Nothing waiting: the new version already took control 218 // (e.g. a sibling tab updated). No controllerchange will 219 // fire, so reload directly to land on the active worker 220 // â this also clears the now-stale banner. 221 window.location.reload(); 222 } 223 }).catch(function () { 224 // Couldn't resolve the registration â reload is the safe fallback. 225 window.location.reload(); 226 }); 227 } 228 229 function _swShowUpdateBanner() { 230 if (document.getElementById('sw-update-banner')) return; 231 232 var banner = document.createElement('div'); 233 banner.id = 'sw-update-banner'; 234 banner.setAttribute('role', 'status'); 235 banner.setAttribute('aria-live', 'polite'); 236 banner.className = 237 'fixed bottom-4 right-4 z-50 max-w-sm card-st p-4 shadow-lg'; 238 banner.innerHTML = 239 '<div class="flex items-start gap-3">' + 240 '<div class="flex-1 min-w-0">' + 241 '<div class="font-semibold text-theme-primary mb-1">Update available</div>' + 242 '<div class="text-sm text-theme-secondary mb-3">A new version of smrtBuild is ready. Reload now or finish what you\'re doing first.</div>' + 243 '<div class="flex gap-2">' + 244 '<button type="button" id="sw-update-now-btn" class="btn-st-primary btn-st-sm">Update Now</button>' + 245 '<button type="button" id="sw-update-later-btn" class="btn-st-ghost btn-st-sm">Later</button>' + 246 '</div>' + 247 '</div>' + 248 '<button type="button" id="sw-update-dismiss-btn" class="text-theme-secondary hover:text-theme-primary p-1 -mt-1 -mr-1" aria-label="Dismiss update notification">' + 249 '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' + 250 '</button>' + 251 '</div>'; 252 253 document.body.appendChild(banner); 254 255 document.getElementById('sw-update-now-btn') 256 .addEventListener('click', _swTriggerUpdate); 257 document.getElementById('sw-update-later-btn') 258 .addEventListener('click', _swDismissBanner); 259 document.getElementById('sw-update-dismiss-btn') 260 .addEventListener('click', _swDismissBanner); 261 } 262 263 window.addEventListener('load', function () { 264 navigator.serviceWorker.register('/sw.js') 265 .then(function (registration) { 266 console.log('[smrtBuild] Service Worker registered:', registration.scope); 267 268 // If a new SW was already waiting when we registered 269 // (e.g. user closed the banner last visit and reopened), 270 // show the prompt again. Only when hadController â first 271 // install can leave reg.waiting on some browsers. 272 if (registration.waiting && hadController) {
273 _swShowUpdateBanner(); 274 } 275 276 // Hourly update checks (supplements browser's 24hr check) 277 setInterval(function () { registration.update(); }, 3600000); 278 279 // Watch for new updates landing while the app is open 280 registration.addEventListener('updatefound', function () { 281 var newWorker = registration.installing; 282 if (!newWorker) return; 283 newWorker.addEventListener('statechange', function () { 284 // installed + existing controller = waiting for user 285 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 286 _swShowUpdateBanner(); 287 } 288 }); 289 }); 290 }) 291 .catch(function (err) { 292 console.error('[smrtBuild] Service Worker registration failed:', err); 293 }); 294 295 // controllerchange fires when the activating SW takes control. 296 // We only reload if the user explicitly asked â otherwise an 297 // out-of-band activation (e.g. sibling tab clicked Update Now) 298 // would yank this tab's state away from the user. 299 navigator.serviceWorker.addEventListener('controllerchange', function () { 300 if (!hadController || refreshing || !userRequestedUpdate) return; 301 refreshing = true; 302 window.location.reload(); 303 }); 304 }); 305 } 306 </script>
306 307</body> 308</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.