PageSourceSearch

https://smrtbld.ai/

html smrtbld.ai collected 2026-10-05 15:06:34 UTC 15,771 bytes, 308 lines download raw bytes

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.