-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhelp.php
More file actions
1655 lines (1564 loc) · 94.2 KB
/
Copy pathhelp.php
File metadata and controls
1655 lines (1564 loc) · 94.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* NewUI v4.0 - Built-in Help System
*
* Searchable help topics organized by category.
* All content stored as PHP arrays — no database needed.
* Client-side search filtering with print support.
*/
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/inc/i18n.php';
require_once __DIR__ . '/inc/rbac.php';
// 2026-07-04 (GH #13) — pick the session profile matching the
// client's cookie (TCADMOBILE vs PHPSESSID). Without this, a
// browser holding a mobile cookie opens an empty desktop session
// here and bounces to login -> redirect loop.
require_once __DIR__ . '/inc/session-bootstrap.php';
sess_bootstrap_auto();
session_start();
if (empty($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once __DIR__ . '/inc/force-pw-change.php';
force_pw_change_redirect();
$user = e($_SESSION['user']);
$level = current_role_name();
$theme = $_SESSION['day_night'] ?? 'Day';
$bs_theme = ($theme === 'Night') ? 'dark' : 'light';
$csrf = csrf_token();
$active_page = 'help';
// ── Help Content ────────────────────────────────────────────────
// Each category has an icon, a label, and an array of topics.
// Each topic has a slug (used for anchoring), a title, and HTML body.
$help_categories = [
'getting-started' => [
'icon' => 'bi-rocket-takeoff',
'label' => 'Getting Started',
'topics' => [
[
'slug' => 'logging-in',
'title' => 'Logging In',
'body' => '
<p>Open your web browser and navigate to the address your administrator provided (for example, <code>http://your-server/newui/</code>).</p>
<ol>
<li>Enter your <strong>Username</strong> and <strong>Password</strong>.</li>
<li>Choose your preferred theme — <strong>Day</strong> (light) or <strong>Night</strong> (dark).</li>
<li>Click <strong>Log In</strong>.</li>
</ol>
<p>If your administrator has enabled Two-Factor Authentication (2FA), you will see an additional screen asking for a 6-digit code from your authenticator app. Enter the code and it will auto-submit.</p>
<p>If your account is locked after too many failed attempts, a countdown timer will appear. Wait for the timer to expire, then try again.</p>
'
],
[
'slug' => 'dashboard-overview',
'title' => 'The Dashboard',
'body' => '
<p>After logging in you land on the <strong>Dashboard</strong>. This is your main workspace — a live overview of your dispatch operation.</p>
<p>The dashboard uses movable, resizable panels called <strong>widgets</strong>:</p>
<table class="table table-sm table-bordered">
<thead><tr><th>Widget</th><th>What It Shows</th></tr></thead>
<tbody>
<tr><td>Statistics</td><td>Quick counts: open incidents, unassigned calls, units on scene, available responders, and more.</td></tr>
<tr><td>Active Incidents</td><td>A table of every open incident. Click a row to see details.</td></tr>
<tr><td>Responders</td><td>All responder units with name, handle, type, status, and assignment count.</td></tr>
<tr><td>Facilities</td><td>Hospitals, shelters, stations with name, type, status, and hours.</td></tr>
<tr><td>Map</td><td>Live map with markers for incidents, units, and facilities.</td></tr>
<tr><td>Activity Log</td><td>Recent events: dispatches, status changes, notes.</td></tr>
<tr><td>Controls</td><td>Quick-action buttons for common tasks.</td></tr>
<tr><td>Communications</td><td>Buttons for Chat, SMS, Radio, Zello, and Alerts.</td></tr>
</tbody>
</table>
<h6>Moving and Resizing</h6>
<ul>
<li><strong>Move:</strong> Click and drag a widget's title bar.</li>
<li><strong>Resize:</strong> Drag the bottom-right corner of any widget.</li>
<li><strong>Toggle:</strong> Use the Widget Toggles bar to show or hide individual widgets.</li>
</ul>
<h6>Layout Tools</h6>
<ul>
<li><strong>Reset</strong> — Restore the default widget positions.</li>
<li><strong>Undo</strong> — Reverse the last layout change.</li>
<li><strong>Snapshots</strong> — Save named layouts (e.g., "Normal Ops" or "Major Event").</li>
</ul>
'
],
[
'slug' => 'navigation',
'title' => 'Navigating the Menu Bar',
'body' => '
<p>The navigation bar runs across the top of every page:</p>
<table class="table table-sm table-bordered">
<thead><tr><th>Button</th><th>Destination</th></tr></thead>
<tbody>
<tr><td>Situation</td><td>Main dashboard (home page).</td></tr>
<tr><td>Full Screen</td><td>Full-screen Situation View in a new tab (wall display).</td></tr>
<tr><td>New</td><td>New Incident form.</td></tr>
<tr><td>Units</td><td>All responder units and their status.</td></tr>
<tr><td>Fac's</td><td>Facilities (hospitals, shelters, stations).</td></tr>
<tr><td>Search</td><td>Incident search page.</td></tr>
<tr><td>Personnel</td><td>Dropdown: Roster, Teams, Scheduling, Vehicles, Equipment.</td></tr>
<tr><td>Reports</td><td>Reporting and statistics.</td></tr>
<tr><td>Config</td><td>Settings page (administrators only).</td></tr>
<tr><td>SOP</td><td>Standard Operating Procedures viewer.</td></tr>
<tr><td>Contacts</td><td>Constituents (external contacts).</td></tr>
<tr><td>Messages</td><td>Internal messaging system.</td></tr>
</tbody>
</table>
<p>On the right side: SSE connection indicator, 24-hour clock, message badge, audio mute, theme toggle, org switcher (if applicable), and your user menu.</p>
'
],
[
'slug' => 'theme-toggle',
'title' => 'Day / Night Theme',
'body' => '
<p>In the upper-right corner you will see two buttons with a sun and moon icon:</p>
<ul>
<li><i class="bi bi-sun-fill text-warning"></i> <strong>Day</strong> — Light backgrounds. Best for well-lit rooms.</li>
<li><i class="bi bi-moon-fill text-primary"></i> <strong>Night</strong> — Dark backgrounds. Easier on the eyes in low-light environments.</li>
</ul>
<p>Your theme choice is saved to your account and persists across sessions.</p>
'
],
],
],
'dispatch' => [
'icon' => 'bi-telephone-inbound',
'label' => 'Dispatch Operations',
'topics' => [
[
'slug' => 'new-incident',
'title' => 'Creating a New Incident',
'body' => '
<p>Click <strong>New</strong> in the navigation bar. The form has two columns:</p>
<ul>
<li><strong>Left:</strong> Form fields in 8 collapsible sections.</li>
<li><strong>Right:</strong> Map for location and responder assignment panel.</li>
</ul>
<h6>The 8 Sections</h6>
<ol>
<li><strong>Classification</strong> — Incident type, severity, scope, description, signal, major incident link.</li>
<li><strong>Location</strong> — Street address, city, state, zip, cross street, coordinates, destination.</li>
<li><strong>Contact</strong> — Caller name, phone, notes.</li>
<li><strong>Facilities</strong> — Receiving facility selection.</li>
<li><strong>Time & Status</strong> — Reported time, initial status.</li>
<li><strong>Call History</strong> — Search previous calls by phone or address.</li>
<li><strong>Patients</strong> — Add/remove patients with name, age, gender, chief complaint.</li>
<li><strong>Additional Details</strong> — Internal notes.</li>
</ol>
<p>When you select an incident type that has a <strong>response protocol</strong>, the protocol text appears above the map. Read it to the caller as appropriate.</p>
<p>Click <strong>Submit Incident</strong> (green button) or press <strong>Ctrl+Enter</strong> from any field.</p>
'
],
[
'slug' => 'assign-units',
'title' => 'Assigning Responder Units',
'body' => '
<p>Below the map on the New Incident form is the responder assignment panel.</p>
<ol>
<li>Use the <strong>search filter</strong> to find a unit by name or callsign.</li>
<li>Check the box next to each unit you want to dispatch.</li>
<li>When you submit the incident, all checked units are assigned automatically.</li>
</ol>
<p>You can also assign units from the <strong>Incident Detail</strong> page after the incident is created.</p>
'
],
[
'slug' => 'close-incident',
'title' => 'Closing an Incident',
'body' => '
<ol>
<li>Open the incident detail view (click the incident row on the dashboard).</li>
<li>Change the status to <strong>Closed</strong>.</li>
<li>Add any closing notes.</li>
<li>Click <strong>Save</strong>.</li>
</ol>
<p>To reopen a closed incident, change the status back to <strong>Open</strong> and save.</p>
'
],
[
'slug' => 'major-incidents',
'title' => 'Major Incidents',
'body' => '
<p>A major incident groups related calls under one umbrella. For example, a wildfire might generate separate calls for structure fires, evacuations, and medical emergencies.</p>
<ul>
<li><strong>Create</strong> a major incident from the incident detail view.</li>
<li><strong>Link</strong> existing incidents to it.</li>
<li><strong>Command structure</strong> fields record Gold, Silver, and Bronze command positions.</li>
<li><strong>Navigate</strong> between linked incidents from any call in the group.</li>
</ul>
'
],
[
'slug' => 'incident-detail',
'title' => 'The Incident Detail View',
'body' => '
<p>Click any incident on the dashboard to open its detail view. This page shows:</p>
<ul>
<li><strong>Header</strong> — Incident number, type, severity (color-coded), status.</li>
<li><strong>Location</strong> — Address, city, state, and a map with the incident marker.</li>
<li><strong>Timeline</strong> — Chronological log of notes, status changes, and assignments.</li>
<li><strong>Assignments</strong> — Currently assigned responder units.</li>
<li><strong>Patients</strong> — Patient information (if any).</li>
</ul>
<h6>Action Buttons</h6>
<ul>
<li><strong>Add a note</strong> — Append text to the timeline.</li>
<li><strong>Change status</strong> — Update the incident status.</li>
<li><strong>Assign/remove units</strong> — Manage responders.</li>
<li><strong>Navigate</strong> — Open turn-by-turn directions externally.</li>
<li><strong>ICS-213 Export</strong> — Generate a Winlink-compatible XML form.</li>
</ul>
'
],
],
],
'maps' => [
'icon' => 'bi-map',
'label' => 'Maps',
'topics' => [
[
'slug' => 'map-controls',
'title' => 'Basic Map Controls',
'body' => '
<ul>
<li><strong>Pan:</strong> Click and drag to move the map.</li>
<li><strong>Zoom:</strong> Use the scroll wheel, or click the + and − buttons.</li>
<li><strong>Click a marker:</strong> See details about the incident, unit, or facility.</li>
</ul>
<p>Maps appear on the dashboard, new incident form, incident detail views, and the full-screen situation view.</p>
'
],
[
'slug' => 'address-search',
'title' => 'Address Search / Geocoding',
'body' => '
<h6>Forward Geocoding (address to map)</h6>
<ol>
<li>Type the street address in the Address field.</li>
<li>Click <strong>Lookup</strong> or Tab to it and press Enter.</li>
<li>The map zooms to the result and drops a marker. City, State, and Cross Street auto-fill.</li>
</ol>
<h6>Reverse Geocoding (map click to address)</h6>
<ol>
<li>Click anywhere on the map.</li>
<li>A marker appears and address fields fill in automatically.</li>
</ol>
<p><strong>Tip:</strong> The geocoder prioritizes addresses near your configured location. If results are wrong, try including the city and state in your search.</p>
'
],
[
'slug' => 'map-markups',
'title' => 'Map Markups (Drawing)',
'body' => '
<p>Markups let you draw shapes and annotations on the map for operational planning:</p>
<ul>
<li><strong>Polygons</strong> — Outline search zones, perimeters, staging areas.</li>
<li><strong>Circles</strong> — Mark radius zones (hazmat, evacuation rings).</li>
<li><strong>Lines</strong> — Draw routes, boundaries, or barriers.</li>
<li><strong>Rectangles</strong> — Mark rectangular areas.</li>
<li><strong>Markers</strong> — Place labeled points of interest.</li>
</ul>
<p>Each markup has a name, description, color, and category. Categories can be toggled on/off. Markups persist in the database and are visible to all users.</p>
'
],
[
'slug' => 'weather-overlay',
'title' => 'Weather Overlay',
'body' => '
<p>If your administrator has configured a weather API key, the dashboard map can display weather overlays for temperature, precipitation, cloud cover, and wind.</p>
<p>Weather tiles are cached to reduce load and update periodically.</p>
'
],
[
'slug' => 'situation-view',
'title' => 'Situation View (Full-Screen EOC Display)',
'body' => '
<p>The <strong>Full Screen</strong> link in the top nav opens the Situation
View — a wall-display map showing every active incident, unit, and
facility, with a live side panel. It is built to run unattended for hours.</p>
<h6>The view-lock (why the map stops re-centering)</h6>
<ul>
<li>The map auto-fits to show all active incidents, and re-fits only when
the set of incidents changes — never on an idle refresh.</li>
<li><strong>Any manual pan or zoom — including the on-screen +/−
buttons — locks the view.</strong> After that, automatic refreshes
stop moving the map, so you can watch one area without being snapped
back.</li>
<li>A <strong>★ (star)</strong> button appears top-right once the view
is locked. Click it to resume auto-fit and jump back to the incidents.</li>
<li>The +/− tightness buttons bias how tight the auto-fit is (fill the
screen vs. leave margin to watch approaching weather).</li>
</ul>
<h6>Click-to-zoom with restore</h6>
<p>Click any incident, unit, or facility row in the side panel to zoom to it.
Click the <strong>same</strong> row again to restore the exact view you had
before you zoomed in.</p>
<h6>Radar — two layers</h6>
<p>The layer control (top-right) offers two radar layers:</p>
<ul>
<li><strong>Radar — US (NWS)</strong> — NOAA/NWS high-resolution
radar (1 km, ~2-minute updates). Stays sharp at any zoom. <em>US
coverage only.</em> Best for watching a cell approach a specific
site.</li>
<li><strong>Radar — Global</strong> — RainViewer worldwide
mosaic. Use it outside the US or for a country-wide glance. Looks coarse
when zoomed in (that is expected — switch to the NWS layer for a
sharp US close-up).</li>
</ul>
<p class="small text-body-secondary">Both radar layers, plus your chosen
weather/road/markup overlays and the selected side-panel tab, are remembered
per browser across reloads. Radar and weather require internet access to the
providers; an air-gapped display will not show them.</p>
'
],
[
'slug' => 'zone-coverage',
'title' => 'Zone Coverage — Who is where',
'body' => '
<p>The <strong>Zone Coverage</strong> board answers one question at a glance:
<em>how many units are in each zone right now?</em> It is built for events where
volunteers roam between areas — a festival, parade, or fair — so
anyone can decide whether to <strong>meet up</strong> with another unit or
<strong>spread out</strong> for better coverage.</p>
<h6>How it relates to Net Control</h6>
<p><strong>Net Control</strong> is the dispatcher's command board — a
dense grid where net control moves units between zones, runs PAR checks, and
logs everything to the ICS-214. <strong>Zone Coverage</strong> is the
volunteer's companion view: big, phone-friendly cards, visible to
<em>every</em> role (including Field Unit). Both read the same zones and the same
live unit positions — changing a unit's zone in one shows up in the
other within seconds.</p>
<h6>Reading the board</h6>
<ul>
<li>Each <strong>zone</strong> is a card with its colour, a big
<strong>unit count</strong>, and the units currently in it (callsign +
the lead's name).</li>
<li>A <strong>No zone reported yet</strong> bucket at the bottom lists units
that are assigned to the event but have not been placed in a zone, so
nobody is invisible.</li>
<li>The board <strong>updates in real time</strong> — a small
<em>live</em> indicator top-right shows the connection. It also re-polls
every 15 seconds as a safety net.</li>
</ul>
<h6>Reporting your own zone (one tap)</h6>
<p>If you are the person on a unit assigned to the event, an <strong>“I'm
in:”</strong> strip appears with a button for each zone. Tap one and your
unit moves there instantly — no radio call needed — and it shows up
for everyone on the board. It also writes an ICS-214 activity note
(<em>“Alpha → North Gate (self-reported)”</em>) so the
after-action log is complete. You can only ever move <strong>your own</strong>
unit this way; net control still moves any unit from the Net Control board.</p>
<h6>Choosing the event</h6>
<p>The board automatically shows the active event that has zones. If you run more
than one event with zones at once, an event picker appears top-right; your choice
is remembered on that device.</p>
<h6>Setting up zones</h6>
<p>Zones are defined on the <strong>Net Control</strong> board (draw them on the
map, or name them). Once zones exist and units are assigned to the event, Zone
Coverage fills in on its own. If the board says no zones are set up yet, that is
the place to start.</p>
<h6>Who can see and use it</h6>
<ul>
<li><strong>See the board</strong> — every role, including Field Unit
(permission <code>screen.zone_coverage</code>).</li>
<li><strong>Report own zone</strong> — Super Admin, Org Admin,
Dispatcher, Operator, and Field Unit (permission
<code>action.set_own_zone</code>); Read-Only can watch but not change.</li>
</ul>
'
],
[
'slug' => 'tile-providers',
'title' => 'Tile Providers — Map Basemap Reference',
'body' => '
<p>TicketsCAD ships with a registry of tile (basemap) providers. An admin
picks the default in <strong>Settings » Tile Providers</strong>; individual
dispatchers can also switch between any of the configured providers from
each map's layer-control widget. This reference covers what every
built-in option is, whether it needs a key, and what attribution it
requires.</p>
<h6>Free — no key required (recommended)</h6>
<table class="table table-sm table-bordered">
<thead><tr><th>Name</th><th>Coverage</th><th>Best for</th><th>Attribution</th></tr></thead>
<tbody>
<tr><td><strong>OpenStreetMap — Standard</strong></td>
<td>Worldwide</td>
<td>General-purpose street map. Best default.</td>
<td>© OpenStreetMap contributors</td></tr>
<tr><td><strong>OpenStreetMap — Humanitarian (HOT)</strong></td>
<td>Worldwide</td>
<td>Emergency-focused styling. Hospitals, schools, evacuation features highlighted. Great for dispatch.</td>
<td>© OpenStreetMap contributors, Humanitarian OSM Team</td></tr>
<tr><td><strong>USGS — Topographic</strong></td>
<td>United States only</td>
<td>Public-domain US topo map. Contour lines, hydrography, shaded relief.</td>
<td>US Geological Survey (public domain)</td></tr>
<tr><td><strong>USGS — Imagery</strong></td>
<td>United States only</td>
<td>US satellite imagery, no labels.</td>
<td>US Geological Survey</td></tr>
<tr><td><strong>USGS — Imagery + Topo</strong></td>
<td>United States only</td>
<td>Satellite imagery with topographic overlay.</td>
<td>US Geological Survey</td></tr>
<tr><td><strong>CartoDB — Positron</strong></td>
<td>Worldwide</td>
<td>Light grey base, low-distraction. Good when overlays carry the visual weight (incidents, units).</td>
<td>© OpenStreetMap contributors, © CARTO</td></tr>
<tr><td><strong>CartoDB — Dark Matter</strong></td>
<td>Worldwide</td>
<td>Dark variant. Pairs with the NewUI dark theme.</td>
<td>© OpenStreetMap contributors, © CARTO</td></tr>
<tr><td><strong>Esri — Street</strong></td>
<td>Worldwide</td>
<td>Esri-styled street map. Useful when matching agency's existing ArcGIS visuals.</td>
<td>Esri, HERE, Garmin, FAO, NOAA, USGS</td></tr>
<tr><td><strong>Esri — World Imagery</strong></td>
<td>Worldwide</td>
<td>High-resolution satellite imagery from Esri. Excellent zoom-in detail.</td>
<td>Esri, Maxar, Earthstar Geographics</td></tr>
<tr><td><strong>Esri — Topographic</strong></td>
<td>Worldwide</td>
<td>Esri-styled topo. Good worldwide alternative to USGS Topo.</td>
<td>Esri, USGS, NOAA</td></tr>
</tbody>
</table>
<p class="small text-body-secondary">
These are usable without any account or key. They will continue to
work over time as long as the tile servers themselves stay online.
Heavy production traffic should be considerate of the public
OSM/USGS infrastructure — the providers ask that you not hammer
them with sustained high-volume requests. For a 24/7 EOC display
with dozens of dispatchers, a CDN-backed paid plan (Mapbox,
MapTiler, Stadia, Azure Maps) is more appropriate.
</p>
<h6>Requires an API key</h6>
<table class="table table-sm table-bordered">
<thead><tr><th>Name</th><th>How to get a key</th><th>Free tier (approx.)</th></tr></thead>
<tbody>
<tr><td><strong>Mapbox</strong></td>
<td>account.mapbox.com — sign up for a free account, copy the default public token.</td>
<td>50,000 free map loads/month, then $0.60/1,000.</td></tr>
<tr><td><strong>Custom URL</strong> (Azure Maps, MapTiler, Stadia, Thunderforest, your own tileserver, etc.)</td>
<td>Sign up with the provider, get their tile URL template, paste it into the <strong>Custom URL</strong> field. Substitute the key in the template (e.g. <code>?key={key}</code> with the key value in <strong>API Key</strong>).</td>
<td>Varies by provider.</td></tr>
</tbody>
</table>
<h6>Azure Maps (Microsoft's Bing replacement)</h6>
<p>If you want Microsoft tiles, the path forward is Azure Maps:</p>
<ol>
<li>Sign up at <code>portal.azure.com</code>, create an Azure Maps account (free Azure subscription works).</li>
<li>Get a subscription key from the Authentication page of your Azure Maps account.</li>
<li>In TicketsCAD: Settings » Tile Providers, choose <strong>Custom URL</strong>, and paste:</li>
</ol>
<pre><code>https://atlas.microsoft.com/map/tile?subscription-key={key}&api-version=2024-04-01&tilesetId=microsoft.imagery&zoom={z}&x={x}&y={y}</code></pre>
<p>Put your subscription key in the <strong>API Key</strong> field. Other tilesets to try by swapping <code>microsoft.imagery</code>: <code>microsoft.base.road</code>, <code>microsoft.base.darkgrey</code>, <code>microsoft.weather.infrared.main</code>.</p>
<p>Azure Maps tile pricing is roughly $0.50 per 1,000 transactions on the Gen2 pay-as-you-go tier (tiles are <strong>not</strong> included in the 250k/month free transactions on Gen2). For a small fire department / ARES group typical usage runs under $1/month; for a busy 24/7 EOC, model the cost before committing.</p>
<h6>Backward compatibility — not recommended for new use</h6>
<table class="table table-sm table-bordered">
<thead><tr><th>Name</th><th>Status</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><strong>Google Maps</strong> (Streets / Satellite / Hybrid)</td>
<td>Unofficial</td>
<td>Google does not publish a public tile URL or a free Maps Static / Tile API for this kind of consumption. The URLs in our dropdown hit Google's tile servers directly — they work today but are not licensed and can be blocked at any time. For supported Google tiles, use the Maps JavaScript API (different integration, not a drop-in tile URL).</td></tr>
<tr><td><strong>Bing Maps — Road / Aerial</strong></td>
<td><span class="badge bg-danger">Retired by Microsoft</span></td>
<td>Microsoft discontinued Bing Maps for Enterprise. Basic (free) accounts were shut down on <strong>June 30, 2025</strong>; existing paid Enterprise accounts run through <strong>June 30, 2028</strong> but renewal terms tighten after July 2026 and new accounts are no longer issued. The dropdown options are kept so existing configs don't break; new deployments should pick Azure Maps (see above) or one of the free no-key options.</td></tr>
</tbody>
</table>
<h6>Troubleshooting</h6>
<ul>
<li><strong>Tiles don't load:</strong> open browser DevTools » Network and look for the tile URL. If you see 401/403, the provider needs a key. If you see 404, the URL template is malformed. If you see CORS errors, the provider may not allow direct browser access — use the tile-proxy mode under Settings.</li>
<li><strong>USGS only shows blank tiles outside the US:</strong> USGS basemaps are US-only. Switch to OpenStreetMap or Esri for international coverage.</li>
<li><strong>Bing tiles stopped working:</strong> if your install was using Bing before mid-2025, the Basic account was shut down. Switch to any free option or migrate to Azure Maps.</li>
</ul>
'
],
[
'slug' => 'road-conditions',
'title' => 'Road Conditions',
'body' => '
<p>Road condition markers show hazards on the map: slippery roads, flooding, closures, construction, and debris.</p>
<ul>
<li>Manage conditions from the Controls widget on the dashboard (click <strong>Roads</strong>).</li>
<li>Conditions can auto-expire after a set time.</li>
<li>Visual markers appear directly on the map so dispatchers can warn responders.</li>
</ul>
'
],
],
],
'communications' => [
'icon' => 'bi-chat-dots',
'label' => 'Communications',
'topics' => [
[
'slug' => 'chat',
'title' => 'Chat',
'body' => '
<p>TicketsCAD has a built-in chat system. Open it from the Communications widget on the dashboard.</p>
<h6>Channels</h6>
<ul>
<li><strong>General</strong> — Visible to everyone.</li>
<li><strong>Dispatch</strong> — Dispatch operators only.</li>
<li><strong>Admin</strong> — Administrators only.</li>
</ul>
<p>Select a channel, type your message, and press <strong>Enter</strong> or click <strong>Send</strong>.</p>
<h6>Signal Codes</h6>
<p>Click the <strong>Codes</strong> button to see predefined signal codes. Clicking a code sends it instantly.</p>
'
],
[
'slug' => 'audio-alerts',
'title' => 'Audio Alerts',
'body' => '
<p>TicketsCAD can play sounds when important events occur (new incidents, status changes, chat messages).</p>
<p>Click the <strong>speaker icon</strong> in the navigation bar to mute/unmute. Sound configuration is managed by your administrator in Config > App Preferences > Sound / Alerts.</p>
<p><strong>Note:</strong> Browsers require at least one click on the page before they allow audio playback.</p>
'
],
[
'slug' => 'has-broadcast',
'title' => 'HAS Broadcast',
'body' => '
<p>Dispatchers and administrators can send a <strong>HAS (Hearing All Stations) Broadcast</strong> — a message that goes to all connected users immediately.</p>
<p>Click the <i class="bi bi-megaphone-fill"></i> megaphone icon in the navigation bar. Enter your message and send. All logged-in users will see the broadcast.</p>
'
],
[
'slug' => 'other-integrations',
'title' => 'SMS, Radio & Meshtastic',
'body' => '
<p>Depending on your configuration, you may also have access to:</p>
<ul>
<li><strong>SMS messaging</strong> — Send text messages to responders or contacts.</li>
<li><strong>DMR radio messaging</strong> — Send text messages over digital radio.</li>
<li><strong>Meshtastic</strong> — Send messages over LoRa mesh networks.</li>
<li><strong>Zello push-to-talk</strong> — Voice and text messaging over Zello channels.</li>
<li><strong>Slack</strong> — Forward notifications to Slack channels.</li>
</ul>
<p>These are configured in Settings > Communications.</p>
'
],
[
'slug' => 'comms-console',
'title' => 'Communications Console',
'body' => '
<p>The <strong>Console</strong> page (navbar > Console) shows every configured communications channel as a vertical <strong>channel strip</strong> — Zello channels, DMR talkgroups, Meshtastic, local chat, weather alerts, and more. Each strip has a status light (green connected / amber degraded / red down / grey unknown), the last caller and how long ago they were heard, and the controls that channel supports.</p>
<ul>
<li><strong>Voice channels</strong> (Zello, DMR) — a button opens the matching radio widget for listening and push-to-talk.</li>
<li><strong>Text channels</strong> — a Messages drawer shows recent traffic and lets you send directly onto the channel.</li>
<li><strong>Feeds</strong> (weather alerts, event bus) — read-only activity drawers.</li>
</ul>
<p><strong>Views:</strong> administrators can author named layouts in the <strong>Console Designer</strong> (Design Views button) — pick which channels appear, their order, colors, labels, and controls. Published views appear as tabs across the top of the console for every dispatcher; the built-in <em>All Channels</em> tab always shows everything enabled. Your last-used tab is remembered per browser.</p>
<p>Access requires the <code>screen.console</code> permission; transmitting requires <code>action.console_tx</code>; authoring shared views requires <code>console.design</code>.</p>
'
],
[
'slug' => 'weather-alerts',
'title' => 'Weather Alerts (NWS)',
'body' => '
<p>If your administrator has enabled it, TicketsCAD can surface <strong>National Weather Service</strong> watches and warnings for your area. When a matching alert is issued you will see a <i class="bi bi-cloud-lightning-rain-fill text-danger"></i> card in the notification tray (bell icon) with an audible chime, and a banner on the Situation view.</p>
<p>Weather alerts are <strong>off by default</strong> and configured per-install. Administrators set coverage areas (a state, forecast zones, or a point + radius) and routing rules (which severity of alert goes where) at <strong>Settings > Communications & Integrations > Weather Alerts</strong>. Later phases can route alerts to chat/SMS/email and read severe warnings out over DMR/Zello radio.</p>
<p>See the <a href="docs/WEATHER-ALERTS-GUIDE.md" target="_blank">Weather Alerts administrator guide</a> for setup.</p>
'
],
],
],
'personnel' => [
'icon' => 'bi-person-badge',
'label' => 'Personnel',
'topics' => [
[
'slug' => 'roster',
'title' => 'The Roster',
'body' => '
<p>Click <strong>Personnel > Roster</strong> to manage all members. The page has a two-column layout:</p>
<ul>
<li><strong>Left:</strong> Searchable, filterable member list with count badge.</li>
<li><strong>Right:</strong> Detail panel for the selected member.</li>
</ul>
<h6>Searching and Filtering</h6>
<ul>
<li>Search by name, callsign, phone, or email.</li>
<li>Filter by status (Active/Inactive), team, or member type.</li>
</ul>
<h6>Adding a Member</h6>
<ol>
<li>Click <strong>New Member</strong>.</li>
<li>Fill in name, contact details, member type, status.</li>
<li>Click <strong>Save</strong>.</li>
</ol>
<h6>Removing Members</h6>
<p>Delete one member from the detail panel's <strong>Delete</strong>
button (soft delete — the member moves to the Wastebasket and can be
restored).</p>
<p>If your account holds the <strong>Bulk Delete Members</strong> permission,
a checkbox column appears at the right edge of the roster table with a
select-all box in the header:</p>
<ol>
<li>Tick the members to remove (or use the header box to select every
member currently shown).</li>
<li>In the bulk-actions bar, click <strong>Delete Selected</strong>.</li>
<li>Confirm in the dialog. Removed members go to the Wastebasket (soft
delete); up to 500 per request.</li>
</ol>
<p class="small text-body-secondary">The selection persists as you change the
search/filter, so a bulk delete can span members from more than one filter
view — use <strong>Clear</strong> to reset. The checkbox column only
appears for accounts with the Bulk Delete Members permission (Super Admin by
default; an admin can grant it to other roles under Config » Roles).</p>
'
],
[
'slug' => 'teams',
'title' => 'Teams',
'body' => '
<p>Click <strong>Personnel > Teams</strong>. Each team has a name, NIMS resource type, members, and description.</p>
<p>Use teams to organize members into functional groups (e.g., "Engine 1 Crew," "CERT Alpha," "Net Control").</p>
'
],
[
'slug' => 'certifications',
'title' => 'Certifications & Training',
'body' => '
<p>Each member can have certifications (EMT, HAZMAT, CPR, etc.) and training records (ICS-100, FEMA IS-700, etc.) attached to their profile.</p>
<ul>
<li>Certifications have issue and expiration dates for renewal tracking.</li>
<li>Types are configured by your administrator under Config > Personnel.</li>
</ul>
'
],
[
'slug' => 'scheduling',
'title' => 'Scheduling',
'body' => '
<p>Click <strong>Personnel > Scheduling</strong>. Two types of time management are supported:</p>
<h6>Shifts</h6>
<ul>
<li>View and manage recurring shift patterns.</li>
<li>See who is on duty at any given time.</li>
<li>Assign members to specific shifts.</li>
</ul>
<h6>Events</h6>
<ul>
<li>Create one-time or recurring events (training, meetings, community events).</li>
<li>Define time slots with specific roles needed.</li>
<li>Members can self-sign up for available slots.</li>
</ul>
'
],
[
'slug' => 'fcc-lookup',
'title' => 'FCC Callsign Lookup',
'body' => '
<p>When adding a member with an amateur radio or GMRS license:</p>
<ol>
<li>Enter the callsign in the lookup field.</li>
<li>Click <strong>Lookup</strong> to query the FCC database.</li>
<li>If found, click <strong>Apply to Form</strong> to auto-fill name and license details.</li>
</ol>
<p>Works for both amateur radio and GMRS callsigns.</p>
<p>The lookup source is set under <em>Settings → FCC Lookup</em>. The default,
<strong>OpenCallbook</strong>, is a free public service that covers both the amateur
and GMRS databases. Other choices are a local offline copy of the FCC data, the
amateur-only callook.info, or a self-hosted FCC-ULS-API. You can also choose how much
detail an internet lookup service is told about who is querying (the User-Agent).</p>
'
],
],
],
'reports' => [
'icon' => 'bi-bar-chart-line',
'label' => 'Reports',
'topics' => [
[
'slug' => 'search-incidents',
'title' => 'Searching Past Incidents',
'body' => '
<p>Click <strong>Search</strong> in the navigation bar. Filter by:</p>
<ul>
<li>Text search (scope, description, address, caller info)</li>
<li>Incident type, status, severity</li>
<li>Date range</li>
</ul>
<p>Results appear in a sortable table. Click any row to view the incident.</p>
'
],
[
'slug' => 'generating-reports',
'title' => 'Generating Reports',
'body' => '
<p>Click <strong>Reports</strong> in the navigation bar. Available reports include:</p>
<ul>
<li><strong>Incident Summary</strong> — Counts by type, severity, or time period.</li>
<li><strong>Response Time Analysis</strong> — Dispatch-to-on-scene times.</li>
<li><strong>Unit Activity</strong> — Hours, incidents, mileage per unit.</li>
<li><strong>Daily/Weekly/Monthly Activity</strong> — Volume over time.</li>
</ul>
<p>All reports can be filtered by date range, type, unit, and other criteria.</p>
'
],
[
'slug' => 'ics-213-export',
'title' => 'ICS-213 Export',
'body' => '
<p>Export incident data as ICS-213 (General Message) forms compatible with Winlink:</p>
<ol>
<li>Open the Incident Detail view.</li>
<li>Click the <strong>ICS-213</strong> export button.</li>
<li>Download the generated XML file.</li>
<li>Import it into Winlink Express or Pat for radio transmission.</li>
</ol>
'
],
[
'slug' => 'import-export',
'title' => 'Import & Export',
'body' => '
<p>The Import/Export page (Config > System > Import / Export) allows:</p>
<ul>
<li><strong>Exporting</strong> incidents, members, facilities to CSV or JSON.</li>
<li><strong>Importing</strong> data from CSV with column mapping and preview.</li>
<li><strong>Legacy migration</strong> from Tickets v3.x.</li>
</ul>
'
],
],
],
'configuration' => [
'icon' => 'bi-gear',
'label' => 'Configuration',
'topics' => [
[
'slug' => 'settings-overview',
'title' => 'Settings Overview',
'body' => '
<p>The Settings page (admin only) has a sidebar with 9 sections:</p>
<table class="table table-sm table-bordered">
<thead><tr><th>Section</th><th>Contains</th></tr></thead>
<tbody>
<tr><td>System</td><td>Health dashboard, Audit Log, Import/Export.</td></tr>
<tr><td>Installation</td><td>System Settings, API Keys, Lookup Services, Database Info, Backup.</td></tr>
<tr><td>App Preferences</td><td>Incident Types, Severity, Signals, Unit Statuses, Facility Types, Display, Sound, Numbering.</td></tr>
<tr><td>Users</td><td>Accounts, Roles, Login Settings, 2FA, Encryption.</td></tr>
<tr><td>Personnel</td><td>Organizations, Members, Teams, Certs, ICS Positions, Equipment, Vehicles, Training.</td></tr>
<tr><td>Communications</td><td>Notifications, Email, SMS, Slack, Radio, Zello, Meshtastic, Webhooks, Chat.</td></tr>
<tr><td>Locations</td><td>Facilities, Regions, Places, Road Conditions.</td></tr>
<tr><td>Maps</td><td>Map Defaults, Tile Providers.</td></tr>
<tr><td>Location Tracking</td><td>APRS, Meshtastic, OwnTracks, DMR, Zello providers, Geofencing.</td></tr>
</tbody>
</table>
'
],
[
'slug' => 'automatic-backups',
'title' => 'Automatic Backups',
'body' => '
<p>TicketsCAD backs itself up. Automatic backups are <strong>on by default</strong> — the
machines this software runs on lose power, and a backup nobody enabled is the backup nobody has.</p>
<p>Configure it at <strong>Settings > Backup / Maintenance > Automatic Backups</strong>.</p>
<table class="table table-sm table-bordered">
<thead><tr><th>Setting</th><th>Default</th><th>What it does</th></tr></thead>
<tbody>
<tr><td>Take automatic backups</td><td>On</td><td>Master switch. Off means nothing runs.</td></tr>
<tr><td>Run without a scheduler</td><td>On</td><td>Lets a due backup start after a page load, so backups happen even with no cron or Task Scheduler.</td></tr>
<tr><td>Back up every (hours)</td><td>24</td><td>How often a backup becomes due.</td></tr>
<tr><td>Keep this many</td><td>7</td><td>Older copies beyond this are deleted.</td></tr>
<tr><td>Delete older than (days)</td><td>0 (off)</td><td>Optional age limit, on top of the count.</td></tr>
<tr><td>Keep free disk (MB)</td><td>1024</td><td>Backups stop rather than eat into this reserve.</td></tr>
<tr><td>Backup folder limit (MB)</td><td>5120</td><td>Ceiling on the total size of stored backups.</td></tr>
<tr><td>Backup directory</td><td>app folder / backups</td><td>Where archives are written.</td></tr>
</tbody>
</table>
<h6 class="mt-3">Backups cannot fill your disk</h6>
<p>Before writing anything, TicketsCAD checks free space on both the backup folder and the
temporary folder. If the backup would drop free space below your reserve, or push the backup
folder past its limit, <strong>the backup is refused</strong> and the reason is shown on the
System Health page. A backup that fails loudly is better than one that fills the disk and takes
your dispatch system down.</p>
<p>Cleanup never leaves you with nothing: the newest backup is never deleted, whatever the age,
count or size rules say. Only files TicketsCAD created (named <code>ticketscad-*</code>) are ever
removed, so it is safe to point the backup folder at somewhere you keep your own archives.</p>
<h6 class="mt-3">Checking that it is working</h6>
<ul>
<li><strong>Settings > Backup / Maintenance</strong> shows how many backups exist, how much
space they use, free disk remaining, when the last one succeeded, and what it said. There is
a <strong>Back up now</strong> button that honours the same disk guard.</li>
<li><strong>Status > System Health</strong> has a <strong>Backups</strong> card. Amber means
the last verified backup is stale or storage is nearing a limit. Red means the last attempt
was refused, or nothing has ever succeeded.</li>
<li>The backup history table lists every archive, manual and automatic, and lets you download one.</li>
</ul>
<h6 class="mt-3">If a backup is refused</h6>
<p>The message names the limit that stopped it. Either free up disk space, raise the limit, reduce
how many copies are kept, or move old archives off the machine. Nothing is wrong with your
database — only the backup copy was affected.</p>
<div class="alert alert-warning small">
<strong>Backups do not include your encryption keys.</strong> Copy the <code>keys</code> folder
(next to the application folder) somewhere safe separately. Without <code>tfa.key</code>, every
two-factor enrollment is permanently unrecoverable.
</div>
'
],
[
'slug' => 'incident-types-config',
'title' => 'Managing Incident Types',
'body' => '
<p>Go to Config > App Preferences > Incident Types. Each type has:</p>
<ul>
<li><strong>Name</strong> — Short description (e.g., "Structure Fire").</li>
<li><strong>Group</strong> — Category (Fire, EMS, Law, Admin).</li>
<li><strong>Protocol</strong> — Response instructions shown to the dispatcher.</li>
<li><strong>Severity</strong> — Default severity level.</li>
<li><strong>Color</strong> — Display color on maps and lists.</li>
</ul>
'
],
[
'slug' => 'unit-history',
'title' => 'Unit History & Notes',
'body' => '
<p>Every unit / responder has a dedicated timeline showing what happened to it in one place — dispatches, status changes, action-log entries, and free-form notes. Reach it from <strong>Personnel > Units > (pick a unit) > History</strong>, or directly at <code>unit-history.php?responder_id=N</code>.</p>
<h6>What ends up on the timeline</h6>
<ul>
<li><strong>Dispatches + clears</strong> — every <code>assigns</code> row (dispatch and clear timestamps) for this unit.</li>
<li><strong>Status changes</strong> — every transition through <code>responder_set_status_internal()</code>, including any <code>extra_data</code> that came with the change.</li>
<li><strong>Action-log notes</strong> — every <code>action</code> row tagged with <code>responder = this_id</code>.</li>
<li><strong>Free-form notes</strong> — anything you type in via the <em>Add note</em> card.</li>
</ul>
<h6>Notes are ICS-214-friendly</h6>
<p>Type what the unit actually did in plain English — "advanced hoseline to attic, opened truss space", "transported patient from 812 Main to St Joe\'s ER", "held safety officer for CERT Alpha through the training exercise". Add a <em>category</em> (default <code>general</code>) so the person building the ICS-214 later can filter by "incident-XYZ" or "ics-214" or anything else you find useful. The <strong>Copy notes to clipboard</strong> button dumps a chronological plain-text stream you can paste straight into an ICS-214 activity log.</p>
<h6>Answering "where does extra_data go?"</h6>
<p>This page is also the canonical answer to a beta tester\'s GH #15 question. When you configure a unit status with <code>extra_data_target = unit</code>, the value lands on the responder row (mileage, note, etc.). Every write shows up here as a status-change event with the value in the description. If you\'re not sure a specific status is behaving the way you configured it, come here and eyeball the timeline — the answer is one glance away.</p>
<h6>Permissions</h6>
<ul>
<li>View: anyone who can view the responder page.</li>
<li>Add / delete notes: <code>action.change_unit_status</code> or admin.</li>
</ul>
'
],
[
'slug' => '2fa-remember',
'title' => 'Two-Factor "Remember Device" & Fingerprinting',
'body' => '
<p>When 2FA is enabled and a user checks <strong>Remember this device</strong> on the 2FA form, the browser gets a signed cookie so subsequent logins from that same device skip the code prompt for the configured number of days. Two knobs control who can use this feature and how strictly the "same device" check verifies it.</p>
<h6>Who sees the "Remember device" checkbox</h6>
<ul>
<li>By default, the checkbox only shows to clients coming from a <strong>trusted CIDR</strong> — the ranges in <code>settings.tfa_trusted_cidrs</code>. The default set is the four RFC1918 private ranges (127/8, 10/8, 172.16/12, 192.168/16), which fits a station-networked install.</li>
<li>Field responders on public networks (LTE, home Wi-Fi, hotel Wi-Fi) fail that check and never see the checkbox — so they get the code prompt on every login. That surprised beta users.</li>
<li>Fix: flip <code>tfa_trusted_cidrs_any_network</code> to <code>1</code> in Settings. The checkbox then shows to everyone, and the anti-replay defence comes from the fingerprint bindings below instead of from the network address.</li>
</ul>
<h6>How "same device" is verified (the fingerprint)</h6>
<p>The cookie is bound to a SHA-256 of a bag of attributes. If the attributes reproduce, the cookie is honored — otherwise it\'s rejected and a fresh 2FA code is required. Each attribute is admin-toggleable via <code>tfa_fingerprint_include_*</code> settings so you can trade friction for strictness:</p>
<table class="table table-sm table-bordered">
<thead><tr><th>Setting</th><th>Default</th><th>What it binds to</th></tr></thead>
<tbody>
<tr><td><code>tfa_fingerprint_include_ua</code></td> <td>on</td> <td>User-Agent header.</td></tr>
<tr><td><code>tfa_fingerprint_include_accept_lang</code></td> <td>on</td> <td>Accept-Language header.</td></tr>
<tr><td><code>tfa_fingerprint_include_sec_ch_ua</code></td> <td>on</td> <td>Sec-CH-UA (Chromium client hints).</td></tr>
<tr><td><code>tfa_fingerprint_include_timezone</code></td> <td>on</td> <td><code>Intl.DateTimeFormat().resolvedOptions().timeZone</code> — client-side.</td></tr>
<tr><td><code>tfa_fingerprint_include_screen</code></td> <td>on</td> <td><code>screen.availWidth × availHeight</code> — client-side.</td></tr>
<tr><td><code>tfa_fingerprint_include_platform</code></td> <td>on</td> <td><code>navigator.platform</code> — client-side.</td></tr>
<tr><td><code>tfa_fingerprint_include_ip_prefix</code></td> <td>OFF</td> <td>Client IP\'s /24 (IPv4) or /48 (IPv6). Was on until Phase 104e; caused false rejections on roaming mobile clients where the /24 rotates with the carrier.</td></tr>
</tbody>
</table>
<h6>The tradeoff — read this before flipping settings</h6>
<ul>
<li>Loose fingerprint (just UA + Accept-Language) = a stolen <code>tfa_remember</code> cookie is replay-safe for anyone who sets the same UA + language for the cookie\'s whole 30-day life. Low friction, weak defence.</li>
<li>Default (UA + language + Sec-CH-UA + timezone + screen + platform) = a thief needs to reproduce all six on their machine before the cookie honours. Timezone + screen size especially are non-trivial to fake without knowing the victim\'s device. Reasonable friction, strong defence.</li>
<li>Add <code>tfa_fingerprint_include_ip_prefix</code> back on if your install is station-networked and roaming isn\'t a concern. That adds a network-locality bind, at the cost of one round of re-2FA whenever a user\'s IP /24 changes.</li>
</ul>
<h6>Cookie lifetime</h6>
<ul>
<li><code>settings.tfa_remember_days</code> — how long the cookie is valid. Default 30. Range 1–365.</li>
<li>Every successful verify EXTENDS the remaining time to a full <code>tfa_remember_days</code> (rolling window).</li>
</ul>
<h6>Revoking</h6>
<ul>
<li>Users: <em>Profile > Security > Forget all remembered devices</em>.</li>
<li>Admins: <em>Settings > Users > Accounts > Edit > Revoke remembered devices</em>.</li>
<li>Deleting rows from <code>tfa_remember_tokens</code> also works and takes effect on the next request.</li>
</ul>
'
],
[
'slug' => 'auto-close',
'title' => 'Auto-close on all-clear',
'body' => '
<p>When the last active unit clears from an incident, TicketsCAD can close the incident automatically after a configurable grace period. Two settings drive this — both live at <strong>Config > Configuration > Incident Lifecycle</strong>.</p>
<h6>Setting 1 — Auto-close on all-clear (on / off)</h6>
<ul>
<li><strong>On (default):</strong> when the last active assignment on an incident stamps <code>clear</code>, the incident is scheduled to close after the grace period.</li>
<li><strong>Off:</strong> nothing fires automatically; an incident stays open until a dispatcher closes it via the standard Close button.</li>
</ul>
<h6>Setting 2 — Grace period</h6>
<ul>
<li>Enter a number (1–999) plus a unit: <em>seconds</em>, <em>minutes</em>, or <em>hours</em>. Default is 90 seconds.</li>
<li>Range is 1 second to 999 hours (~41 days). Values are stored as raw seconds in the <code>settings.auto_close_grace_seconds</code> row.</li>
</ul>
<h6>Cancellation on re-dispatch</h6>
<p>If a unit is assigned to the incident inside the grace window, the pending auto-close is cancelled and the incident stays open. This handles the "we thought we were done but need someone back" case without a dispatcher having to reopen a closed incident.</p>
<h6>How the close actually fires</h6>
<ul>
<li>The scheduled time lives on <code>ticket.auto_close_scheduled_at</code> until it fires or is cancelled.</li>
<li>A lightweight sweeper runs on every fetch of <code>/api/incidents.php</code> — no cron required. Bounded to 20 tickets per call so a huge backlog can\'t stall the request.</li>
<li>The close itself goes through the same <code>incident_update_status_internal()</code> path a dispatcher-driven Close uses — audit log, SSE, webhook, and any auto-clear-on-close behaviour all run normally.</li>
<li>Every auto-close writes an audit-log entry with reason "Phase 104d — grace period expired" so it\'s traceable.</li>
</ul>
<h6>Off-by-safety</h6>
<ul>
<li>Fails soft: if the auto-close helper errors, the underlying status change still commits. A misconfigured grace window can never block a legitimate close.</li>
<li>Safety re-check: even if the sweeper hits a ticket whose scheduled time passed, it re-verifies "no active assigns remain" before closing. A re-dispatch that skipped the cancel path is still respected.</li>
</ul>
'
],
[
'slug' => 'pwa-limitations',
'title' => 'PWA Sound & Critical Alerts (Limitations)',
'body' => '
<p>The TicketsCAD mobile experience runs as a <strong>Progressive Web App</strong> (PWA) — the same code that renders in a desktop browser, wrapped in an installable icon on iOS and Android. That gives us fast iteration and one codebase, but two known limitations affect audio behavior and iOS "Critical Alerts".</p>
<h6>Sound on push notifications</h6>
<ul>
<li>When the PWA is <strong>open</strong>, TicketsCAD plays the configured tones through <code>assets/js/audio-alerts.js</code>. This works — the audio settings in Config > App Preferences > Sound apply as expected.</li>
<li>When the PWA is <strong>backgrounded or closed</strong>, a push notification wakes the service worker (<code>sw.js</code>). The Web Push spec allows a <code>sound</code> property on <code>showNotification()</code>, but <strong>iOS Safari ignores it</strong>, and Android Chrome only partially honors it. You get the OS default notification chime, not the TicketsCAD tone. This is a browser-platform constraint, not a bug.</li>
</ul>
<h6>iOS Critical Alerts</h6>
<ul>
<li>Critical Alerts is an <strong>Apple entitlement</strong> restricted to registered native apps in specific "public safety, health, home security" categories. A PWA — no matter how it is installed — cannot request it. Do-Not-Disturb and Silent Mode will keep suppressing push notifications on iOS.</li>
</ul>
<h6>Roadmap</h6>
<p>Both limits go away when TicketsCAD ships a <strong>native mobile shell</strong> — currently estimated for <strong>Q4 2026</strong>. The native wrapper will register local notifications tied directly to your configured tones and let us apply for the Critical Alerts entitlement so New Incident dispatches, PAR checks, and priority messages bypass silent mode on iOS. Until then, dispatchers relying on audio should keep the PWA in the foreground during their shift.</p>
<h6>Interim workarounds</h6>
<ul>
<li>Leave the PWA foregrounded during a shift — foreground audio plays configured tones.</li>
<li>Set the iOS device to a ringtone-loud profile with Do-Not-Disturb OFF for the duration of the shift.</li>
<li>Use the SMS route (Config > Communications) with the phone\'s own SMS ringtone as a secondary audible cue.</li>
</ul>
'
],
[
'slug' => 'bed-counts',
'title' => 'How Facility Bed Counts Update',
'body' => '
<p>Each facility has two simple counters on its detail page: <strong>Beds Available</strong> and <strong>Beds Occupied</strong>. These come from the <code>facilities.beds_a</code> and <code>facilities.beds_o</code> columns. How they change depends on the facility\'s <strong>Bed Count Updates</strong> setting on the Edit Facility page.</p>
<h6>Manual mode <em>(default)</em></h6>