From 59491ccdc6bcff142ab25234db09d7fd0de4c856 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 5 Sep 2025 13:01:17 -0600 Subject: [PATCH] Refactor of admin.js into readable files. Big refactor --- .VSCodeCounter/2025-09-05_12-42-08/details.md | 350 ++ .../2025-09-05_12-42-08/diff-details.md | 15 + .VSCodeCounter/2025-09-05_12-42-08/diff.csv | 2 + .VSCodeCounter/2025-09-05_12-42-08/diff.md | 19 + .VSCodeCounter/2025-09-05_12-42-08/diff.txt | 22 + .../2025-09-05_12-42-08/results.csv | 337 ++ .../2025-09-05_12-42-08/results.json | 1 + .VSCodeCounter/2025-09-05_12-42-08/results.md | 148 + .../2025-09-05_12-42-08/results.txt | 486 +++ map/app/public/admin.html | 16 +- map/app/public/js/admin-auth.js | 87 + map/app/public/js/admin-core.js | 324 ++ map/app/public/js/admin-email.js | 394 +++ map/app/public/js/admin-integration.js | 189 ++ map/app/public/js/admin-map.js | 249 ++ map/app/public/js/admin-new.js | 143 + map/app/public/js/admin-old.js | 1791 +++++++++++ map/app/public/js/admin-shift-volunteers.js | 530 +++ map/app/public/js/admin-shifts.js | 419 +++ map/app/public/js/admin-users.js | 365 +++ map/app/public/js/admin-walksheet.js | 471 +++ map/app/public/js/admin.js | 2847 +---------------- map/app/public/js/admin/auth.js | 0 map/app/public/js/admin/navigation.js | 0 map/app/public/js/admin/shifts.js | 0 map/app/public/js/admin/startLocation.js | 0 map/app/public/js/admin/users.js | 0 map/app/public/js/admin/utils.js | 0 map/app/public/js/admin/walkSheet.js | 0 map/files-explainer.md | 38 +- 30 files changed, 6501 insertions(+), 2742 deletions(-) create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/details.md create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/diff-details.md create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/diff.csv create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/diff.md create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/diff.txt create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/results.csv create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/results.json create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/results.md create mode 100644 .VSCodeCounter/2025-09-05_12-42-08/results.txt create mode 100644 map/app/public/js/admin-auth.js create mode 100644 map/app/public/js/admin-core.js create mode 100644 map/app/public/js/admin-email.js create mode 100644 map/app/public/js/admin-integration.js create mode 100644 map/app/public/js/admin-map.js create mode 100644 map/app/public/js/admin-new.js create mode 100644 map/app/public/js/admin-old.js create mode 100644 map/app/public/js/admin-shift-volunteers.js create mode 100644 map/app/public/js/admin-shifts.js create mode 100644 map/app/public/js/admin-users.js create mode 100644 map/app/public/js/admin-walksheet.js delete mode 100644 map/app/public/js/admin/auth.js delete mode 100644 map/app/public/js/admin/navigation.js delete mode 100644 map/app/public/js/admin/shifts.js delete mode 100644 map/app/public/js/admin/startLocation.js delete mode 100644 map/app/public/js/admin/users.js delete mode 100644 map/app/public/js/admin/utils.js delete mode 100644 map/app/public/js/admin/walkSheet.js diff --git a/.VSCodeCounter/2025-09-05_12-42-08/details.md b/.VSCodeCounter/2025-09-05_12-42-08/details.md new file mode 100644 index 0000000..e39dcba --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/details.md @@ -0,0 +1,350 @@ +# Details + +Date : 2025-09-05 12:42:08 + +Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite + +Total : 335 files, 91924 codes, 4969 comments, 47542 blanks, all 144435 lines + +[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md) + +## Files +| filename | language | code | comment | blank | total | +| :--- | :--- | ---: | ---: | ---: | ---: | +| [README.md](/README.md) | Markdown | 55 | 0 | 24 | 79 | +| [combined.log](/combined.log) | Log | 13 | 0 | 3 | 16 | +| [config.sh](/config.sh) | Shell Script | 810 | 177 | 224 | 1,211 | +| [configs/homepage/bookmarks.yaml](/configs/homepage/bookmarks.yaml) | YAML | 47 | 2 | 5 | 54 | +| [configs/homepage/custom.css](/configs/homepage/custom.css) | PostCSS | 0 | 0 | 1 | 1 | +| [configs/homepage/custom.js](/configs/homepage/custom.js) | JavaScript | 0 | 0 | 1 | 1 | +| [configs/homepage/docker.yaml](/configs/homepage/docker.yaml) | YAML | 3 | 2 | 2 | 7 | +| [configs/homepage/kubernetes.yaml](/configs/homepage/kubernetes.yaml) | YAML | 1 | 1 | 1 | 3 | +| [configs/homepage/logs/homepage.log](/configs/homepage/logs/homepage.log) | Log | 692 | 0 | 14 | 706 | +| [configs/homepage/services.yaml](/configs/homepage/services.yaml) | YAML | 59 | 1 | 15 | 75 | +| [configs/homepage/settings.yaml](/configs/homepage/settings.yaml) | YAML | 36 | 4 | 10 | 50 | +| [configs/homepage/widgets.yaml](/configs/homepage/widgets.yaml) | YAML | 26 | 2 | 5 | 33 | +| [configs/mkdocs-site/default.conf](/configs/mkdocs-site/default.conf) | Properties | 33 | 7 | 9 | 49 | +| [docker-compose.yml](/docker-compose.yml) | YAML | 238 | 2 | 13 | 253 | +| [map/Instuctions.md](/map/Instuctions.md) | Markdown | 56 | 0 | 21 | 77 | +| [map/README.md](/map/README.md) | Markdown | 851 | 0 | 252 | 1,103 | +| [map/app/Dockerfile](/map/app/Dockerfile) | Docker | 20 | 7 | 10 | 37 | +| [map/app/config/index.js](/map/app/config/index.js) | JavaScript | 118 | 15 | 18 | 151 | +| [map/app/controllers/authController.js](/map/app/controllers/authController.js) | JavaScript | 172 | 13 | 25 | 210 | +| [map/app/controllers/cutsController.js](/map/app/controllers/cutsController.js) | JavaScript | 276 | 41 | 47 | 364 | +| [map/app/controllers/dashboardController.js](/map/app/controllers/dashboardController.js) | JavaScript | 70 | 9 | 15 | 94 | +| [map/app/controllers/dataConvertController.js](/map/app/controllers/dataConvertController.js) | JavaScript | 352 | 50 | 76 | 478 | +| [map/app/controllers/externalDataController.js](/map/app/controllers/externalDataController.js) | JavaScript | 101 | 11 | 21 | 133 | +| [map/app/controllers/listmonkController.js](/map/app/controllers/listmonkController.js) | JavaScript | 212 | 12 | 29 | 253 | +| [map/app/controllers/locationsController.js](/map/app/controllers/locationsController.js) | JavaScript | 333 | 23 | 58 | 414 | +| [map/app/controllers/passwordRecoveryController.js](/map/app/controllers/passwordRecoveryController.js) | JavaScript | 45 | 4 | 12 | 61 | +| [map/app/controllers/publicShiftsController.js](/map/app/controllers/publicShiftsController.js) | JavaScript | 212 | 21 | 38 | 271 | +| [map/app/controllers/settingsController.js](/map/app/controllers/settingsController.js) | JavaScript | 303 | 17 | 50 | 370 | +| [map/app/controllers/shiftsController.js](/map/app/controllers/shiftsController.js) | JavaScript | 643 | 57 | 123 | 823 | +| [map/app/controllers/usersController.js](/map/app/controllers/usersController.js) | JavaScript | 341 | 18 | 54 | 413 | +| [map/app/middleware/auth.js](/map/app/middleware/auth.js) | JavaScript | 170 | 13 | 25 | 208 | +| [map/app/middleware/rateLimiter.js](/map/app/middleware/rateLimiter.js) | JavaScript | 85 | 10 | 9 | 104 | +| [map/app/package-lock.json](/map/app/package-lock.json) | JSON | 2,203 | 0 | 1 | 2,204 | +| [map/app/package.json](/map/app/package.json) | JSON | 43 | 0 | 1 | 44 | +| [map/app/public/admin.html](/map/app/public/admin.html) | HTML | 1,080 | 46 | 135 | 1,261 | +| [map/app/public/css/REFACTORING\_SUMMARY.md](/map/app/public/css/REFACTORING_SUMMARY.md) | Markdown | 51 | 0 | 13 | 64 | +| [map/app/public/css/admin.css](/map/app/public/css/admin.css) | PostCSS | 12 | 4 | 3 | 19 | +| [map/app/public/css/admin/cuts-shifts.css](/map/app/public/css/admin/cuts-shifts.css) | PostCSS | 225 | 13 | 44 | 282 | +| [map/app/public/css/admin/data-convert.css](/map/app/public/css/admin/data-convert.css) | PostCSS | 176 | 7 | 33 | 216 | +| [map/app/public/css/admin/forms.css](/map/app/public/css/admin/forms.css) | PostCSS | 207 | 9 | 36 | 252 | +| [map/app/public/css/admin/layout.css](/map/app/public/css/admin/layout.css) | PostCSS | 202 | 7 | 36 | 245 | +| [map/app/public/css/admin/modals.css](/map/app/public/css/admin/modals.css) | PostCSS | 259 | 6 | 46 | 311 | +| [map/app/public/css/admin/nocodb-links.css](/map/app/public/css/admin/nocodb-links.css) | PostCSS | 109 | 4 | 23 | 136 | +| [map/app/public/css/admin/responsive.css](/map/app/public/css/admin/responsive.css) | PostCSS | 552 | 28 | 112 | 692 | +| [map/app/public/css/admin/status-messages.css](/map/app/public/css/admin/status-messages.css) | PostCSS | 154 | 9 | 27 | 190 | +| [map/app/public/css/admin/user-management.css](/map/app/public/css/admin/user-management.css) | PostCSS | 179 | 10 | 31 | 220 | +| [map/app/public/css/admin/variables.css](/map/app/public/css/admin/variables.css) | PostCSS | 48 | 9 | 10 | 67 | +| [map/app/public/css/admin/walk-sheet.css](/map/app/public/css/admin/walk-sheet.css) | PostCSS | 252 | 12 | 39 | 303 | +| [map/app/public/css/modules/apartment-marker.css](/map/app/public/css/modules/apartment-marker.css) | PostCSS | 35 | 2 | 5 | 42 | +| [map/app/public/css/modules/apartment-popup.css](/map/app/public/css/modules/apartment-popup.css) | PostCSS | 197 | 9 | 30 | 236 | +| [map/app/public/css/modules/base.css](/map/app/public/css/modules/base.css) | PostCSS | 68 | 22 | 12 | 102 | +| [map/app/public/css/modules/buttons.css](/map/app/public/css/modules/buttons.css) | PostCSS | 70 | 2 | 16 | 88 | +| [map/app/public/css/modules/cache-busting.css](/map/app/public/css/modules/cache-busting.css) | PostCSS | 99 | 2 | 13 | 114 | +| [map/app/public/css/modules/cuts.css](/map/app/public/css/modules/cuts.css) | PostCSS | 829 | 22 | 146 | 997 | +| [map/app/public/css/modules/dashboard.css](/map/app/public/css/modules/dashboard.css) | PostCSS | 129 | 3 | 29 | 161 | +| [map/app/public/css/modules/doc-search.css](/map/app/public/css/modules/doc-search.css) | PostCSS | 123 | 2 | 20 | 145 | +| [map/app/public/css/modules/forms.css](/map/app/public/css/modules/forms.css) | PostCSS | 105 | 2 | 18 | 125 | +| [map/app/public/css/modules/layout.css](/map/app/public/css/modules/layout.css) | PostCSS | 83 | 5 | 11 | 99 | +| [map/app/public/css/modules/leaflet-custom.css](/map/app/public/css/modules/leaflet-custom.css) | PostCSS | 147 | 20 | 32 | 199 | +| [map/app/public/css/modules/listmonk.css](/map/app/public/css/modules/listmonk.css) | PostCSS | 295 | 4 | 55 | 354 | +| [map/app/public/css/modules/map-controls.css](/map/app/public/css/modules/map-controls.css) | PostCSS | 281 | 13 | 45 | 339 | +| [map/app/public/css/modules/mobile-ui.css](/map/app/public/css/modules/mobile-ui.css) | PostCSS | 205 | 8 | 36 | 249 | +| [map/app/public/css/modules/modal.css](/map/app/public/css/modules/modal.css) | PostCSS | 73 | 1 | 10 | 84 | +| [map/app/public/css/modules/nocodb-dropdown.css](/map/app/public/css/modules/nocodb-dropdown.css) | PostCSS | 134 | 4 | 24 | 162 | +| [map/app/public/css/modules/notifications.css](/map/app/public/css/modules/notifications.css) | PostCSS | 105 | 5 | 16 | 126 | +| [map/app/public/css/modules/print.css](/map/app/public/css/modules/print.css) | PostCSS | 11 | 1 | 2 | 14 | +| [map/app/public/css/modules/qr-code.css](/map/app/public/css/modules/qr-code.css) | PostCSS | 59 | 3 | 13 | 75 | +| [map/app/public/css/modules/responsive.css](/map/app/public/css/modules/responsive.css) | PostCSS | 150 | 12 | 29 | 191 | +| [map/app/public/css/modules/start-location-marker.css](/map/app/public/css/modules/start-location-marker.css) | PostCSS | 65 | 3 | 8 | 76 | +| [map/app/public/css/modules/temp-user.css](/map/app/public/css/modules/temp-user.css) | PostCSS | 46 | 6 | 5 | 57 | +| [map/app/public/css/modules/unified-search.css](/map/app/public/css/modules/unified-search.css) | PostCSS | 580 | 30 | 99 | 709 | +| [map/app/public/css/public-shifts.css](/map/app/public/css/public-shifts.css) | PostCSS | 418 | 24 | 83 | 525 | +| [map/app/public/css/shifts.css](/map/app/public/css/shifts.css) | PostCSS | 608 | 21 | 118 | 747 | +| [map/app/public/css/style.css](/map/app/public/css/style.css) | PostCSS | 21 | 0 | 1 | 22 | +| [map/app/public/css/user.css](/map/app/public/css/user.css) | PostCSS | 364 | 13 | 70 | 447 | +| [map/app/public/index.html](/map/app/public/index.html) | HTML | 384 | 28 | 45 | 457 | +| [map/app/public/js/admin-auth.js](/map/app/public/js/admin-auth.js) | JavaScript | 63 | 11 | 14 | 88 | +| [map/app/public/js/admin-core.js](/map/app/public/js/admin-core.js) | JavaScript | 239 | 46 | 40 | 325 | +| [map/app/public/js/admin-cuts.js](/map/app/public/js/admin-cuts.js) | JavaScript | 1,505 | 184 | 302 | 1,991 | +| [map/app/public/js/admin-email.js](/map/app/public/js/admin-email.js) | JavaScript | 319 | 29 | 47 | 395 | +| [map/app/public/js/admin-map.js](/map/app/public/js/admin-map.js) | JavaScript | 178 | 26 | 46 | 250 | +| [map/app/public/js/admin-shift-volunteers.js](/map/app/public/js/admin-shift-volunteers.js) | JavaScript | 416 | 49 | 66 | 531 | +| [map/app/public/js/admin-shifts.js](/map/app/public/js/admin-shifts.js) | JavaScript | 330 | 35 | 55 | 420 | +| [map/app/public/js/admin-users.js](/map/app/public/js/admin-users.js) | JavaScript | 305 | 16 | 45 | 366 | +| [map/app/public/js/admin-walksheet.js](/map/app/public/js/admin-walksheet.js) | JavaScript | 387 | 35 | 50 | 472 | +| [map/app/public/js/admin.js](/map/app/public/js/admin.js) | JavaScript | 2,309 | 178 | 288 | 2,775 | +| [map/app/public/js/admin/auth.js](/map/app/public/js/admin/auth.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/admin/navigation.js](/map/app/public/js/admin/navigation.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/admin/shifts.js](/map/app/public/js/admin/shifts.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/admin/startLocation.js](/map/app/public/js/admin/startLocation.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/admin/users.js](/map/app/public/js/admin/users.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/admin/utils.js](/map/app/public/js/admin/utils.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/admin/walkSheet.js](/map/app/public/js/admin/walkSheet.js) | JavaScript | 0 | 0 | 1 | 1 | +| [map/app/public/js/auth.js](/map/app/public/js/auth.js) | JavaScript | 181 | 28 | 34 | 243 | +| [map/app/public/js/cache-manager.js](/map/app/public/js/cache-manager.js) | JavaScript | 219 | 71 | 28 | 318 | +| [map/app/public/js/config.js](/map/app/public/js/config.js) | JavaScript | 32 | 3 | 3 | 38 | +| [map/app/public/js/cut-controls.js](/map/app/public/js/cut-controls.js) | JavaScript | 982 | 163 | 145 | 1,290 | +| [map/app/public/js/cut-drawing.js](/map/app/public/js/cut-drawing.js) | JavaScript | 206 | 72 | 59 | 337 | +| [map/app/public/js/cut-manager.js](/map/app/public/js/cut-manager.js) | JavaScript | 359 | 96 | 79 | 534 | +| [map/app/public/js/dashboard.js](/map/app/public/js/dashboard.js) | JavaScript | 333 | 23 | 33 | 389 | +| [map/app/public/js/data-convert.js](/map/app/public/js/data-convert.js) | JavaScript | 510 | 64 | 118 | 692 | +| [map/app/public/js/database-search.js](/map/app/public/js/database-search.js) | JavaScript | 186 | 76 | 51 | 313 | +| [map/app/public/js/external-layers.js](/map/app/public/js/external-layers.js) | JavaScript | 513 | 39 | 45 | 597 | +| [map/app/public/js/listmonk-admin.js](/map/app/public/js/listmonk-admin.js) | JavaScript | 410 | 76 | 85 | 571 | +| [map/app/public/js/listmonk-status.js](/map/app/public/js/listmonk-status.js) | JavaScript | 169 | 25 | 32 | 226 | +| [map/app/public/js/location-manager.js](/map/app/public/js/location-manager.js) | JavaScript | 998 | 56 | 91 | 1,145 | +| [map/app/public/js/main.js](/map/app/public/js/main.js) | JavaScript | 178 | 40 | 36 | 254 | +| [map/app/public/js/map-manager.js](/map/app/public/js/map-manager.js) | JavaScript | 82 | 12 | 19 | 113 | +| [map/app/public/js/map-search.js](/map/app/public/js/map-search.js) | JavaScript | 120 | 44 | 36 | 200 | +| [map/app/public/js/mkdocs-search.js](/map/app/public/js/mkdocs-search.js) | JavaScript | 263 | 36 | 52 | 351 | +| [map/app/public/js/public-shifts.js](/map/app/public/js/public-shifts.js) | JavaScript | 518 | 15 | 25 | 558 | +| [map/app/public/js/search-manager.js](/map/app/public/js/search-manager.js) | JavaScript | 407 | 138 | 100 | 645 | +| [map/app/public/js/shifts.js](/map/app/public/js/shifts.js) | JavaScript | 1,059 | 35 | 55 | 1,149 | +| [map/app/public/js/ui-controls.js](/map/app/public/js/ui-controls.js) | JavaScript | 572 | 74 | 101 | 747 | +| [map/app/public/js/user.js](/map/app/public/js/user.js) | JavaScript | 81 | 12 | 19 | 112 | +| [map/app/public/js/utils.js](/map/app/public/js/utils.js) | JavaScript | 96 | 21 | 25 | 142 | +| [map/app/public/login.html](/map/app/public/login.html) | HTML | 459 | 3 | 76 | 538 | +| [map/app/public/public-shifts.html](/map/app/public/public-shifts.html) | HTML | 88 | 4 | 7 | 99 | +| [map/app/public/shifts.html](/map/app/public/shifts.html) | HTML | 73 | 5 | 8 | 86 | +| [map/app/public/user.html](/map/app/public/user.html) | HTML | 47 | 7 | 9 | 63 | +| [map/app/routes/admin.js](/map/app/routes/admin.js) | JavaScript | 80 | 15 | 16 | 111 | +| [map/app/routes/auth.js](/map/app/routes/auth.js) | JavaScript | 14 | 5 | 6 | 25 | +| [map/app/routes/cuts.js](/map/app/routes/cuts.js) | JavaScript | 18 | 6 | 7 | 31 | +| [map/app/routes/dashboard.js](/map/app/routes/dashboard.js) | JavaScript | 5 | 0 | 3 | 8 | +| [map/app/routes/dataConvert.js](/map/app/routes/dataConvert.js) | JavaScript | 21 | 4 | 6 | 31 | +| [map/app/routes/debug.js](/map/app/routes/debug.js) | JavaScript | 236 | 10 | 36 | 282 | +| [map/app/routes/external.js](/map/app/routes/external.js) | JavaScript | 7 | 2 | 4 | 13 | +| [map/app/routes/geocoding.js](/map/app/routes/geocoding.js) | JavaScript | 120 | 24 | 31 | 175 | +| [map/app/routes/index.js](/map/app/routes/index.js) | JavaScript | 153 | 29 | 36 | 218 | +| [map/app/routes/listmonk.js](/map/app/routes/listmonk.js) | JavaScript | 12 | 5 | 9 | 26 | +| [map/app/routes/locations.js](/map/app/routes/locations.js) | JavaScript | 11 | 5 | 6 | 22 | +| [map/app/routes/public.js](/map/app/routes/public.js) | JavaScript | 8 | 1 | 3 | 12 | +| [map/app/routes/publicShifts.js](/map/app/routes/publicShifts.js) | JavaScript | 8 | 1 | 2 | 11 | +| [map/app/routes/qr.js](/map/app/routes/qr.js) | JavaScript | 39 | 1 | 8 | 48 | +| [map/app/routes/settings.js](/map/app/routes/settings.js) | JavaScript | 8 | 2 | 3 | 13 | +| [map/app/routes/shifts.js](/map/app/routes/shifts.js) | JavaScript | 16 | 4 | 5 | 25 | +| [map/app/routes/users.js](/map/app/routes/users.js) | JavaScript | 11 | 6 | 7 | 24 | +| [map/app/server.js](/map/app/server.js) | JavaScript | 249 | 43 | 49 | 341 | +| [map/app/services/accountExpiration.js](/map/app/services/accountExpiration.js) | JavaScript | 59 | 11 | 19 | 89 | +| [map/app/services/email.js](/map/app/services/email.js) | JavaScript | 109 | 4 | 19 | 132 | +| [map/app/services/emailTemplates.js](/map/app/services/emailTemplates.js) | JavaScript | 59 | 10 | 16 | 85 | +| [map/app/services/geocoding.js](/map/app/services/geocoding.js) | JavaScript | 219 | 51 | 44 | 314 | +| [map/app/services/listmonk.js](/map/app/services/listmonk.js) | JavaScript | 412 | 36 | 66 | 514 | +| [map/app/services/nocodb.js](/map/app/services/nocodb.js) | JavaScript | 172 | 22 | 36 | 230 | +| [map/app/services/qrcode.js](/map/app/services/qrcode.js) | JavaScript | 110 | 33 | 20 | 163 | +| [map/app/services/socrata.js](/map/app/services/socrata.js) | JavaScript | 49 | 9 | 10 | 68 | +| [map/app/templates/email/login-details.html](/map/app/templates/email/login-details.html) | HTML | 113 | 0 | 4 | 117 | +| [map/app/templates/email/password-recovery.html](/map/app/templates/email/password-recovery.html) | HTML | 79 | 0 | 1 | 80 | +| [map/app/templates/email/public-shift-signup-existing.html](/map/app/templates/email/public-shift-signup-existing.html) | HTML | 161 | 0 | 22 | 183 | +| [map/app/templates/email/public-shift-signup-new.html](/map/app/templates/email/public-shift-signup-new.html) | HTML | 31 | 0 | 5 | 36 | +| [map/app/templates/email/shift-details.html](/map/app/templates/email/shift-details.html) | HTML | 152 | 0 | 5 | 157 | +| [map/app/templates/email/user-broadcast.html](/map/app/templates/email/user-broadcast.html) | HTML | 108 | 0 | 2 | 110 | +| [map/app/utils/cacheBusting.js](/map/app/utils/cacheBusting.js) | JavaScript | 105 | 51 | 24 | 180 | +| [map/app/utils/helpers.js](/map/app/utils/helpers.js) | JavaScript | 149 | 25 | 28 | 202 | +| [map/app/utils/logger.js](/map/app/utils/logger.js) | JavaScript | 38 | 2 | 3 | 43 | +| [map/build-nocodb.sh](/map/build-nocodb.sh) | Shell Script | 925 | 60 | 93 | 1,078 | +| [map/data/City of Edmonton - Neighbourhoods (Centroid Point)\_20250807.geojson](/map/data/City%20of%20Edmonton%20-%20Neighbourhoods%20(Centroid%20Point)_20250807.geojson) | JSON | 3 | 0 | 1 | 4 | +| [map/data/City of Edmonton - Neighbourhoods\_20250807.geojson](/map/data/City%20of%20Edmonton%20-%20Neighbourhoods_20250807.geojson) | JSON | 5 | 0 | 0 | 5 | +| [map/data/City of Edmonton Ward Boundary and Council Composition\_Current\_20250807.geojson](/map/data/City%20of%20Edmonton%20Ward%20Boundary%20and%20Council%20Composition_Current_20250807.geojson) | JSON | 5 | 0 | 0 | 5 | +| [map/data/City\_of\_Edmonton\_-\_Neighbourhoods\_20250807.csv](/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv) | CSV | 689 | 0 | 1 | 690 | +| [map/data/City\_of\_Edmonton\_-\_Neighbourhoods\_\_Centroid\_Point\_\_20250807.csv](/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv) | CSV | 407 | 0 | 1 | 408 | +| [map/data/City\_of\_Edmonton\_Ward\_Boundary\_and\_Council\_Composition\_Current\_20250807.csv](/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv) | CSV | 13 | 0 | 1 | 14 | +| [map/data/convert\_edmonton\_optimized.js](/map/data/convert_edmonton_optimized.js) | JavaScript | 185 | 21 | 43 | 249 | +| [map/data/convert\_ward\_boundaries\_optimized.js](/map/data/convert_ward_boundaries_optimized.js) | JavaScript | 173 | 24 | 45 | 242 | +| [map/data/edmonton\_neighborhoods\_nocodb\_optimized.csv](/map/data/edmonton_neighborhoods_nocodb_optimized.csv) | CSV | 689 | 0 | 0 | 689 | +| [map/data/edmonton\_nocodb\_optimized.csv](/map/data/edmonton_nocodb_optimized.csv) | CSV | 1,095 | 0 | 406 | 1,501 | +| [map/data/edmonton\_ward\_boundaries\_optimized.csv](/map/data/edmonton_ward_boundaries_optimized.csv) | CSV | 13 | 0 | 0 | 13 | +| [map/data/exampledata.csv](/map/data/exampledata.csv) | CSV | 7 | 0 | 0 | 7 | +| [map/docker-compose.yml](/map/docker-compose.yml) | YAML | 31 | 0 | 3 | 34 | +| [map/files-explainer.md](/map/files-explainer.md) | Markdown | 292 | 0 | 293 | 585 | +| [map/instruct/ADMIN\_IMPLEMENTATION.md](/map/instruct/ADMIN_IMPLEMENTATION.md) | Markdown | 102 | 0 | 28 | 130 | +| [map/instruct/CUT\_IMPLEMENTATION\_SUMMARY.md](/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md) | Markdown | 156 | 0 | 34 | 190 | +| [map/instruct/CUT\_PUBLIC\_IMPLEMENTATION.md](/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md) | Markdown | 124 | 5 | 31 | 160 | +| [map/instruct/CUT\_SIMPLIFICATION\_SUMMARY.md](/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md) | Markdown | 65 | 0 | 21 | 86 | +| [map/instruct/LISTMONK\_INTEGRATION\_GUIDE.md](/map/instruct/LISTMONK_INTEGRATION_GUIDE.md) | Markdown | 249 | 0 | 70 | 319 | +| [map/instruct/SHIFT\_PERFORMANCE\_FIX.md](/map/instruct/SHIFT_PERFORMANCE_FIX.md) | Markdown | 71 | 0 | 22 | 93 | +| [map/instruct/TEMP\_USER\_IMPLEMENTATION.md](/map/instruct/TEMP_USER_IMPLEMENTATION.md) | Markdown | 88 | 0 | 28 | 116 | +| [map/instruct/TEMP\_USER\_TEST.md](/map/instruct/TEMP_USER_TEST.md) | Markdown | 113 | 0 | 34 | 147 | +| [map/instruct/build-nocodb.md](/map/instruct/build-nocodb.md) | Markdown | 374 | 0 | 67 | 441 | +| [map/package-lock.json](/map/package-lock.json) | JSON | 17 | 0 | 1 | 18 | +| [map/package.json](/map/package.json) | JSON | 5 | 0 | 1 | 6 | +| [mkdocs/docs/adv/ansible.md](/mkdocs/docs/adv/ansible.md) | Markdown | 373 | 0 | 152 | 525 | +| [mkdocs/docs/adv/index.md](/mkdocs/docs/adv/index.md) | Markdown | 2 | 0 | 1 | 3 | +| [mkdocs/docs/adv/vscode-ssh.md](/mkdocs/docs/adv/vscode-ssh.md) | Markdown | 511 | 0 | 174 | 685 | +| [mkdocs/docs/assets/repo-data/admin-changemaker.lite.json](/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/anthropics-claude-code.json](/mkdocs/docs/assets/repo-data/anthropics-claude-code.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/coder-code-server.json](/mkdocs/docs/assets/repo-data/coder-code-server.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/gethomepage-homepage.json](/mkdocs/docs/assets/repo-data/gethomepage-homepage.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/go-gitea-gitea.json](/mkdocs/docs/assets/repo-data/go-gitea-gitea.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/knadh-listmonk.json](/mkdocs/docs/assets/repo-data/knadh-listmonk.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/lyqht-mini-qr.json](/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/n8n-io-n8n.json](/mkdocs/docs/assets/repo-data/n8n-io-n8n.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/nocodb-nocodb.json](/mkdocs/docs/assets/repo-data/nocodb-nocodb.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/ollama-ollama.json](/mkdocs/docs/assets/repo-data/ollama-ollama.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json](/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/docs/blog/index.md](/mkdocs/docs/blog/index.md) | Markdown | 0 | 0 | 1 | 1 | +| [mkdocs/docs/blog/posts/1.md](/mkdocs/docs/blog/posts/1.md) | Markdown | 6 | 0 | 3 | 9 | +| [mkdocs/docs/blog/posts/2.md](/mkdocs/docs/blog/posts/2.md) | Markdown | 10 | 0 | 4 | 14 | +| [mkdocs/docs/blog/posts/3.md](/mkdocs/docs/blog/posts/3.md) | Markdown | 54 | 0 | 21 | 75 | +| [mkdocs/docs/build/index.md](/mkdocs/docs/build/index.md) | Markdown | 336 | 0 | 150 | 486 | +| [mkdocs/docs/build/map.md](/mkdocs/docs/build/map.md) | Markdown | 155 | 0 | 62 | 217 | +| [mkdocs/docs/build/server.md](/mkdocs/docs/build/server.md) | Markdown | 122 | 0 | 27 | 149 | +| [mkdocs/docs/build/site.md](/mkdocs/docs/build/site.md) | Markdown | 74 | 0 | 34 | 108 | +| [mkdocs/docs/config/cloudflare-config.md](/mkdocs/docs/config/cloudflare-config.md) | Markdown | 45 | 0 | 16 | 61 | +| [mkdocs/docs/config/coder.md](/mkdocs/docs/config/coder.md) | Markdown | 139 | 0 | 77 | 216 | +| [mkdocs/docs/config/index.md](/mkdocs/docs/config/index.md) | Markdown | 3 | 0 | 4 | 7 | +| [mkdocs/docs/config/map.md](/mkdocs/docs/config/map.md) | Markdown | 299 | 0 | 91 | 390 | +| [mkdocs/docs/config/mkdocs.md](/mkdocs/docs/config/mkdocs.md) | Markdown | 102 | 0 | 42 | 144 | +| [mkdocs/docs/hooks/repo\_widget\_hook.py](/mkdocs/docs/hooks/repo_widget_hook.py) | Python | 119 | 24 | 16 | 159 | +| [mkdocs/docs/how to/canvass.md](/mkdocs/docs/how%20to/canvass.md) | Markdown | 2 | 0 | 3 | 5 | +| [mkdocs/docs/index.md](/mkdocs/docs/index.md) | Markdown | 85 | 0 | 29 | 114 | +| [mkdocs/docs/javascripts/gitea-widget.js](/mkdocs/docs/javascripts/gitea-widget.js) | JavaScript | 121 | 3 | 8 | 132 | +| [mkdocs/docs/javascripts/github-widget.js](/mkdocs/docs/javascripts/github-widget.js) | JavaScript | 144 | 10 | 14 | 168 | +| [mkdocs/docs/javascripts/home.js](/mkdocs/docs/javascripts/home.js) | JavaScript | 271 | 30 | 37 | 338 | +| [mkdocs/docs/manual/index.md](/mkdocs/docs/manual/index.md) | Markdown | 2 | 0 | 1 | 3 | +| [mkdocs/docs/manual/map.md](/mkdocs/docs/manual/map.md) | Markdown | 91 | 0 | 48 | 139 | +| [mkdocs/docs/overrides/lander.html](/mkdocs/docs/overrides/lander.html) | HTML | 1,872 | 11 | 259 | 2,142 | +| [mkdocs/docs/overrides/main.html](/mkdocs/docs/overrides/main.html) | HTML | 8 | 1 | 3 | 12 | +| [mkdocs/docs/phil/cost-comparison.md](/mkdocs/docs/phil/cost-comparison.md) | Markdown | 153 | 0 | 51 | 204 | +| [mkdocs/docs/phil/index.md](/mkdocs/docs/phil/index.md) | Markdown | 95 | 0 | 69 | 164 | +| [mkdocs/docs/services/code-server.md](/mkdocs/docs/services/code-server.md) | Markdown | 41 | 0 | 21 | 62 | +| [mkdocs/docs/services/gitea.md](/mkdocs/docs/services/gitea.md) | Markdown | 39 | 0 | 19 | 58 | +| [mkdocs/docs/services/homepage.md](/mkdocs/docs/services/homepage.md) | Markdown | 146 | 0 | 66 | 212 | +| [mkdocs/docs/services/index.md](/mkdocs/docs/services/index.md) | Markdown | 101 | 0 | 17 | 118 | +| [mkdocs/docs/services/listmonk.md](/mkdocs/docs/services/listmonk.md) | Markdown | 47 | 0 | 22 | 69 | +| [mkdocs/docs/services/map.md](/mkdocs/docs/services/map.md) | Markdown | 70 | 0 | 27 | 97 | +| [mkdocs/docs/services/mini-qr.md](/mkdocs/docs/services/mini-qr.md) | Markdown | 23 | 0 | 15 | 38 | +| [mkdocs/docs/services/mkdocs.md](/mkdocs/docs/services/mkdocs.md) | Markdown | 86 | 0 | 47 | 133 | +| [mkdocs/docs/services/n8n.md](/mkdocs/docs/services/n8n.md) | Markdown | 109 | 0 | 49 | 158 | +| [mkdocs/docs/services/nocodb.md](/mkdocs/docs/services/nocodb.md) | Markdown | 108 | 0 | 55 | 163 | +| [mkdocs/docs/services/postgresql.md](/mkdocs/docs/services/postgresql.md) | Markdown | 59 | 0 | 32 | 91 | +| [mkdocs/docs/services/static-server.md](/mkdocs/docs/services/static-server.md) | Markdown | 69 | 0 | 32 | 101 | +| [mkdocs/docs/stylesheets/extra.css](/mkdocs/docs/stylesheets/extra.css) | PostCSS | 498 | 18 | 82 | 598 | +| [mkdocs/docs/stylesheets/home.css](/mkdocs/docs/stylesheets/home.css) | PostCSS | 866 | 54 | 153 | 1,073 | +| [mkdocs/docs/test.md](/mkdocs/docs/test.md) | Markdown | 5 | 0 | 6 | 11 | +| [mkdocs/mkdocs.yml](/mkdocs/mkdocs.yml) | YAML | 167 | 11 | 11 | 189 | +| [mkdocs/site/404.html](/mkdocs/site/404.html) | HTML | 251 | 1 | 437 | 689 | +| [mkdocs/site/adv/ansible/index.html](/mkdocs/site/adv/ansible/index.html) | HTML | 1,653 | 1 | 1,326 | 2,980 | +| [mkdocs/site/adv/index.html](/mkdocs/site/adv/index.html) | HTML | 549 | 1 | 1,083 | 1,633 | +| [mkdocs/site/adv/vscode-ssh/index.html](/mkdocs/site/adv/vscode-ssh/index.html) | HTML | 1,926 | 1 | 1,358 | 3,285 | +| [mkdocs/site/assets/javascripts/bundle.50899def.min.js](/mkdocs/site/assets/javascripts/bundle.50899def.min.js) | JavaScript | 14 | 1 | 2 | 17 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js) | JavaScript | 1 | 16 | 1 | 18 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js) | JavaScript | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/javascripts/lunr/tinyseg.js](/mkdocs/site/assets/javascripts/lunr/tinyseg.js) | JavaScript | 176 | 21 | 9 | 206 | +| [mkdocs/site/assets/javascripts/lunr/wordcut.js](/mkdocs/site/assets/javascripts/lunr/wordcut.js) | JavaScript | 4,882 | 915 | 911 | 6,708 | +| [mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js](/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js) | JavaScript | 38 | 3 | 2 | 43 | +| [mkdocs/site/assets/repo-data/admin-changemaker.lite.json](/mkdocs/site/assets/repo-data/admin-changemaker.lite.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/anthropics-claude-code.json](/mkdocs/site/assets/repo-data/anthropics-claude-code.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/coder-code-server.json](/mkdocs/site/assets/repo-data/coder-code-server.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/gethomepage-homepage.json](/mkdocs/site/assets/repo-data/gethomepage-homepage.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/go-gitea-gitea.json](/mkdocs/site/assets/repo-data/go-gitea-gitea.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/knadh-listmonk.json](/mkdocs/site/assets/repo-data/knadh-listmonk.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/lyqht-mini-qr.json](/mkdocs/site/assets/repo-data/lyqht-mini-qr.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/n8n-io-n8n.json](/mkdocs/site/assets/repo-data/n8n-io-n8n.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/nocodb-nocodb.json](/mkdocs/site/assets/repo-data/nocodb-nocodb.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/ollama-ollama.json](/mkdocs/site/assets/repo-data/ollama-ollama.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json](/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json) | JSON | 16 | 0 | 0 | 16 | +| [mkdocs/site/assets/stylesheets/main.7e37652d.min.css](/mkdocs/site/assets/stylesheets/main.7e37652d.min.css) | PostCSS | 1 | 0 | 0 | 1 | +| [mkdocs/site/assets/stylesheets/palette.06af60db.min.css](/mkdocs/site/assets/stylesheets/palette.06af60db.min.css) | PostCSS | 1 | 0 | 0 | 1 | +| [mkdocs/site/blog/2025/07/03/blog-1/index.html](/mkdocs/site/blog/2025/07/03/blog-1/index.html) | HTML | 374 | 1 | 582 | 957 | +| [mkdocs/site/blog/2025/07/10/2/index.html](/mkdocs/site/blog/2025/07/10/2/index.html) | HTML | 392 | 1 | 583 | 976 | +| [mkdocs/site/blog/2025/08/01/3/index.html](/mkdocs/site/blog/2025/08/01/3/index.html) | HTML | 455 | 1 | 590 | 1,046 | +| [mkdocs/site/blog/archive/2025/index.html](/mkdocs/site/blog/archive/2025/index.html) | HTML | 442 | 1 | 582 | 1,025 | +| [mkdocs/site/blog/index.html](/mkdocs/site/blog/index.html) | HTML | 451 | 1 | 572 | 1,024 | +| [mkdocs/site/build/index.html](/mkdocs/site/build/index.html) | HTML | 1,312 | 1 | 1,199 | 2,512 | +| [mkdocs/site/build/map/index.html](/mkdocs/site/build/map/index.html) | HTML | 983 | 1 | 1,182 | 2,166 | +| [mkdocs/site/build/server/index.html](/mkdocs/site/build/server/index.html) | HTML | 1,045 | 1 | 1,234 | 2,280 | +| [mkdocs/site/build/site/index.html](/mkdocs/site/build/site/index.html) | HTML | 748 | 1 | 1,138 | 1,887 | +| [mkdocs/site/config/cloudflare-config/index.html](/mkdocs/site/config/cloudflare-config/index.html) | HTML | 740 | 1 | 1,146 | 1,887 | +| [mkdocs/site/config/coder/index.html](/mkdocs/site/config/coder/index.html) | HTML | 1,117 | 1 | 1,240 | 2,358 | +| [mkdocs/site/config/index.html](/mkdocs/site/config/index.html) | HTML | 550 | 1 | 1,083 | 1,634 | +| [mkdocs/site/config/map/index.html](/mkdocs/site/config/map/index.html) | HTML | 1,672 | 1 | 1,346 | 3,019 | +| [mkdocs/site/config/mkdocs/index.html](/mkdocs/site/config/mkdocs/index.html) | HTML | 833 | 1 | 1,151 | 1,985 | +| [mkdocs/site/hooks/repo\_widget\_hook.py](/mkdocs/site/hooks/repo_widget_hook.py) | Python | 119 | 24 | 16 | 159 | +| [mkdocs/site/how to/canvass/index.html](/mkdocs/site/how%20to/canvass/index.html) | HTML | 275 | 1 | 484 | 760 | +| [mkdocs/site/index.html](/mkdocs/site/index.html) | HTML | 1,872 | 11 | 259 | 2,142 | +| [mkdocs/site/javascripts/gitea-widget.js](/mkdocs/site/javascripts/gitea-widget.js) | JavaScript | 121 | 3 | 8 | 132 | +| [mkdocs/site/javascripts/github-widget.js](/mkdocs/site/javascripts/github-widget.js) | JavaScript | 144 | 10 | 14 | 168 | +| [mkdocs/site/javascripts/home.js](/mkdocs/site/javascripts/home.js) | JavaScript | 271 | 30 | 37 | 338 | +| [mkdocs/site/manual/index.html](/mkdocs/site/manual/index.html) | HTML | 549 | 1 | 1,083 | 1,633 | +| [mkdocs/site/manual/map/index.html](/mkdocs/site/manual/map/index.html) | HTML | 987 | 1 | 1,202 | 2,190 | +| [mkdocs/site/overrides/lander.html](/mkdocs/site/overrides/lander.html) | HTML | 1,872 | 11 | 259 | 2,142 | +| [mkdocs/site/overrides/main.html](/mkdocs/site/overrides/main.html) | HTML | 8 | 1 | 3 | 12 | +| [mkdocs/site/phil/cost-comparison/index.html](/mkdocs/site/phil/cost-comparison/index.html) | HTML | 1,300 | 1 | 766 | 2,067 | +| [mkdocs/site/phil/index.html](/mkdocs/site/phil/index.html) | HTML | 717 | 1 | 661 | 1,379 | +| [mkdocs/site/search/search\_index.json](/mkdocs/site/search/search_index.json) | JSON | 1 | 0 | 0 | 1 | +| [mkdocs/site/services/code-server/index.html](/mkdocs/site/services/code-server/index.html) | HTML | 755 | 1 | 1,155 | 1,911 | +| [mkdocs/site/services/gitea/index.html](/mkdocs/site/services/gitea/index.html) | HTML | 737 | 1 | 1,151 | 1,889 | +| [mkdocs/site/services/homepage/index.html](/mkdocs/site/services/homepage/index.html) | HTML | 1,119 | 1 | 1,235 | 2,355 | +| [mkdocs/site/services/index.html](/mkdocs/site/services/index.html) | HTML | 769 | 1 | 1,113 | 1,883 | +| [mkdocs/site/services/listmonk/index.html](/mkdocs/site/services/listmonk/index.html) | HTML | 775 | 1 | 1,159 | 1,935 | +| [mkdocs/site/services/map/index.html](/mkdocs/site/services/map/index.html) | HTML | 877 | 1 | 1,170 | 2,048 | +| [mkdocs/site/services/mini-qr/index.html](/mkdocs/site/services/mini-qr/index.html) | HTML | 707 | 1 | 1,147 | 1,855 | +| [mkdocs/site/services/mkdocs/index.html](/mkdocs/site/services/mkdocs/index.html) | HTML | 905 | 1 | 1,187 | 2,093 | +| [mkdocs/site/services/n8n/index.html](/mkdocs/site/services/n8n/index.html) | HTML | 1,057 | 1 | 1,223 | 2,281 | +| [mkdocs/site/services/nocodb/index.html](/mkdocs/site/services/nocodb/index.html) | HTML | 1,040 | 1 | 1,219 | 2,260 | +| [mkdocs/site/services/postgresql/index.html](/mkdocs/site/services/postgresql/index.html) | HTML | 880 | 1 | 1,190 | 2,071 | +| [mkdocs/site/services/static-server/index.html](/mkdocs/site/services/static-server/index.html) | HTML | 877 | 1 | 1,182 | 2,060 | +| [mkdocs/site/sitemap.xml](/mkdocs/site/sitemap.xml) | XML | 147 | 0 | 0 | 147 | +| [mkdocs/site/stylesheets/extra.css](/mkdocs/site/stylesheets/extra.css) | PostCSS | 498 | 18 | 82 | 598 | +| [mkdocs/site/stylesheets/home.css](/mkdocs/site/stylesheets/home.css) | PostCSS | 866 | 54 | 153 | 1,073 | +| [mkdocs/site/test/index.html](/mkdocs/site/test/index.html) | HTML | 278 | 1 | 484 | 763 | +| [reset-site.sh](/reset-site.sh) | Shell Script | 258 | 28 | 73 | 359 | +| [start-production.sh](/start-production.sh) | Shell Script | 487 | 77 | 98 | 662 | +| [test.md](/test.md) | Markdown | 1 | 0 | 0 | 1 | + +[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/diff-details.md b/.VSCodeCounter/2025-09-05_12-42-08/diff-details.md new file mode 100644 index 0000000..9a10925 --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/diff-details.md @@ -0,0 +1,15 @@ +# Diff Details + +Date : 2025-09-05 12:42:08 + +Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite + +Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines + +[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details + +## Files +| filename | language | code | comment | blank | total | +| :--- | :--- | ---: | ---: | ---: | ---: | + +[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/diff.csv b/.VSCodeCounter/2025-09-05_12-42-08/diff.csv new file mode 100644 index 0000000..b7d8d75 --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/diff.csv @@ -0,0 +1,2 @@ +"filename", "language", "", "comment", "blank", "total" +"Total", "-", , 0, 0, 0 \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/diff.md b/.VSCodeCounter/2025-09-05_12-42-08/diff.md new file mode 100644 index 0000000..cca6e34 --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/diff.md @@ -0,0 +1,19 @@ +# Diff Summary + +Date : 2025-09-05 12:42:08 + +Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite + +Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines + +[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md) + +## Languages +| language | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | + +## Directories +| path | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | + +[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/diff.txt b/.VSCodeCounter/2025-09-05_12-42-08/diff.txt new file mode 100644 index 0000000..d2d43e3 --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/diff.txt @@ -0,0 +1,22 @@ +Date : 2025-09-05 12:42:08 +Directory : /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite +Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines + +Languages ++----------+------------+------------+------------+------------+------------+ +| language | files | code | comment | blank | total | ++----------+------------+------------+------------+------------+------------+ ++----------+------------+------------+------------+------------+------------+ + +Directories ++------+------------+------------+------------+------------+------------+ +| path | files | code | comment | blank | total | ++------+------------+------------+------------+------------+------------+ ++------+------------+------------+------------+------------+------------+ + +Files ++----------+----------+------------+------------+------------+------------+ +| filename | language | code | comment | blank | total | ++----------+----------+------------+------------+------------+------------+ +| Total | | 0 | 0 | 0 | 0 | ++----------+----------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/results.csv b/.VSCodeCounter/2025-09-05_12-42-08/results.csv new file mode 100644 index 0000000..46b80d8 --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/results.csv @@ -0,0 +1,337 @@ +"filename", "language", "Shell Script", "Markdown", "Log", "YAML", "JSON", "Properties", "JavaScript", "HTML", "PostCSS", "Python", "Docker", "CSV", "XML", "comment", "blank", "total" +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/README.md", "Markdown", 0, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 79 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/combined.log", "Log", 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/config.sh", "Shell Script", 810, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 177, 224, 1211 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/bookmarks.yaml", "YAML", 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 5, 54 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/docker.yaml", "YAML", 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 7 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/kubernetes.yaml", "YAML", 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/logs/homepage.log", "Log", 0, 0, 692, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 706 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/services.yaml", "YAML", 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 15, 75 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/settings.yaml", "YAML", 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 50 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/widgets.yaml", "YAML", 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 5, 33 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/mkdocs-site/default.conf", "Properties", 0, 0, 0, 0, 0, 33, 0, 0, 0, 0, 0, 0, 0, 7, 9, 49 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/docker-compose.yml", "YAML", 0, 0, 0, 238, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 13, 253 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/Instuctions.md", "Markdown", 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 77 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/README.md", "Markdown", 0, 851, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 1103 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/Dockerfile", "Docker", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 7, 10, 37 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/config/index.js", "JavaScript", 0, 0, 0, 0, 0, 0, 118, 0, 0, 0, 0, 0, 0, 15, 18, 151 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/authController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 0, 0, 0, 13, 25, 210 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/cutsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 276, 0, 0, 0, 0, 0, 0, 41, 47, 364 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dashboardController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 9, 15, 94 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dataConvertController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 352, 0, 0, 0, 0, 0, 0, 50, 76, 478 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/externalDataController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 11, 21, 133 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/listmonkController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 212, 0, 0, 0, 0, 0, 0, 12, 29, 253 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/locationsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 333, 0, 0, 0, 0, 0, 0, 23, 58, 414 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/passwordRecoveryController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 4, 12, 61 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/publicShiftsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 212, 0, 0, 0, 0, 0, 0, 21, 38, 271 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/settingsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 303, 0, 0, 0, 0, 0, 0, 17, 50, 370 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/shiftsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 643, 0, 0, 0, 0, 0, 0, 57, 123, 823 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/usersController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 341, 0, 0, 0, 0, 0, 0, 18, 54, 413 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 170, 0, 0, 0, 0, 0, 0, 13, 25, 208 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/rateLimiter.js", "JavaScript", 0, 0, 0, 0, 0, 0, 85, 0, 0, 0, 0, 0, 0, 10, 9, 104 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package-lock.json", "JSON", 0, 0, 0, 0, 2203, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2204 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package.json", "JSON", 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 44 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/admin.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1080, 0, 0, 0, 0, 0, 46, 135, 1261 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/REFACTORING_SUMMARY.md", "Markdown", 0, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 64 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 4, 3, 19 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/cuts-shifts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 225, 0, 0, 0, 0, 13, 44, 282 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/data-convert.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 176, 0, 0, 0, 0, 7, 33, 216 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/forms.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 207, 0, 0, 0, 0, 9, 36, 252 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/layout.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 202, 0, 0, 0, 0, 7, 36, 245 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/modals.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 259, 0, 0, 0, 0, 6, 46, 311 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/nocodb-links.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 4, 23, 136 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/responsive.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 552, 0, 0, 0, 0, 28, 112, 692 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/status-messages.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 154, 0, 0, 0, 0, 9, 27, 190 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/user-management.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 179, 0, 0, 0, 0, 10, 31, 220 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/variables.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 9, 10, 67 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/walk-sheet.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 12, 39, 303 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-marker.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 2, 5, 42 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-popup.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 197, 0, 0, 0, 0, 9, 30, 236 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/base.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 0, 22, 12, 102 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/buttons.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 2, 16, 88 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cache-busting.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 2, 13, 114 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cuts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 829, 0, 0, 0, 0, 22, 146, 997 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/dashboard.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 0, 3, 29, 161 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/doc-search.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 0, 0, 2, 20, 145 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/forms.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 2, 18, 125 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/layout.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 83, 0, 0, 0, 0, 5, 11, 99 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/leaflet-custom.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 147, 0, 0, 0, 0, 20, 32, 199 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/listmonk.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 295, 0, 0, 0, 0, 4, 55, 354 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/map-controls.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 281, 0, 0, 0, 0, 13, 45, 339 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/mobile-ui.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 205, 0, 0, 0, 0, 8, 36, 249 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/modal.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 0, 1, 10, 84 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/nocodb-dropdown.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 134, 0, 0, 0, 0, 4, 24, 162 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/notifications.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 5, 16, 126 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/print.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 1, 2, 14 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/qr-code.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 3, 13, 75 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/responsive.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 150, 0, 0, 0, 0, 12, 29, 191 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/start-location-marker.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 0, 3, 8, 76 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/temp-user.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 6, 5, 57 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/unified-search.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 580, 0, 0, 0, 0, 30, 99, 709 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/public-shifts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 418, 0, 0, 0, 0, 24, 83, 525 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/shifts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 608, 0, 0, 0, 0, 21, 118, 747 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/style.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 1, 22 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/user.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 364, 0, 0, 0, 0, 13, 70, 447 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 384, 0, 0, 0, 0, 0, 28, 45, 457 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 11, 14, 88 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-core.js", "JavaScript", 0, 0, 0, 0, 0, 0, 239, 0, 0, 0, 0, 0, 0, 46, 40, 325 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-cuts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1505, 0, 0, 0, 0, 0, 0, 184, 302, 1991 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-email.js", "JavaScript", 0, 0, 0, 0, 0, 0, 319, 0, 0, 0, 0, 0, 0, 29, 47, 395 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-map.js", "JavaScript", 0, 0, 0, 0, 0, 0, 178, 0, 0, 0, 0, 0, 0, 26, 46, 250 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shift-volunteers.js", "JavaScript", 0, 0, 0, 0, 0, 0, 416, 0, 0, 0, 0, 0, 0, 49, 66, 531 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 330, 0, 0, 0, 0, 0, 0, 35, 55, 420 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-users.js", "JavaScript", 0, 0, 0, 0, 0, 0, 305, 0, 0, 0, 0, 0, 0, 16, 45, 366 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-walksheet.js", "JavaScript", 0, 0, 0, 0, 0, 0, 387, 0, 0, 0, 0, 0, 0, 35, 50, 472 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin.js", "JavaScript", 0, 0, 0, 0, 0, 0, 2309, 0, 0, 0, 0, 0, 0, 178, 288, 2775 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/navigation.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/startLocation.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/users.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/utils.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/walkSheet.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 181, 0, 0, 0, 0, 0, 0, 28, 34, 243 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cache-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 219, 0, 0, 0, 0, 0, 0, 71, 28, 318 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/config.js", "JavaScript", 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 3, 3, 38 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-controls.js", "JavaScript", 0, 0, 0, 0, 0, 0, 982, 0, 0, 0, 0, 0, 0, 163, 145, 1290 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-drawing.js", "JavaScript", 0, 0, 0, 0, 0, 0, 206, 0, 0, 0, 0, 0, 0, 72, 59, 337 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 359, 0, 0, 0, 0, 0, 0, 96, 79, 534 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/dashboard.js", "JavaScript", 0, 0, 0, 0, 0, 0, 333, 0, 0, 0, 0, 0, 0, 23, 33, 389 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/data-convert.js", "JavaScript", 0, 0, 0, 0, 0, 0, 510, 0, 0, 0, 0, 0, 0, 64, 118, 692 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/database-search.js", "JavaScript", 0, 0, 0, 0, 0, 0, 186, 0, 0, 0, 0, 0, 0, 76, 51, 313 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/external-layers.js", "JavaScript", 0, 0, 0, 0, 0, 0, 513, 0, 0, 0, 0, 0, 0, 39, 45, 597 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-admin.js", "JavaScript", 0, 0, 0, 0, 0, 0, 410, 0, 0, 0, 0, 0, 0, 76, 85, 571 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-status.js", "JavaScript", 0, 0, 0, 0, 0, 0, 169, 0, 0, 0, 0, 0, 0, 25, 32, 226 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/location-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 998, 0, 0, 0, 0, 0, 0, 56, 91, 1145 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/main.js", "JavaScript", 0, 0, 0, 0, 0, 0, 178, 0, 0, 0, 0, 0, 0, 40, 36, 254 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 82, 0, 0, 0, 0, 0, 0, 12, 19, 113 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-search.js", "JavaScript", 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 44, 36, 200 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/mkdocs-search.js", "JavaScript", 0, 0, 0, 0, 0, 0, 263, 0, 0, 0, 0, 0, 0, 36, 52, 351 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/public-shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 518, 0, 0, 0, 0, 0, 0, 15, 25, 558 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/search-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 407, 0, 0, 0, 0, 0, 0, 138, 100, 645 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1059, 0, 0, 0, 0, 0, 0, 35, 55, 1149 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/ui-controls.js", "JavaScript", 0, 0, 0, 0, 0, 0, 572, 0, 0, 0, 0, 0, 0, 74, 101, 747 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/user.js", "JavaScript", 0, 0, 0, 0, 0, 0, 81, 0, 0, 0, 0, 0, 0, 12, 19, 112 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/utils.js", "JavaScript", 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 21, 25, 142 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/login.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 459, 0, 0, 0, 0, 0, 3, 76, 538 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/public-shifts.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 88, 0, 0, 0, 0, 0, 4, 7, 99 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/shifts.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 0, 0, 5, 8, 86 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/user.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 0, 0, 7, 9, 63 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/admin.js", "JavaScript", 0, 0, 0, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 15, 16, 111 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 5, 6, 25 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/cuts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 6, 7, 31 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dashboard.js", "JavaScript", 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3, 8 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dataConvert.js", "JavaScript", 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 4, 6, 31 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/debug.js", "JavaScript", 0, 0, 0, 0, 0, 0, 236, 0, 0, 0, 0, 0, 0, 10, 36, 282 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/external.js", "JavaScript", 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 2, 4, 13 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/geocoding.js", "JavaScript", 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 24, 31, 175 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/index.js", "JavaScript", 0, 0, 0, 0, 0, 0, 153, 0, 0, 0, 0, 0, 0, 29, 36, 218 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/listmonk.js", "JavaScript", 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 5, 9, 26 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/locations.js", "JavaScript", 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 5, 6, 22 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/public.js", "JavaScript", 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 3, 12 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/publicShifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 2, 11 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/qr.js", "JavaScript", 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 1, 8, 48 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/settings.js", "JavaScript", 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 2, 3, 13 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 4, 5, 25 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/users.js", "JavaScript", 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 6, 7, 24 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/server.js", "JavaScript", 0, 0, 0, 0, 0, 0, 249, 0, 0, 0, 0, 0, 0, 43, 49, 341 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/accountExpiration.js", "JavaScript", 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 11, 19, 89 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/email.js", "JavaScript", 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 0, 0, 4, 19, 132 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/emailTemplates.js", "JavaScript", 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 10, 16, 85 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/geocoding.js", "JavaScript", 0, 0, 0, 0, 0, 0, 219, 0, 0, 0, 0, 0, 0, 51, 44, 314 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/listmonk.js", "JavaScript", 0, 0, 0, 0, 0, 0, 412, 0, 0, 0, 0, 0, 0, 36, 66, 514 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/nocodb.js", "JavaScript", 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 0, 0, 0, 22, 36, 230 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/qrcode.js", "JavaScript", 0, 0, 0, 0, 0, 0, 110, 0, 0, 0, 0, 0, 0, 33, 20, 163 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/socrata.js", "JavaScript", 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 9, 10, 68 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/login-details.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 113, 0, 0, 0, 0, 0, 0, 4, 117 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/password-recovery.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 0, 0, 0, 1, 80 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-existing.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 161, 0, 0, 0, 0, 0, 0, 22, 183 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-new.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 5, 36 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/shift-details.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 152, 0, 0, 0, 0, 0, 0, 5, 157 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/user-broadcast.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 2, 110 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/cacheBusting.js", "JavaScript", 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 51, 24, 180 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/helpers.js", "JavaScript", 0, 0, 0, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 25, 28, 202 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/logger.js", "JavaScript", 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 2, 3, 43 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/build-nocodb.sh", "Shell Script", 925, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 93, 1078 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods (Centroid Point)_20250807.geojson", "JSON", 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods_20250807.geojson", "JSON", 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton Ward Boundary and Council Composition_Current_20250807.geojson", "JSON", 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 689, 0, 0, 1, 690 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 407, 0, 0, 1, 408 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 1, 14 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_edmonton_optimized.js", "JavaScript", 0, 0, 0, 0, 0, 0, 185, 0, 0, 0, 0, 0, 0, 21, 43, 249 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_ward_boundaries_optimized.js", "JavaScript", 0, 0, 0, 0, 0, 0, 173, 0, 0, 0, 0, 0, 0, 24, 45, 242 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_neighborhoods_nocodb_optimized.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 689, 0, 0, 0, 689 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_nocodb_optimized.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1095, 0, 0, 406, 1501 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_ward_boundaries_optimized.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/exampledata.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/docker-compose.yml", "YAML", 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 34 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/files-explainer.md", "Markdown", 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 293, 585 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/ADMIN_IMPLEMENTATION.md", "Markdown", 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 130 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md", "Markdown", 0, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 190 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md", "Markdown", 0, 124, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 31, 160 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md", "Markdown", 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 86 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/LISTMONK_INTEGRATION_GUIDE.md", "Markdown", 0, 249, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 319 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/SHIFT_PERFORMANCE_FIX.md", "Markdown", 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 93 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_IMPLEMENTATION.md", "Markdown", 0, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 116 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_TEST.md", "Markdown", 0, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 147 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/build-nocodb.md", "Markdown", 0, 374, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67, 441 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package-lock.json", "JSON", 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package.json", "JSON", 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/ansible.md", "Markdown", 0, 373, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 152, 525 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/index.md", "Markdown", 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/vscode-ssh.md", "Markdown", 0, 511, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 174, 685 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/anthropics-claude-code.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/coder-code-server.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/gethomepage-homepage.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/go-gitea-gitea.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/knadh-listmonk.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/n8n-io-n8n.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/nocodb-nocodb.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/ollama-ollama.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/index.md", "Markdown", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/1.md", "Markdown", 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/2.md", "Markdown", 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 14 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/3.md", "Markdown", 0, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 75 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/index.md", "Markdown", 0, 336, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 486 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/map.md", "Markdown", 0, 155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 217 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/server.md", "Markdown", 0, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 149 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/site.md", "Markdown", 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 108 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/cloudflare-config.md", "Markdown", 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 61 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/coder.md", "Markdown", 0, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 216 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/index.md", "Markdown", 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 7 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/map.md", "Markdown", 0, 299, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 390 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/mkdocs.md", "Markdown", 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 144 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/hooks/repo_widget_hook.py", "Python", 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 0, 0, 0, 24, 16, 159 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/how to/canvass.md", "Markdown", 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 5 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/index.md", "Markdown", 0, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 114 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/gitea-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 121, 0, 0, 0, 0, 0, 0, 3, 8, 132 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/github-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 10, 14, 168 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/home.js", "JavaScript", 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 30, 37, 338 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/index.md", "Markdown", 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/map.md", "Markdown", 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 139 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/lander.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1872, 0, 0, 0, 0, 0, 11, 259, 2142 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/main.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 1, 3, 12 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/cost-comparison.md", "Markdown", 0, 153, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 204 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/index.md", "Markdown", 0, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 164 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/code-server.md", "Markdown", 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 62 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/gitea.md", "Markdown", 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 58 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/homepage.md", "Markdown", 0, 146, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 212 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/index.md", "Markdown", 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 118 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/listmonk.md", "Markdown", 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 69 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/map.md", "Markdown", 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 97 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mini-qr.md", "Markdown", 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 38 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mkdocs.md", "Markdown", 0, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 133 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/n8n.md", "Markdown", 0, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 158 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/nocodb.md", "Markdown", 0, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 163 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/postgresql.md", "Markdown", 0, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 91 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/static-server.md", "Markdown", 0, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 101 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/extra.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 498, 0, 0, 0, 0, 18, 82, 598 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/home.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 866, 0, 0, 0, 0, 54, 153, 1073 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/test.md", "Markdown", 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 11 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/mkdocs.yml", "YAML", 0, 0, 0, 167, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 189 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/404.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 251, 0, 0, 0, 0, 0, 1, 437, 689 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/ansible/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1653, 0, 0, 0, 0, 0, 1, 1326, 2980 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 549, 0, 0, 0, 0, 0, 1, 1083, 1633 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/vscode-ssh/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1926, 0, 0, 0, 0, 0, 1, 1358, 3285 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/bundle.50899def.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 1, 2, 17 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/tinyseg.js", "JavaScript", 0, 0, 0, 0, 0, 0, 176, 0, 0, 0, 0, 0, 0, 21, 9, 206 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/wordcut.js", "JavaScript", 0, 0, 0, 0, 0, 0, 4882, 0, 0, 0, 0, 0, 0, 915, 911, 6708 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 3, 2, 43 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/admin-changemaker.lite.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/anthropics-claude-code.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/coder-code-server.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/gethomepage-homepage.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/go-gitea-gitea.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/knadh-listmonk.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/lyqht-mini-qr.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/n8n-io-n8n.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/nocodb-nocodb.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/ollama-ollama.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/main.7e37652d.min.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/palette.06af60db.min.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/03/blog-1/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 374, 0, 0, 0, 0, 0, 1, 582, 957 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/10/2/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 392, 0, 0, 0, 0, 0, 1, 583, 976 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/08/01/3/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 455, 0, 0, 0, 0, 0, 1, 590, 1046 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/archive/2025/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 442, 0, 0, 0, 0, 0, 1, 582, 1025 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 451, 0, 0, 0, 0, 0, 1, 572, 1024 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1312, 0, 0, 0, 0, 0, 1, 1199, 2512 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 983, 0, 0, 0, 0, 0, 1, 1182, 2166 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/server/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1045, 0, 0, 0, 0, 0, 1, 1234, 2280 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/site/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 748, 0, 0, 0, 0, 0, 1, 1138, 1887 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/cloudflare-config/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 740, 0, 0, 0, 0, 0, 1, 1146, 1887 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/coder/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1117, 0, 0, 0, 0, 0, 1, 1240, 2358 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 550, 0, 0, 0, 0, 0, 1, 1083, 1634 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1672, 0, 0, 0, 0, 0, 1, 1346, 3019 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/mkdocs/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 833, 0, 0, 0, 0, 0, 1, 1151, 1985 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/hooks/repo_widget_hook.py", "Python", 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 0, 0, 0, 24, 16, 159 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/how to/canvass/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 275, 0, 0, 0, 0, 0, 1, 484, 760 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1872, 0, 0, 0, 0, 0, 11, 259, 2142 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/gitea-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 121, 0, 0, 0, 0, 0, 0, 3, 8, 132 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/github-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 10, 14, 168 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/home.js", "JavaScript", 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 30, 37, 338 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 549, 0, 0, 0, 0, 0, 1, 1083, 1633 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 987, 0, 0, 0, 0, 0, 1, 1202, 2190 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/lander.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1872, 0, 0, 0, 0, 0, 11, 259, 2142 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/main.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 1, 3, 12 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/cost-comparison/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1300, 0, 0, 0, 0, 0, 1, 766, 2067 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 717, 0, 0, 0, 0, 0, 1, 661, 1379 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/search/search_index.json", "JSON", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/code-server/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 755, 0, 0, 0, 0, 0, 1, 1155, 1911 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/gitea/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 737, 0, 0, 0, 0, 0, 1, 1151, 1889 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/homepage/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1119, 0, 0, 0, 0, 0, 1, 1235, 2355 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 769, 0, 0, 0, 0, 0, 1, 1113, 1883 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/listmonk/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 775, 0, 0, 0, 0, 0, 1, 1159, 1935 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 877, 0, 0, 0, 0, 0, 1, 1170, 2048 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mini-qr/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 707, 0, 0, 0, 0, 0, 1, 1147, 1855 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mkdocs/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 905, 0, 0, 0, 0, 0, 1, 1187, 2093 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/n8n/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1057, 0, 0, 0, 0, 0, 1, 1223, 2281 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/nocodb/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1040, 0, 0, 0, 0, 0, 1, 1219, 2260 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/postgresql/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 880, 0, 0, 0, 0, 0, 1, 1190, 2071 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/static-server/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 877, 0, 0, 0, 0, 0, 1, 1182, 2060 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/sitemap.xml", "XML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 0, 0, 147 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/extra.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 498, 0, 0, 0, 0, 18, 82, 598 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/home.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 866, 0, 0, 0, 0, 54, 153, 1073 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/test/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 278, 0, 0, 0, 0, 0, 1, 484, 763 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/reset-site.sh", "Shell Script", 258, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 73, 359 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/start-production.sh", "Shell Script", 487, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 98, 662 +"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/test.md", "Markdown", 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 +"Total", "-", 2480, 6210, 705, 608, 2634, 33, 27027, 38504, 10405, 238, 20, 2913, 147, 4969, 47542, 144435 \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/results.json b/.VSCodeCounter/2025-09-05_12-42-08/results.json new file mode 100644 index 0000000..90ac7a1 --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/results.json @@ -0,0 +1 @@ +{"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/start-production.sh":{"language":"Shell Script","code":487,"comment":77,"blank":98},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/README.md":{"language":"Markdown","code":55,"comment":0,"blank":24},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/combined.log":{"language":"Log","code":13,"comment":0,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/test.md":{"language":"Markdown","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/reset-site.sh":{"language":"Shell Script","code":258,"comment":28,"blank":73},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/docker-compose.yml":{"language":"YAML","code":238,"comment":2,"blank":13},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/config.sh":{"language":"Shell Script","code":810,"comment":177,"blank":224},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/Instuctions.md":{"language":"Markdown","code":56,"comment":0,"blank":21},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/build-nocodb.sh":{"language":"Shell Script","code":925,"comment":60,"blank":93},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package-lock.json":{"language":"JSON","code":17,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/mkdocs.yml":{"language":"YAML","code":167,"comment":11,"blank":11},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/README.md":{"language":"Markdown","code":851,"comment":0,"blank":252},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/build-nocodb.md":{"language":"Markdown","code":374,"comment":0,"blank":67},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package.json":{"language":"JSON","code":5,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/files-explainer.md":{"language":"Markdown","code":292,"comment":0,"blank":293},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/mkdocs-site/default.conf":{"language":"Properties","code":33,"comment":7,"blank":9},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/LISTMONK_INTEGRATION_GUIDE.md":{"language":"Markdown","code":249,"comment":0,"blank":70},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_IMPLEMENTATION.md":{"language":"Markdown","code":88,"comment":0,"blank":28},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/map.md":{"language":"Markdown","code":155,"comment":0,"blank":62},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/site.md":{"language":"Markdown","code":74,"comment":0,"blank":34},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/map.md":{"language":"Markdown","code":70,"comment":0,"blank":27},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md":{"language":"Markdown","code":124,"comment":5,"blank":31},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/index.md":{"language":"Markdown","code":336,"comment":0,"blank":150},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/index.md":{"language":"Markdown","code":85,"comment":0,"blank":29},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/vscode-ssh.md":{"language":"Markdown","code":511,"comment":0,"blank":174},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/server.md":{"language":"Markdown","code":122,"comment":0,"blank":27},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/nocodb.md":{"language":"Markdown","code":108,"comment":0,"blank":55},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/ADMIN_IMPLEMENTATION.md":{"language":"Markdown","code":102,"comment":0,"blank":28},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/cloudflare-config.md":{"language":"Markdown","code":45,"comment":0,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/map.md":{"language":"Markdown","code":91,"comment":0,"blank":48},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/static-server.md":{"language":"Markdown","code":69,"comment":0,"blank":32},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/github-widget.js":{"language":"JavaScript","code":144,"comment":10,"blank":14},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/index.md":{"language":"Markdown","code":2,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mkdocs.md":{"language":"Markdown","code":86,"comment":0,"blank":47},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/index.md":{"language":"Markdown","code":3,"comment":0,"blank":4},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/map.md":{"language":"Markdown","code":299,"comment":0,"blank":91},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/mkdocs.md":{"language":"Markdown","code":102,"comment":0,"blank":42},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/home.js":{"language":"JavaScript","code":271,"comment":30,"blank":37},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mini-qr.md":{"language":"Markdown","code":23,"comment":0,"blank":15},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/gitea.md":{"language":"Markdown","code":39,"comment":0,"blank":19},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/lander.html":{"language":"HTML","code":1872,"comment":11,"blank":259},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/index.md":{"language":"Markdown","code":101,"comment":0,"blank":17},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/postgresql.md":{"language":"Markdown","code":59,"comment":0,"blank":32},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/coder.md":{"language":"Markdown","code":139,"comment":0,"blank":77},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/index.md":{"language":"Markdown","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/gitea-widget.js":{"language":"JavaScript","code":121,"comment":3,"blank":8},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_TEST.md":{"language":"Markdown","code":113,"comment":0,"blank":34},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/homepage.md":{"language":"Markdown","code":146,"comment":0,"blank":66},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md":{"language":"Markdown","code":156,"comment":0,"blank":34},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/index.md":{"language":"Markdown","code":2,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/main.html":{"language":"HTML","code":8,"comment":1,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/3.md":{"language":"Markdown","code":54,"comment":0,"blank":21},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/n8n.md":{"language":"Markdown","code":109,"comment":0,"blank":49},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/code-server.md":{"language":"Markdown","code":41,"comment":0,"blank":21},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/test.md":{"language":"Markdown","code":5,"comment":0,"blank":6},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/listmonk.md":{"language":"Markdown","code":47,"comment":0,"blank":22},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/2.md":{"language":"Markdown","code":10,"comment":0,"blank":4},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/ansible.md":{"language":"Markdown","code":373,"comment":0,"blank":152},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/docker-compose.yml":{"language":"YAML","code":31,"comment":0,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/SHIFT_PERFORMANCE_FIX.md":{"language":"Markdown","code":71,"comment":0,"blank":22},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md":{"language":"Markdown","code":65,"comment":0,"blank":21},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/1.md":{"language":"Markdown","code":6,"comment":0,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/index.md":{"language":"Markdown","code":95,"comment":0,"blank":69},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/extra.css":{"language":"PostCSS","code":498,"comment":18,"blank":82},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/cost-comparison.md":{"language":"Markdown","code":153,"comment":0,"blank":51},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/home.css":{"language":"PostCSS","code":866,"comment":54,"blank":153},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/hooks/repo_widget_hook.py":{"language":"Python","code":119,"comment":24,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package.json":{"language":"JSON","code":43,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package-lock.json":{"language":"JSON","code":2203,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/login.html":{"language":"HTML","code":459,"comment":3,"blank":76},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/settings.yaml":{"language":"YAML","code":36,"comment":4,"blank":10},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/gethomepage-homepage.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/index.html":{"language":"HTML","code":384,"comment":28,"blank":45},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/admin.html":{"language":"HTML","code":1080,"comment":46,"blank":135},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/coder-code-server.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/auth.js":{"language":"JavaScript","code":170,"comment":13,"blank":25},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/config/index.js":{"language":"JavaScript","code":118,"comment":15,"blank":18},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/rateLimiter.js":{"language":"JavaScript","code":85,"comment":10,"blank":9},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/anthropics-claude-code.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/go-gitea-gitea.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/public-shifts.html":{"language":"HTML","code":88,"comment":4,"blank":7},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/user.html":{"language":"HTML","code":47,"comment":7,"blank":9},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/database-search.js":{"language":"JavaScript","code":186,"comment":76,"blank":51},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-admin.js":{"language":"JavaScript","code":410,"comment":76,"blank":85},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/user.js":{"language":"JavaScript","code":81,"comment":12,"blank":19},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/shifts.html":{"language":"HTML","code":73,"comment":5,"blank":8},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/helpers.js":{"language":"JavaScript","code":149,"comment":25,"blank":28},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/widgets.yaml":{"language":"YAML","code":26,"comment":2,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/cacheBusting.js":{"language":"JavaScript","code":105,"comment":51,"blank":24},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/logger.js":{"language":"JavaScript","code":38,"comment":2,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/kubernetes.yaml":{"language":"YAML","code":1,"comment":1,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-email.js":{"language":"JavaScript","code":319,"comment":29,"blank":47},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/docker.yaml":{"language":"YAML","code":3,"comment":2,"blank":2},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/bookmarks.yaml":{"language":"YAML","code":47,"comment":2,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/services.yaml":{"language":"YAML","code":59,"comment":1,"blank":15},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.css":{"language":"PostCSS","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/server.js":{"language":"JavaScript","code":249,"comment":43,"blank":49},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin.css":{"language":"PostCSS","code":12,"comment":4,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/config.js":{"language":"JavaScript","code":32,"comment":3,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/shifts.js":{"language":"JavaScript","code":1059,"comment":35,"blank":55},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/search-manager.js":{"language":"JavaScript","code":407,"comment":138,"blank":100},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/users.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-search.js":{"language":"JavaScript","code":120,"comment":44,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/logs/homepage.log":{"language":"Log","code":692,"comment":0,"blank":14},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-auth.js":{"language":"JavaScript","code":63,"comment":11,"blank":14},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/nocodb-nocodb.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/Dockerfile":{"language":"Docker","code":20,"comment":7,"blank":10},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/REFACTORING_SUMMARY.md":{"language":"Markdown","code":51,"comment":0,"blank":13},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/startLocation.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/shifts.css":{"language":"PostCSS","code":608,"comment":21,"blank":118},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/ui-controls.js":{"language":"JavaScript","code":572,"comment":74,"blank":101},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/shifts.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/passwordRecoveryController.js":{"language":"JavaScript","code":45,"comment":4,"blank":12},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/responsive.css":{"language":"PostCSS","code":552,"comment":28,"blank":112},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/forms.css":{"language":"PostCSS","code":207,"comment":9,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/external.js":{"language":"JavaScript","code":7,"comment":2,"blank":4},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/authController.js":{"language":"JavaScript","code":172,"comment":13,"blank":25},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/user-broadcast.html":{"language":"HTML","code":108,"comment":0,"blank":2},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/public-shifts.js":{"language":"JavaScript","code":518,"comment":15,"blank":25},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/status-messages.css":{"language":"PostCSS","code":154,"comment":9,"blank":27},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/locations.js":{"language":"JavaScript","code":11,"comment":5,"blank":6},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/data-convert.js":{"language":"JavaScript","code":510,"comment":64,"blank":118},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/shifts.js":{"language":"JavaScript","code":16,"comment":4,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/user-management.css":{"language":"PostCSS","code":179,"comment":10,"blank":31},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/ollama-ollama.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/location-manager.js":{"language":"JavaScript","code":998,"comment":56,"blank":91},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/cuts.js":{"language":"JavaScript","code":18,"comment":6,"blank":7},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-walksheet.js":{"language":"JavaScript","code":387,"comment":35,"blank":50},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/modals.css":{"language":"PostCSS","code":259,"comment":6,"blank":46},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/publicShiftsController.js":{"language":"JavaScript","code":212,"comment":21,"blank":38},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/cuts-shifts.css":{"language":"PostCSS","code":225,"comment":13,"blank":44},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/users.js":{"language":"JavaScript","code":11,"comment":6,"blank":7},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/auth.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/knadh-listmonk.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-controls.js":{"language":"JavaScript","code":982,"comment":163,"blank":145},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/debug.js":{"language":"JavaScript","code":236,"comment":10,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/publicShifts.js":{"language":"JavaScript","code":8,"comment":1,"blank":2},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/main.js":{"language":"JavaScript","code":178,"comment":40,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/email.js":{"language":"JavaScript","code":109,"comment":4,"blank":19},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/utils.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/navigation.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/settingsController.js":{"language":"JavaScript","code":303,"comment":17,"blank":50},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/public-shifts.css":{"language":"PostCSS","code":418,"comment":24,"blank":83},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/public.js":{"language":"JavaScript","code":8,"comment":1,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/n8n-io-n8n.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-map.js":{"language":"JavaScript","code":178,"comment":26,"blank":46},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/dashboard.js":{"language":"JavaScript","code":333,"comment":23,"blank":33},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/style.css":{"language":"PostCSS","code":21,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin.js":{"language":"JavaScript","code":2309,"comment":178,"blank":288},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dashboardController.js":{"language":"JavaScript","code":70,"comment":9,"blank":15},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-users.js":{"language":"JavaScript","code":305,"comment":16,"blank":45},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-core.js":{"language":"JavaScript","code":239,"comment":46,"blank":40},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/walkSheet.js":{"language":"JavaScript","code":0,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/layout.css":{"language":"PostCSS","code":202,"comment":7,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-drawing.js":{"language":"JavaScript","code":206,"comment":72,"blank":59},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/qrcode.js":{"language":"JavaScript","code":110,"comment":33,"blank":20},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/listmonkController.js":{"language":"JavaScript","code":212,"comment":12,"blank":29},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dashboard.js":{"language":"JavaScript","code":5,"comment":0,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/nocodb-links.css":{"language":"PostCSS","code":109,"comment":4,"blank":23},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/password-recovery.html":{"language":"HTML","code":79,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/geocoding.js":{"language":"JavaScript","code":219,"comment":51,"blank":44},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-status.js":{"language":"JavaScript","code":169,"comment":25,"blank":32},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-manager.js":{"language":"JavaScript","code":82,"comment":12,"blank":19},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-cuts.js":{"language":"JavaScript","code":1505,"comment":184,"blank":302},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/geocoding.js":{"language":"JavaScript","code":120,"comment":24,"blank":31},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/shift-details.html":{"language":"HTML","code":152,"comment":0,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/walk-sheet.css":{"language":"PostCSS","code":252,"comment":12,"blank":39},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/utils.js":{"language":"JavaScript","code":96,"comment":21,"blank":25},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/socrata.js":{"language":"JavaScript","code":49,"comment":9,"blank":10},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/admin.js":{"language":"JavaScript","code":80,"comment":15,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dataConvertController.js":{"language":"JavaScript","code":352,"comment":50,"blank":76},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-existing.html":{"language":"HTML","code":161,"comment":0,"blank":22},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/data-convert.css":{"language":"PostCSS","code":176,"comment":7,"blank":33},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/user.css":{"language":"PostCSS","code":364,"comment":13,"blank":70},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dataConvert.js":{"language":"JavaScript","code":21,"comment":4,"blank":6},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/shiftsController.js":{"language":"JavaScript","code":643,"comment":57,"blank":123},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-manager.js":{"language":"JavaScript","code":359,"comment":96,"blank":79},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/login-details.html":{"language":"HTML","code":113,"comment":0,"blank":4},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/auth.js":{"language":"JavaScript","code":181,"comment":28,"blank":34},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/usersController.js":{"language":"JavaScript","code":341,"comment":18,"blank":54},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/cutsController.js":{"language":"JavaScript","code":276,"comment":41,"blank":47},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/index.js":{"language":"JavaScript","code":153,"comment":29,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-new.html":{"language":"HTML","code":31,"comment":0,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/how%20to/canvass.md":{"language":"Markdown","code":2,"comment":0,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/nocodb.js":{"language":"JavaScript","code":172,"comment":22,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-marker.css":{"language":"PostCSS","code":35,"comment":2,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/variables.css":{"language":"PostCSS","code":48,"comment":9,"blank":10},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/mkdocs-search.js":{"language":"JavaScript","code":263,"comment":36,"blank":52},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/leaflet-custom.css":{"language":"PostCSS","code":147,"comment":20,"blank":32},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/base.css":{"language":"PostCSS","code":68,"comment":22,"blank":12},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/emailTemplates.js":{"language":"JavaScript","code":59,"comment":10,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cache-busting.css":{"language":"PostCSS","code":99,"comment":2,"blank":13},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/notifications.css":{"language":"PostCSS","code":105,"comment":5,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/external-layers.js":{"language":"JavaScript","code":513,"comment":39,"blank":45},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shift-volunteers.js":{"language":"JavaScript","code":416,"comment":49,"blank":66},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/locationsController.js":{"language":"JavaScript","code":333,"comment":23,"blank":58},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/externalDataController.js":{"language":"JavaScript","code":101,"comment":11,"blank":21},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/nocodb-dropdown.css":{"language":"PostCSS","code":134,"comment":4,"blank":24},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/unified-search.css":{"language":"PostCSS","code":580,"comment":30,"blank":99},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cache-manager.js":{"language":"JavaScript","code":219,"comment":71,"blank":28},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/qr.js":{"language":"JavaScript","code":39,"comment":1,"blank":8},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/auth.js":{"language":"JavaScript","code":14,"comment":5,"blank":6},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shifts.js":{"language":"JavaScript","code":330,"comment":35,"blank":55},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cuts.css":{"language":"PostCSS","code":829,"comment":22,"blank":146},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_ward_boundaries_optimized.js":{"language":"JavaScript","code":173,"comment":24,"blank":45},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/layout.css":{"language":"PostCSS","code":83,"comment":5,"blank":11},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/settings.js":{"language":"JavaScript","code":8,"comment":2,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/buttons.css":{"language":"PostCSS","code":70,"comment":2,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/forms.css":{"language":"PostCSS","code":105,"comment":2,"blank":18},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/listmonk.js":{"language":"JavaScript","code":12,"comment":5,"blank":9},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/listmonk.js":{"language":"JavaScript","code":412,"comment":36,"blank":66},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/accountExpiration.js":{"language":"JavaScript","code":59,"comment":11,"blank":19},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/responsive.css":{"language":"PostCSS","code":150,"comment":12,"blank":29},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/index.html":{"language":"HTML","code":1872,"comment":11,"blank":259},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_ward_boundaries_optimized.csv":{"language":"CSV","code":13,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/modal.css":{"language":"PostCSS","code":73,"comment":1,"blank":10},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/start-location-marker.css":{"language":"PostCSS","code":65,"comment":3,"blank":8},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/home.css":{"language":"PostCSS","code":866,"comment":54,"blank":153},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/404.html":{"language":"HTML","code":251,"comment":1,"blank":437},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/n8n/index.html":{"language":"HTML","code":1057,"comment":1,"blank":1223},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/index.html":{"language":"HTML","code":717,"comment":1,"blank":661},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/static-server/index.html":{"language":"HTML","code":877,"comment":1,"blank":1182},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/test/index.html":{"language":"HTML","code":278,"comment":1,"blank":484},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/listmonk/index.html":{"language":"HTML","code":775,"comment":1,"blank":1159},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/extra.css":{"language":"PostCSS","code":498,"comment":18,"blank":82},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/cost-comparison/index.html":{"language":"HTML","code":1300,"comment":1,"blank":766},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/gitea/index.html":{"language":"HTML","code":737,"comment":1,"blank":1151},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/nocodb/index.html":{"language":"HTML","code":1040,"comment":1,"blank":1219},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/lander.html":{"language":"HTML","code":1872,"comment":11,"blank":259},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/main.html":{"language":"HTML","code":8,"comment":1,"blank":3},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/gitea-widget.js":{"language":"JavaScript","code":121,"comment":3,"blank":8},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/index.html":{"language":"HTML","code":451,"comment":1,"blank":572},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/github-widget.js":{"language":"JavaScript","code":144,"comment":10,"blank":14},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/postgresql/index.html":{"language":"HTML","code":880,"comment":1,"blank":1190},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/map/index.html":{"language":"HTML","code":877,"comment":1,"blank":1170},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/home.js":{"language":"JavaScript","code":271,"comment":30,"blank":37},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/code-server/index.html":{"language":"HTML","code":755,"comment":1,"blank":1155},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/homepage/index.html":{"language":"HTML","code":1119,"comment":1,"blank":1235},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/index.html":{"language":"HTML","code":549,"comment":1,"blank":1083},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/map/index.html":{"language":"HTML","code":987,"comment":1,"blank":1202},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mini-qr/index.html":{"language":"HTML","code":707,"comment":1,"blank":1147},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/go-gitea-gitea.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/nocodb-nocodb.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/bundle.50899def.min.js":{"language":"JavaScript","code":14,"comment":1,"blank":2},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/gethomepage-homepage.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/admin-changemaker.lite.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/tinyseg.js":{"language":"JavaScript","code":176,"comment":21,"blank":9},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/coder-code-server.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/main.7e37652d.min.css":{"language":"PostCSS","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js":{"language":"JavaScript","code":38,"comment":3,"blank":2},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/anthropics-claude-code.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/palette.06af60db.min.css":{"language":"PostCSS","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/n8n-io-n8n.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/lyqht-mini-qr.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/knadh-listmonk.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/10/2/index.html":{"language":"HTML","code":392,"comment":1,"blank":583},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/ollama-ollama.json":{"language":"JSON","code":16,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/archive/2025/index.html":{"language":"HTML","code":442,"comment":1,"blank":582},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_edmonton_optimized.js":{"language":"JavaScript","code":185,"comment":21,"blank":43},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/dashboard.css":{"language":"PostCSS","code":129,"comment":3,"blank":29},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/index.html":{"language":"HTML","code":769,"comment":1,"blank":1113},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mkdocs/index.html":{"language":"HTML","code":905,"comment":1,"blank":1187},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/cloudflare-config/index.html":{"language":"HTML","code":740,"comment":1,"blank":1146},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/index.html":{"language":"HTML","code":550,"comment":1,"blank":1083},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/coder/index.html":{"language":"HTML","code":1117,"comment":1,"blank":1240},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js":{"language":"JavaScript","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js":{"language":"JavaScript","code":1,"comment":16,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/qr-code.css":{"language":"PostCSS","code":59,"comment":3,"blank":13},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/map-controls.css":{"language":"PostCSS","code":281,"comment":13,"blank":45},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/doc-search.css":{"language":"PostCSS","code":123,"comment":2,"blank":20},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City%20of%20Edmonton%20-%20Neighbourhoods%20%28Centroid%20Point%29_20250807.geojson":{"language":"JSON","code":3,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/print.css":{"language":"PostCSS","code":11,"comment":1,"blank":2},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/map/index.html":{"language":"HTML","code":1672,"comment":1,"blank":1346},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/08/01/3/index.html":{"language":"HTML","code":455,"comment":1,"blank":590},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/03/blog-1/index.html":{"language":"HTML","code":374,"comment":1,"blank":582},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/listmonk.css":{"language":"PostCSS","code":295,"comment":4,"blank":55},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/temp-user.css":{"language":"PostCSS","code":46,"comment":6,"blank":5},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/mkdocs/index.html":{"language":"HTML","code":833,"comment":1,"blank":1151},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/search/search_index.json":{"language":"JSON","code":1,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/sitemap.xml":{"language":"XML","code":147,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/exampledata.csv":{"language":"CSV","code":7,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/mobile-ui.css":{"language":"PostCSS","code":205,"comment":8,"blank":36},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/hooks/repo_widget_hook.py":{"language":"Python","code":119,"comment":24,"blank":16},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-popup.css":{"language":"PostCSS","code":197,"comment":9,"blank":30},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv":{"language":"CSV","code":407,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/site/index.html":{"language":"HTML","code":748,"comment":1,"blank":1138},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/how%20to/canvass/index.html":{"language":"HTML","code":275,"comment":1,"blank":484},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/server/index.html":{"language":"HTML","code":1045,"comment":1,"blank":1234},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/index.html":{"language":"HTML","code":549,"comment":1,"blank":1083},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/index.html":{"language":"HTML","code":1312,"comment":1,"blank":1199},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/map/index.html":{"language":"HTML","code":983,"comment":1,"blank":1182},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/vscode-ssh/index.html":{"language":"HTML","code":1926,"comment":1,"blank":1358},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/ansible/index.html":{"language":"HTML","code":1653,"comment":1,"blank":1326},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv":{"language":"CSV","code":13,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/wordcut.js":{"language":"JavaScript","code":4882,"comment":915,"blank":911},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City%20of%20Edmonton%20Ward%20Boundary%20and%20Council%20Composition_Current_20250807.geojson":{"language":"JSON","code":5,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_neighborhoods_nocodb_optimized.csv":{"language":"CSV","code":689,"comment":0,"blank":0},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_nocodb_optimized.csv":{"language":"CSV","code":1095,"comment":0,"blank":406},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv":{"language":"CSV","code":689,"comment":0,"blank":1},"file:///mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City%20of%20Edmonton%20-%20Neighbourhoods_20250807.geojson":{"language":"JSON","code":5,"comment":0,"blank":0}} \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/results.md b/.VSCodeCounter/2025-09-05_12-42-08/results.md new file mode 100644 index 0000000..ef84f9b --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/results.md @@ -0,0 +1,148 @@ +# Summary + +Date : 2025-09-05 12:42:08 + +Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite + +Total : 335 files, 91924 codes, 4969 comments, 47542 blanks, all 144435 lines + +Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md) + +## Languages +| language | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| HTML | 53 | 38,504 | 164 | 37,745 | 76,413 | +| JavaScript | 129 | 27,027 | 3,870 | 4,499 | 35,396 | +| PostCSS | 46 | 10,405 | 501 | 1,857 | 12,763 | +| Markdown | 50 | 6,210 | 5 | 2,406 | 8,621 | +| CSV | 7 | 2,913 | 0 | 409 | 3,322 | +| JSON | 30 | 2,634 | 0 | 5 | 2,639 | +| Shell Script | 4 | 2,480 | 342 | 488 | 3,310 | +| Log | 2 | 705 | 0 | 17 | 722 | +| YAML | 9 | 608 | 25 | 65 | 698 | +| Python | 2 | 238 | 48 | 32 | 318 | +| XML | 1 | 147 | 0 | 0 | 147 | +| Properties | 1 | 33 | 7 | 9 | 49 | +| Docker | 1 | 20 | 7 | 10 | 37 | + +## Directories +| path | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| . | 335 | 91,924 | 4,969 | 47,542 | 144,435 | +| . (Files) | 7 | 1,862 | 284 | 435 | 2,581 | +| configs | 10 | 897 | 19 | 63 | 979 | +| configs/homepage | 9 | 864 | 12 | 54 | 930 | +| configs/homepage (Files) | 8 | 172 | 12 | 40 | 224 | +| configs/homepage/logs | 1 | 692 | 0 | 14 | 706 | +| configs/mkdocs-site | 1 | 33 | 7 | 9 | 49 | +| map | 167 | 40,025 | 3,126 | 6,580 | 49,731 | +| map (Files) | 7 | 2,177 | 60 | 664 | 2,901 | +| map/app | 139 | 33,222 | 3,016 | 5,083 | 41,321 | +| map/app (Files) | 4 | 2,515 | 50 | 61 | 2,626 | +| map/app/config | 1 | 118 | 15 | 18 | 151 | +| map/app/controllers | 12 | 3,060 | 276 | 548 | 3,884 | +| map/app/middleware | 2 | 255 | 23 | 34 | 312 | +| map/app/public | 86 | 24,382 | 2,278 | 3,910 | 30,570 | +| map/app/public (Files) | 6 | 2,131 | 93 | 280 | 2,504 | +| map/app/public/css | 40 | 7,726 | 357 | 1,399 | 9,482 | +| map/app/public/css (Files) | 6 | 1,474 | 62 | 288 | 1,824 | +| map/app/public/css/admin | 11 | 2,363 | 114 | 437 | 2,914 | +| map/app/public/css/modules | 23 | 3,889 | 181 | 674 | 4,744 | +| map/app/public/js | 40 | 14,525 | 1,828 | 2,231 | 18,584 | +| map/app/public/js (Files) | 33 | 14,525 | 1,828 | 2,224 | 18,577 | +| map/app/public/js/admin | 7 | 0 | 0 | 7 | 7 | +| map/app/routes | 17 | 767 | 120 | 188 | 1,075 | +| map/app/services | 8 | 1,189 | 176 | 230 | 1,595 | +| map/app/templates | 6 | 644 | 0 | 39 | 683 | +| map/app/templates/email | 6 | 644 | 0 | 39 | 683 | +| map/app/utils | 3 | 292 | 78 | 55 | 425 | +| map/data | 12 | 3,284 | 45 | 498 | 3,827 | +| map/instruct | 9 | 1,342 | 5 | 335 | 1,682 | +| mkdocs | 151 | 49,140 | 1,540 | 40,464 | 91,144 | +| mkdocs (Files) | 1 | 167 | 11 | 11 | 189 | +| mkdocs/docs | 54 | 7,637 | 151 | 2,040 | 9,828 | +| mkdocs/docs (Files) | 2 | 90 | 0 | 35 | 125 | +| mkdocs/docs/adv | 3 | 886 | 0 | 327 | 1,213 | +| mkdocs/docs/assets | 11 | 176 | 0 | 0 | 176 | +| mkdocs/docs/assets/repo-data | 11 | 176 | 0 | 0 | 176 | +| mkdocs/docs/blog | 4 | 70 | 0 | 29 | 99 | +| mkdocs/docs/blog (Files) | 1 | 0 | 0 | 1 | 1 | +| mkdocs/docs/blog/posts | 3 | 70 | 0 | 28 | 98 | +| mkdocs/docs/build | 4 | 687 | 0 | 273 | 960 | +| mkdocs/docs/config | 5 | 588 | 0 | 230 | 818 | +| mkdocs/docs/hooks | 1 | 119 | 24 | 16 | 159 | +| mkdocs/docs/how to | 1 | 2 | 0 | 3 | 5 | +| mkdocs/docs/javascripts | 3 | 536 | 43 | 59 | 638 | +| mkdocs/docs/manual | 2 | 93 | 0 | 49 | 142 | +| mkdocs/docs/overrides | 2 | 1,880 | 12 | 262 | 2,154 | +| mkdocs/docs/phil | 2 | 248 | 0 | 120 | 368 | +| mkdocs/docs/services | 12 | 898 | 0 | 402 | 1,300 | +| mkdocs/docs/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 | +| mkdocs/site | 96 | 41,336 | 1,378 | 38,413 | 81,127 | +| mkdocs/site (Files) | 3 | 2,270 | 12 | 696 | 2,978 | +| mkdocs/site/adv | 3 | 4,128 | 3 | 3,767 | 7,898 | +| mkdocs/site/adv (Files) | 1 | 549 | 1 | 1,083 | 1,633 | +| mkdocs/site/adv/ansible | 1 | 1,653 | 1 | 1,326 | 2,980 | +| mkdocs/site/adv/vscode-ssh | 1 | 1,926 | 1 | 1,358 | 3,285 | +| mkdocs/site/assets | 49 | 5,320 | 1,180 | 939 | 7,439 | +| mkdocs/site/assets/javascripts | 36 | 5,142 | 1,180 | 939 | 7,261 | +| mkdocs/site/assets/javascripts (Files) | 1 | 14 | 1 | 2 | 17 | +| mkdocs/site/assets/javascripts/lunr | 34 | 5,090 | 1,176 | 935 | 7,201 | +| mkdocs/site/assets/javascripts/lunr (Files) | 2 | 5,058 | 936 | 920 | 6,914 | +| mkdocs/site/assets/javascripts/lunr/min | 32 | 32 | 240 | 15 | 287 | +| mkdocs/site/assets/javascripts/workers | 1 | 38 | 3 | 2 | 43 | +| mkdocs/site/assets/repo-data | 11 | 176 | 0 | 0 | 176 | +| mkdocs/site/assets/stylesheets | 2 | 2 | 0 | 0 | 2 | +| mkdocs/site/blog | 5 | 2,114 | 5 | 2,909 | 5,028 | +| mkdocs/site/blog (Files) | 1 | 451 | 1 | 572 | 1,024 | +| mkdocs/site/blog/2025 | 3 | 1,221 | 3 | 1,755 | 2,979 | +| mkdocs/site/blog/2025/07 | 2 | 766 | 2 | 1,165 | 1,933 | +| mkdocs/site/blog/2025/07/03 | 1 | 374 | 1 | 582 | 957 | +| mkdocs/site/blog/2025/07/03/blog-1 | 1 | 374 | 1 | 582 | 957 | +| mkdocs/site/blog/2025/07/10 | 1 | 392 | 1 | 583 | 976 | +| mkdocs/site/blog/2025/07/10/2 | 1 | 392 | 1 | 583 | 976 | +| mkdocs/site/blog/2025/08 | 1 | 455 | 1 | 590 | 1,046 | +| mkdocs/site/blog/2025/08/01 | 1 | 455 | 1 | 590 | 1,046 | +| mkdocs/site/blog/2025/08/01/3 | 1 | 455 | 1 | 590 | 1,046 | +| mkdocs/site/blog/archive | 1 | 442 | 1 | 582 | 1,025 | +| mkdocs/site/blog/archive/2025 | 1 | 442 | 1 | 582 | 1,025 | +| mkdocs/site/build | 4 | 4,088 | 4 | 4,753 | 8,845 | +| mkdocs/site/build (Files) | 1 | 1,312 | 1 | 1,199 | 2,512 | +| mkdocs/site/build/map | 1 | 983 | 1 | 1,182 | 2,166 | +| mkdocs/site/build/server | 1 | 1,045 | 1 | 1,234 | 2,280 | +| mkdocs/site/build/site | 1 | 748 | 1 | 1,138 | 1,887 | +| mkdocs/site/config | 5 | 4,912 | 5 | 5,966 | 10,883 | +| mkdocs/site/config (Files) | 1 | 550 | 1 | 1,083 | 1,634 | +| mkdocs/site/config/cloudflare-config | 1 | 740 | 1 | 1,146 | 1,887 | +| mkdocs/site/config/coder | 1 | 1,117 | 1 | 1,240 | 2,358 | +| mkdocs/site/config/map | 1 | 1,672 | 1 | 1,346 | 3,019 | +| mkdocs/site/config/mkdocs | 1 | 833 | 1 | 1,151 | 1,985 | +| mkdocs/site/hooks | 1 | 119 | 24 | 16 | 159 | +| mkdocs/site/how to | 1 | 275 | 1 | 484 | 760 | +| mkdocs/site/how to/canvass | 1 | 275 | 1 | 484 | 760 | +| mkdocs/site/javascripts | 3 | 536 | 43 | 59 | 638 | +| mkdocs/site/manual | 2 | 1,536 | 2 | 2,285 | 3,823 | +| mkdocs/site/manual (Files) | 1 | 549 | 1 | 1,083 | 1,633 | +| mkdocs/site/manual/map | 1 | 987 | 1 | 1,202 | 2,190 | +| mkdocs/site/overrides | 2 | 1,880 | 12 | 262 | 2,154 | +| mkdocs/site/phil | 2 | 2,017 | 2 | 1,427 | 3,446 | +| mkdocs/site/phil (Files) | 1 | 717 | 1 | 661 | 1,379 | +| mkdocs/site/phil/cost-comparison | 1 | 1,300 | 1 | 766 | 2,067 | +| mkdocs/site/search | 1 | 1 | 0 | 0 | 1 | +| mkdocs/site/services | 12 | 10,498 | 12 | 14,131 | 24,641 | +| mkdocs/site/services (Files) | 1 | 769 | 1 | 1,113 | 1,883 | +| mkdocs/site/services/code-server | 1 | 755 | 1 | 1,155 | 1,911 | +| mkdocs/site/services/gitea | 1 | 737 | 1 | 1,151 | 1,889 | +| mkdocs/site/services/homepage | 1 | 1,119 | 1 | 1,235 | 2,355 | +| mkdocs/site/services/listmonk | 1 | 775 | 1 | 1,159 | 1,935 | +| mkdocs/site/services/map | 1 | 877 | 1 | 1,170 | 2,048 | +| mkdocs/site/services/mini-qr | 1 | 707 | 1 | 1,147 | 1,855 | +| mkdocs/site/services/mkdocs | 1 | 905 | 1 | 1,187 | 2,093 | +| mkdocs/site/services/n8n | 1 | 1,057 | 1 | 1,223 | 2,281 | +| mkdocs/site/services/nocodb | 1 | 1,040 | 1 | 1,219 | 2,260 | +| mkdocs/site/services/postgresql | 1 | 880 | 1 | 1,190 | 2,071 | +| mkdocs/site/services/static-server | 1 | 877 | 1 | 1,182 | 2,060 | +| mkdocs/site/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 | +| mkdocs/site/test | 1 | 278 | 1 | 484 | 763 | + +Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2025-09-05_12-42-08/results.txt b/.VSCodeCounter/2025-09-05_12-42-08/results.txt new file mode 100644 index 0000000..64c7e1a --- /dev/null +++ b/.VSCodeCounter/2025-09-05_12-42-08/results.txt @@ -0,0 +1,486 @@ +Date : 2025-09-05 12:42:08 +Directory : /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite +Total : 335 files, 91924 codes, 4969 comments, 47542 blanks, all 144435 lines + +Languages ++--------------+------------+------------+------------+------------+------------+ +| language | files | code | comment | blank | total | ++--------------+------------+------------+------------+------------+------------+ +| HTML | 53 | 38,504 | 164 | 37,745 | 76,413 | +| JavaScript | 129 | 27,027 | 3,870 | 4,499 | 35,396 | +| PostCSS | 46 | 10,405 | 501 | 1,857 | 12,763 | +| Markdown | 50 | 6,210 | 5 | 2,406 | 8,621 | +| CSV | 7 | 2,913 | 0 | 409 | 3,322 | +| JSON | 30 | 2,634 | 0 | 5 | 2,639 | +| Shell Script | 4 | 2,480 | 342 | 488 | 3,310 | +| Log | 2 | 705 | 0 | 17 | 722 | +| YAML | 9 | 608 | 25 | 65 | 698 | +| Python | 2 | 238 | 48 | 32 | 318 | +| XML | 1 | 147 | 0 | 0 | 147 | +| Properties | 1 | 33 | 7 | 9 | 49 | +| Docker | 1 | 20 | 7 | 10 | 37 | ++--------------+------------+------------+------------+------------+------------+ + +Directories ++---------------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| path | files | code | comment | blank | total | ++---------------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| . | 335 | 91,924 | 4,969 | 47,542 | 144,435 | +| . (Files) | 7 | 1,862 | 284 | 435 | 2,581 | +| configs | 10 | 897 | 19 | 63 | 979 | +| configs/homepage | 9 | 864 | 12 | 54 | 930 | +| configs/homepage (Files) | 8 | 172 | 12 | 40 | 224 | +| configs/homepage/logs | 1 | 692 | 0 | 14 | 706 | +| configs/mkdocs-site | 1 | 33 | 7 | 9 | 49 | +| map | 167 | 40,025 | 3,126 | 6,580 | 49,731 | +| map (Files) | 7 | 2,177 | 60 | 664 | 2,901 | +| map/app | 139 | 33,222 | 3,016 | 5,083 | 41,321 | +| map/app (Files) | 4 | 2,515 | 50 | 61 | 2,626 | +| map/app/config | 1 | 118 | 15 | 18 | 151 | +| map/app/controllers | 12 | 3,060 | 276 | 548 | 3,884 | +| map/app/middleware | 2 | 255 | 23 | 34 | 312 | +| map/app/public | 86 | 24,382 | 2,278 | 3,910 | 30,570 | +| map/app/public (Files) | 6 | 2,131 | 93 | 280 | 2,504 | +| map/app/public/css | 40 | 7,726 | 357 | 1,399 | 9,482 | +| map/app/public/css (Files) | 6 | 1,474 | 62 | 288 | 1,824 | +| map/app/public/css/admin | 11 | 2,363 | 114 | 437 | 2,914 | +| map/app/public/css/modules | 23 | 3,889 | 181 | 674 | 4,744 | +| map/app/public/js | 40 | 14,525 | 1,828 | 2,231 | 18,584 | +| map/app/public/js (Files) | 33 | 14,525 | 1,828 | 2,224 | 18,577 | +| map/app/public/js/admin | 7 | 0 | 0 | 7 | 7 | +| map/app/routes | 17 | 767 | 120 | 188 | 1,075 | +| map/app/services | 8 | 1,189 | 176 | 230 | 1,595 | +| map/app/templates | 6 | 644 | 0 | 39 | 683 | +| map/app/templates/email | 6 | 644 | 0 | 39 | 683 | +| map/app/utils | 3 | 292 | 78 | 55 | 425 | +| map/data | 12 | 3,284 | 45 | 498 | 3,827 | +| map/instruct | 9 | 1,342 | 5 | 335 | 1,682 | +| mkdocs | 151 | 49,140 | 1,540 | 40,464 | 91,144 | +| mkdocs (Files) | 1 | 167 | 11 | 11 | 189 | +| mkdocs/docs | 54 | 7,637 | 151 | 2,040 | 9,828 | +| mkdocs/docs (Files) | 2 | 90 | 0 | 35 | 125 | +| mkdocs/docs/adv | 3 | 886 | 0 | 327 | 1,213 | +| mkdocs/docs/assets | 11 | 176 | 0 | 0 | 176 | +| mkdocs/docs/assets/repo-data | 11 | 176 | 0 | 0 | 176 | +| mkdocs/docs/blog | 4 | 70 | 0 | 29 | 99 | +| mkdocs/docs/blog (Files) | 1 | 0 | 0 | 1 | 1 | +| mkdocs/docs/blog/posts | 3 | 70 | 0 | 28 | 98 | +| mkdocs/docs/build | 4 | 687 | 0 | 273 | 960 | +| mkdocs/docs/config | 5 | 588 | 0 | 230 | 818 | +| mkdocs/docs/hooks | 1 | 119 | 24 | 16 | 159 | +| mkdocs/docs/how to | 1 | 2 | 0 | 3 | 5 | +| mkdocs/docs/javascripts | 3 | 536 | 43 | 59 | 638 | +| mkdocs/docs/manual | 2 | 93 | 0 | 49 | 142 | +| mkdocs/docs/overrides | 2 | 1,880 | 12 | 262 | 2,154 | +| mkdocs/docs/phil | 2 | 248 | 0 | 120 | 368 | +| mkdocs/docs/services | 12 | 898 | 0 | 402 | 1,300 | +| mkdocs/docs/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 | +| mkdocs/site | 96 | 41,336 | 1,378 | 38,413 | 81,127 | +| mkdocs/site (Files) | 3 | 2,270 | 12 | 696 | 2,978 | +| mkdocs/site/adv | 3 | 4,128 | 3 | 3,767 | 7,898 | +| mkdocs/site/adv (Files) | 1 | 549 | 1 | 1,083 | 1,633 | +| mkdocs/site/adv/ansible | 1 | 1,653 | 1 | 1,326 | 2,980 | +| mkdocs/site/adv/vscode-ssh | 1 | 1,926 | 1 | 1,358 | 3,285 | +| mkdocs/site/assets | 49 | 5,320 | 1,180 | 939 | 7,439 | +| mkdocs/site/assets/javascripts | 36 | 5,142 | 1,180 | 939 | 7,261 | +| mkdocs/site/assets/javascripts (Files) | 1 | 14 | 1 | 2 | 17 | +| mkdocs/site/assets/javascripts/lunr | 34 | 5,090 | 1,176 | 935 | 7,201 | +| mkdocs/site/assets/javascripts/lunr (Files) | 2 | 5,058 | 936 | 920 | 6,914 | +| mkdocs/site/assets/javascripts/lunr/min | 32 | 32 | 240 | 15 | 287 | +| mkdocs/site/assets/javascripts/workers | 1 | 38 | 3 | 2 | 43 | +| mkdocs/site/assets/repo-data | 11 | 176 | 0 | 0 | 176 | +| mkdocs/site/assets/stylesheets | 2 | 2 | 0 | 0 | 2 | +| mkdocs/site/blog | 5 | 2,114 | 5 | 2,909 | 5,028 | +| mkdocs/site/blog (Files) | 1 | 451 | 1 | 572 | 1,024 | +| mkdocs/site/blog/2025 | 3 | 1,221 | 3 | 1,755 | 2,979 | +| mkdocs/site/blog/2025/07 | 2 | 766 | 2 | 1,165 | 1,933 | +| mkdocs/site/blog/2025/07/03 | 1 | 374 | 1 | 582 | 957 | +| mkdocs/site/blog/2025/07/03/blog-1 | 1 | 374 | 1 | 582 | 957 | +| mkdocs/site/blog/2025/07/10 | 1 | 392 | 1 | 583 | 976 | +| mkdocs/site/blog/2025/07/10/2 | 1 | 392 | 1 | 583 | 976 | +| mkdocs/site/blog/2025/08 | 1 | 455 | 1 | 590 | 1,046 | +| mkdocs/site/blog/2025/08/01 | 1 | 455 | 1 | 590 | 1,046 | +| mkdocs/site/blog/2025/08/01/3 | 1 | 455 | 1 | 590 | 1,046 | +| mkdocs/site/blog/archive | 1 | 442 | 1 | 582 | 1,025 | +| mkdocs/site/blog/archive/2025 | 1 | 442 | 1 | 582 | 1,025 | +| mkdocs/site/build | 4 | 4,088 | 4 | 4,753 | 8,845 | +| mkdocs/site/build (Files) | 1 | 1,312 | 1 | 1,199 | 2,512 | +| mkdocs/site/build/map | 1 | 983 | 1 | 1,182 | 2,166 | +| mkdocs/site/build/server | 1 | 1,045 | 1 | 1,234 | 2,280 | +| mkdocs/site/build/site | 1 | 748 | 1 | 1,138 | 1,887 | +| mkdocs/site/config | 5 | 4,912 | 5 | 5,966 | 10,883 | +| mkdocs/site/config (Files) | 1 | 550 | 1 | 1,083 | 1,634 | +| mkdocs/site/config/cloudflare-config | 1 | 740 | 1 | 1,146 | 1,887 | +| mkdocs/site/config/coder | 1 | 1,117 | 1 | 1,240 | 2,358 | +| mkdocs/site/config/map | 1 | 1,672 | 1 | 1,346 | 3,019 | +| mkdocs/site/config/mkdocs | 1 | 833 | 1 | 1,151 | 1,985 | +| mkdocs/site/hooks | 1 | 119 | 24 | 16 | 159 | +| mkdocs/site/how to | 1 | 275 | 1 | 484 | 760 | +| mkdocs/site/how to/canvass | 1 | 275 | 1 | 484 | 760 | +| mkdocs/site/javascripts | 3 | 536 | 43 | 59 | 638 | +| mkdocs/site/manual | 2 | 1,536 | 2 | 2,285 | 3,823 | +| mkdocs/site/manual (Files) | 1 | 549 | 1 | 1,083 | 1,633 | +| mkdocs/site/manual/map | 1 | 987 | 1 | 1,202 | 2,190 | +| mkdocs/site/overrides | 2 | 1,880 | 12 | 262 | 2,154 | +| mkdocs/site/phil | 2 | 2,017 | 2 | 1,427 | 3,446 | +| mkdocs/site/phil (Files) | 1 | 717 | 1 | 661 | 1,379 | +| mkdocs/site/phil/cost-comparison | 1 | 1,300 | 1 | 766 | 2,067 | +| mkdocs/site/search | 1 | 1 | 0 | 0 | 1 | +| mkdocs/site/services | 12 | 10,498 | 12 | 14,131 | 24,641 | +| mkdocs/site/services (Files) | 1 | 769 | 1 | 1,113 | 1,883 | +| mkdocs/site/services/code-server | 1 | 755 | 1 | 1,155 | 1,911 | +| mkdocs/site/services/gitea | 1 | 737 | 1 | 1,151 | 1,889 | +| mkdocs/site/services/homepage | 1 | 1,119 | 1 | 1,235 | 2,355 | +| mkdocs/site/services/listmonk | 1 | 775 | 1 | 1,159 | 1,935 | +| mkdocs/site/services/map | 1 | 877 | 1 | 1,170 | 2,048 | +| mkdocs/site/services/mini-qr | 1 | 707 | 1 | 1,147 | 1,855 | +| mkdocs/site/services/mkdocs | 1 | 905 | 1 | 1,187 | 2,093 | +| mkdocs/site/services/n8n | 1 | 1,057 | 1 | 1,223 | 2,281 | +| mkdocs/site/services/nocodb | 1 | 1,040 | 1 | 1,219 | 2,260 | +| mkdocs/site/services/postgresql | 1 | 880 | 1 | 1,190 | 2,071 | +| mkdocs/site/services/static-server | 1 | 877 | 1 | 1,182 | 2,060 | +| mkdocs/site/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 | +| mkdocs/site/test | 1 | 278 | 1 | 484 | 763 | ++---------------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ + +Files ++---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+ +| filename | language | code | comment | blank | total | ++---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+ +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/README.md | Markdown | 55 | 0 | 24 | 79 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/combined.log | Log | 13 | 0 | 3 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/config.sh | Shell Script | 810 | 177 | 224 | 1,211 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/bookmarks.yaml | YAML | 47 | 2 | 5 | 54 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.css | PostCSS | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/docker.yaml | YAML | 3 | 2 | 2 | 7 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/kubernetes.yaml | YAML | 1 | 1 | 1 | 3 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/logs/homepage.log | Log | 692 | 0 | 14 | 706 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/services.yaml | YAML | 59 | 1 | 15 | 75 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/settings.yaml | YAML | 36 | 4 | 10 | 50 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/widgets.yaml | YAML | 26 | 2 | 5 | 33 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/mkdocs-site/default.conf | Properties | 33 | 7 | 9 | 49 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/docker-compose.yml | YAML | 238 | 2 | 13 | 253 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/Instuctions.md | Markdown | 56 | 0 | 21 | 77 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/README.md | Markdown | 851 | 0 | 252 | 1,103 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/Dockerfile | Docker | 20 | 7 | 10 | 37 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/config/index.js | JavaScript | 118 | 15 | 18 | 151 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/authController.js | JavaScript | 172 | 13 | 25 | 210 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/cutsController.js | JavaScript | 276 | 41 | 47 | 364 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dashboardController.js | JavaScript | 70 | 9 | 15 | 94 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dataConvertController.js | JavaScript | 352 | 50 | 76 | 478 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/externalDataController.js | JavaScript | 101 | 11 | 21 | 133 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/listmonkController.js | JavaScript | 212 | 12 | 29 | 253 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/locationsController.js | JavaScript | 333 | 23 | 58 | 414 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/passwordRecoveryController.js | JavaScript | 45 | 4 | 12 | 61 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/publicShiftsController.js | JavaScript | 212 | 21 | 38 | 271 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/settingsController.js | JavaScript | 303 | 17 | 50 | 370 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/shiftsController.js | JavaScript | 643 | 57 | 123 | 823 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/usersController.js | JavaScript | 341 | 18 | 54 | 413 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/auth.js | JavaScript | 170 | 13 | 25 | 208 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/rateLimiter.js | JavaScript | 85 | 10 | 9 | 104 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package-lock.json | JSON | 2,203 | 0 | 1 | 2,204 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package.json | JSON | 43 | 0 | 1 | 44 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/admin.html | HTML | 1,080 | 46 | 135 | 1,261 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/REFACTORING_SUMMARY.md | Markdown | 51 | 0 | 13 | 64 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin.css | PostCSS | 12 | 4 | 3 | 19 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/cuts-shifts.css | PostCSS | 225 | 13 | 44 | 282 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/data-convert.css | PostCSS | 176 | 7 | 33 | 216 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/forms.css | PostCSS | 207 | 9 | 36 | 252 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/layout.css | PostCSS | 202 | 7 | 36 | 245 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/modals.css | PostCSS | 259 | 6 | 46 | 311 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/nocodb-links.css | PostCSS | 109 | 4 | 23 | 136 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/responsive.css | PostCSS | 552 | 28 | 112 | 692 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/status-messages.css | PostCSS | 154 | 9 | 27 | 190 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/user-management.css | PostCSS | 179 | 10 | 31 | 220 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/variables.css | PostCSS | 48 | 9 | 10 | 67 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/walk-sheet.css | PostCSS | 252 | 12 | 39 | 303 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-marker.css | PostCSS | 35 | 2 | 5 | 42 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-popup.css | PostCSS | 197 | 9 | 30 | 236 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/base.css | PostCSS | 68 | 22 | 12 | 102 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/buttons.css | PostCSS | 70 | 2 | 16 | 88 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cache-busting.css | PostCSS | 99 | 2 | 13 | 114 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cuts.css | PostCSS | 829 | 22 | 146 | 997 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/dashboard.css | PostCSS | 129 | 3 | 29 | 161 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/doc-search.css | PostCSS | 123 | 2 | 20 | 145 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/forms.css | PostCSS | 105 | 2 | 18 | 125 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/layout.css | PostCSS | 83 | 5 | 11 | 99 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/leaflet-custom.css | PostCSS | 147 | 20 | 32 | 199 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/listmonk.css | PostCSS | 295 | 4 | 55 | 354 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/map-controls.css | PostCSS | 281 | 13 | 45 | 339 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/mobile-ui.css | PostCSS | 205 | 8 | 36 | 249 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/modal.css | PostCSS | 73 | 1 | 10 | 84 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/nocodb-dropdown.css | PostCSS | 134 | 4 | 24 | 162 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/notifications.css | PostCSS | 105 | 5 | 16 | 126 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/print.css | PostCSS | 11 | 1 | 2 | 14 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/qr-code.css | PostCSS | 59 | 3 | 13 | 75 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/responsive.css | PostCSS | 150 | 12 | 29 | 191 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/start-location-marker.css | PostCSS | 65 | 3 | 8 | 76 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/temp-user.css | PostCSS | 46 | 6 | 5 | 57 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/unified-search.css | PostCSS | 580 | 30 | 99 | 709 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/public-shifts.css | PostCSS | 418 | 24 | 83 | 525 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/shifts.css | PostCSS | 608 | 21 | 118 | 747 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/style.css | PostCSS | 21 | 0 | 1 | 22 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/user.css | PostCSS | 364 | 13 | 70 | 447 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/index.html | HTML | 384 | 28 | 45 | 457 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-auth.js | JavaScript | 63 | 11 | 14 | 88 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-core.js | JavaScript | 239 | 46 | 40 | 325 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-cuts.js | JavaScript | 1,505 | 184 | 302 | 1,991 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-email.js | JavaScript | 319 | 29 | 47 | 395 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-map.js | JavaScript | 178 | 26 | 46 | 250 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shift-volunteers.js | JavaScript | 416 | 49 | 66 | 531 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shifts.js | JavaScript | 330 | 35 | 55 | 420 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-users.js | JavaScript | 305 | 16 | 45 | 366 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-walksheet.js | JavaScript | 387 | 35 | 50 | 472 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin.js | JavaScript | 2,309 | 178 | 288 | 2,775 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/auth.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/navigation.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/shifts.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/startLocation.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/users.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/utils.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/walkSheet.js | JavaScript | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/auth.js | JavaScript | 181 | 28 | 34 | 243 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cache-manager.js | JavaScript | 219 | 71 | 28 | 318 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/config.js | JavaScript | 32 | 3 | 3 | 38 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-controls.js | JavaScript | 982 | 163 | 145 | 1,290 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-drawing.js | JavaScript | 206 | 72 | 59 | 337 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-manager.js | JavaScript | 359 | 96 | 79 | 534 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/dashboard.js | JavaScript | 333 | 23 | 33 | 389 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/data-convert.js | JavaScript | 510 | 64 | 118 | 692 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/database-search.js | JavaScript | 186 | 76 | 51 | 313 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/external-layers.js | JavaScript | 513 | 39 | 45 | 597 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-admin.js | JavaScript | 410 | 76 | 85 | 571 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-status.js | JavaScript | 169 | 25 | 32 | 226 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/location-manager.js | JavaScript | 998 | 56 | 91 | 1,145 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/main.js | JavaScript | 178 | 40 | 36 | 254 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-manager.js | JavaScript | 82 | 12 | 19 | 113 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-search.js | JavaScript | 120 | 44 | 36 | 200 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/mkdocs-search.js | JavaScript | 263 | 36 | 52 | 351 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/public-shifts.js | JavaScript | 518 | 15 | 25 | 558 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/search-manager.js | JavaScript | 407 | 138 | 100 | 645 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/shifts.js | JavaScript | 1,059 | 35 | 55 | 1,149 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/ui-controls.js | JavaScript | 572 | 74 | 101 | 747 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/user.js | JavaScript | 81 | 12 | 19 | 112 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/utils.js | JavaScript | 96 | 21 | 25 | 142 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/login.html | HTML | 459 | 3 | 76 | 538 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/public-shifts.html | HTML | 88 | 4 | 7 | 99 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/shifts.html | HTML | 73 | 5 | 8 | 86 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/user.html | HTML | 47 | 7 | 9 | 63 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/admin.js | JavaScript | 80 | 15 | 16 | 111 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/auth.js | JavaScript | 14 | 5 | 6 | 25 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/cuts.js | JavaScript | 18 | 6 | 7 | 31 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dashboard.js | JavaScript | 5 | 0 | 3 | 8 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dataConvert.js | JavaScript | 21 | 4 | 6 | 31 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/debug.js | JavaScript | 236 | 10 | 36 | 282 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/external.js | JavaScript | 7 | 2 | 4 | 13 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/geocoding.js | JavaScript | 120 | 24 | 31 | 175 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/index.js | JavaScript | 153 | 29 | 36 | 218 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/listmonk.js | JavaScript | 12 | 5 | 9 | 26 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/locations.js | JavaScript | 11 | 5 | 6 | 22 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/public.js | JavaScript | 8 | 1 | 3 | 12 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/publicShifts.js | JavaScript | 8 | 1 | 2 | 11 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/qr.js | JavaScript | 39 | 1 | 8 | 48 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/settings.js | JavaScript | 8 | 2 | 3 | 13 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/shifts.js | JavaScript | 16 | 4 | 5 | 25 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/users.js | JavaScript | 11 | 6 | 7 | 24 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/server.js | JavaScript | 249 | 43 | 49 | 341 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/accountExpiration.js | JavaScript | 59 | 11 | 19 | 89 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/email.js | JavaScript | 109 | 4 | 19 | 132 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/emailTemplates.js | JavaScript | 59 | 10 | 16 | 85 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/geocoding.js | JavaScript | 219 | 51 | 44 | 314 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/listmonk.js | JavaScript | 412 | 36 | 66 | 514 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/nocodb.js | JavaScript | 172 | 22 | 36 | 230 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/qrcode.js | JavaScript | 110 | 33 | 20 | 163 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/socrata.js | JavaScript | 49 | 9 | 10 | 68 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/login-details.html | HTML | 113 | 0 | 4 | 117 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/password-recovery.html | HTML | 79 | 0 | 1 | 80 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-existing.html | HTML | 161 | 0 | 22 | 183 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-new.html | HTML | 31 | 0 | 5 | 36 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/shift-details.html | HTML | 152 | 0 | 5 | 157 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/user-broadcast.html | HTML | 108 | 0 | 2 | 110 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/cacheBusting.js | JavaScript | 105 | 51 | 24 | 180 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/helpers.js | JavaScript | 149 | 25 | 28 | 202 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/logger.js | JavaScript | 38 | 2 | 3 | 43 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/build-nocodb.sh | Shell Script | 925 | 60 | 93 | 1,078 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods (Centroid Point)_20250807.geojson | JSON | 3 | 0 | 1 | 4 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods_20250807.geojson | JSON | 5 | 0 | 0 | 5 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton Ward Boundary and Council Composition_Current_20250807.geojson | JSON | 5 | 0 | 0 | 5 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv | CSV | 689 | 0 | 1 | 690 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv | CSV | 407 | 0 | 1 | 408 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv | CSV | 13 | 0 | 1 | 14 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_edmonton_optimized.js | JavaScript | 185 | 21 | 43 | 249 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_ward_boundaries_optimized.js | JavaScript | 173 | 24 | 45 | 242 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_neighborhoods_nocodb_optimized.csv | CSV | 689 | 0 | 0 | 689 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_nocodb_optimized.csv | CSV | 1,095 | 0 | 406 | 1,501 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_ward_boundaries_optimized.csv | CSV | 13 | 0 | 0 | 13 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/exampledata.csv | CSV | 7 | 0 | 0 | 7 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/docker-compose.yml | YAML | 31 | 0 | 3 | 34 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/files-explainer.md | Markdown | 292 | 0 | 293 | 585 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/ADMIN_IMPLEMENTATION.md | Markdown | 102 | 0 | 28 | 130 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md | Markdown | 156 | 0 | 34 | 190 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md | Markdown | 124 | 5 | 31 | 160 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md | Markdown | 65 | 0 | 21 | 86 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/LISTMONK_INTEGRATION_GUIDE.md | Markdown | 249 | 0 | 70 | 319 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/SHIFT_PERFORMANCE_FIX.md | Markdown | 71 | 0 | 22 | 93 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_IMPLEMENTATION.md | Markdown | 88 | 0 | 28 | 116 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_TEST.md | Markdown | 113 | 0 | 34 | 147 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/build-nocodb.md | Markdown | 374 | 0 | 67 | 441 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package-lock.json | JSON | 17 | 0 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package.json | JSON | 5 | 0 | 1 | 6 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/ansible.md | Markdown | 373 | 0 | 152 | 525 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/index.md | Markdown | 2 | 0 | 1 | 3 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/vscode-ssh.md | Markdown | 511 | 0 | 174 | 685 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/anthropics-claude-code.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/coder-code-server.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/gethomepage-homepage.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/go-gitea-gitea.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/knadh-listmonk.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/n8n-io-n8n.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/nocodb-nocodb.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/ollama-ollama.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/index.md | Markdown | 0 | 0 | 1 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/1.md | Markdown | 6 | 0 | 3 | 9 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/2.md | Markdown | 10 | 0 | 4 | 14 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/3.md | Markdown | 54 | 0 | 21 | 75 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/index.md | Markdown | 336 | 0 | 150 | 486 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/map.md | Markdown | 155 | 0 | 62 | 217 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/server.md | Markdown | 122 | 0 | 27 | 149 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/site.md | Markdown | 74 | 0 | 34 | 108 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/cloudflare-config.md | Markdown | 45 | 0 | 16 | 61 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/coder.md | Markdown | 139 | 0 | 77 | 216 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/index.md | Markdown | 3 | 0 | 4 | 7 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/map.md | Markdown | 299 | 0 | 91 | 390 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/mkdocs.md | Markdown | 102 | 0 | 42 | 144 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/hooks/repo_widget_hook.py | Python | 119 | 24 | 16 | 159 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/how to/canvass.md | Markdown | 2 | 0 | 3 | 5 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/index.md | Markdown | 85 | 0 | 29 | 114 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/gitea-widget.js | JavaScript | 121 | 3 | 8 | 132 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/github-widget.js | JavaScript | 144 | 10 | 14 | 168 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/home.js | JavaScript | 271 | 30 | 37 | 338 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/index.md | Markdown | 2 | 0 | 1 | 3 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/map.md | Markdown | 91 | 0 | 48 | 139 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/lander.html | HTML | 1,872 | 11 | 259 | 2,142 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/main.html | HTML | 8 | 1 | 3 | 12 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/cost-comparison.md | Markdown | 153 | 0 | 51 | 204 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/index.md | Markdown | 95 | 0 | 69 | 164 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/code-server.md | Markdown | 41 | 0 | 21 | 62 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/gitea.md | Markdown | 39 | 0 | 19 | 58 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/homepage.md | Markdown | 146 | 0 | 66 | 212 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/index.md | Markdown | 101 | 0 | 17 | 118 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/listmonk.md | Markdown | 47 | 0 | 22 | 69 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/map.md | Markdown | 70 | 0 | 27 | 97 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mini-qr.md | Markdown | 23 | 0 | 15 | 38 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mkdocs.md | Markdown | 86 | 0 | 47 | 133 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/n8n.md | Markdown | 109 | 0 | 49 | 158 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/nocodb.md | Markdown | 108 | 0 | 55 | 163 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/postgresql.md | Markdown | 59 | 0 | 32 | 91 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/static-server.md | Markdown | 69 | 0 | 32 | 101 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/extra.css | PostCSS | 498 | 18 | 82 | 598 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/home.css | PostCSS | 866 | 54 | 153 | 1,073 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/test.md | Markdown | 5 | 0 | 6 | 11 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/mkdocs.yml | YAML | 167 | 11 | 11 | 189 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/404.html | HTML | 251 | 1 | 437 | 689 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/ansible/index.html | HTML | 1,653 | 1 | 1,326 | 2,980 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/index.html | HTML | 549 | 1 | 1,083 | 1,633 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/vscode-ssh/index.html | HTML | 1,926 | 1 | 1,358 | 3,285 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/bundle.50899def.min.js | JavaScript | 14 | 1 | 2 | 17 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js | JavaScript | 1 | 16 | 1 | 18 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js | JavaScript | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/tinyseg.js | JavaScript | 176 | 21 | 9 | 206 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/wordcut.js | JavaScript | 4,882 | 915 | 911 | 6,708 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js | JavaScript | 38 | 3 | 2 | 43 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/admin-changemaker.lite.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/anthropics-claude-code.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/coder-code-server.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/gethomepage-homepage.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/go-gitea-gitea.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/knadh-listmonk.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/lyqht-mini-qr.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/n8n-io-n8n.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/nocodb-nocodb.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/ollama-ollama.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json | JSON | 16 | 0 | 0 | 16 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/main.7e37652d.min.css | PostCSS | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/palette.06af60db.min.css | PostCSS | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/03/blog-1/index.html | HTML | 374 | 1 | 582 | 957 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/10/2/index.html | HTML | 392 | 1 | 583 | 976 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/08/01/3/index.html | HTML | 455 | 1 | 590 | 1,046 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/archive/2025/index.html | HTML | 442 | 1 | 582 | 1,025 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/index.html | HTML | 451 | 1 | 572 | 1,024 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/index.html | HTML | 1,312 | 1 | 1,199 | 2,512 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/map/index.html | HTML | 983 | 1 | 1,182 | 2,166 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/server/index.html | HTML | 1,045 | 1 | 1,234 | 2,280 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/site/index.html | HTML | 748 | 1 | 1,138 | 1,887 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/cloudflare-config/index.html | HTML | 740 | 1 | 1,146 | 1,887 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/coder/index.html | HTML | 1,117 | 1 | 1,240 | 2,358 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/index.html | HTML | 550 | 1 | 1,083 | 1,634 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/map/index.html | HTML | 1,672 | 1 | 1,346 | 3,019 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/mkdocs/index.html | HTML | 833 | 1 | 1,151 | 1,985 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/hooks/repo_widget_hook.py | Python | 119 | 24 | 16 | 159 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/how to/canvass/index.html | HTML | 275 | 1 | 484 | 760 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/index.html | HTML | 1,872 | 11 | 259 | 2,142 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/gitea-widget.js | JavaScript | 121 | 3 | 8 | 132 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/github-widget.js | JavaScript | 144 | 10 | 14 | 168 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/home.js | JavaScript | 271 | 30 | 37 | 338 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/index.html | HTML | 549 | 1 | 1,083 | 1,633 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/map/index.html | HTML | 987 | 1 | 1,202 | 2,190 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/lander.html | HTML | 1,872 | 11 | 259 | 2,142 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/main.html | HTML | 8 | 1 | 3 | 12 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/cost-comparison/index.html | HTML | 1,300 | 1 | 766 | 2,067 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/index.html | HTML | 717 | 1 | 661 | 1,379 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/search/search_index.json | JSON | 1 | 0 | 0 | 1 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/code-server/index.html | HTML | 755 | 1 | 1,155 | 1,911 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/gitea/index.html | HTML | 737 | 1 | 1,151 | 1,889 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/homepage/index.html | HTML | 1,119 | 1 | 1,235 | 2,355 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/index.html | HTML | 769 | 1 | 1,113 | 1,883 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/listmonk/index.html | HTML | 775 | 1 | 1,159 | 1,935 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/map/index.html | HTML | 877 | 1 | 1,170 | 2,048 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mini-qr/index.html | HTML | 707 | 1 | 1,147 | 1,855 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mkdocs/index.html | HTML | 905 | 1 | 1,187 | 2,093 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/n8n/index.html | HTML | 1,057 | 1 | 1,223 | 2,281 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/nocodb/index.html | HTML | 1,040 | 1 | 1,219 | 2,260 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/postgresql/index.html | HTML | 880 | 1 | 1,190 | 2,071 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/static-server/index.html | HTML | 877 | 1 | 1,182 | 2,060 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/sitemap.xml | XML | 147 | 0 | 0 | 147 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/extra.css | PostCSS | 498 | 18 | 82 | 598 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/home.css | PostCSS | 866 | 54 | 153 | 1,073 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/test/index.html | HTML | 278 | 1 | 484 | 763 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/reset-site.sh | Shell Script | 258 | 28 | 73 | 359 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/start-production.sh | Shell Script | 487 | 77 | 98 | 662 | +| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/test.md | Markdown | 1 | 0 | 0 | 1 | +| Total | | 91,924 | 4,969 | 47,542 | 144,435 | ++---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/map/app/public/admin.html b/map/app/public/admin.html index 3914d56..2487112 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -1253,8 +1253,20 @@ - - + + + + + + + + + + + + + + diff --git a/map/app/public/js/admin-auth.js b/map/app/public/js/admin-auth.js new file mode 100644 index 0000000..b30d737 --- /dev/null +++ b/map/app/public/js/admin-auth.js @@ -0,0 +1,87 @@ +/** + * Admin Authentication Module + * Handles user authentication, session management, and admin authorization + */ + +// Check if user is authenticated as admin +async function checkAdminAuth() { + try { + const response = await fetch('/api/auth/check'); + const data = await response.json(); + + console.log('Admin auth check result:', data); + + if (!data.authenticated || !data.user?.isAdmin) { + console.log('Redirecting to login - not authenticated or not admin'); + window.location.href = '/login.html'; + return; + } + + console.log('User is authenticated as admin:', data.user); + + // Display admin info (desktop) + const adminInfoEl = document.getElementById('admin-info'); + if (adminInfoEl) { + adminInfoEl.innerHTML = ` + 👤 ${window.adminCore.escapeHtml(data.user.email)} + + `; + + // Add logout event listener + const logoutBtn = document.getElementById('logout-btn'); + if (logoutBtn) { + logoutBtn.addEventListener('click', handleLogout); + } + } + + // Display admin info (mobile) + const mobileAdminInfo = document.getElementById('mobile-admin-info'); + if (mobileAdminInfo) { + mobileAdminInfo.innerHTML = ` +
👤 ${window.adminCore.escapeHtml(data.user.email)}
+ + `; + + // Add logout listener for mobile button + const mobileLogoutBtn = document.getElementById('mobile-logout-btn'); + if (mobileLogoutBtn) { + mobileLogoutBtn.addEventListener('click', handleLogout); + } + } + + } catch (error) { + console.error('Auth check failed:', error); + window.location.href = '/login.html'; + } +} + +// Handle logout +async function handleLogout() { + if (!confirm('Are you sure you want to logout?')) { + return; + } + + try { + const response = await fetch('/api/auth/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + window.location.href = '/login.html'; + } else { + window.adminCore.showStatus('Logout failed. Please try again.', 'error'); + } + } catch (error) { + console.error('Logout error:', error); + window.adminCore.showStatus('Logout failed. Please try again.', 'error'); + } +} + +// Export authentication functions +window.adminAuth = { + checkAdminAuth, + handleLogout +}; diff --git a/map/app/public/js/admin-core.js b/map/app/public/js/admin-core.js new file mode 100644 index 0000000..a9ff84d --- /dev/null +++ b/map/app/public/js/admin-core.js @@ -0,0 +1,324 @@ +/** + * Admin Core Module + * Contains core utilities, navigation, viewport management, and initialization coordination + */ + +// Global admin state +let adminAppState = { + currentSection: 'dashboard' +}; + +// Utility function to create a local date from YYYY-MM-DD string +// This prevents timezone issues when displaying dates +function createLocalDate(dateString) { + if (!dateString) return null; + const parts = dateString.split('-'); + if (parts.length !== 3) return new Date(dateString); // fallback to original behavior + // Create date using local timezone (year, month-1, day) + return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); +} + +// A function to set viewport dimensions for admin page +function setAdminViewportDimensions() { + const doc = document.documentElement; + + // Set height and width + doc.style.setProperty('--app-height', `${window.innerHeight}px`); + doc.style.setProperty('--app-width', `${window.innerWidth}px`); + + // Handle safe area insets for devices with notches or home indicators + if (CSS.supports('padding: env(safe-area-inset-top)')) { + doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)'); + doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)'); + doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)'); + doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)'); + } else { + doc.style.setProperty('--safe-area-top', '0px'); + doc.style.setProperty('--safe-area-bottom', '0px'); + doc.style.setProperty('--safe-area-left', '0px'); + doc.style.setProperty('--safe-area-right', '0px'); + } +} + +// Add mobile menu functionality +function setupMobileMenu() { + const menuToggle = document.getElementById('mobile-menu-toggle'); + const sidebar = document.getElementById('admin-sidebar'); + const closeSidebar = document.getElementById('close-sidebar'); + const adminNavLinks = document.querySelectorAll('.admin-nav a'); + + if (menuToggle && sidebar) { + // Toggle menu + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + menuToggle.classList.toggle('active'); + document.body.classList.toggle('sidebar-open'); + }); + + // Close sidebar button + if (closeSidebar) { + closeSidebar.addEventListener('click', () => { + sidebar.classList.remove('active'); + menuToggle.classList.remove('active'); + document.body.classList.remove('sidebar-open'); + }); + } + + // Close sidebar when clicking outside + document.addEventListener('click', (e) => { + if (sidebar.classList.contains('active') && + !sidebar.contains(e.target) && + !menuToggle.contains(e.target)) { + sidebar.classList.remove('active'); + menuToggle.classList.remove('active'); + document.body.classList.remove('sidebar-open'); + } + }); + + // Close sidebar when navigation link is clicked on mobile + adminNavLinks.forEach(link => { + link.addEventListener('click', () => { + if (window.innerWidth <= 768) { + sidebar.classList.remove('active'); + menuToggle.classList.remove('active'); + document.body.classList.remove('sidebar-open'); + } + }); + }); + } +} + +// Setup navigation between admin sections +function setupNavigation() { + const navLinks = document.querySelectorAll('.admin-nav a'); + const sections = document.querySelectorAll('.admin-section'); + + navLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + + const targetId = link.getAttribute('href').substring(1); + + // Update active nav + navLinks.forEach(l => l.classList.remove('active')); + link.classList.add('active'); + + // Show target section + sections.forEach(section => { + section.style.display = section.id === targetId ? 'block' : 'none'; + }); + + // Update URL hash and app state + window.location.hash = targetId; + adminAppState.currentSection = targetId; + + // Load section-specific data + loadSectionData(targetId); + + // Close mobile menu if open + const sidebar = document.getElementById('admin-sidebar'); + if (sidebar && sidebar.classList.contains('open')) { + sidebar.classList.remove('open'); + } + }); + }); + + // Set initial active state based on current hash or default + const currentHash = window.location.hash || '#dashboard'; + const activeLink = document.querySelector(`.admin-nav a[href="${currentHash}"]`); + if (activeLink) { + activeLink.classList.add('active'); + } + + // Check for initial hash routing + const hash = window.location.hash; + if (hash) { + const sectionId = hash.substring(1); + adminAppState.currentSection = sectionId; + if (sectionId === 'shifts') { + showSection('shifts'); + } + } +} + +// Helper function to show a specific section +function showSection(sectionId) { + const sections = document.querySelectorAll('.admin-section'); + const navLinks = document.querySelectorAll('.admin-nav a'); + + // Hide all sections + sections.forEach(section => { + section.style.display = section.id === sectionId ? 'block' : 'none'; + }); + + // Update active nav + navLinks.forEach(link => { + const linkTarget = link.getAttribute('href').substring(1); + link.classList.toggle('active', linkTarget === sectionId); + }); + + // Update app state + adminAppState.currentSection = sectionId; + + // Load section-specific data + loadSectionData(sectionId); +} + +// Load section-specific data based on current section +function loadSectionData(sectionId) { + switch(sectionId) { + case 'walk-sheet': + if (typeof checkAndLoadWalkSheetConfig === 'function') { + checkAndLoadWalkSheetConfig(); + } + break; + case 'dashboard': + if (typeof loadDashboardData === 'function') { + loadDashboardData(); + } + break; + case 'shifts': + if (typeof loadAdminShifts === 'function') { + console.log('Loading shifts for admin panel...'); + loadAdminShifts(); + } + break; + case 'users': + if (typeof loadUsers === 'function') { + loadUsers(); + } + break; + case 'convert-data': + // Initialize data convert event listeners when section is shown + setTimeout(() => { + if (typeof window.setupDataConvertEventListeners === 'function') { + console.log('Setting up data convert event listeners...'); + window.setupDataConvertEventListeners(); + } else { + console.error('setupDataConvertEventListeners not found'); + } + }, 100); + break; + case 'cuts': + // Initialize admin cuts manager when section is shown + setTimeout(() => { + if (typeof window.adminCutsManager === 'object' && window.adminCutsManager.initialize) { + if (!window.adminCutsManager.isInitialized) { + console.log('Initializing admin cuts manager from showSection...'); + window.adminCutsManager.initialize().catch(error => { + console.error('Failed to initialize cuts manager:', error); + }); + } else { + console.log('Admin cuts manager already initialized'); + } + } else { + console.error('adminCutsManager not found in showSection'); + } + }, 100); + break; + } +} + +// Show status message +function showStatus(message, type = 'info') { + let container = document.getElementById('status-container'); + + // Create container if it doesn't exist + if (!container) { + container = document.createElement('div'); + container.id = 'status-container'; + container.className = 'status-container'; + // Ensure proper z-index even if CSS hasn't loaded + container.style.zIndex = '12300'; + document.body.appendChild(container); + } + + const messageDiv = document.createElement('div'); + messageDiv.className = `status-message ${type}`; + messageDiv.textContent = message; + + // Add click to dismiss functionality + messageDiv.addEventListener('click', () => { + messageDiv.remove(); + }); + + // Add a small close button for better UX + const closeBtn = document.createElement('span'); + closeBtn.innerHTML = ' ×'; + closeBtn.style.float = 'right'; + closeBtn.style.fontWeight = 'bold'; + closeBtn.style.marginLeft = '10px'; + closeBtn.style.cursor = 'pointer'; + closeBtn.setAttribute('title', 'Click to dismiss'); + messageDiv.appendChild(closeBtn); + + container.appendChild(messageDiv); + + // Auto-remove after 5 seconds + setTimeout(() => { + if (messageDiv.parentNode) { + messageDiv.remove(); + } + }, 5000); +} + +// Escape HTML +function escapeHtml(text) { + if (text === null || text === undefined) { + return ''; + } + const div = document.createElement('div'); + div.textContent = String(text); + return div.innerHTML; +} + +// Debounce function for input events +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Initialize the admin core when DOM is loaded +function initializeAdminCore() { + // Set initial viewport dimensions and listen for resize events + setAdminViewportDimensions(); + window.addEventListener('resize', setAdminViewportDimensions); + window.addEventListener('orientationchange', () => { + // Add a small delay for orientation change to complete + setTimeout(setAdminViewportDimensions, 100); + }); + + setupNavigation(); + setupMobileMenu(); + + // Check if URL has a hash to show specific section + const hash = window.location.hash; + if (hash === '#walk-sheet') { + showSection('walk-sheet'); + } else if (hash === '#convert-data') { + showSection('convert-data'); + } else if (hash === '#cuts') { + showSection('cuts'); + } else { + // Default to dashboard + showSection('dashboard'); + } +} + +// Export functions for use by other modules +window.adminCore = { + showSection, + showStatus, + escapeHtml, + debounce, + createLocalDate, + initializeAdminCore, + adminAppState +}; diff --git a/map/app/public/js/admin-email.js b/map/app/public/js/admin-email.js new file mode 100644 index 0000000..4af1d69 --- /dev/null +++ b/map/app/public/js/admin-email.js @@ -0,0 +1,394 @@ +/** + * Admin Email Broadcasting Module + * Handles email composition, broadcasting to all users, and progress tracking + */ + +// Email state +let allUsersData = []; + +// Email All Users Functions +async function showEmailUsersModal() { + // Load current users data + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (data.success && data.users) { + allUsersData = data.users; + + // Update recipients count + const recipientsCount = document.getElementById('recipients-count'); + if (recipientsCount) { + recipientsCount.textContent = `${allUsersData.length}`; + } + } + } catch (error) { + console.error('Error loading users for email:', error); + window.adminCore.showStatus('Failed to load user data', 'error'); + return; + } + + // Show modal + const modal = document.getElementById('email-users-modal'); + if (modal) { + modal.style.display = 'flex'; + + // Clear previous content + const subjectInput = document.getElementById('email-subject'); + const contentEditor = document.getElementById('email-content'); + const previewCheckbox = document.getElementById('show-preview'); + const previewDiv = document.getElementById('email-preview'); + + if (subjectInput) subjectInput.value = ''; + if (contentEditor) contentEditor.innerHTML = ''; + if (previewCheckbox) previewCheckbox.checked = false; + if (previewDiv) previewDiv.style.display = 'none'; + } +} + +function closeEmailUsersModal() { + const modal = document.getElementById('email-users-modal'); + if (modal) { + modal.style.display = 'none'; + } +} + +function setupRichTextEditor() { + const toolbar = document.querySelector('.rich-text-toolbar'); + const editor = document.getElementById('email-content'); + + if (!toolbar || !editor) return; + + // Handle toolbar button clicks + toolbar.addEventListener('click', (e) => { + if (e.target.classList.contains('toolbar-btn')) { + e.preventDefault(); + const command = e.target.getAttribute('data-command'); + + if (command === 'createLink') { + const url = prompt('Enter the URL:'); + if (url) { + document.execCommand(command, false, url); + } + } else { + document.execCommand(command, false, null); + } + + // Update preview if visible + updateEmailPreview(); + } + }); + + // Update preview on content change + editor.addEventListener('input', updateEmailPreview); + + // Handle preview toggle + const showPreviewCheckbox = document.getElementById('show-preview'); + if (showPreviewCheckbox) { + showPreviewCheckbox.addEventListener('change', togglePreview); + } + + // Update preview when subject changes + const subjectInput = document.getElementById('email-subject'); + if (subjectInput) { + subjectInput.addEventListener('input', updateEmailPreview); + } +} + +function togglePreview() { + const preview = document.getElementById('email-preview'); + const checkbox = document.getElementById('show-preview'); + + if (preview && checkbox) { + if (checkbox.checked) { + preview.style.display = 'block'; + updateEmailPreview(); + } else { + preview.style.display = 'none'; + } + } +} + +function updateEmailPreview() { + const previewSubject = document.getElementById('preview-subject'); + const previewBody = document.getElementById('preview-body'); + const subjectInput = document.getElementById('email-subject'); + const contentEditor = document.getElementById('email-content'); + + if (previewSubject && subjectInput) { + previewSubject.textContent = subjectInput.value || 'Your subject will appear here'; + } + + if (previewBody && contentEditor) { + const content = contentEditor.innerHTML || 'Your message will appear here'; + previewBody.innerHTML = content; + } +} + +async function sendEmailToAllUsers(e) { + e.preventDefault(); + + const subjectInput = document.getElementById('email-subject'); + const contentEditor = document.getElementById('email-content'); + + const subject = subjectInput?.value.trim(); + const content = contentEditor?.innerHTML.trim(); + + if (!subject) { + window.adminCore.showStatus('Please enter an email subject', 'error'); + return; + } + + if (!content || content === '
' || content === '') { + window.adminCore.showStatus('Please enter email content', 'error'); + return; + } + + if (allUsersData.length === 0) { + window.adminCore.showStatus('No users found to email', 'error'); + return; + } + + const confirmMessage = `Send this email to all ${allUsersData.length} users?`; + if (!confirm(confirmMessage)) { + return; + } + + // Initialize progress tracking + initializeEmailProgress(allUsersData.length); + + try { + const response = await fetch('/api/users/email-all', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subject: subject, + content: content + }) + }); + + const data = await response.json(); + + if (data.success) { + // Display detailed results + updateEmailProgress(data.results); + window.adminCore.showStatus(data.message, 'success'); + console.log('Email results:', data.results); + } else { + showEmailError(data.error || 'Failed to send emails'); + if (data.details) { + console.error('Failed email details:', data.details); + } + } + } catch (error) { + console.error('Error sending emails to all users:', error); + showEmailError('Failed to send emails - Network error'); + } +} + +// Initialize email progress display +function initializeEmailProgress(totalCount) { + const progressContainer = document.getElementById('email-progress-container'); + const statusList = document.getElementById('email-status-list'); + const pendingCountEl = document.getElementById('pending-count'); + const successCountEl = document.getElementById('success-count'); + const errorCountEl = document.getElementById('error-count'); + const progressBar = document.getElementById('email-progress-bar'); + const progressText = document.getElementById('progress-text'); + const closeBtn = document.getElementById('close-progress-btn'); + + if (!progressContainer) return; + + // Show progress container + progressContainer.classList.add('show'); + + // Reset counters + if (pendingCountEl) pendingCountEl.textContent = totalCount; + if (successCountEl) successCountEl.textContent = '0'; + if (errorCountEl) errorCountEl.textContent = '0'; + + // Reset progress bar + if (progressBar) { + progressBar.style.width = '0%'; + progressBar.classList.remove('complete', 'error'); + } + if (progressText) progressText.textContent = '0%'; + + // Clear status list + if (statusList) statusList.innerHTML = ''; + + // Hide close button initially + if (closeBtn) closeBtn.style.display = 'none'; + + // Add status items for each user + allUsersData.forEach(user => { + if (statusList) { + const statusItem = document.createElement('div'); + statusItem.className = 'email-status-item'; + statusItem.innerHTML = ` +
${user.Name || user.Email}
+
+
+ Sending... +
+ `; + statusList.appendChild(statusItem); + } + }); +} + +// Update progress with results +function updateEmailProgress(results) { + const statusList = document.getElementById('email-status-list'); + const pendingCountEl = document.getElementById('pending-count'); + const successCountEl = document.getElementById('success-count'); + const errorCountEl = document.getElementById('error-count'); + const progressBar = document.getElementById('email-progress-bar'); + const progressText = document.getElementById('progress-text'); + const closeBtn = document.getElementById('close-progress-btn'); + + const successful = results.successful || []; + const failed = results.failed || []; + const total = results.total || (successful.length + failed.length); + + // Update counters + if (successCountEl) successCountEl.textContent = successful.length; + if (errorCountEl) errorCountEl.textContent = failed.length; + if (pendingCountEl) pendingCountEl.textContent = '0'; + + // Update progress bar + const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); + if (progressBar && progressText) { + progressBar.style.width = percentage + '%'; + progressText.textContent = percentage + '%'; + + if (failed.length > 0) { + progressBar.classList.add('error'); + } else { + progressBar.classList.add('complete'); + } + } + + // Update individual status items + if (statusList) { + const statusItems = statusList.children; + + // Update successful emails + successful.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✓ Sent + `; + } + }); + + // Update failed emails + failed.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✗ Failed + `; + } + }); + } + + // Show close button + if (closeBtn) { + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + const progressContainer = document.getElementById('email-progress-container'); + if (progressContainer) { + progressContainer.classList.remove('show'); + } + closeEmailUsersModal(); + }; + } +} + +// Show email error +function showEmailError(message) { + const progressContainer = document.getElementById('email-progress-container'); + const progressBar = document.getElementById('email-progress-bar'); + const progressText = document.getElementById('progress-text'); + const closeBtn = document.getElementById('close-progress-btn'); + + // Show progress container if not visible + if (progressContainer) { + progressContainer.classList.add('show'); + } + + // Update progress bar to show error + if (progressBar && progressText) { + progressBar.style.width = '100%'; + progressBar.classList.add('error'); + progressText.textContent = 'Error'; + } + + // Show close button + if (closeBtn) { + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + if (progressContainer) { + progressContainer.classList.remove('show'); + } + }; + } + + window.adminCore.showStatus(message, 'error'); +} + +// Setup email modal event listeners +function setupEmailModalEventListeners() { + // Email all users functionality + const closeEmailModalBtn = document.getElementById('close-email-modal'); + const cancelEmailBtn = document.getElementById('cancel-email-btn'); + const emailUsersForm = document.getElementById('email-users-form'); + const emailModal = document.getElementById('email-users-modal'); + + if (closeEmailModalBtn) { + closeEmailModalBtn.addEventListener('click', closeEmailUsersModal); + } + + if (cancelEmailBtn) { + cancelEmailBtn.addEventListener('click', closeEmailUsersModal); + } + + if (emailUsersForm) { + emailUsersForm.addEventListener('submit', sendEmailToAllUsers); + } + + // Close modal when clicking outside + if (emailModal) { + emailModal.addEventListener('click', function(e) { + if (e.target === emailModal) { + closeEmailUsersModal(); + } + }); + } + + // Setup rich text editor functionality + setupRichTextEditor(); +} + +// Export email broadcasting functions +window.adminEmail = { + showEmailUsersModal, + closeEmailUsersModal, + setupRichTextEditor, + togglePreview, + updateEmailPreview, + sendEmailToAllUsers, + setupEmailModalEventListeners, + getAllUsersData: () => allUsersData, + setAllUsersData: (data) => { allUsersData = data; } +}; diff --git a/map/app/public/js/admin-integration.js b/map/app/public/js/admin-integration.js new file mode 100644 index 0000000..3dc48d5 --- /dev/null +++ b/map/app/public/js/admin-integration.js @@ -0,0 +1,189 @@ +/** + * Admin Integration Module + * Handles external service integrations (NocoDB and Listmonk) + */ + +// Initialize NocoDB links in admin panel +async function initializeNocodbLinks() { + console.log('Starting NocoDB links initialization...'); + + try { + // Since we're in the admin panel, the user is already verified as admin + // by the requireAdmin middleware. Let's get the URLs from the server directly. + console.log('Fetching NocoDB URLs for admin panel...'); + const configResponse = await fetch('/api/admin/nocodb-urls'); + + if (!configResponse.ok) { + throw new Error(`NocoDB URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); + } + + const config = await configResponse.json(); + console.log('NocoDB URLs received:', config); + + if (config.success && config.nocodbUrls) { + console.log('Setting up NocoDB links with URLs:', config.nocodbUrls); + + // Set up admin dashboard NocoDB links + setAdminNocodbLink('admin-nocodb-view-link', config.nocodbUrls.viewUrl); + setAdminNocodbLink('admin-nocodb-login-link', config.nocodbUrls.loginSheet); + setAdminNocodbLink('admin-nocodb-settings-link', config.nocodbUrls.settingsSheet); + setAdminNocodbLink('admin-nocodb-shifts-link', config.nocodbUrls.shiftsSheet); + setAdminNocodbLink('admin-nocodb-signups-link', config.nocodbUrls.shiftSignupsSheet); + + console.log('NocoDB links initialized in admin panel'); + } else { + console.warn('No NocoDB URLs found in admin config response'); + // Hide the NocoDB section if no URLs are available + const nocodbSection = document.getElementById('nocodb-links'); + const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); + if (nocodbSection) { + nocodbSection.style.display = 'none'; + console.log('Hidden NocoDB section'); + } + if (nocodbNav) { + nocodbNav.style.display = 'none'; + console.log('Hidden NocoDB nav link'); + } + } + } catch (error) { + console.error('Error initializing NocoDB links in admin panel:', error); + // Hide the NocoDB section on error + const nocodbSection = document.getElementById('nocodb-links'); + const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); + if (nocodbSection) { + nocodbSection.style.display = 'none'; + console.log('Hidden NocoDB section due to error'); + } + if (nocodbNav) { + nocodbNav.style.display = 'none'; + console.log('Hidden NocoDB nav link due to error'); + } + } +} + +// Helper function to set admin NocoDB link href +function setAdminNocodbLink(elementId, url) { + console.log(`Setting up NocoDB link: ${elementId} = ${url}`); + const element = document.getElementById(elementId); + + if (element && url) { + element.href = url; + element.style.display = 'inline-flex'; + // Remove any disabled state + element.classList.remove('btn-disabled'); + element.removeAttribute('disabled'); + console.log(`✓ Successfully set up ${elementId}`); + } else if (element) { + element.style.display = 'none'; + // Add disabled state if no URL + element.classList.add('btn-disabled'); + element.setAttribute('disabled', 'disabled'); + element.href = '#'; + console.log(`⚠ Disabled ${elementId} - no URL provided`); + } else { + console.error(`✗ Element not found: ${elementId}`); + } +} + +// Initialize Listmonk links in admin panel +async function initializeListmonkLinks() { + console.log('Starting Listmonk links initialization...'); + + try { + // Since we're in the admin panel, the user is already verified as admin + // by the requireAdmin middleware. Let's get the URLs from the server directly. + console.log('Fetching Listmonk URLs for admin panel...'); + const configResponse = await fetch('/api/admin/listmonk-urls'); + + if (!configResponse.ok) { + throw new Error(`Listmonk URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); + } + + const config = await configResponse.json(); + console.log('Listmonk URLs received:', config); + + if (config.success && config.listmonkUrls) { + console.log('Setting up Listmonk links with URLs:', config.listmonkUrls); + + // Set up admin dashboard Listmonk links + setAdminListmonkLink('admin-listmonk-admin-link', config.listmonkUrls.adminUrl); + setAdminListmonkLink('admin-listmonk-lists-link', config.listmonkUrls.listsUrl); + setAdminListmonkLink('admin-listmonk-campaigns-link', config.listmonkUrls.campaignsUrl); + setAdminListmonkLink('admin-listmonk-subscribers-link', config.listmonkUrls.subscribersUrl); + setAdminListmonkLink('admin-listmonk-settings-link', config.listmonkUrls.settingsUrl); + + console.log('Listmonk links initialized in admin panel'); + } else { + console.warn('No Listmonk URLs found in admin config response'); + // Hide the Listmonk section if no URLs are available + const listmonkSection = document.getElementById('listmonk-links'); + const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); + if (listmonkSection) { + listmonkSection.style.display = 'none'; + console.log('Hidden Listmonk section'); + } + if (listmonkNav) { + listmonkNav.style.display = 'none'; + console.log('Hidden Listmonk nav link'); + } + } + } catch (error) { + console.error('Error initializing Listmonk links in admin panel:', error); + // Hide the Listmonk section on error + const listmonkSection = document.getElementById('listmonk-links'); + const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); + if (listmonkSection) { + listmonkSection.style.display = 'none'; + console.log('Hidden Listmonk section due to error'); + } + if (listmonkNav) { + listmonkNav.style.display = 'none'; + console.log('Hidden Listmonk nav link due to error'); + } + } +} + +// Helper function to set admin Listmonk link href +function setAdminListmonkLink(elementId, url) { + console.log(`Setting up Listmonk link: ${elementId} = ${url}`); + const element = document.getElementById(elementId); + + if (element && url) { + element.href = url; + element.style.display = 'inline-flex'; + // Remove any disabled state + element.classList.remove('btn-disabled'); + element.removeAttribute('disabled'); + console.log(`✓ Successfully set up ${elementId}`); + } else if (element) { + element.style.display = 'none'; + // Add disabled state if no URL + element.classList.add('btn-disabled'); + element.setAttribute('disabled', 'disabled'); + element.href = '#'; + console.log(`⚠ Disabled ${elementId} - no URL provided`); + } else { + console.error(`✗ Element not found: ${elementId}`); + } +} + +// Initialize all integrations +async function initializeAllIntegrations() { + try { + // Initialize both integrations with a small delay to ensure DOM is ready + await initializeNocodbLinks(); + await initializeListmonkLinks(); + console.log('All integrations initialized successfully'); + } catch (error) { + console.error('Error initializing integrations:', error); + } +} + +// Export integration functions +window.adminIntegration = { + initializeNocodbLinks, + initializeListmonkLinks, + initializeAllIntegrations, + setAdminNocodbLink, + setAdminListmonkLink +}; diff --git a/map/app/public/js/admin-map.js b/map/app/public/js/admin-map.js new file mode 100644 index 0000000..4978bde --- /dev/null +++ b/map/app/public/js/admin-map.js @@ -0,0 +1,249 @@ +/** + * Admin Map Management Module + * Handles map initialization, start location management, and coordinate operations + */ + +// Map state +let adminMap = null; +let startMarker = null; + +// Initialize the admin map +function initializeAdminMap() { + adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + maxZoom: 19, + minZoom: 2 + }).addTo(adminMap); + + // Add crosshair to center of map + const crosshairIcon = L.divIcon({ + className: 'crosshair', + iconSize: [20, 20], + html: '
' + }); + + const crosshair = L.marker(adminMap.getCenter(), { + icon: crosshairIcon, + interactive: false, + zIndexOffset: 1000 + }).addTo(adminMap); + + // Update crosshair position when map moves + adminMap.on('move', function() { + crosshair.setLatLng(adminMap.getCenter()); + }); + + // Add click handler to set location + adminMap.on('click', handleMapClick); + + // Update coordinates when map moves + adminMap.on('moveend', updateCoordinatesFromMap); +} + +// Load current start location +async function loadCurrentStartLocation() { + try { + const response = await fetch('/api/admin/start-location'); + const data = await response.json(); + + if (data.success) { + const { latitude, longitude, zoom } = data.location; + + // Update form fields + const latInput = document.getElementById('start-lat'); + const lngInput = document.getElementById('start-lng'); + const zoomInput = document.getElementById('start-zoom'); + + if (latInput) latInput.value = latitude; + if (lngInput) lngInput.value = longitude; + if (zoomInput) zoomInput.value = zoom; + + // Update map + if (adminMap) { + adminMap.setView([latitude, longitude], zoom); + updateStartMarker(latitude, longitude); + } + + // Show source info + if (data.source) { + const sourceText = data.source === 'database' ? 'Loaded from database' : + data.source === 'environment' ? 'Using environment defaults' : + 'Using system defaults'; + window.adminCore.showStatus(sourceText, 'info'); + } + } + + } catch (error) { + console.error('Failed to load start location:', error); + window.adminCore.showStatus('Failed to load current start location', 'error'); + } +} + +// Handle map click +function handleMapClick(e) { + const { lat, lng } = e.latlng; + + const latInput = document.getElementById('start-lat'); + const lngInput = document.getElementById('start-lng'); + + if (latInput) latInput.value = lat.toFixed(6); + if (lngInput) lngInput.value = lng.toFixed(6); + + updateStartMarker(lat, lng); +} + +// Update marker position +function updateStartMarker(lat, lng) { + if (startMarker) { + startMarker.setLatLng([lat, lng]); + } else { + startMarker = L.marker([lat, lng], { + draggable: true, + title: 'Start Location' + }).addTo(adminMap); + + // Update coordinates when marker is dragged + startMarker.on('dragend', (e) => { + const position = e.target.getLatLng(); + const latInput = document.getElementById('start-lat'); + const lngInput = document.getElementById('start-lng'); + + if (latInput) latInput.value = position.lat.toFixed(6); + if (lngInput) lngInput.value = position.lng.toFixed(6); + }); + } +} + +// Update coordinates from current map view +function updateCoordinatesFromMap() { + const center = adminMap.getCenter(); + const zoom = adminMap.getZoom(); + + const zoomInput = document.getElementById('start-zoom'); + if (zoomInput) { + zoomInput.value = zoom; + } +} + +// Update map from input fields +function updateMapFromInputs() { + const latInput = document.getElementById('start-lat'); + const lngInput = document.getElementById('start-lng'); + const zoomInput = document.getElementById('start-zoom'); + + const lat = parseFloat(latInput?.value); + const lng = parseFloat(lngInput?.value); + const zoom = parseInt(zoomInput?.value); + + if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom) && adminMap) { + adminMap.setView([lat, lng], zoom); + updateStartMarker(lat, lng); + } +} + +// Save start location +async function saveStartLocation() { + const latInput = document.getElementById('start-lat'); + const lngInput = document.getElementById('start-lng'); + const zoomInput = document.getElementById('start-zoom'); + + const lat = parseFloat(latInput?.value); + const lng = parseFloat(lngInput?.value); + const zoom = parseInt(zoomInput?.value); + + // Validate + if (isNaN(lat) || isNaN(lng) || isNaN(zoom)) { + window.adminCore.showStatus('Please enter valid coordinates and zoom level', 'error'); + return; + } + + if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { + window.adminCore.showStatus('Coordinates out of valid range', 'error'); + return; + } + + if (zoom < 2 || zoom > 19) { + window.adminCore.showStatus('Zoom level must be between 2 and 19', 'error'); + return; + } + + try { + const response = await fetch('/api/admin/start-location', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + latitude: lat, + longitude: lng, + zoom: zoom + }) + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus('Start location saved successfully!', 'success'); + } else { + throw new Error(data.error || 'Failed to save'); + } + + } catch (error) { + console.error('Save error:', error); + window.adminCore.showStatus(error.message || 'Failed to save start location', 'error'); + } +} + +// Setup map-related event listeners +function setupMapEventListeners() { + // Use current view button + const useCurrentViewBtn = document.getElementById('use-current-view'); + if (useCurrentViewBtn) { + useCurrentViewBtn.addEventListener('click', () => { + if (!adminMap) return; + + const center = adminMap.getCenter(); + const zoom = adminMap.getZoom(); + + const latInput = document.getElementById('start-lat'); + const lngInput = document.getElementById('start-lng'); + const zoomInput = document.getElementById('start-zoom'); + + if (latInput) latInput.value = center.lat.toFixed(6); + if (lngInput) lngInput.value = center.lng.toFixed(6); + if (zoomInput) zoomInput.value = zoom; + + updateStartMarker(center.lat, center.lng); + window.adminCore.showStatus('Captured current map view', 'success'); + }); + } + + // Save button + const saveLocationBtn = document.getElementById('save-start-location'); + if (saveLocationBtn) { + saveLocationBtn.addEventListener('click', saveStartLocation); + } + + // Coordinate input changes + const startLatInput = document.getElementById('start-lat'); + const startLngInput = document.getElementById('start-lng'); + const startZoomInput = document.getElementById('start-zoom'); + + if (startLatInput) startLatInput.addEventListener('change', updateMapFromInputs); + if (startLngInput) startLngInput.addEventListener('change', updateMapFromInputs); + if (startZoomInput) startZoomInput.addEventListener('change', updateMapFromInputs); +} + +// Export map management functions +window.adminMap = { + initializeAdminMap, + loadCurrentStartLocation, + setupMapEventListeners, + saveStartLocation, + updateMapFromInputs, + handleMapClick, + updateStartMarker, + getAdminMap: () => adminMap +}; diff --git a/map/app/public/js/admin-new.js b/map/app/public/js/admin-new.js new file mode 100644 index 0000000..6af5838 --- /dev/null +++ b/map/app/public/js/admin-new.js @@ -0,0 +1,143 @@ +/** + * Main Admin Panel Coordinator + * This refactored admin.js coordinates all admin modules while maintaining functionality + * Modules: core, auth, map, walksheet, shifts, shift-volunteers, users, email, integration + */ + +/** + * Main Event Listener Setup + * Coordinates event listeners across all modules + */ +function setupAllEventListeners() { + // Setup authentication listeners + if (window.adminAuth && typeof window.adminAuth.setupAuthEventListeners === 'function') { + window.adminAuth.setupAuthEventListeners(); + } + + // Setup map listeners + if (window.adminMap && typeof window.adminMap.setupMapEventListeners === 'function') { + window.adminMap.setupMapEventListeners(); + } + + // Setup walk sheet listeners + if (window.adminWalkSheet && typeof window.adminWalkSheet.setupWalkSheetEventListeners === 'function') { + window.adminWalkSheet.setupWalkSheetEventListeners(); + } + + // Setup shift listeners + if (window.adminShifts && typeof window.adminShifts.setupShiftEventListeners === 'function') { + window.adminShifts.setupShiftEventListeners(); + } + + // Setup shift volunteer listeners + if (window.adminShiftVolunteers && typeof window.adminShiftVolunteers.setupShiftVolunteerEventListeners === 'function') { + window.adminShiftVolunteers.setupShiftVolunteerEventListeners(); + } + + // Setup user listeners + if (window.adminUsers && typeof window.adminUsers.setupUserEventListeners === 'function') { + window.adminUsers.setupUserEventListeners(); + } + + // Setup email listeners + if (window.adminEmail && typeof window.adminEmail.setupEmailEventListeners === 'function') { + window.adminEmail.setupEmailEventListeners(); + } +} + +// Main admin initialization when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize core functionality + if (window.adminCore && typeof window.adminCore.initializeAdminCore === 'function') { + window.adminCore.initializeAdminCore(); + } + + // Initialize authentication + if (window.adminAuth && typeof window.adminAuth.checkAdminAuth === 'function') { + window.adminAuth.checkAdminAuth(); + } + + // Initialize admin map + if (window.adminMap && typeof window.adminMap.initializeAdminMap === 'function') { + window.adminMap.initializeAdminMap(); + } + + // Load current start location + if (window.adminMap && typeof window.adminMap.loadCurrentStartLocation === 'function') { + window.adminMap.loadCurrentStartLocation(); + } + + // Setup all event listeners + setupAllEventListeners(); + + // Initialize integrations with a small delay to ensure DOM is ready + setTimeout(() => { + if (window.adminWalkSheet && typeof window.adminWalkSheet.loadWalkSheetConfig === 'function') { + window.adminWalkSheet.loadWalkSheetConfig(); + } + if (window.adminIntegration && typeof window.adminIntegration.initializeAllIntegrations === 'function') { + window.adminIntegration.initializeAllIntegrations(); + } + }, 100); + + // Check if URL has a hash to show specific section + const hash = window.location.hash; + if (hash === '#walk-sheet') { + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('walk-sheet'); + } + if (window.adminWalkSheet && typeof window.adminWalkSheet.checkAndLoadWalkSheetConfig === 'function') { + window.adminWalkSheet.checkAndLoadWalkSheetConfig(); + } + } else if (hash === '#convert-data') { + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('convert-data'); + } + } else if (hash === '#cuts') { + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('cuts'); + } + } else { + // Default to dashboard + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('dashboard'); + } + // Load dashboard data on initial page load + if (typeof loadDashboardData === 'function') { + loadDashboardData(); + } + } +}); + +/** + * Dashboard Functions + * Loads dashboard data and displays summary statistics + */ +async function loadDashboardData() { + try { + const response = await fetch('/api/admin/dashboard'); + const data = await response.json(); + + if (data.success) { + document.getElementById('total-users').textContent = data.stats.totalUsers; + document.getElementById('total-shifts').textContent = data.stats.totalShifts; + document.getElementById('total-signups').textContent = data.stats.totalSignups; + document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers; + } + } catch (error) { + console.error('Failed to load dashboard data:', error); + } +} + +/** + * Legacy function redirects for backward compatibility + * These ensure existing functionality continues to work + */ +window.loadDashboardData = loadDashboardData; + +// Export dashboard function for module coordination +if (typeof window.adminDashboard === 'undefined') { + window.adminDashboard = { + loadDashboardData: loadDashboardData + }; +} diff --git a/map/app/public/js/admin-old.js b/map/app/public/js/admin-old.js new file mode 100644 index 0000000..26fb532 --- /dev/null +++ b/map/app/public/js/admin-old.js @@ -0,0 +1,1791 @@ +/** + * Main Admin Panel Coordinator + * This refactored admin.js coordinates all admin modules while maintaining functionality + * Modules: core, auth, map, walksheet, shifts, shift-volunteers, users, email, integration + */ + +/** + * Main Event Listener Setup + * Coordinates event listeners across all modules + */ +function setupAllEventListeners() { + // Setup authentication listeners + if (window.adminAuth && typeof window.adminAuth.setupAuthEventListeners === 'function') { + window.adminAuth.setupAuthEventListeners(); + } + + // Setup map listeners + if (window.adminMap && typeof window.adminMap.setupMapEventListeners === 'function') { + window.adminMap.setupMapEventListeners(); + } + + // Setup walk sheet listeners + if (window.adminWalkSheet && typeof window.adminWalkSheet.setupWalkSheetEventListeners === 'function') { + window.adminWalkSheet.setupWalkSheetEventListeners(); + } + + // Setup shift listeners + if (window.adminShifts && typeof window.adminShifts.setupShiftEventListeners === 'function') { + window.adminShifts.setupShiftEventListeners(); + } + + // Setup shift volunteer listeners + if (window.adminShiftVolunteers && typeof window.adminShiftVolunteers.setupShiftVolunteerEventListeners === 'function') { + window.adminShiftVolunteers.setupShiftVolunteerEventListeners(); + } + + // Setup user listeners + if (window.adminUsers && typeof window.adminUsers.setupUserEventListeners === 'function') { + window.adminUsers.setupUserEventListeners(); + } + + // Setup email listeners + if (window.adminEmail && typeof window.adminEmail.setupEmailEventListeners === 'function') { + window.adminEmail.setupEmailEventListeners(); + } +} + +// Main admin initialization when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize core functionality + if (window.adminCore && typeof window.adminCore.initializeAdminCore === 'function') { + window.adminCore.initializeAdminCore(); + } + + // Initialize authentication + if (window.adminAuth && typeof window.adminAuth.checkAdminAuth === 'function') { + window.adminAuth.checkAdminAuth(); + } + + // Initialize admin map + if (window.adminMap && typeof window.adminMap.initializeAdminMap === 'function') { + window.adminMap.initializeAdminMap(); + } + + // Load current start location + if (window.adminMap && typeof window.adminMap.loadCurrentStartLocation === 'function') { + window.adminMap.loadCurrentStartLocation(); + } + + // Setup all event listeners + setupAllEventListeners(); + + // Initialize integrations with a small delay to ensure DOM is ready + setTimeout(() => { + if (window.adminWalkSheet && typeof window.adminWalkSheet.loadWalkSheetConfig === 'function') { + window.adminWalkSheet.loadWalkSheetConfig(); + } + if (window.adminIntegration && typeof window.adminIntegration.initializeAllIntegrations === 'function') { + window.adminIntegration.initializeAllIntegrations(); + } + }, 100); + + // Check if URL has a hash to show specific section + const hash = window.location.hash; + if (hash === '#walk-sheet') { + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('walk-sheet'); + } + if (window.adminWalkSheet && typeof window.adminWalkSheet.checkAndLoadWalkSheetConfig === 'function') { + window.adminWalkSheet.checkAndLoadWalkSheetConfig(); + } + } else if (hash === '#convert-data') { + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('convert-data'); + } + } else if (hash === '#cuts') { + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('cuts'); + } + } else { + // Default to dashboard + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('dashboard'); + } + // Load dashboard data on initial page load + if (typeof loadDashboardData === 'function') { + loadDashboardData(); + } + } +}); + +/** + * Dashboard Functions + * Loads dashboard data and displays summary statistics + */ +async function loadDashboardData() { + try { + const response = await fetch('/api/admin/dashboard'); + const data = await response.json(); + + if (data.success) { + document.getElementById('total-users').textContent = data.stats.totalUsers; + document.getElementById('total-shifts').textContent = data.stats.totalShifts; + document.getElementById('total-signups').textContent = data.stats.totalSignups; + document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers; + } + } catch (error) { + console.error('Failed to load dashboard data:', error); + } +} + +/** + * Legacy function redirects for backward compatibility + * These ensure existing functionality continues to work + */ +window.loadDashboardData = loadDashboardData; + +// Export dashboard function for module coordination +if (typeof window.adminDashboard === 'undefined') { + window.adminDashboard = { + loadDashboardData: loadDashboardData + }; +} + +// Add shift management functions +async function loadAdminShifts() { + const list = document.getElementById('admin-shifts-list'); + if (list) { + list.innerHTML = '

Loading shifts...

'; + } + + try { + console.log('Loading admin shifts...'); + const response = await fetch('/api/shifts/admin'); + const data = await response.json(); + + if (data.success) { + console.log('Successfully loaded', data.shifts.length, 'shifts'); + displayAdminShifts(data.shifts); + } else { + console.error('Failed to load shifts:', data.error); + if (list) { + list.innerHTML = '

Failed to load shifts

'; + } + showStatus('Failed to load shifts', 'error'); + } + } catch (error) { + console.error('Error loading admin shifts:', error); + if (list) { + list.innerHTML = '

Error loading shifts

'; + } + showStatus('Failed to load shifts', 'error'); + } +} + +function displayAdminShifts(shifts) { + const list = document.getElementById('admin-shifts-list'); + + if (!list) { + console.error('Admin shifts list element not found'); + return; + } + + if (shifts.length === 0) { + list.innerHTML = '

No shifts created yet.

'; + return; + } + + list.innerHTML = shifts.map(shift => { + const shiftDate = createLocalDate(shift.Date); + const signupCount = shift.signups ? shift.signups.length : 0; + const isPublic = shift['Is Public'] !== false; + + console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []); + + return ` +
+
+

${escapeHtml(shift.Title)}

+

📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}

+

📍 ${escapeHtml(shift.Location || 'TBD')}

+

👥 ${signupCount}/${shift['Max Volunteers']} volunteers

+

${shift.Status || 'Open'}

+

${isPublic ? '🌐 Public' : '🔒 Private'}

+ ${isPublic ? ` + + ` : ''} +
+
+ + + +
+
+ `; + }).join(''); + + // Add event listeners using delegation + setupShiftActionListeners(); +} + +// Fix the setupShiftActionListeners function +function setupShiftActionListeners() { + const list = document.getElementById('admin-shifts-list'); + if (!list) return; + + // Remove any existing listeners to avoid duplicates + const newList = list.cloneNode(true); + list.parentNode.replaceChild(newList, list); + + // Get the updated reference + const updatedList = document.getElementById('admin-shifts-list'); + + updatedList.addEventListener('click', function(e) { + if (e.target.classList.contains('delete-shift-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Delete button clicked for shift:', shiftId); + deleteShift(shiftId); + } else if (e.target.classList.contains('edit-shift-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Edit button clicked for shift:', shiftId); + editShift(shiftId); + } else if (e.target.classList.contains('manage-volunteers-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + const shiftData = JSON.parse(e.target.getAttribute('data-shift').replace(/'/g, "'")); + console.log('Manage volunteers clicked for shift:', shiftId); + showShiftUserModal(shiftId, shiftData); + } else if (e.target.classList.contains('copy-shift-link-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Copy link button clicked for shift:', shiftId); + copyShiftLink(shiftId); + } else if (e.target.classList.contains('open-shift-link-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Open link button clicked for shift:', shiftId); + openShiftLink(shiftId); + } + }); +} + +// Update the deleteShift function (remove window. prefix) +async function deleteShift(shiftId) { + if (!confirm('Are you sure you want to delete this shift? All signups will be cancelled.')) { + return; + } + + try { + const response = await fetch(`/api/shifts/admin/${shiftId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + showStatus('Shift deleted successfully', 'success'); + await loadAdminShifts(); + console.log('Refreshed shifts list after deleting shift'); + } else { + showStatus(data.error || 'Failed to delete shift', 'error'); + } + } catch (error) { + console.error('Error deleting shift:', error); + showStatus('Failed to delete shift', 'error'); + } +} + +// Update editShift function (remove window. prefix) +async function editShift(shiftId) { + try { + // Find the shift in the current data + const response = await fetch('/api/shifts/admin'); + const data = await response.json(); + + if (!data.success) { + showStatus('Failed to load shift data', 'error'); + return; + } + + const shift = data.shifts.find(s => s.ID === parseInt(shiftId)); + if (!shift) { + showStatus('Shift not found', 'error'); + return; + } + + // Set editing mode + editingShiftId = shiftId; + + // Populate the form + document.getElementById('shift-title').value = shift.Title || ''; + document.getElementById('shift-description').value = shift.Description || ''; + document.getElementById('shift-date').value = shift.Date || ''; + document.getElementById('shift-start').value = shift['Start Time'] || ''; + document.getElementById('shift-end').value = shift['End Time'] || ''; + document.getElementById('shift-location').value = shift.Location || ''; + document.getElementById('shift-max-volunteers').value = shift['Max Volunteers'] || ''; + + // Update public checkbox if it exists + const publicCheckbox = document.getElementById('shift-is-public'); + if (publicCheckbox) { + publicCheckbox.checked = shift['Is Public'] !== false; + } + + // Change submit button text + const submitBtn = document.querySelector('#shift-form button[type="submit"]'); + if (submitBtn) { + submitBtn.textContent = 'Update Shift'; + } + + // Remove editing class from any previous item + document.querySelectorAll('.shift-admin-item.editing').forEach(el => { + el.classList.remove('editing'); + }); + + // Add editing class to current item + const shiftElement = document.querySelector(`[data-shift-id="${shiftId}"]`); + if (shiftElement) { + const shiftItem = shiftElement.closest('.shift-admin-item'); + if (shiftItem) { + shiftItem.classList.add('editing'); + } + } + + // Scroll to form + document.getElementById('shift-form').scrollIntoView({ behavior: 'smooth' }); + + showStatus('Editing shift: ' + shift.Title, 'info'); + + } catch (error) { + console.error('Error loading shift for edit:', error); + showStatus('Failed to load shift for editing', 'error'); + } +} + +// Add function to create shift +async function createShift(e) { + e.preventDefault(); + + const title = document.getElementById('shift-title').value; + const description = document.getElementById('shift-description').value; + const date = document.getElementById('shift-date').value; + const startTime = document.getElementById('shift-start').value; + const endTime = document.getElementById('shift-end').value; + const location = document.getElementById('shift-location').value; + const maxVolunteers = document.getElementById('shift-max-volunteers').value; + + // Get public checkbox value + const isPublic = document.getElementById('shift-is-public')?.checked ?? true; + + const shiftData = { + title, + description, + date, + startTime, + endTime, + location, + maxVolunteers: parseInt(maxVolunteers), + isPublic + }; + + try { + let response; + if (editingShiftId) { + // Update existing shift + response = await fetch(`/api/shifts/admin/${editingShiftId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(shiftData) + }); + } else { + // Create new shift + response = await fetch('/api/shifts/admin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(shiftData) + }); + } + + const data = await response.json(); + + if (data.success) { + showStatus(editingShiftId ? 'Shift updated successfully' : 'Shift created successfully', 'success'); + clearShiftForm(); + await loadAdminShifts(); + console.log('Refreshed shifts list after saving shift'); + } else { + showStatus(data.error || 'Failed to save shift', 'error'); + } + } catch (error) { + console.error('Error saving shift:', error); + showStatus('Failed to save shift', 'error'); + } +} + +function clearShiftForm() { + const form = document.getElementById('shift-form'); + if (form) { + form.reset(); + + // Reset editing state + editingShiftId = null; + + // Reset submit button text + const submitBtn = document.querySelector('#shift-form button[type="submit"]'); + if (submitBtn) { + submitBtn.textContent = 'Create Shift'; + } + + // Remove editing class from any shift items + document.querySelectorAll('.shift-admin-item.editing').forEach(el => { + el.classList.remove('editing'); + }); + + showStatus('Form cleared', 'info'); + } +} + +// User Management Functions +async function loadUsers() { + const loadingEl = document.getElementById('users-loading'); + const emptyEl = document.getElementById('users-empty'); + const tableBody = document.getElementById('users-table-body'); + + if (loadingEl) loadingEl.style.display = 'block'; + if (emptyEl) emptyEl.style.display = 'none'; + if (tableBody) tableBody.innerHTML = ''; + + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (loadingEl) loadingEl.style.display = 'none'; + + if (data.success && data.users) { + displayUsers(data.users); + } else { + throw new Error(data.error || 'Failed to load users'); + } + + } catch (error) { + console.error('Error loading users:', error); + if (loadingEl) loadingEl.style.display = 'none'; + if (emptyEl) { + emptyEl.textContent = 'Failed to load users'; + emptyEl.style.display = 'block'; + } + showStatus('Failed to load users', 'error'); + } +} + +function displayUsers(users) { + const container = document.querySelector('.users-list'); + if (!container) return; + + // Find or create the users table container, preserving the header + let usersTableContainer = container.querySelector('.users-table-container'); + if (!usersTableContainer) { + // If container doesn't exist, create it after the header + const header = container.querySelector('.users-list-header'); + usersTableContainer = document.createElement('div'); + usersTableContainer.className = 'users-table-container'; + + if (header && header.nextSibling) { + container.insertBefore(usersTableContainer, header.nextSibling); + } else if (header) { + container.appendChild(usersTableContainer); + } else { + container.appendChild(usersTableContainer); + } + } + + if (!users || users.length === 0) { + usersTableContainer.innerHTML = '

No users found.

'; + return; + } + + const tableHtml = ` +
+ + + + + + + + + + + + ${users.map(user => { + const createdDate = user.created_at || user['Created At'] || user.createdAt; + const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A'; + const isAdmin = user.admin || user.Admin || false; + const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user'); + const userId = user.Id || user.id || user.ID; + + // Handle expiration info + let expirationInfo = ''; + if (user.ExpiresAt) { + const expirationDate = new Date(user.ExpiresAt); + const now = new Date(); + const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24)); + + if (daysUntilExpiration < 0) { + expirationInfo = `Expired ${Math.abs(daysUntilExpiration)} days ago`; + } else if (daysUntilExpiration <= 3) { + expirationInfo = `Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}`; + } else { + expirationInfo = `Expires: ${expirationDate.toLocaleDateString()}`; + } + } + + return ` + + + + + + + + `; + }).join('')} + +
EmailNameRoleCreatedActions
${escapeHtml(user.email || user.Email || 'N/A')}${escapeHtml(user.name || user.Name || 'N/A')} + + ${userType.charAt(0).toUpperCase() + userType.slice(1)} + + ${expirationInfo} + ${formattedDate} + +
+
+ + `; + + usersTableContainer.innerHTML = tableHtml; + setupUserActionListeners(); +} + +function setupUserActionListeners() { + const container = document.querySelector('.users-list'); + if (!container) return; + + // Remove existing event listeners by cloning the container + const newContainer = container.cloneNode(true); + container.parentNode.replaceChild(newContainer, container); + + // Get the updated reference + const updatedContainer = document.querySelector('.users-list'); + + updatedContainer.addEventListener('click', function(e) { + if (e.target.classList.contains('delete-user-btn')) { + const userId = e.target.getAttribute('data-user-id'); + const userEmail = e.target.getAttribute('data-user-email'); + console.log('Delete button clicked for user:', userId); + deleteUser(userId, userEmail); + } else if (e.target.classList.contains('send-login-btn')) { + const userId = e.target.getAttribute('data-user-id'); + const userEmail = e.target.getAttribute('data-user-email'); + console.log('Send login details button clicked for user:', userId); + sendLoginDetailsToUser(userId, userEmail); + } else if (e.target.id === 'email-all-users-btn') { + console.log('Email All Users button clicked'); + showEmailUsersModal(); + } + }); +} + +async function deleteUser(userId, userEmail) { + if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) { + return; + } + + try { + const response = await fetch(`/api/users/${userId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + showStatus(`User "${userEmail}" deleted successfully`, 'success'); + loadUsers(); // Reload the users list + } else { + throw new Error(data.error || 'Failed to delete user'); + } + + } catch (error) { + console.error('Error deleting user:', error); + showStatus(`Failed to delete user: ${error.message}`, 'error'); + } +} + +async function sendLoginDetailsToUser(userId, userEmail) { + if (!confirm(`Send login details to "${userEmail}"?`)) { + return; + } + + try { + const response = await fetch(`/api/users/${userId}/send-login-details`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + showStatus(`Login details sent to "${userEmail}" successfully`, 'success'); + } else { + throw new Error(data.error || 'Failed to send login details'); + } + + } catch (error) { + console.error('Error sending login details:', error); + showStatus(`Failed to send login details: ${error.message}`, 'error'); + } +} + +async function createUser(e) { + e.preventDefault(); + + const email = document.getElementById('user-email').value.trim(); + const password = document.getElementById('user-password').value; + const name = document.getElementById('user-name').value.trim(); + const userType = document.getElementById('user-type').value; + const expireDays = userType === 'temp' ? + parseInt(document.getElementById('user-expire-days').value) : null; + const admin = document.getElementById('user-is-admin').checked; + + if (!email || !password) { + showStatus('Email and password are required', 'error'); + return; + } + + if (password.length < 6) { + showStatus('Password must be at least 6 characters long', 'error'); + return; + } + + if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) { + showStatus('Expiration days must be between 1 and 365 for temporary users', 'error'); + return; + } + + try { + const userData = { + email, + password, + name: name || '', + isAdmin: userType === 'admin' || admin, + userType, + expireDays + }; + + const response = await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }); + + const data = await response.json(); + + if (data.success) { + showStatus('User created successfully', 'success'); + clearUserForm(); + loadUsers(); // Reload the users list + } else { + throw new Error(data.error || 'Failed to create user'); + } + + } catch (error) { + console.error('Error creating user:', error); + showStatus(`Failed to create user: ${error.message}`, 'error'); + } +} + +function clearUserForm() { + const form = document.getElementById('create-user-form'); + if (form) { + form.reset(); + + // Reset user type to default + const userTypeSelect = document.getElementById('user-type'); + if (userTypeSelect) { + userTypeSelect.value = 'user'; + } + + // Hide expiration group + const expirationGroup = document.getElementById('expiration-group'); + if (expirationGroup) { + expirationGroup.style.display = 'none'; + } + + // Re-enable admin checkbox + const isAdminCheckbox = document.getElementById('user-is-admin'); + if (isAdminCheckbox) { + isAdminCheckbox.disabled = false; + } + + showStatus('User form cleared', 'info'); + } +} + +// Email All Users Functions +let allUsersData = []; + +async function showEmailUsersModal() { + // Load current users data + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (data.success && data.users) { + allUsersData = data.users; + + // Update recipients count + const recipientsCount = document.getElementById('recipients-count'); + if (recipientsCount) { + recipientsCount.textContent = `${allUsersData.length}`; + } + } + } catch (error) { + console.error('Error loading users for email:', error); + showStatus('Failed to load user data', 'error'); + return; + } + + // Show modal + const modal = document.getElementById('email-users-modal'); + if (modal) { + modal.style.display = 'flex'; + + // Clear previous content + document.getElementById('email-subject').value = ''; + document.getElementById('email-content').innerHTML = ''; + document.getElementById('show-preview').checked = false; + document.getElementById('email-preview').style.display = 'none'; + } +} + +function closeEmailUsersModal() { + const modal = document.getElementById('email-users-modal'); + if (modal) { + modal.style.display = 'none'; + } +} + +function setupRichTextEditor() { + const toolbar = document.querySelector('.rich-text-toolbar'); + const editor = document.getElementById('email-content'); + + if (!toolbar || !editor) return; + + // Handle toolbar button clicks + toolbar.addEventListener('click', (e) => { + if (e.target.classList.contains('toolbar-btn')) { + e.preventDefault(); + const command = e.target.getAttribute('data-command'); + + if (command === 'createLink') { + const url = prompt('Enter the URL:'); + if (url) { + document.execCommand(command, false, url); + } + } else { + document.execCommand(command, false, null); + } + + // Update preview if visible + updateEmailPreview(); + } + }); + + // Update preview on content change + editor.addEventListener('input', updateEmailPreview); + + // Handle preview toggle + const showPreviewCheckbox = document.getElementById('show-preview'); + if (showPreviewCheckbox) { + showPreviewCheckbox.addEventListener('change', togglePreview); + } + + // Update preview when subject changes + const subjectInput = document.getElementById('email-subject'); + if (subjectInput) { + subjectInput.addEventListener('input', updateEmailPreview); + } +} + +function togglePreview() { + const preview = document.getElementById('email-preview'); + const checkbox = document.getElementById('show-preview'); + + if (preview && checkbox) { + if (checkbox.checked) { + preview.style.display = 'block'; + updateEmailPreview(); + } else { + preview.style.display = 'none'; + } + } +} + +function updateEmailPreview() { + const previewSubject = document.getElementById('preview-subject'); + const previewBody = document.getElementById('preview-body'); + const subjectInput = document.getElementById('email-subject'); + const contentEditor = document.getElementById('email-content'); + + if (previewSubject && subjectInput) { + previewSubject.textContent = subjectInput.value || 'Your subject will appear here'; + } + + if (previewBody && contentEditor) { + const content = contentEditor.innerHTML || 'Your message will appear here'; + previewBody.innerHTML = content; + } +} + +async function sendEmailToAllUsers(e) { + e.preventDefault(); + + const subject = document.getElementById('email-subject').value.trim(); + const content = document.getElementById('email-content').innerHTML.trim(); + + if (!subject) { + showStatus('Please enter an email subject', 'error'); + return; + } + + if (!content || content === '
' || content === '') { + showStatus('Please enter email content', 'error'); + return; + } + + if (allUsersData.length === 0) { + showStatus('No users found to email', 'error'); + return; + } + + const confirmMessage = `Send this email to all ${allUsersData.length} users?`; + if (!confirm(confirmMessage)) { + return; + } + + // Initialize progress tracking + initializeEmailProgress(allUsersData.length); + + try { + const response = await fetch('/api/users/email-all', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subject: subject, + content: content + }) + }); + + const data = await response.json(); + + if (data.success) { + // Display detailed results + updateEmailProgress(data.results); + showStatus(data.message, 'success'); + console.log('Email results:', data.results); + } else { + showEmailError(data.error || 'Failed to send emails'); + if (data.details) { + console.error('Failed email details:', data.details); + } + } + } catch (error) { + console.error('Error sending emails to all users:', error); + showEmailError('Failed to send emails - Network error'); + } +} + +// Initialize email progress display +function initializeEmailProgress(totalCount) { + const progressContainer = document.getElementById('email-progress-container'); + const statusList = document.getElementById('email-status-list'); + const pendingCountEl = document.getElementById('pending-count'); + const successCountEl = document.getElementById('success-count'); + const errorCountEl = document.getElementById('error-count'); + const progressBar = document.getElementById('email-progress-bar'); + const progressText = document.getElementById('progress-text'); + const closeBtn = document.getElementById('close-progress-btn'); + + // Show progress container + progressContainer.classList.add('show'); + + // Reset counters + pendingCountEl.textContent = totalCount; + successCountEl.textContent = '0'; + errorCountEl.textContent = '0'; + + // Reset progress bar + progressBar.style.width = '0%'; + progressBar.classList.remove('complete', 'error'); + progressText.textContent = '0%'; + + // Clear status list + statusList.innerHTML = ''; + + // Hide close button initially + closeBtn.style.display = 'none'; + + // Add status items for each user + allUsersData.forEach(user => { + const statusItem = document.createElement('div'); + statusItem.className = 'email-status-item'; + statusItem.innerHTML = ` +
${user.Name || user.Email}
+
+
+ Sending... +
+ `; + statusList.appendChild(statusItem); + }); +} + +// Update progress with results +function updateEmailProgress(results) { + const statusList = document.getElementById('email-status-list'); + const pendingCountEl = document.getElementById('pending-count'); + const successCountEl = document.getElementById('success-count'); + const errorCountEl = document.getElementById('error-count'); + const progressBar = document.getElementById('email-progress-bar'); + const progressText = document.getElementById('progress-text'); + const closeBtn = document.getElementById('close-progress-btn'); + + const successful = results.successful || []; + const failed = results.failed || []; + const total = results.total || (successful.length + failed.length); + + // Update counters + successCountEl.textContent = successful.length; + errorCountEl.textContent = failed.length; + pendingCountEl.textContent = '0'; + + // Update progress bar + const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); + progressBar.style.width = percentage + '%'; + progressText.textContent = percentage + '%'; + + if (failed.length > 0) { + progressBar.classList.add('error'); + } else { + progressBar.classList.add('complete'); + } + + // Update individual status items + const statusItems = statusList.children; + + // Update successful emails + successful.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✓ Sent + `; + } + }); + + // Update failed emails + failed.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✗ Failed + `; + } + }); + + // Show close button + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + document.getElementById('email-progress-container').classList.remove('show'); + closeEmailUsersModal(); + }; +} + +// Show email error +function showEmailError(message) { + const progressContainer = document.getElementById('email-progress-container'); + const progressBar = document.getElementById('email-progress-bar'); + const progressText = document.getElementById('progress-text'); + const closeBtn = document.getElementById('close-progress-btn'); + + // Show progress container if not visible + progressContainer.classList.add('show'); + + // Update progress bar to show error + progressBar.style.width = '100%'; + progressBar.classList.add('error'); + progressText.textContent = 'Error'; + + // Show close button + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + progressContainer.classList.remove('show'); + }; + + showStatus(message, 'error'); +} + +// Initialize NocoDB links in admin panel +async function initializeNocodbLinks() { + console.log('Starting NocoDB links initialization...'); + + try { + // Since we're in the admin panel, the user is already verified as admin + // by the requireAdmin middleware. Let's get the URLs from the server directly. + console.log('Fetching NocoDB URLs for admin panel...'); + const configResponse = await fetch('/api/admin/nocodb-urls'); + + if (!configResponse.ok) { + throw new Error(`NocoDB URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); + } + + const config = await configResponse.json(); + console.log('NocoDB URLs received:', config); + + if (config.success && config.nocodbUrls) { + console.log('Setting up NocoDB links with URLs:', config.nocodbUrls); + + // Set up admin dashboard NocoDB links + setAdminNocodbLink('admin-nocodb-view-link', config.nocodbUrls.viewUrl); + setAdminNocodbLink('admin-nocodb-login-link', config.nocodbUrls.loginSheet); + setAdminNocodbLink('admin-nocodb-settings-link', config.nocodbUrls.settingsSheet); + setAdminNocodbLink('admin-nocodb-shifts-link', config.nocodbUrls.shiftsSheet); + setAdminNocodbLink('admin-nocodb-signups-link', config.nocodbUrls.shiftSignupsSheet); + + console.log('NocoDB links initialized in admin panel'); + } else { + console.warn('No NocoDB URLs found in admin config response'); + // Hide the NocoDB section if no URLs are available + const nocodbSection = document.getElementById('nocodb-links'); + const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); + if (nocodbSection) { + nocodbSection.style.display = 'none'; + console.log('Hidden NocoDB section'); + } + if (nocodbNav) { + nocodbNav.style.display = 'none'; + console.log('Hidden NocoDB nav link'); + } + } + } catch (error) { + console.error('Error initializing NocoDB links in admin panel:', error); + // Hide the NocoDB section on error + const nocodbSection = document.getElementById('nocodb-links'); + const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); + if (nocodbSection) { + nocodbSection.style.display = 'none'; + console.log('Hidden NocoDB section due to error'); + } + if (nocodbNav) { + nocodbNav.style.display = 'none'; + console.log('Hidden NocoDB nav link due to error'); + } + } +} + +// Helper function to set admin NocoDB link href +function setAdminNocodbLink(elementId, url) { + console.log(`Setting up NocoDB link: ${elementId} = ${url}`); + const element = document.getElementById(elementId); + + if (element && url) { + element.href = url; + element.style.display = 'inline-flex'; + // Remove any disabled state + element.classList.remove('btn-disabled'); + element.removeAttribute('disabled'); + console.log(`✓ Successfully set up ${elementId}`); + } else if (element) { + element.style.display = 'none'; + // Add disabled state if no URL + element.classList.add('btn-disabled'); + element.setAttribute('disabled', 'disabled'); + element.href = '#'; + console.log(`⚠ Disabled ${elementId} - no URL provided`); + } else { + console.error(`✗ Element not found: ${elementId}`); + } +} + +// Initialize Listmonk links in admin panel +async function initializeListmonkLinks() { + console.log('Starting Listmonk links initialization...'); + + try { + // Since we're in the admin panel, the user is already verified as admin + // by the requireAdmin middleware. Let's get the URLs from the server directly. + console.log('Fetching Listmonk URLs for admin panel...'); + const configResponse = await fetch('/api/admin/listmonk-urls'); + + if (!configResponse.ok) { + throw new Error(`Listmonk URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); + } + + const config = await configResponse.json(); + console.log('Listmonk URLs received:', config); + + if (config.success && config.listmonkUrls) { + console.log('Setting up Listmonk links with URLs:', config.listmonkUrls); + + // Set up admin dashboard Listmonk links + setAdminListmonkLink('admin-listmonk-admin-link', config.listmonkUrls.adminUrl); + setAdminListmonkLink('admin-listmonk-lists-link', config.listmonkUrls.listsUrl); + setAdminListmonkLink('admin-listmonk-campaigns-link', config.listmonkUrls.campaignsUrl); + setAdminListmonkLink('admin-listmonk-subscribers-link', config.listmonkUrls.subscribersUrl); + setAdminListmonkLink('admin-listmonk-settings-link', config.listmonkUrls.settingsUrl); + + console.log('Listmonk links initialized in admin panel'); + } else { + console.warn('No Listmonk URLs found in admin config response'); + // Hide the Listmonk section if no URLs are available + const listmonkSection = document.getElementById('listmonk-links'); + const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); + if (listmonkSection) { + listmonkSection.style.display = 'none'; + console.log('Hidden Listmonk section'); + } + if (listmonkNav) { + listmonkNav.style.display = 'none'; + console.log('Hidden Listmonk nav link'); + } + } + } catch (error) { + console.error('Error initializing Listmonk links in admin panel:', error); + // Hide the Listmonk section on error + const listmonkSection = document.getElementById('listmonk-links'); + const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); + if (listmonkSection) { + listmonkSection.style.display = 'none'; + console.log('Hidden Listmonk section due to error'); + } + if (listmonkNav) { + listmonkNav.style.display = 'none'; + console.log('Hidden Listmonk nav link due to error'); + } + } +} + +// Helper function to set admin Listmonk link href +function setAdminListmonkLink(elementId, url) { + console.log(`Setting up Listmonk link: ${elementId} = ${url}`); + const element = document.getElementById(elementId); + + if (element && url) { + element.href = url; + element.style.display = 'inline-flex'; + // Remove any disabled state + element.classList.remove('btn-disabled'); + element.removeAttribute('disabled'); + console.log(`✓ Successfully set up ${elementId}`); + } else if (element) { + element.style.display = 'none'; + // Add disabled state if no URL + element.classList.add('btn-disabled'); + element.setAttribute('disabled', 'disabled'); + element.href = '#'; + console.log(`⚠ Disabled ${elementId} - no URL provided`); + } else { + console.error(`✗ Element not found: ${elementId}`); + } +} + +// Shift User Management Functions +let currentShiftData = null; +let allUsers = []; + +// Load all users for the dropdown +async function loadAllUsers() { + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (data.success) { + allUsers = data.users; + populateUserSelect(); + } else { + console.error('Failed to load users:', data.error); + } + } catch (error) { + console.error('Error loading users:', error); + } +} + +// Populate user select dropdown +function populateUserSelect() { + const select = document.getElementById('user-select'); + if (!select) return; + + // Clear existing options except the first one + select.innerHTML = ''; + + allUsers.forEach(user => { + const option = document.createElement('option'); + option.value = user.email || user.Email; + option.textContent = `${user.name || user.Name || ''} (${user.email || user.Email})`; + select.appendChild(option); + }); +} + +// Show the shift user management modal +async function showShiftUserModal(shiftId, shiftData) { + currentShiftData = { ...shiftData, ID: shiftId }; + + // Update modal title and info + document.getElementById('modal-shift-title').textContent = shiftData.Title; + const shiftDate = createLocalDate(shiftData.Date); + document.getElementById('modal-shift-details').textContent = + `${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`; + + // Load users if not already loaded + if (allUsers.length === 0) { + await loadAllUsers(); + } + + // Display current volunteers + displayCurrentVolunteers(shiftData.signups || []); + + // Show modal + document.getElementById('shift-user-modal').style.display = 'flex'; +} + +// Display current volunteers in the modal +function displayCurrentVolunteers(volunteers) { + const container = document.getElementById('current-volunteers-list'); + + if (!volunteers || volunteers.length === 0) { + container.innerHTML = '
No volunteers signed up yet.
'; + return; + } + + container.innerHTML = volunteers.map(volunteer => ` +
+
+
${escapeHtml(volunteer['User Name'] || volunteer['User Email'] || 'Unknown')}
+
${escapeHtml(volunteer['User Email'])}
+
+
+ +
+
+ `).join(''); + + // Add event listeners for remove buttons + setupVolunteerActionListeners(); +} + +// Setup event listeners for volunteer actions +function setupVolunteerActionListeners() { + const container = document.getElementById('current-volunteers-list'); + + container.addEventListener('click', function(e) { + if (e.target.classList.contains('remove-volunteer-btn')) { + const volunteerId = e.target.getAttribute('data-volunteer-id'); + const volunteerEmail = e.target.getAttribute('data-volunteer-email'); + removeVolunteerFromShift(volunteerId, volunteerEmail); + } + }); +} + +// Add user to shift +async function addUserToShift() { + const userSelect = document.getElementById('user-select'); + const userEmail = userSelect.value; + + if (!userEmail) { + showStatus('Please select a user to add', 'error'); + return; + } + + if (!currentShiftData || !currentShiftData.ID) { + showStatus('No shift selected or invalid shift data', 'error'); + console.error('Invalid currentShiftData:', currentShiftData); + return; + } + + try { + const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/add-user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ userEmail }) + }); + + const data = await response.json(); + + if (data.success) { + showStatus('User successfully added to shift', 'success'); + userSelect.value = ''; // Clear selection + + // Refresh the shift data and reload volunteers with better error handling + try { + await refreshCurrentShiftData(); + console.log('Refreshed shift data after adding user'); + } catch (refreshError) { + console.error('Error during refresh after adding user:', refreshError); + // Still show success since the add operation worked + } + } else { + showStatus(data.error || 'Failed to add user to shift', 'error'); + } + } catch (error) { + console.error('Error adding user to shift:', error); + showStatus('Failed to add user to shift', 'error'); + } +} + +// Remove volunteer from shift +async function removeVolunteerFromShift(volunteerId, volunteerEmail) { + if (!confirm(`Are you sure you want to remove ${volunteerEmail} from this shift?`)) { + return; + } + + if (!currentShiftData || !currentShiftData.ID) { + showStatus('No shift selected or invalid shift data', 'error'); + console.error('Invalid currentShiftData:', currentShiftData); + return; + } + + try { + const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/remove-user/${volunteerId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + showStatus('Volunteer successfully removed from shift', 'success'); + + // Refresh the shift data and reload volunteers with better error handling + try { + await refreshCurrentShiftData(); + console.log('Refreshed shift data after removing volunteer'); + } catch (refreshError) { + console.error('Error during refresh after removing volunteer:', refreshError); + // Still show success since the remove operation worked + } + } else { + showStatus(data.error || 'Failed to remove volunteer from shift', 'error'); + } + } catch (error) { + console.error('Error removing volunteer from shift:', error); + showStatus('Failed to remove volunteer from shift', 'error'); + } +} + +// Refresh current shift data +async function refreshCurrentShiftData() { + if (!currentShiftData || !currentShiftData.ID) { + console.warn('No current shift data or missing ID, skipping refresh'); + return; + } + + try { + console.log('Refreshing shift data for shift ID:', currentShiftData.ID); + + // Instead of reloading ALL admin shifts, just get this specific shift's signups + // This prevents the expensive backend call and reduces the refresh cascade + const response = await fetch(`/api/shifts/admin`); + const data = await response.json(); + + if (data.success && data.shifts && Array.isArray(data.shifts)) { + const updatedShift = data.shifts.find(s => s && s.ID === currentShiftData.ID); + if (updatedShift) { + console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers'); + currentShiftData = updatedShift; + displayCurrentVolunteers(updatedShift.signups || []); + + // Only update the specific shift in the main list, don't refresh everything + updateShiftInList(updatedShift); + } else { + console.warn('Could not find updated shift with ID:', currentShiftData.ID); + } + } else { + console.error('Failed to refresh shift data:', data.error || 'Invalid response format'); + } + } catch (error) { + console.error('Error refreshing shift data:', error); + } +} + +// New function to update a single shift in the list without full refresh +function updateShiftInList(updatedShift) { + const shiftElement = document.querySelector(`[data-shift-id="${updatedShift.ID}"]`); + if (shiftElement) { + const shiftItem = shiftElement.closest('.shift-admin-item'); + if (shiftItem) { + const signupCount = updatedShift.signups ? updatedShift.signups.length : 0; + + // Find the volunteer count paragraph (contains 👥) + const volunteerCountElement = Array.from(shiftItem.querySelectorAll('p')).find(p => + p.textContent.includes('👥') + ); + + if (volunteerCountElement) { + volunteerCountElement.textContent = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers`; + } + + // Update the data attribute with new shift data + const manageBtn = shiftItem.querySelector('.manage-volunteers-btn'); + if (manageBtn) { + manageBtn.setAttribute('data-shift', JSON.stringify(updatedShift).replace(/'/g, "'")); + } + } + } +} + +// Close modal +function closeShiftUserModal() { + document.getElementById('shift-user-modal').style.display = 'none'; + currentShiftData = null; + + // Don't refresh the entire shifts list when closing modal + // The shifts list should already be up to date from the individual updates + console.log('Modal closed - shifts list should already be current'); +} + +// Email shift details to all volunteers +async function emailShiftDetails() { + if (!currentShiftData) { + showStatus('No shift selected', 'error'); + return; + } + + // Check if there are volunteers to email + const volunteers = currentShiftData.signups || []; + if (volunteers.length === 0) { + showStatus('No volunteers signed up for this shift', 'error'); + return; + } + + // Confirm action + const confirmMessage = `Send shift details email to ${volunteers.length} volunteer${volunteers.length !== 1 ? 's' : ''}?`; + if (!confirm(confirmMessage)) { + return; + } + + // Initialize progress tracking for shift emails + initializeShiftEmailProgress(volunteers.length); + + try { + const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/email-details`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + // Display detailed results + updateShiftEmailProgress(data.results); + showStatus(data.message, 'success'); + console.log('Email results:', data.results); + } else { + showShiftEmailError(data.error || 'Failed to send emails'); + if (data.details) { + console.error('Failed email details:', data.details); + } + } + } catch (error) { + console.error('Error sending shift details emails:', error); + showShiftEmailError('Failed to send emails - Network error'); + } +} + +// Initialize shift email progress display +function initializeShiftEmailProgress(totalCount) { + const progressContainer = document.getElementById('shift-email-progress-container'); + const statusList = document.getElementById('shift-email-status-list'); + const pendingCountEl = document.getElementById('shift-pending-count'); + const successCountEl = document.getElementById('shift-success-count'); + const errorCountEl = document.getElementById('shift-error-count'); + const progressBar = document.getElementById('shift-email-progress-bar'); + const progressText = document.getElementById('shift-progress-text'); + const closeBtn = document.getElementById('close-shift-progress-btn'); + + // Show progress container + progressContainer.classList.add('show'); + + // Reset counters + pendingCountEl.textContent = totalCount; + successCountEl.textContent = '0'; + errorCountEl.textContent = '0'; + + // Reset progress bar + progressBar.style.width = '0%'; + progressBar.classList.remove('complete', 'error'); + progressText.textContent = '0%'; + + // Clear status list + statusList.innerHTML = ''; + + // Hide close button initially + closeBtn.style.display = 'none'; + + // Add status items for each volunteer + const volunteers = currentShiftData.signups || []; + volunteers.forEach(volunteer => { + const statusItem = document.createElement('div'); + statusItem.className = 'email-status-item'; + statusItem.innerHTML = ` +
${volunteer['User Name'] || volunteer['User Email']}
+
+
+ Sending... +
+ `; + statusList.appendChild(statusItem); + }); +} + +// Update shift email progress with results +function updateShiftEmailProgress(results) { + const statusList = document.getElementById('shift-email-status-list'); + const pendingCountEl = document.getElementById('shift-pending-count'); + const successCountEl = document.getElementById('shift-success-count'); + const errorCountEl = document.getElementById('shift-error-count'); + const progressBar = document.getElementById('shift-email-progress-bar'); + const progressText = document.getElementById('shift-progress-text'); + const closeBtn = document.getElementById('close-shift-progress-btn'); + + const successful = results.successful || []; + const failed = results.failed || []; + const total = results.total || (successful.length + failed.length); + + // Update counters + successCountEl.textContent = successful.length; + errorCountEl.textContent = failed.length; + pendingCountEl.textContent = '0'; + + // Update progress bar + const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); + progressBar.style.width = percentage + '%'; + progressText.textContent = percentage + '%'; + + if (failed.length > 0) { + progressBar.classList.add('error'); + } else { + progressBar.classList.add('complete'); + } + + // Update individual status items + const statusItems = statusList.children; + + // Update successful emails + successful.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✓ Sent + `; + } + }); + + // Update failed emails + failed.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✗ Failed + `; + } + }); + + // Show close button + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + document.getElementById('shift-email-progress-container').classList.remove('show'); + }; +} + +// Show shift email error +function showShiftEmailError(message) { + const progressContainer = document.getElementById('shift-email-progress-container'); + const progressBar = document.getElementById('shift-email-progress-bar'); + const progressText = document.getElementById('shift-progress-text'); + const closeBtn = document.getElementById('close-shift-progress-btn'); + + // Show progress container if not visible + progressContainer.classList.add('show'); + + // Update progress bar to show error + progressBar.style.width = '100%'; + progressBar.classList.add('error'); + progressText.textContent = 'Error'; + + // Show close button + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + progressContainer.classList.remove('show'); + }; + + showStatus(message, 'error'); +} + +// Setup modal event listeners when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + const closeModalBtn = document.getElementById('close-user-modal'); + const addUserBtn = document.getElementById('add-user-btn'); + const emailShiftDetailsBtn = document.getElementById('email-shift-details-btn'); + const modal = document.getElementById('shift-user-modal'); + + if (closeModalBtn) { + closeModalBtn.addEventListener('click', closeShiftUserModal); + } + + if (addUserBtn) { + addUserBtn.addEventListener('click', addUserToShift); + } + + if (emailShiftDetailsBtn) { + emailShiftDetailsBtn.addEventListener('click', emailShiftDetails); + } + + // Close modal when clicking outside + if (modal) { + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeShiftUserModal(); + } + }); + } +}); + +// Setup email users modal event listeners when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Email all users functionality + const closeEmailModalBtn = document.getElementById('close-email-modal'); + const cancelEmailBtn = document.getElementById('cancel-email-btn'); + const emailUsersForm = document.getElementById('email-users-form'); + const emailModal = document.getElementById('email-users-modal'); + + if (closeEmailModalBtn) { + closeEmailModalBtn.addEventListener('click', closeEmailUsersModal); + } + + if (cancelEmailBtn) { + cancelEmailBtn.addEventListener('click', closeEmailUsersModal); + } + + if (emailUsersForm) { + emailUsersForm.addEventListener('submit', sendEmailToAllUsers); + } + + // Close modal when clicking outside + if (emailModal) { + emailModal.addEventListener('click', function(e) { + if (e.target === emailModal) { + closeEmailUsersModal(); + } + }); + } + + // Setup rich text editor functionality + setupRichTextEditor(); +}); + +// Public Shifts Functions +function generateShiftPublicLink(shiftId) { + const baseUrl = window.location.origin; + return `${baseUrl}/public-shifts.html#shift-${shiftId}`; +} + +function copyShiftLink(shiftId) { + const link = generateShiftPublicLink(shiftId); + navigator.clipboard.writeText(link).then(() => { + showStatus('Public shift link copied to clipboard!', 'success'); + }).catch(() => { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = link; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + showStatus('Public shift link copied to clipboard!', 'success'); + }); +} + +function openShiftLink(shiftId) { + const link = generateShiftPublicLink(shiftId); + window.open(link, '_blank'); +} + +// Update the shift form to include Is Public checkbox +function updateShiftFormWithPublicOption() { + const form = document.getElementById('shift-form'); + if (!form) return; + + // Check if public checkbox already exists + if (document.getElementById('shift-is-public')) return; + + const maxVolunteersGroup = form.querySelector('.form-group:has(#shift-max-volunteers)'); + if (maxVolunteersGroup) { + const publicGroup = document.createElement('div'); + publicGroup.className = 'form-group'; + publicGroup.innerHTML = ` + + `; + maxVolunteersGroup.insertAdjacentElement('afterend', publicGroup); + } +} + +// Call this when the shifts section is shown +function enhanceShiftsSection() { + updateShiftFormWithPublicOption(); +} + +// Update the showSection function to call enhanceShiftsSection when shifts section is shown +const originalShowSection = window.showSection || showSection; +window.showSection = function(sectionId) { + if (originalShowSection) { + originalShowSection(sectionId); + } + + if (sectionId === 'shifts') { + enhanceShiftsSection(); + } +}; diff --git a/map/app/public/js/admin-shift-volunteers.js b/map/app/public/js/admin-shift-volunteers.js new file mode 100644 index 0000000..dd6ab71 --- /dev/null +++ b/map/app/public/js/admin-shift-volunteers.js @@ -0,0 +1,530 @@ +/** + * Admin Shift Volunteers Module + * Handles volunteer management modals and shift email functionality + */ + +// Volunteer management state +let currentShiftData = null; +let allUsers = []; + +// Load all users for the dropdown +async function loadAllUsers() { + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (data.success) { + allUsers = data.users; + populateUserSelect(); + } else { + console.error('Failed to load users:', data.error); + } + } catch (error) { + console.error('Error loading users:', error); + } +} + +// Populate user select dropdown +function populateUserSelect() { + const select = document.getElementById('user-select'); + if (!select) return; + + // Clear existing options except the first one + select.innerHTML = ''; + + allUsers.forEach(user => { + const option = document.createElement('option'); + option.value = user.email || user.Email; + option.textContent = `${user.name || user.Name || ''} (${user.email || user.Email})`; + select.appendChild(option); + }); +} + +// Show the shift user management modal +async function showShiftUserModal(shiftId, shiftData) { + currentShiftData = { ...shiftData, ID: shiftId }; + + // Update modal title and info + const modalTitle = document.getElementById('modal-shift-title'); + const modalDetails = document.getElementById('modal-shift-details'); + + if (modalTitle) modalTitle.textContent = shiftData.Title; + + if (modalDetails) { + const shiftDate = window.adminCore.createLocalDate(shiftData.Date); + modalDetails.textContent = + `${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`; + } + + // Load users if not already loaded + if (allUsers.length === 0) { + await loadAllUsers(); + } + + // Display current volunteers + displayCurrentVolunteers(shiftData.signups || []); + + // Show modal + const modal = document.getElementById('shift-user-modal'); + if (modal) { + modal.style.display = 'flex'; + } +} + +// Display current volunteers in the modal +function displayCurrentVolunteers(volunteers) { + const container = document.getElementById('current-volunteers-list'); + + if (!container) return; + + if (!volunteers || volunteers.length === 0) { + container.innerHTML = '
No volunteers signed up yet.
'; + return; + } + + container.innerHTML = volunteers.map(volunteer => ` +
+
+
${window.adminCore.escapeHtml(volunteer['User Name'] || volunteer['User Email'] || 'Unknown')}
+
${window.adminCore.escapeHtml(volunteer['User Email'])}
+
+
+ +
+
+ `).join(''); + + // Add event listeners for remove buttons + setupVolunteerActionListeners(); +} + +// Setup event listeners for volunteer actions +function setupVolunteerActionListeners() { + const container = document.getElementById('current-volunteers-list'); + + if (container) { + container.addEventListener('click', function(e) { + if (e.target.classList.contains('remove-volunteer-btn')) { + const volunteerId = e.target.getAttribute('data-volunteer-id'); + const volunteerEmail = e.target.getAttribute('data-volunteer-email'); + removeVolunteerFromShift(volunteerId, volunteerEmail); + } + }); + } +} + +// Add user to shift +async function addUserToShift() { + const userSelect = document.getElementById('user-select'); + const userEmail = userSelect?.value; + + if (!userEmail) { + window.adminCore.showStatus('Please select a user to add', 'error'); + return; + } + + if (!currentShiftData || !currentShiftData.ID) { + window.adminCore.showStatus('No shift selected or invalid shift data', 'error'); + console.error('Invalid currentShiftData:', currentShiftData); + return; + } + + try { + const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/add-user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ userEmail }) + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus('User successfully added to shift', 'success'); + if (userSelect) userSelect.value = ''; // Clear selection + + // Refresh the shift data and reload volunteers with better error handling + try { + await refreshCurrentShiftData(); + console.log('Refreshed shift data after adding user'); + } catch (refreshError) { + console.error('Error during refresh after adding user:', refreshError); + // Still show success since the add operation worked + } + } else { + window.adminCore.showStatus(data.error || 'Failed to add user to shift', 'error'); + } + } catch (error) { + console.error('Error adding user to shift:', error); + window.adminCore.showStatus('Failed to add user to shift', 'error'); + } +} + +// Remove volunteer from shift +async function removeVolunteerFromShift(volunteerId, volunteerEmail) { + if (!confirm(`Are you sure you want to remove ${volunteerEmail} from this shift?`)) { + return; + } + + if (!currentShiftData || !currentShiftData.ID) { + window.adminCore.showStatus('No shift selected or invalid shift data', 'error'); + console.error('Invalid currentShiftData:', currentShiftData); + return; + } + + try { + const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/remove-user/${volunteerId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus('Volunteer successfully removed from shift', 'success'); + + // Refresh the shift data and reload volunteers with better error handling + try { + await refreshCurrentShiftData(); + console.log('Refreshed shift data after removing volunteer'); + } catch (refreshError) { + console.error('Error during refresh after removing volunteer:', refreshError); + // Still show success since the remove operation worked + } + } else { + window.adminCore.showStatus(data.error || 'Failed to remove volunteer from shift', 'error'); + } + } catch (error) { + console.error('Error removing volunteer from shift:', error); + window.adminCore.showStatus('Failed to remove volunteer from shift', 'error'); + } +} + +// Refresh current shift data +async function refreshCurrentShiftData() { + if (!currentShiftData || !currentShiftData.ID) { + console.warn('No current shift data or missing ID, skipping refresh'); + return; + } + + try { + console.log('Refreshing shift data for shift ID:', currentShiftData.ID); + + // Instead of reloading ALL admin shifts, just get this specific shift's signups + // This prevents the expensive backend call and reduces the refresh cascade + const response = await fetch(`/api/shifts/admin`); + const data = await response.json(); + + if (data.success && data.shifts && Array.isArray(data.shifts)) { + const updatedShift = data.shifts.find(s => s && s.ID === currentShiftData.ID); + if (updatedShift) { + console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers'); + currentShiftData = updatedShift; + displayCurrentVolunteers(updatedShift.signups || []); + + // Only update the specific shift in the main list, don't refresh everything + updateShiftInList(updatedShift); + } else { + console.warn('Could not find updated shift with ID:', currentShiftData.ID); + } + } else { + console.error('Failed to refresh shift data:', data.error || 'Invalid response format'); + } + } catch (error) { + console.error('Error refreshing shift data:', error); + } +} + +// Update a single shift in the list without full refresh +function updateShiftInList(updatedShift) { + const shiftElement = document.querySelector(`[data-shift-id="${updatedShift.ID}"]`); + if (shiftElement) { + const shiftItem = shiftElement.closest('.shift-admin-item'); + if (shiftItem) { + const signupCount = updatedShift.signups ? updatedShift.signups.length : 0; + + // Find the volunteer count paragraph (contains 👥) + const volunteerCountElement = Array.from(shiftItem.querySelectorAll('p')).find(p => + p.textContent.includes('👥') + ); + + if (volunteerCountElement) { + volunteerCountElement.textContent = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers`; + } + + // Update the data attribute with new shift data + const manageBtn = shiftItem.querySelector('.manage-volunteers-btn'); + if (manageBtn) { + manageBtn.setAttribute('data-shift', JSON.stringify(updatedShift).replace(/'/g, "'")); + } + } + } +} + +// Close modal +function closeShiftUserModal() { + const modal = document.getElementById('shift-user-modal'); + if (modal) { + modal.style.display = 'none'; + } + currentShiftData = null; + + // Don't refresh the entire shifts list when closing modal + // The shifts list should already be up to date from the individual updates + console.log('Modal closed - shifts list should already be current'); +} + +// Email shift details to all volunteers +async function emailShiftDetails() { + if (!currentShiftData) { + window.adminCore.showStatus('No shift selected', 'error'); + return; + } + + // Check if there are volunteers to email + const volunteers = currentShiftData.signups || []; + if (volunteers.length === 0) { + window.adminCore.showStatus('No volunteers signed up for this shift', 'error'); + return; + } + + // Confirm action + const confirmMessage = `Send shift details email to ${volunteers.length} volunteer${volunteers.length !== 1 ? 's' : ''}?`; + if (!confirm(confirmMessage)) { + return; + } + + // Initialize progress tracking for shift emails + initializeShiftEmailProgress(volunteers.length); + + try { + const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/email-details`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + // Display detailed results + updateShiftEmailProgress(data.results); + window.adminCore.showStatus(data.message, 'success'); + console.log('Email results:', data.results); + } else { + showShiftEmailError(data.error || 'Failed to send emails'); + if (data.details) { + console.error('Failed email details:', data.details); + } + } + } catch (error) { + console.error('Error sending shift details emails:', error); + showShiftEmailError('Failed to send emails - Network error'); + } +} + +// Initialize shift email progress display +function initializeShiftEmailProgress(totalCount) { + const progressContainer = document.getElementById('shift-email-progress-container'); + const statusList = document.getElementById('shift-email-status-list'); + const pendingCountEl = document.getElementById('shift-pending-count'); + const successCountEl = document.getElementById('shift-success-count'); + const errorCountEl = document.getElementById('shift-error-count'); + const progressBar = document.getElementById('shift-email-progress-bar'); + const progressText = document.getElementById('shift-progress-text'); + const closeBtn = document.getElementById('close-shift-progress-btn'); + + if (!progressContainer) return; + + // Show progress container + progressContainer.classList.add('show'); + + // Reset counters + if (pendingCountEl) pendingCountEl.textContent = totalCount; + if (successCountEl) successCountEl.textContent = '0'; + if (errorCountEl) errorCountEl.textContent = '0'; + + // Reset progress bar + if (progressBar) { + progressBar.style.width = '0%'; + progressBar.classList.remove('complete', 'error'); + } + if (progressText) progressText.textContent = '0%'; + + // Clear status list + if (statusList) statusList.innerHTML = ''; + + // Hide close button initially + if (closeBtn) closeBtn.style.display = 'none'; + + // Add status items for each volunteer + const volunteers = currentShiftData.signups || []; + volunteers.forEach(volunteer => { + if (statusList) { + const statusItem = document.createElement('div'); + statusItem.className = 'email-status-item'; + statusItem.innerHTML = ` +
${volunteer['User Name'] || volunteer['User Email']}
+
+
+ Sending... +
+ `; + statusList.appendChild(statusItem); + } + }); +} + +// Update shift email progress with results +function updateShiftEmailProgress(results) { + const statusList = document.getElementById('shift-email-status-list'); + const pendingCountEl = document.getElementById('shift-pending-count'); + const successCountEl = document.getElementById('shift-success-count'); + const errorCountEl = document.getElementById('shift-error-count'); + const progressBar = document.getElementById('shift-email-progress-bar'); + const progressText = document.getElementById('shift-progress-text'); + const closeBtn = document.getElementById('close-shift-progress-btn'); + + const successful = results.successful || []; + const failed = results.failed || []; + const total = results.total || (successful.length + failed.length); + + // Update counters + if (successCountEl) successCountEl.textContent = successful.length; + if (errorCountEl) errorCountEl.textContent = failed.length; + if (pendingCountEl) pendingCountEl.textContent = '0'; + + // Update progress bar + const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); + if (progressBar) { + progressBar.style.width = percentage + '%'; + progressText.textContent = percentage + '%'; + + if (failed.length > 0) { + progressBar.classList.add('error'); + } else { + progressBar.classList.add('complete'); + } + } + + // Update individual status items + if (statusList) { + const statusItems = statusList.children; + + // Update successful emails + successful.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✓ Sent + `; + } + }); + + // Update failed emails + failed.forEach(result => { + const statusItem = Array.from(statusItems).find(item => + item.querySelector('.email-status-recipient').textContent.includes(result.email) || + item.querySelector('.email-status-recipient').textContent.includes(result.name) + ); + if (statusItem) { + statusItem.querySelector('.email-status-result').innerHTML = ` + ✗ Failed + `; + } + }); + } + + // Show close button + if (closeBtn) { + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + const progressContainer = document.getElementById('shift-email-progress-container'); + if (progressContainer) { + progressContainer.classList.remove('show'); + } + }; + } +} + +// Show shift email error +function showShiftEmailError(message) { + const progressContainer = document.getElementById('shift-email-progress-container'); + const progressBar = document.getElementById('shift-email-progress-bar'); + const progressText = document.getElementById('shift-progress-text'); + const closeBtn = document.getElementById('close-shift-progress-btn'); + + // Show progress container if not visible + if (progressContainer) { + progressContainer.classList.add('show'); + } + + // Update progress bar to show error + if (progressBar) { + progressBar.style.width = '100%'; + progressBar.classList.add('error'); + } + if (progressText) progressText.textContent = 'Error'; + + // Show close button + if (closeBtn) { + closeBtn.style.display = 'block'; + closeBtn.onclick = () => { + if (progressContainer) { + progressContainer.classList.remove('show'); + } + }; + } + + window.adminCore.showStatus(message, 'error'); +} + +// Setup volunteer modal event listeners +function setupVolunteerModalEventListeners() { + const closeModalBtn = document.getElementById('close-user-modal'); + const addUserBtn = document.getElementById('add-user-btn'); + const emailShiftDetailsBtn = document.getElementById('email-shift-details-btn'); + const modal = document.getElementById('shift-user-modal'); + + if (closeModalBtn) { + closeModalBtn.addEventListener('click', closeShiftUserModal); + } + + if (addUserBtn) { + addUserBtn.addEventListener('click', addUserToShift); + } + + if (emailShiftDetailsBtn) { + emailShiftDetailsBtn.addEventListener('click', emailShiftDetails); + } + + // Close modal when clicking outside + if (modal) { + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeShiftUserModal(); + } + }); + } +} + +// Export shift volunteer functions +window.adminShiftVolunteers = { + showShiftUserModal, + closeShiftUserModal, + addUserToShift, + removeVolunteerFromShift, + emailShiftDetails, + setupVolunteerModalEventListeners, + loadAllUsers, + getCurrentShiftData: () => currentShiftData, + getAllUsers: () => allUsers +}; diff --git a/map/app/public/js/admin-shifts.js b/map/app/public/js/admin-shifts.js new file mode 100644 index 0000000..c59b4d0 --- /dev/null +++ b/map/app/public/js/admin-shifts.js @@ -0,0 +1,419 @@ +/** + * Admin Shifts Management Module + * Handles shift CRUD operations, volunteer management, and email functionality + */ + +// Shift state +let editingShiftId = null; +let currentShiftData = null; +let allUsers = []; + +// Add shift management functions +async function loadAdminShifts() { + const list = document.getElementById('admin-shifts-list'); + if (list) { + list.innerHTML = '

Loading shifts...

'; + } + + try { + console.log('Loading admin shifts...'); + const response = await fetch('/api/shifts/admin'); + const data = await response.json(); + + if (data.success) { + console.log('Successfully loaded', data.shifts.length, 'shifts'); + displayAdminShifts(data.shifts); + } else { + console.error('Failed to load shifts:', data.error); + if (list) { + list.innerHTML = '

Failed to load shifts

'; + } + window.adminCore.showStatus('Failed to load shifts', 'error'); + } + } catch (error) { + console.error('Error loading admin shifts:', error); + if (list) { + list.innerHTML = '

Error loading shifts

'; + } + window.adminCore.showStatus('Failed to load shifts', 'error'); + } +} + +function displayAdminShifts(shifts) { + const list = document.getElementById('admin-shifts-list'); + + if (!list) { + console.error('Admin shifts list element not found'); + return; + } + + if (shifts.length === 0) { + list.innerHTML = '

No shifts created yet.

'; + return; + } + + list.innerHTML = shifts.map(shift => { + const shiftDate = window.adminCore.createLocalDate(shift.Date); + const signupCount = shift.signups ? shift.signups.length : 0; + const isPublic = shift['Is Public'] !== false; + + console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []); + + return ` +
+
+

${window.adminCore.escapeHtml(shift.Title)}

+

📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}

+

📍 ${window.adminCore.escapeHtml(shift.Location || 'TBD')}

+

👥 ${signupCount}/${shift['Max Volunteers']} volunteers

+

${shift.Status || 'Open'}

+

${isPublic ? '🌐 Public' : '🔒 Private'}

+ ${isPublic ? ` + + ` : ''} +
+
+ + + +
+
+ `; + }).join(''); + + // Add event listeners using delegation + setupShiftActionListeners(); +} + +// Setup shift action listeners +function setupShiftActionListeners() { + const list = document.getElementById('admin-shifts-list'); + if (!list) return; + + // Remove any existing listeners to avoid duplicates + const newList = list.cloneNode(true); + list.parentNode.replaceChild(newList, list); + + // Get the updated reference + const updatedList = document.getElementById('admin-shifts-list'); + + updatedList.addEventListener('click', function(e) { + if (e.target.classList.contains('delete-shift-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Delete button clicked for shift:', shiftId); + deleteShift(shiftId); + } else if (e.target.classList.contains('edit-shift-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Edit button clicked for shift:', shiftId); + editShift(shiftId); + } else if (e.target.classList.contains('manage-volunteers-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + const shiftData = JSON.parse(e.target.getAttribute('data-shift').replace(/'/g, "'")); + console.log('Manage volunteers clicked for shift:', shiftId); + showShiftUserModal(shiftId, shiftData); + } else if (e.target.classList.contains('copy-shift-link-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Copy link button clicked for shift:', shiftId); + copyShiftLink(shiftId); + } else if (e.target.classList.contains('open-shift-link-btn')) { + const shiftId = e.target.getAttribute('data-shift-id'); + console.log('Open link button clicked for shift:', shiftId); + openShiftLink(shiftId); + } + }); +} + +// Delete shift +async function deleteShift(shiftId) { + if (!confirm('Are you sure you want to delete this shift? All signups will be cancelled.')) { + return; + } + + try { + const response = await fetch(`/api/shifts/admin/${shiftId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus('Shift deleted successfully', 'success'); + await loadAdminShifts(); + console.log('Refreshed shifts list after deleting shift'); + } else { + window.adminCore.showStatus(data.error || 'Failed to delete shift', 'error'); + } + } catch (error) { + console.error('Error deleting shift:', error); + window.adminCore.showStatus('Failed to delete shift', 'error'); + } +} + +// Edit shift +async function editShift(shiftId) { + try { + // Find the shift in the current data + const response = await fetch('/api/shifts/admin'); + const data = await response.json(); + + if (!data.success) { + window.adminCore.showStatus('Failed to load shift data', 'error'); + return; + } + + const shift = data.shifts.find(s => s.ID === parseInt(shiftId)); + if (!shift) { + window.adminCore.showStatus('Shift not found', 'error'); + return; + } + + // Set editing mode + editingShiftId = shiftId; + + // Populate the form + const titleInput = document.getElementById('shift-title'); + const descInput = document.getElementById('shift-description'); + const dateInput = document.getElementById('shift-date'); + const startInput = document.getElementById('shift-start'); + const endInput = document.getElementById('shift-end'); + const locationInput = document.getElementById('shift-location'); + const maxVolInput = document.getElementById('shift-max-volunteers'); + + if (titleInput) titleInput.value = shift.Title || ''; + if (descInput) descInput.value = shift.Description || ''; + if (dateInput) dateInput.value = shift.Date || ''; + if (startInput) startInput.value = shift['Start Time'] || ''; + if (endInput) endInput.value = shift['End Time'] || ''; + if (locationInput) locationInput.value = shift.Location || ''; + if (maxVolInput) maxVolInput.value = shift['Max Volunteers'] || ''; + + // Update public checkbox if it exists + const publicCheckbox = document.getElementById('shift-is-public'); + if (publicCheckbox) { + publicCheckbox.checked = shift['Is Public'] !== false; + } + + // Change submit button text + const submitBtn = document.querySelector('#shift-form button[type="submit"]'); + if (submitBtn) { + submitBtn.textContent = 'Update Shift'; + } + + // Remove editing class from any previous item + document.querySelectorAll('.shift-admin-item.editing').forEach(el => { + el.classList.remove('editing'); + }); + + // Add editing class to current item + const shiftElement = document.querySelector(`[data-shift-id="${shiftId}"]`); + if (shiftElement) { + const shiftItem = shiftElement.closest('.shift-admin-item'); + if (shiftItem) { + shiftItem.classList.add('editing'); + } + } + + // Scroll to form + const form = document.getElementById('shift-form'); + if (form) { + form.scrollIntoView({ behavior: 'smooth' }); + } + + window.adminCore.showStatus('Editing shift: ' + shift.Title, 'info'); + + } catch (error) { + console.error('Error loading shift for edit:', error); + window.adminCore.showStatus('Failed to load shift for editing', 'error'); + } +} + +// Create or update shift +async function createShift(e) { + e.preventDefault(); + + const titleInput = document.getElementById('shift-title'); + const descInput = document.getElementById('shift-description'); + const dateInput = document.getElementById('shift-date'); + const startInput = document.getElementById('shift-start'); + const endInput = document.getElementById('shift-end'); + const locationInput = document.getElementById('shift-location'); + const maxVolInput = document.getElementById('shift-max-volunteers'); + + const title = titleInput?.value; + const description = descInput?.value; + const date = dateInput?.value; + const startTime = startInput?.value; + const endTime = endInput?.value; + const location = locationInput?.value; + const maxVolunteers = maxVolInput?.value; + + // Get public checkbox value + const publicCheckbox = document.getElementById('shift-is-public'); + const isPublic = publicCheckbox?.checked ?? true; + + const shiftData = { + title, + description, + date, + startTime, + endTime, + location, + maxVolunteers: parseInt(maxVolunteers), + isPublic + }; + + try { + let response; + if (editingShiftId) { + // Update existing shift + response = await fetch(`/api/shifts/admin/${editingShiftId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(shiftData) + }); + } else { + // Create new shift + response = await fetch('/api/shifts/admin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(shiftData) + }); + } + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus(editingShiftId ? 'Shift updated successfully' : 'Shift created successfully', 'success'); + clearShiftForm(); + await loadAdminShifts(); + console.log('Refreshed shifts list after saving shift'); + } else { + window.adminCore.showStatus(data.error || 'Failed to save shift', 'error'); + } + } catch (error) { + console.error('Error saving shift:', error); + window.adminCore.showStatus('Failed to save shift', 'error'); + } +} + +function clearShiftForm() { + const form = document.getElementById('shift-form'); + if (form) { + form.reset(); + + // Reset editing state + editingShiftId = null; + + // Reset submit button text + const submitBtn = document.querySelector('#shift-form button[type="submit"]'); + if (submitBtn) { + submitBtn.textContent = 'Create Shift'; + } + + // Remove editing class from any shift items + document.querySelectorAll('.shift-admin-item.editing').forEach(el => { + el.classList.remove('editing'); + }); + + window.adminCore.showStatus('Form cleared', 'info'); + } +} + +// Public Shifts Functions +function generateShiftPublicLink(shiftId) { + const baseUrl = window.location.origin; + return `${baseUrl}/public-shifts.html#shift-${shiftId}`; +} + +function copyShiftLink(shiftId) { + const link = generateShiftPublicLink(shiftId); + navigator.clipboard.writeText(link).then(() => { + window.adminCore.showStatus('Public shift link copied to clipboard!', 'success'); + }).catch(() => { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = link; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + window.adminCore.showStatus('Public shift link copied to clipboard!', 'success'); + }); +} + +function openShiftLink(shiftId) { + const link = generateShiftPublicLink(shiftId); + window.open(link, '_blank'); +} + +// Update the shift form to include Is Public checkbox +function updateShiftFormWithPublicOption() { + const form = document.getElementById('shift-form'); + if (!form) return; + + // Check if public checkbox already exists + if (document.getElementById('shift-is-public')) return; + + const maxVolunteersGroup = form.querySelector('.form-group:has(#shift-max-volunteers)'); + if (maxVolunteersGroup) { + const publicGroup = document.createElement('div'); + publicGroup.className = 'form-group'; + publicGroup.innerHTML = ` + + `; + maxVolunteersGroup.insertAdjacentElement('afterend', publicGroup); + } +} + +// Setup shift event listeners +function setupShiftEventListeners() { + // Shift form submission + const shiftForm = document.getElementById('shift-form'); + if (shiftForm) { + shiftForm.addEventListener('submit', createShift); + } + + // Clear shift form button + const clearShiftBtn = document.getElementById('clear-shift-form'); + if (clearShiftBtn) { + clearShiftBtn.addEventListener('click', function() { + const wasEditing = editingShiftId !== null; + clearShiftForm(); + if (wasEditing) { + window.adminCore.showStatus('Edit cancelled', 'info'); + } + }); + } +} + +// Export shift management functions +window.adminShifts = { + loadAdminShifts, + displayAdminShifts, + deleteShift, + editShift, + createShift, + clearShiftForm, + generateShiftPublicLink, + copyShiftLink, + openShiftLink, + updateShiftFormWithPublicOption, + setupShiftEventListeners, + getEditingShiftId: () => editingShiftId, + getCurrentShiftData: () => currentShiftData, + getAllUsers: () => allUsers +}; diff --git a/map/app/public/js/admin-users.js b/map/app/public/js/admin-users.js new file mode 100644 index 0000000..7d03aac --- /dev/null +++ b/map/app/public/js/admin-users.js @@ -0,0 +1,365 @@ +/** + * Admin Users Management Module + * Handles user CRUD operations, email broadcasting, and user administration + */ + +// User management state +let allUsersData = []; + +// User Management Functions +async function loadUsers() { + const loadingEl = document.getElementById('users-loading'); + const emptyEl = document.getElementById('users-empty'); + const tableBody = document.getElementById('users-table-body'); + + if (loadingEl) loadingEl.style.display = 'block'; + if (emptyEl) emptyEl.style.display = 'none'; + if (tableBody) tableBody.innerHTML = ''; + + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + if (loadingEl) loadingEl.style.display = 'none'; + + if (data.success && data.users) { + displayUsers(data.users); + } else { + throw new Error(data.error || 'Failed to load users'); + } + + } catch (error) { + console.error('Error loading users:', error); + if (loadingEl) loadingEl.style.display = 'none'; + if (emptyEl) { + emptyEl.textContent = 'Failed to load users'; + emptyEl.style.display = 'block'; + } + window.adminCore.showStatus('Failed to load users', 'error'); + } +} + +function displayUsers(users) { + const container = document.querySelector('.users-list'); + if (!container) return; + + // Find or create the users table container, preserving the header + let usersTableContainer = container.querySelector('.users-table-container'); + if (!usersTableContainer) { + // If container doesn't exist, create it after the header + const header = container.querySelector('.users-list-header'); + usersTableContainer = document.createElement('div'); + usersTableContainer.className = 'users-table-container'; + + if (header && header.nextSibling) { + container.insertBefore(usersTableContainer, header.nextSibling); + } else if (header) { + container.appendChild(usersTableContainer); + } else { + container.appendChild(usersTableContainer); + } + } + + if (!users || users.length === 0) { + usersTableContainer.innerHTML = '

No users found.

'; + return; + } + + const tableHtml = ` +
+ + + + + + + + + + + + ${users.map(user => { + const createdDate = user.created_at || user['Created At'] || user.createdAt; + const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A'; + const isAdmin = user.admin || user.Admin || false; + const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user'); + const userId = user.Id || user.id || user.ID; + + // Handle expiration info + let expirationInfo = ''; + if (user.ExpiresAt) { + const expirationDate = new Date(user.ExpiresAt); + const now = new Date(); + const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24)); + + if (daysUntilExpiration < 0) { + expirationInfo = `Expired ${Math.abs(daysUntilExpiration)} days ago`; + } else if (daysUntilExpiration <= 3) { + expirationInfo = `Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}`; + } else { + expirationInfo = `Expires: ${expirationDate.toLocaleDateString()}`; + } + } + + return ` + + + + + + + + `; + }).join('')} + +
EmailNameRoleCreatedActions
${window.adminCore.escapeHtml(user.email || user.Email || 'N/A')}${window.adminCore.escapeHtml(user.name || user.Name || 'N/A')} + + ${userType.charAt(0).toUpperCase() + userType.slice(1)} + + ${expirationInfo} + ${formattedDate} + +
+
+ + `; + + usersTableContainer.innerHTML = tableHtml; + setupUserActionListeners(); +} + +function setupUserActionListeners() { + const container = document.querySelector('.users-list'); + if (!container) return; + + // Remove existing event listeners by cloning the container + const newContainer = container.cloneNode(true); + container.parentNode.replaceChild(newContainer, container); + + // Get the updated reference + const updatedContainer = document.querySelector('.users-list'); + + updatedContainer.addEventListener('click', function(e) { + if (e.target.classList.contains('delete-user-btn')) { + const userId = e.target.getAttribute('data-user-id'); + const userEmail = e.target.getAttribute('data-user-email'); + console.log('Delete button clicked for user:', userId); + deleteUser(userId, userEmail); + } else if (e.target.classList.contains('send-login-btn')) { + const userId = e.target.getAttribute('data-user-id'); + const userEmail = e.target.getAttribute('data-user-email'); + console.log('Send login details button clicked for user:', userId); + sendLoginDetailsToUser(userId, userEmail); + } else if (e.target.id === 'email-all-users-btn') { + console.log('Email All Users button clicked'); + showEmailUsersModal(); + } + }); +} + +async function deleteUser(userId, userEmail) { + if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) { + return; + } + + try { + const response = await fetch(`/api/users/${userId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus(`User "${userEmail}" deleted successfully`, 'success'); + loadUsers(); // Reload the users list + } else { + throw new Error(data.error || 'Failed to delete user'); + } + + } catch (error) { + console.error('Error deleting user:', error); + window.adminCore.showStatus(`Failed to delete user: ${error.message}`, 'error'); + } +} + +async function sendLoginDetailsToUser(userId, userEmail) { + if (!confirm(`Send login details to "${userEmail}"?`)) { + return; + } + + try { + const response = await fetch(`/api/users/${userId}/send-login-details`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus(`Login details sent to "${userEmail}" successfully`, 'success'); + } else { + throw new Error(data.error || 'Failed to send login details'); + } + + } catch (error) { + console.error('Error sending login details:', error); + window.adminCore.showStatus(`Failed to send login details: ${error.message}`, 'error'); + } +} + +async function createUser(e) { + e.preventDefault(); + + const emailInput = document.getElementById('user-email'); + const passwordInput = document.getElementById('user-password'); + const nameInput = document.getElementById('user-name'); + const userTypeSelect = document.getElementById('user-type'); + const expireDaysInput = document.getElementById('user-expire-days'); + const adminCheckbox = document.getElementById('user-is-admin'); + + const email = emailInput?.value.trim(); + const password = passwordInput?.value; + const name = nameInput?.value.trim(); + const userType = userTypeSelect?.value; + const expireDays = userType === 'temp' ? + parseInt(expireDaysInput?.value) : null; + const admin = adminCheckbox?.checked; + + if (!email || !password) { + window.adminCore.showStatus('Email and password are required', 'error'); + return; + } + + if (password.length < 6) { + window.adminCore.showStatus('Password must be at least 6 characters long', 'error'); + return; + } + + if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) { + window.adminCore.showStatus('Expiration days must be between 1 and 365 for temporary users', 'error'); + return; + } + + try { + const userData = { + email, + password, + name: name || '', + isAdmin: userType === 'admin' || admin, + userType, + expireDays + }; + + const response = await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }); + + const data = await response.json(); + + if (data.success) { + window.adminCore.showStatus('User created successfully', 'success'); + clearUserForm(); + loadUsers(); // Reload the users list + } else { + throw new Error(data.error || 'Failed to create user'); + } + + } catch (error) { + console.error('Error creating user:', error); + window.adminCore.showStatus(`Failed to create user: ${error.message}`, 'error'); + } +} + +function clearUserForm() { + const form = document.getElementById('create-user-form'); + if (form) { + form.reset(); + + // Reset user type to default + const userTypeSelect = document.getElementById('user-type'); + if (userTypeSelect) { + userTypeSelect.value = 'user'; + } + + // Hide expiration group + const expirationGroup = document.getElementById('expiration-group'); + if (expirationGroup) { + expirationGroup.style.display = 'none'; + } + + // Re-enable admin checkbox + const isAdminCheckbox = document.getElementById('user-is-admin'); + if (isAdminCheckbox) { + isAdminCheckbox.disabled = false; + } + + window.adminCore.showStatus('User form cleared', 'info'); + } +} + +// Setup user-related event listeners +function setupUserEventListeners() { + // User form submission + const userForm = document.getElementById('create-user-form'); + if (userForm) { + userForm.addEventListener('submit', createUser); + } + + // Clear user form button + const clearUserBtn = document.getElementById('clear-user-form'); + if (clearUserBtn) { + clearUserBtn.addEventListener('click', clearUserForm); + } + + // User type change listener + const userTypeSelect = document.getElementById('user-type'); + if (userTypeSelect) { + userTypeSelect.addEventListener('change', (e) => { + const expirationGroup = document.getElementById('expiration-group'); + const isAdminCheckbox = document.getElementById('user-is-admin'); + + if (e.target.value === 'temp') { + if (expirationGroup) expirationGroup.style.display = 'block'; + if (isAdminCheckbox) { + isAdminCheckbox.checked = false; + isAdminCheckbox.disabled = true; + } + } else { + if (expirationGroup) expirationGroup.style.display = 'none'; + if (isAdminCheckbox) isAdminCheckbox.disabled = false; + + if (e.target.value === 'admin') { + if (isAdminCheckbox) isAdminCheckbox.checked = true; + } else { + if (isAdminCheckbox) isAdminCheckbox.checked = false; + } + } + }); + } +} + +// Export user management functions +window.adminUsers = { + loadUsers, + displayUsers, + deleteUser, + sendLoginDetailsToUser, + createUser, + clearUserForm, + setupUserEventListeners, + getAllUsersData: () => allUsersData, + setAllUsersData: (data) => { allUsersData = data; } +}; diff --git a/map/app/public/js/admin-walksheet.js b/map/app/public/js/admin-walksheet.js new file mode 100644 index 0000000..366d2c6 --- /dev/null +++ b/map/app/public/js/admin-walksheet.js @@ -0,0 +1,471 @@ +/** + * Admin Walk Sheet Module + * Handles walk sheet configuration, preview generation, QR codes, and printing + */ + +// Walk sheet state +let storedQRCodes = {}; + +// Save walk sheet configuration +async function saveWalkSheetConfig() { + const config = { + walk_sheet_title: document.getElementById('walk-sheet-title')?.value || '', + walk_sheet_subtitle: document.getElementById('walk-sheet-subtitle')?.value || '', + walk_sheet_footer: document.getElementById('walk-sheet-footer')?.value || '', + qr_code_1_url: document.getElementById('qr-code-1-url')?.value || '', + qr_code_1_label: document.getElementById('qr-code-1-label')?.value || '', + qr_code_2_url: document.getElementById('qr-code-2-url')?.value || '', + qr_code_2_label: document.getElementById('qr-code-2-label')?.value || '', + qr_code_3_url: document.getElementById('qr-code-3-url')?.value || '', + qr_code_3_label: document.getElementById('qr-code-3-label')?.value || '' + }; + + console.log('Saving walk sheet config:', config); + + // Show loading state + const saveButton = document.getElementById('save-walk-sheet'); + if (!saveButton) { + window.adminCore.showStatus('Save button not found', 'error'); + return; + } + + const originalText = saveButton.textContent; + saveButton.textContent = 'Saving...'; + saveButton.disabled = true; + + try { + const response = await fetch('/api/admin/walk-sheet-config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }); + + const data = await response.json(); + console.log('Save response:', data); + + if (data.success) { + window.adminCore.showStatus('Walk sheet configuration saved successfully!', 'success'); + console.log('Configuration saved successfully'); + // Don't reload config here - the form already has the latest values + // Just regenerate the preview + generateWalkSheetPreview(); + } else { + throw new Error(data.error || 'Failed to save'); + } + } catch (error) { + console.error('Save error:', error); + window.adminCore.showStatus(error.message || 'Failed to save walk sheet configuration', 'error'); + } finally { + saveButton.textContent = originalText; + saveButton.disabled = false; + } +} + +// Generate walk sheet preview +function generateWalkSheetPreview() { + const title = document.getElementById('walk-sheet-title')?.value || 'Campaign Walk Sheet'; + const subtitle = document.getElementById('walk-sheet-subtitle')?.value || 'Door-to-Door Canvassing Form'; + const footer = document.getElementById('walk-sheet-footer')?.value || 'Thank you for your support!'; + + let previewHTML = ` +
+

${window.adminCore.escapeHtml(title)}

+

${window.adminCore.escapeHtml(subtitle)}

+
+ `; + + // Add QR codes section + const qrCodesHTML = []; + for (let i = 1; i <= 3; i++) { + const urlInput = document.getElementById(`qr-code-${i}-url`); + const labelInput = document.getElementById(`qr-code-${i}-label`); + + const url = urlInput?.value || ''; + const label = labelInput?.value || ''; + + if (url) { + qrCodesHTML.push(` +
+
+ +
+
${window.adminCore.escapeHtml(label || `QR Code ${i}`)}
+
+ `); + } + } + + if (qrCodesHTML.length > 0) { + previewHTML += ` +
+ ${qrCodesHTML.join('')} +
+ `; + } + + // Add form fields based on the main map form + previewHTML += ` +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+ 1 + 2 + 3 + 4 +
+
+
+ +
+ Y Yes + N No +
+
+
+ +
+
+ +
+ R + L + U +
+
+
+ +
+
+
+
+ +
+
Notes & Comments
+
+
+ `; + + // Add footer + if (footer) { + previewHTML += ` + + `; + } + + // Update preview + const previewContent = document.getElementById('walk-sheet-preview-content'); + if (previewContent) { + previewContent.innerHTML = previewHTML; + + // Generate QR codes after DOM is updated + setTimeout(() => { + generatePreviewQRCodes(); + }, 100); + } else { + console.warn('Walk sheet preview content container not found'); + } +} + +// Generate QR codes for preview +async function generatePreviewQRCodes() { + for (let i = 1; i <= 3; i++) { + const urlInput = document.getElementById(`qr-code-${i}-url`); + const url = urlInput?.value || ''; + const qrContainer = document.getElementById(`preview-qr-${i}`); + + if (url && qrContainer) { + try { + // Use our local QR code generation endpoint with size matching display + const qrImageUrl = `/api/qr?text=${encodeURIComponent(url)}&size=200`; // Generate at higher res + qrContainer.innerHTML = `QR Code ${i}`; // Display smaller + } catch (error) { + console.error(`Failed to display QR code ${i}:`, error); + qrContainer.innerHTML = '
QR Error
'; + } + } else if (qrContainer) { + // Clear empty QR containers + qrContainer.innerHTML = ''; + } + } +} + +// Print walk sheet +function printWalkSheet() { + // First generate fresh preview to ensure QR codes are generated + generateWalkSheetPreview(); + + // Wait for QR codes to generate, then print + setTimeout(() => { + const previewContent = document.getElementById('walk-sheet-preview-content'); + if (!previewContent) return; + + const clonedContent = previewContent.cloneNode(true); + + // Convert canvas elements to images for printing + const canvases = previewContent.querySelectorAll('canvas'); + const clonedCanvases = clonedContent.querySelectorAll('canvas'); + + canvases.forEach((canvas, index) => { + if (canvas && clonedCanvases[index]) { + const img = document.createElement('img'); + img.src = canvas.toDataURL('image/png'); + img.width = canvas.width; + img.height = canvas.height; + img.style.width = canvas.style.width || `${canvas.width}px`; + img.style.height = canvas.style.height || `${canvas.height}px`; + clonedCanvases[index].parentNode.replaceChild(img, clonedCanvases[index]); + } + }); + + // Create a print-specific window + const printWindow = window.open('', '_blank'); + + printWindow.document.write(` + + + + Walk Sheet - Print + + + + + +
+ ${clonedContent.innerHTML} +
+ + + `); + + printWindow.document.close(); + + printWindow.onload = function() { + setTimeout(() => { + printWindow.print(); + // User can close manually after printing + }, 500); + }; + }, 1000); // Give QR codes time to generate +} + +// Load walk sheet configuration +async function loadWalkSheetConfig() { + try { + console.log('Loading walk sheet config...'); + const response = await fetch('/api/admin/walk-sheet-config'); + const data = await response.json(); + + console.log('Loaded walk sheet config:', data); + + if (data.success) { + // The config object contains the actual configuration + const config = data.config || {}; + + console.log('Config object:', config); + + // Populate form fields - use the exact field names from the backend + const titleInput = document.getElementById('walk-sheet-title'); + const subtitleInput = document.getElementById('walk-sheet-subtitle'); + const footerInput = document.getElementById('walk-sheet-footer'); + + console.log('Found form elements:', { + title: !!titleInput, + subtitle: !!subtitleInput, + footer: !!footerInput + }); + + if (titleInput) { + titleInput.value = config.walk_sheet_title || 'Campaign Walk Sheet'; + console.log('Set title to:', titleInput.value); + } + if (subtitleInput) { + subtitleInput.value = config.walk_sheet_subtitle || 'Door-to-Door Canvassing Form'; + console.log('Set subtitle to:', subtitleInput.value); + } + if (footerInput) { + footerInput.value = config.walk_sheet_footer || 'Thank you for your support!'; + console.log('Set footer to:', footerInput.value); + } + + // Populate QR code fields + for (let i = 1; i <= 3; i++) { + const urlField = document.getElementById(`qr-code-${i}-url`); + const labelField = document.getElementById(`qr-code-${i}-label`); + + console.log(`QR ${i} fields found:`, { + url: !!urlField, + label: !!labelField + }); + + if (urlField) { + urlField.value = config[`qr_code_${i}_url`] || ''; + console.log(`Set QR ${i} URL to:`, urlField.value); + } + if (labelField) { + labelField.value = config[`qr_code_${i}_label`] || ''; + console.log(`Set QR ${i} label to:`, labelField.value); + } + } + + return true; + } else { + console.error('Failed to load config:', data.error); + window.adminCore.showStatus('Failed to load walk sheet configuration', 'error'); + return false; + } + + } catch (error) { + console.error('Failed to load walk sheet config:', error); + window.adminCore.showStatus('Failed to load walk sheet configuration', 'error'); + return false; + } +} + +// Check if walk sheet section is visible and load config if needed +function checkAndLoadWalkSheetConfig() { + const walkSheetSection = document.getElementById('walk-sheet'); + if (walkSheetSection && walkSheetSection.style.display !== 'none') { + console.log('Walk sheet section is visible, loading config...'); + loadWalkSheetConfig().then((success) => { + if (success) { + generateWalkSheetPreview(); + } + }); + } +} + +// Setup walk sheet event listeners +function setupWalkSheetEventListeners() { + // Walk Sheet buttons + const saveWalkSheetBtn = document.getElementById('save-walk-sheet'); + const previewWalkSheetBtn = document.getElementById('preview-walk-sheet'); + const printWalkSheetBtn = document.getElementById('print-walk-sheet'); + const refreshPreviewBtn = document.getElementById('refresh-preview'); + + if (saveWalkSheetBtn) saveWalkSheetBtn.addEventListener('click', saveWalkSheetConfig); + if (previewWalkSheetBtn) previewWalkSheetBtn.addEventListener('click', generateWalkSheetPreview); + if (printWalkSheetBtn) printWalkSheetBtn.addEventListener('click', printWalkSheet); + if (refreshPreviewBtn) refreshPreviewBtn.addEventListener('click', generateWalkSheetPreview); + + // Auto-update preview on input change + const walkSheetInputs = document.querySelectorAll( + '#walk-sheet-title, #walk-sheet-subtitle, #walk-sheet-footer, ' + + '[id^="qr-code-"][id$="-url"], [id^="qr-code-"][id$="-label"]' + ); + + walkSheetInputs.forEach(input => { + if (input) { + input.addEventListener('input', window.adminCore.debounce(() => { + generateWalkSheetPreview(); + }, 500)); + } + }); + + // Add URL change listeners to detect when QR codes need regeneration + for (let i = 1; i <= 3; i++) { + const urlInput = document.getElementById(`qr-code-${i}-url`); + if (urlInput) { + let previousUrl = urlInput.value; + + urlInput.addEventListener('change', () => { + const currentUrl = urlInput.value; + if (currentUrl !== previousUrl) { + console.log(`QR Code ${i} URL changed from "${previousUrl}" to "${currentUrl}"`); + // Remove stored QR code so it gets regenerated + delete storedQRCodes[currentUrl]; + previousUrl = currentUrl; + generateWalkSheetPreview(); + } + }); + } + } +} + +// Export walk sheet functions +window.adminWalkSheet = { + saveWalkSheetConfig, + generateWalkSheetPreview, + printWalkSheet, + loadWalkSheetConfig, + checkAndLoadWalkSheetConfig, + setupWalkSheetEventListeners +}; diff --git a/map/app/public/js/admin.js b/map/app/public/js/admin.js index 2d8fa12..6af5838 100644 --- a/map/app/public/js/admin.js +++ b/map/app/public/js/admin.js @@ -1,2774 +1,143 @@ -// Admin panel JavaScript -let adminMap = null; -let startMarker = null; -let storedQRCodes = {}; -let editingShiftId = null; +/** + * Main Admin Panel Coordinator + * This refactored admin.js coordinates all admin modules while maintaining functionality + * Modules: core, auth, map, walksheet, shifts, shift-volunteers, users, email, integration + */ -// Utility function to create a local date from YYYY-MM-DD string -// This prevents timezone issues when displaying dates -function createLocalDate(dateString) { - if (!dateString) return null; - const parts = dateString.split('-'); - if (parts.length !== 3) return new Date(dateString); // fallback to original behavior - // Create date using local timezone (year, month-1, day) - return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); -} - -// A function to set viewport dimensions for admin page -function setAdminViewportDimensions() { - const doc = document.documentElement; +/** + * Main Event Listener Setup + * Coordinates event listeners across all modules + */ +function setupAllEventListeners() { + // Setup authentication listeners + if (window.adminAuth && typeof window.adminAuth.setupAuthEventListeners === 'function') { + window.adminAuth.setupAuthEventListeners(); + } - // Set height and width - doc.style.setProperty('--app-height', `${window.innerHeight}px`); - doc.style.setProperty('--app-width', `${window.innerWidth}px`); + // Setup map listeners + if (window.adminMap && typeof window.adminMap.setupMapEventListeners === 'function') { + window.adminMap.setupMapEventListeners(); + } - // Handle safe area insets for devices with notches or home indicators - if (CSS.supports('padding: env(safe-area-inset-top)')) { - doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)'); - doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)'); - doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)'); - doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)'); - } else { - doc.style.setProperty('--safe-area-top', '0px'); - doc.style.setProperty('--safe-area-bottom', '0px'); - doc.style.setProperty('--safe-area-left', '0px'); - doc.style.setProperty('--safe-area-right', '0px'); + // Setup walk sheet listeners + if (window.adminWalkSheet && typeof window.adminWalkSheet.setupWalkSheetEventListeners === 'function') { + window.adminWalkSheet.setupWalkSheetEventListeners(); + } + + // Setup shift listeners + if (window.adminShifts && typeof window.adminShifts.setupShiftEventListeners === 'function') { + window.adminShifts.setupShiftEventListeners(); + } + + // Setup shift volunteer listeners + if (window.adminShiftVolunteers && typeof window.adminShiftVolunteers.setupShiftVolunteerEventListeners === 'function') { + window.adminShiftVolunteers.setupShiftVolunteerEventListeners(); + } + + // Setup user listeners + if (window.adminUsers && typeof window.adminUsers.setupUserEventListeners === 'function') { + window.adminUsers.setupUserEventListeners(); + } + + // Setup email listeners + if (window.adminEmail && typeof window.adminEmail.setupEmailEventListeners === 'function') { + window.adminEmail.setupEmailEventListeners(); } } -// Initialize when DOM is loaded +// Main admin initialization when DOM is loaded document.addEventListener('DOMContentLoaded', () => { - // Set initial viewport dimensions and listen for resize events - setAdminViewportDimensions(); - window.addEventListener('resize', setAdminViewportDimensions); - window.addEventListener('orientationchange', () => { - // Add a small delay for orientation change to complete - setTimeout(setAdminViewportDimensions, 100); - }); + // Initialize core functionality + if (window.adminCore && typeof window.adminCore.initializeAdminCore === 'function') { + window.adminCore.initializeAdminCore(); + } - checkAdminAuth(); - initializeAdminMap(); - loadCurrentStartLocation(); - setupEventListeners(); - setupNavigation(); - setupMobileMenu(); + // Initialize authentication + if (window.adminAuth && typeof window.adminAuth.checkAdminAuth === 'function') { + window.adminAuth.checkAdminAuth(); + } - // Initialize NocoDB links with a small delay to ensure DOM is ready + // Initialize admin map + if (window.adminMap && typeof window.adminMap.initializeAdminMap === 'function') { + window.adminMap.initializeAdminMap(); + } + + // Load current start location + if (window.adminMap && typeof window.adminMap.loadCurrentStartLocation === 'function') { + window.adminMap.loadCurrentStartLocation(); + } + + // Setup all event listeners + setupAllEventListeners(); + + // Initialize integrations with a small delay to ensure DOM is ready setTimeout(() => { - loadWalkSheetConfig(); - initializeNocodbLinks(); - initializeListmonkLinks(); + if (window.adminWalkSheet && typeof window.adminWalkSheet.loadWalkSheetConfig === 'function') { + window.adminWalkSheet.loadWalkSheetConfig(); + } + if (window.adminIntegration && typeof window.adminIntegration.initializeAllIntegrations === 'function') { + window.adminIntegration.initializeAllIntegrations(); + } }, 100); // Check if URL has a hash to show specific section const hash = window.location.hash; if (hash === '#walk-sheet') { - showSection('walk-sheet'); - checkAndLoadWalkSheetConfig(); + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('walk-sheet'); + } + if (window.adminWalkSheet && typeof window.adminWalkSheet.checkAndLoadWalkSheetConfig === 'function') { + window.adminWalkSheet.checkAndLoadWalkSheetConfig(); + } } else if (hash === '#convert-data') { - showSection('convert-data'); + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('convert-data'); + } } else if (hash === '#cuts') { - showSection('cuts'); + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('cuts'); + } } else { // Default to dashboard - showSection('dashboard'); + if (window.adminCore && typeof window.adminCore.showSection === 'function') { + window.adminCore.showSection('dashboard'); + } // Load dashboard data on initial page load - loadDashboardData(); + if (typeof loadDashboardData === 'function') { + loadDashboardData(); + } } }); -// Add mobile menu functionality -function setupMobileMenu() { - const menuToggle = document.getElementById('mobile-menu-toggle'); - const sidebar = document.getElementById('admin-sidebar'); - const closeSidebar = document.getElementById('close-sidebar'); - const adminNavLinks = document.querySelectorAll('.admin-nav a'); - - if (menuToggle && sidebar) { - // Toggle menu - menuToggle.addEventListener('click', () => { - sidebar.classList.toggle('active'); - menuToggle.classList.toggle('active'); - document.body.classList.toggle('sidebar-open'); // This line already exists - }); - - // Close sidebar button - if (closeSidebar) { - closeSidebar.addEventListener('click', () => { - sidebar.classList.remove('active'); - menuToggle.classList.remove('active'); - document.body.classList.remove('sidebar-open'); - }); - } - - // Close sidebar when clicking outside - document.addEventListener('click', (e) => { - if (sidebar.classList.contains('active') && - !sidebar.contains(e.target) && - !menuToggle.contains(e.target)) { - sidebar.classList.remove('active'); - menuToggle.classList.remove('active'); - document.body.classList.remove('sidebar-open'); - } - }); - - // Close sidebar when navigation link is clicked on mobile - adminNavLinks.forEach(link => { - link.addEventListener('click', () => { - if (window.innerWidth <= 768) { - sidebar.classList.remove('active'); - menuToggle.classList.remove('active'); - document.body.classList.remove('sidebar-open'); - } - }); - }); - } -} - -// Check if user is authenticated as admin -async function checkAdminAuth() { +/** + * Dashboard Functions + * Loads dashboard data and displays summary statistics + */ +async function loadDashboardData() { try { - const response = await fetch('/api/auth/check'); - const data = await response.json(); - - console.log('Admin auth check result:', data); - - if (!data.authenticated || !data.user?.isAdmin) { - console.log('Redirecting to login - not authenticated or not admin'); - window.location.href = '/login.html'; - return; - } - - console.log('User is authenticated as admin:', data.user); - - // Display admin info (desktop) - document.getElementById('admin-info').innerHTML = ` - 👤 ${escapeHtml(data.user.email)} - - `; - - // Display admin info (mobile) - const mobileAdminInfo = document.getElementById('mobile-admin-info'); - if (mobileAdminInfo) { - mobileAdminInfo.innerHTML = ` -
👤 ${escapeHtml(data.user.email)}
- - `; - - // Add logout listener for mobile button - document.getElementById('mobile-logout-btn')?.addEventListener('click', handleLogout); - } - - document.getElementById('logout-btn').addEventListener('click', handleLogout); - - } catch (error) { - console.error('Auth check failed:', error); - window.location.href = '/login.html'; - } -} - -// Initialize the admin map -function initializeAdminMap() { - adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - maxZoom: 19, - minZoom: 2 - }).addTo(adminMap); - - // Add crosshair to center of map - const crosshairIcon = L.divIcon({ - className: 'crosshair', - iconSize: [20, 20], - html: '
' - }); - - const crosshair = L.marker(adminMap.getCenter(), { - icon: crosshairIcon, - interactive: false, - zIndexOffset: 1000 - }).addTo(adminMap); - - // Update crosshair position when map moves - adminMap.on('move', function() { - crosshair.setLatLng(adminMap.getCenter()); - }); - - // Add click handler to set location - adminMap.on('click', handleMapClick); - - // Update coordinates when map moves - adminMap.on('moveend', updateCoordinatesFromMap); -} - -// Load current start location -async function loadCurrentStartLocation() { - try { - const response = await fetch('/api/admin/start-location'); + const response = await fetch('/api/admin/dashboard'); const data = await response.json(); if (data.success) { - const { latitude, longitude, zoom } = data.location; - - // Update form fields - document.getElementById('start-lat').value = latitude; - document.getElementById('start-lng').value = longitude; - document.getElementById('start-zoom').value = zoom; - - // Update map - adminMap.setView([latitude, longitude], zoom); - updateStartMarker(latitude, longitude); - - // Show source info - if (data.source) { - const sourceText = data.source === 'database' ? 'Loaded from database' : - data.source === 'environment' ? 'Using environment defaults' : - 'Using system defaults'; - showStatus(sourceText, 'info'); - } - } - - } catch (error) { - console.error('Failed to load start location:', error); - showStatus('Failed to load current start location', 'error'); - } -} - -// Handle map click -function handleMapClick(e) { - const { lat, lng } = e.latlng; - - document.getElementById('start-lat').value = lat.toFixed(6); - document.getElementById('start-lng').value = lng.toFixed(6); - - updateStartMarker(lat, lng); -} - -// Update marker position -function updateStartMarker(lat, lng) { - if (startMarker) { - startMarker.setLatLng([lat, lng]); - } else { - startMarker = L.marker([lat, lng], { - draggable: true, - title: 'Start Location' - }).addTo(adminMap); - - // Update coordinates when marker is dragged - startMarker.on('dragend', (e) => { - const position = e.target.getLatLng(); - document.getElementById('start-lat').value = position.lat.toFixed(6); - document.getElementById('start-lng').value = position.lng.toFixed(6); - }); - } -} - -// Update coordinates from current map view -function updateCoordinatesFromMap() { - const center = adminMap.getCenter(); - const zoom = adminMap.getZoom(); - - document.getElementById('start-zoom').value = zoom; -} - -// Setup event listeners -function setupEventListeners() { - // Use current view button - const useCurrentViewBtn = document.getElementById('use-current-view'); - if (useCurrentViewBtn) { - useCurrentViewBtn.addEventListener('click', () => { - const center = adminMap.getCenter(); - const zoom = adminMap.getZoom(); - - document.getElementById('start-lat').value = center.lat.toFixed(6); - document.getElementById('start-lng').value = center.lng.toFixed(6); - document.getElementById('start-zoom').value = zoom; - - updateStartMarker(center.lat, center.lng); - showStatus('Captured current map view', 'success'); - }); - } - - // Save button - const saveLocationBtn = document.getElementById('save-start-location'); - if (saveLocationBtn) { - saveLocationBtn.addEventListener('click', saveStartLocation); - } - - // Coordinate input changes - const startLatInput = document.getElementById('start-lat'); - const startLngInput = document.getElementById('start-lng'); - const startZoomInput = document.getElementById('start-zoom'); - - if (startLatInput) startLatInput.addEventListener('change', updateMapFromInputs); - if (startLngInput) startLngInput.addEventListener('change', updateMapFromInputs); - if (startZoomInput) startZoomInput.addEventListener('change', updateMapFromInputs); - - // Walk Sheet buttons - const saveWalkSheetBtn = document.getElementById('save-walk-sheet'); - const previewWalkSheetBtn = document.getElementById('preview-walk-sheet'); - const printWalkSheetBtn = document.getElementById('print-walk-sheet'); - const refreshPreviewBtn = document.getElementById('refresh-preview'); - - if (saveWalkSheetBtn) saveWalkSheetBtn.addEventListener('click', saveWalkSheetConfig); - if (previewWalkSheetBtn) previewWalkSheetBtn.addEventListener('click', generateWalkSheetPreview); - if (printWalkSheetBtn) printWalkSheetBtn.addEventListener('click', printWalkSheet); - if (refreshPreviewBtn) refreshPreviewBtn.addEventListener('click', generateWalkSheetPreview); - - // Auto-update preview on input change - const walkSheetInputs = document.querySelectorAll( - '#walk-sheet-title, #walk-sheet-subtitle, #walk-sheet-footer, ' + - '[id^="qr-code-"][id$="-url"], [id^="qr-code-"][id$="-label"]' - ); - - walkSheetInputs.forEach(input => { - if (input) { - input.addEventListener('input', debounce(() => { - generateWalkSheetPreview(); - }, 500)); - } - }); - - // Add URL change listeners to detect when QR codes need regeneration - for (let i = 1; i <= 3; i++) { - const urlInput = document.getElementById(`qr-code-${i}-url`); - if (urlInput) { - let previousUrl = urlInput.value; - - urlInput.addEventListener('change', () => { - const currentUrl = urlInput.value; - if (currentUrl !== previousUrl) { - console.log(`QR Code ${i} URL changed from "${previousUrl}" to "${currentUrl}"`); - // Remove stored QR code so it gets regenerated - delete storedQRCodes[currentUrl]; - previousUrl = currentUrl; - generateWalkSheetPreview(); - } - }); - } - } - - // Shift form submission - const shiftForm = document.getElementById('shift-form'); - if (shiftForm) { - shiftForm.addEventListener('submit', createShift); - } - - // Clear shift form button - const clearShiftBtn = document.getElementById('clear-shift-form'); - if (clearShiftBtn) { - clearShiftBtn.addEventListener('click', function() { - const wasEditing = editingShiftId !== null; - clearShiftForm(); - if (wasEditing) { - showStatus('Edit cancelled', 'info'); - } - }); - } - - // User form submission - const userForm = document.getElementById('create-user-form'); - if (userForm) { - userForm.addEventListener('submit', createUser); - } - - // Clear user form button - const clearUserBtn = document.getElementById('clear-user-form'); - if (clearUserBtn) { - clearUserBtn.addEventListener('click', clearUserForm); - } - - // User type change listener - const userTypeSelect = document.getElementById('user-type'); - if (userTypeSelect) { - userTypeSelect.addEventListener('change', (e) => { - const expirationGroup = document.getElementById('expiration-group'); - const isAdminCheckbox = document.getElementById('user-is-admin'); - - if (e.target.value === 'temp') { - expirationGroup.style.display = 'block'; - isAdminCheckbox.checked = false; - isAdminCheckbox.disabled = true; - } else { - expirationGroup.style.display = 'none'; - isAdminCheckbox.disabled = false; - - if (e.target.value === 'admin') { - isAdminCheckbox.checked = true; - } else { - isAdminCheckbox.checked = false; - } - } - }); - } -} - -// Setup navigation between admin sections -function setupNavigation() { - const navLinks = document.querySelectorAll('.admin-nav a'); - const sections = document.querySelectorAll('.admin-section'); - - navLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - - const targetId = link.getAttribute('href').substring(1); - - // Update active nav - navLinks.forEach(l => l.classList.remove('active')); - link.classList.add('active'); - - // Show target section - sections.forEach(section => { - section.style.display = section.id === targetId ? 'block' : 'none'; - }); - - // Update URL hash - window.location.hash = targetId; - - // Load section-specific data - if (targetId === 'walk-sheet') { - checkAndLoadWalkSheetConfig(); - } else if (targetId === 'dashboard') { - loadDashboardData(); - } else if (targetId === 'shifts') { - loadAdminShifts(); - } else if (targetId === 'users') { - loadUsers(); - } else if (targetId === 'convert-data') { - // Initialize data convert event listeners when section is shown - setTimeout(() => { - if (typeof window.setupDataConvertEventListeners === 'function') { - console.log('Setting up data convert event listeners...'); - window.setupDataConvertEventListeners(); - } else { - console.error('setupDataConvertEventListeners not found'); - } - }, 100); - } - - // Close mobile menu if open - const sidebar = document.getElementById('admin-sidebar'); - if (sidebar && sidebar.classList.contains('open')) { - sidebar.classList.remove('open'); - } - }); - }); - - // Set initial active state based on current hash or default - const currentHash = window.location.hash || '#dashboard'; - const activeLink = document.querySelector(`.admin-nav a[href="${currentHash}"]`); - if (activeLink) { - activeLink.classList.add('active'); - } - - // Also check if we're already on the shifts page (via hash) - const hash = window.location.hash; - if (hash === '#shifts') { - showSection('shifts'); - loadAdminShifts(); - } -} - -// Helper function to show a specific section -function showSection(sectionId) { - const sections = document.querySelectorAll('.admin-section'); - const navLinks = document.querySelectorAll('.admin-nav a'); - - // Hide all sections - sections.forEach(section => { - section.style.display = section.id === sectionId ? 'block' : 'none'; - }); - - // Update active nav - navLinks.forEach(link => { - const linkTarget = link.getAttribute('href').substring(1); - link.classList.toggle('active', linkTarget === sectionId); - }); - - // Special handling for convert-data section - if (sectionId === 'convert-data') { - // Initialize data convert event listeners when section is shown - setTimeout(() => { - if (typeof window.setupDataConvertEventListeners === 'function') { - console.log('Setting up data convert event listeners from showSection...'); - window.setupDataConvertEventListeners(); - } else { - console.error('setupDataConvertEventListeners not found in showSection'); - } - }, 100); - } - - // Special handling for cuts section - if (sectionId === 'cuts') { - // Initialize admin cuts manager when section is shown - setTimeout(() => { - if (typeof window.adminCutsManager === 'object' && window.adminCutsManager.initialize) { - if (!window.adminCutsManager.isInitialized) { - console.log('Initializing admin cuts manager from showSection...'); - window.adminCutsManager.initialize().catch(error => { - console.error('Failed to initialize cuts manager:', error); - }); - } else { - console.log('Admin cuts manager already initialized'); - } - } else { - console.error('adminCutsManager not found in showSection'); - } - }, 100); - } - - // Special handling for shifts section - if (sectionId === 'shifts') { - console.log('Loading shifts for admin panel...'); - loadAdminShifts(); - } -} - -// Update map from input fields -function updateMapFromInputs() { - const lat = parseFloat(document.getElementById('start-lat').value); - const lng = parseFloat(document.getElementById('start-lng').value); - const zoom = parseInt(document.getElementById('start-zoom').value); - - if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) { - adminMap.setView([lat, lng], zoom); - updateStartMarker(lat, lng); - } -} - -// Save start location -async function saveStartLocation() { - const lat = parseFloat(document.getElementById('start-lat').value); - const lng = parseFloat(document.getElementById('start-lng').value); - const zoom = parseInt(document.getElementById('start-zoom').value); - - // Validate - if (isNaN(lat) || isNaN(lng) || isNaN(zoom)) { - showStatus('Please enter valid coordinates and zoom level', 'error'); - return; - } - - if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { - showStatus('Coordinates out of valid range', 'error'); - return; - } - - if (zoom < 2 || zoom > 19) { - showStatus('Zoom level must be between 2 and 19', 'error'); - return; - } - - try { - const response = await fetch('/api/admin/start-location', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - latitude: lat, - longitude: lng, - zoom: zoom - }) - }); - - const data = await response.json(); - - if (data.success) { - showStatus('Start location saved successfully!', 'success'); - } else { - throw new Error(data.error || 'Failed to save'); - } - - } catch (error) { - console.error('Save error:', error); - showStatus(error.message || 'Failed to save start location', 'error'); - } -} - -// Save walk sheet configuration -async function saveWalkSheetConfig() { - const config = { - walk_sheet_title: document.getElementById('walk-sheet-title')?.value || '', - walk_sheet_subtitle: document.getElementById('walk-sheet-subtitle')?.value || '', - walk_sheet_footer: document.getElementById('walk-sheet-footer')?.value || '', - qr_code_1_url: document.getElementById('qr-code-1-url')?.value || '', - qr_code_1_label: document.getElementById('qr-code-1-label')?.value || '', - qr_code_2_url: document.getElementById('qr-code-2-url')?.value || '', - qr_code_2_label: document.getElementById('qr-code-2-label')?.value || '', - qr_code_3_url: document.getElementById('qr-code-3-url')?.value || '', - qr_code_3_label: document.getElementById('qr-code-3-label')?.value || '' - }; - - console.log('Saving walk sheet config:', config); - - // Show loading state - const saveButton = document.getElementById('save-walk-sheet'); - if (!saveButton) { - showStatus('Save button not found', 'error'); - return; - } - - const originalText = saveButton.textContent; - saveButton.textContent = 'Saving...'; - saveButton.disabled = true; - - try { - const response = await fetch('/api/admin/walk-sheet-config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(config) - }); - - const data = await response.json(); - console.log('Save response:', data); - - if (data.success) { - showStatus('Walk sheet configuration saved successfully!', 'success'); - console.log('Configuration saved successfully'); - // Don't reload config here - the form already has the latest values - // Just regenerate the preview - generateWalkSheetPreview(); - } else { - throw new Error(data.error || 'Failed to save'); + document.getElementById('total-users').textContent = data.stats.totalUsers; + document.getElementById('total-shifts').textContent = data.stats.totalShifts; + document.getElementById('total-signups').textContent = data.stats.totalSignups; + document.getElementById('this-month-users').textContent = data.stats.thisMonthUsers; } } catch (error) { - console.error('Save error:', error); - showStatus(error.message || 'Failed to save walk sheet configuration', 'error'); - } finally { - saveButton.textContent = originalText; - saveButton.disabled = false; + console.error('Failed to load dashboard data:', error); } } -// Generate walk sheet preview -function generateWalkSheetPreview() { - const title = document.getElementById('walk-sheet-title')?.value || 'Campaign Walk Sheet'; - const subtitle = document.getElementById('walk-sheet-subtitle')?.value || 'Door-to-Door Canvassing Form'; - const footer = document.getElementById('walk-sheet-footer')?.value || 'Thank you for your support!'; - - let previewHTML = ` -
-

${escapeHtml(title)}

-

${escapeHtml(subtitle)}

-
- `; - - // Add QR codes section - const qrCodesHTML = []; - for (let i = 1; i <= 3; i++) { - const urlInput = document.getElementById(`qr-code-${i}-url`); - const labelInput = document.getElementById(`qr-code-${i}-label`); - - const url = urlInput?.value || ''; - const label = labelInput?.value || ''; - - if (url) { - qrCodesHTML.push(` -
-
- -
-
${escapeHtml(label || `QR Code ${i}`)}
-
- `); - } - } - - if (qrCodesHTML.length > 0) { - previewHTML += ` -
- ${qrCodesHTML.join('')} -
- `; - } - - // Add form fields based on the main map form - previewHTML += ` -
-
-
- -
-
-
- -
-
-
- -
-
- -
-
-
- -
-
-
- -
-
- -
-
-
- -
-
-
- -
-
- -
- 1 - 2 - 3 - 4 -
-
-
- -
- Y Yes - N No -
-
-
- -
-
- -
- R - L - U -
-
-
- -
-
-
-
- -
-
Notes & Comments
-
-
- `; - - // Add footer - if (footer) { - previewHTML += ` - - `; - } - - // Update preview - const previewContent = document.getElementById('walk-sheet-preview-content'); - if (previewContent) { - previewContent.innerHTML = previewHTML; - - // Generate QR codes after DOM is updated - setTimeout(() => { - generatePreviewQRCodes(); - }, 100); - } else { - console.warn('Walk sheet preview content container not found'); - } -} +/** + * Legacy function redirects for backward compatibility + * These ensure existing functionality continues to work + */ +window.loadDashboardData = loadDashboardData; -// Update the generatePreviewQRCodes function to use smaller size -async function generatePreviewQRCodes() { - for (let i = 1; i <= 3; i++) { - const urlInput = document.getElementById(`qr-code-${i}-url`); - const url = urlInput?.value || ''; - const qrContainer = document.getElementById(`preview-qr-${i}`); - - if (url && qrContainer) { - try { - // Use our local QR code generation endpoint with size matching display - const qrImageUrl = `/api/qr?text=${encodeURIComponent(url)}&size=200`; // Generate at higher res - qrContainer.innerHTML = `QR Code ${i}`; // Display smaller - } catch (error) { - console.error(`Failed to display QR code ${i}:`, error); - qrContainer.innerHTML = '
QR Error
'; - } - } else if (qrContainer) { - // Clear empty QR containers - qrContainer.innerHTML = ''; - } - } -} - - -// Print walk sheet -function printWalkSheet() { - // First generate fresh preview to ensure QR codes are generated - generateWalkSheetPreview(); - - // Wait for QR codes to generate, then print - setTimeout(() => { - const previewContent = document.getElementById('walk-sheet-preview-content'); - const clonedContent = previewContent.cloneNode(true); - - // Convert canvas elements to images for printing - const canvases = previewContent.querySelectorAll('canvas'); - const clonedCanvases = clonedContent.querySelectorAll('canvas'); - - canvases.forEach((canvas, index) => { - if (canvas && clonedCanvases[index]) { - const img = document.createElement('img'); - img.src = canvas.toDataURL('image/png'); - img.width = canvas.width; - img.height = canvas.height; - img.style.width = canvas.style.width || `${canvas.width}px`; - img.style.height = canvas.style.height || `${canvas.height}px`; - clonedCanvases[index].parentNode.replaceChild(img, clonedCanvases[index]); - } - }); - - // Create a print-specific window - const printWindow = window.open('', '_blank'); - - printWindow.document.write(` - - - - Walk Sheet - Print - - - - - -
- ${clonedContent.innerHTML} -
- - - `); - - printWindow.document.close(); - - printWindow.onload = function() { - setTimeout(() => { - printWindow.print(); - // User can close manually after printing - }, 500); - }; - }, 1000); // Give QR codes time to generate -} - -// Load walk sheet configuration -async function loadWalkSheetConfig() { - try { - console.log('Loading walk sheet config...'); - const response = await fetch('/api/admin/walk-sheet-config'); - const data = await response.json(); - - console.log('Loaded walk sheet config:', data); - - if (data.success) { - // The config object contains the actual configuration - const config = data.config || {}; - - console.log('Config object:', config); - - // Populate form fields - use the exact field names from the backend - const titleInput = document.getElementById('walk-sheet-title'); - const subtitleInput = document.getElementById('walk-sheet-subtitle'); - const footerInput = document.getElementById('walk-sheet-footer'); - - console.log('Found form elements:', { - title: !!titleInput, - subtitle: !!subtitleInput, - footer: !!footerInput - }); - - if (titleInput) { - titleInput.value = config.walk_sheet_title || 'Campaign Walk Sheet'; - console.log('Set title to:', titleInput.value); - } - if (subtitleInput) { - subtitleInput.value = config.walk_sheet_subtitle || 'Door-to-Door Canvassing Form'; - console.log('Set subtitle to:', subtitleInput.value); - } - if (footerInput) { - footerInput.value = config.walk_sheet_footer || 'Thank you for your support!'; - console.log('Set footer to:', footerInput.value); - } - - // Populate QR code fields - for (let i = 1; i <= 3; i++) { - const urlField = document.getElementById(`qr-code-${i}-url`); - const labelField = document.getElementById(`qr-code-${i}-label`); - - console.log(`QR ${i} fields found:`, { - url: !!urlField, - label: !!labelField - }); - - if (urlField) { - urlField.value = config[`qr_code_${i}_url`] || ''; - console.log(`Set QR ${i} URL to:`, urlField.value); - } - if (labelField) { - labelField.value = config[`qr_code_${i}_label`] || ''; - console.log(`Set QR ${i} label to:`, labelField.value); - } - } - - return true; - } else { - console.error('Failed to load config:', data.error); - showStatus('Failed to load walk sheet configuration', 'error'); - return false; - } - - } catch (error) { - console.error('Failed to load walk sheet config:', error); - showStatus('Failed to load walk sheet configuration', 'error'); - return false; - } -} - -// Check if walk sheet section is visible and load config if needed -function checkAndLoadWalkSheetConfig() { - const walkSheetSection = document.getElementById('walk-sheet'); - if (walkSheetSection && walkSheetSection.style.display !== 'none') { - console.log('Walk sheet section is visible, loading config...'); - loadWalkSheetConfig().then((success) => { - if (success) { - generateWalkSheetPreview(); - } - }); - } -} - -// Add a function to force load config when walk sheet section is accessed -function showWalkSheetSection() { - const walkSheetSection = document.getElementById('walk-sheet'); - const startLocationSection = document.getElementById('start-location'); - - if (startLocationSection) { - startLocationSection.style.display = 'none'; - } - - if (walkSheetSection) { - walkSheetSection.style.display = 'block'; - - // Load config after section is shown - setTimeout(() => { - loadWalkSheetConfig().then((success) => { - if (success) { - generateWalkSheetPreview(); - } - }); - }, 100); // Small delay to ensure DOM is ready - } -} - -// Add event listener to trigger config load when walking sheet nav is clicked -document.addEventListener('DOMContentLoaded', function() { - // Add additional event listener for walk sheet nav - const walkSheetNav = document.querySelector('.admin-nav a[href="#walk-sheet"]'); - if (walkSheetNav) { - walkSheetNav.addEventListener('click', function(e) { - e.preventDefault(); - showWalkSheetSection(); - - // Update nav state - document.querySelectorAll('.admin-nav a').forEach(link => { - link.classList.remove('active'); - }); - this.classList.add('active'); - }); - } -}); - -// Handle logout -async function handleLogout() { - if (!confirm('Are you sure you want to logout?')) { - return; - } - - try { - const response = await fetch('/api/auth/logout', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - window.location.href = '/login.html'; - } else { - showStatus('Logout failed. Please try again.', 'error'); - } - } catch (error) { - console.error('Logout error:', error); - showStatus('Logout failed. Please try again.', 'error'); - } -} - -// Show status message -function showStatus(message, type = 'info') { - let container = document.getElementById('status-container'); - - // Create container if it doesn't exist - if (!container) { - container = document.createElement('div'); - container.id = 'status-container'; - container.className = 'status-container'; - // Ensure proper z-index even if CSS hasn't loaded - container.style.zIndex = '12300'; - document.body.appendChild(container); - } - - const messageDiv = document.createElement('div'); - messageDiv.className = `status-message ${type}`; - messageDiv.textContent = message; - - // Add click to dismiss functionality - messageDiv.addEventListener('click', () => { - messageDiv.remove(); - }); - - // Add a small close button for better UX - const closeBtn = document.createElement('span'); - closeBtn.innerHTML = ' ×'; - closeBtn.style.float = 'right'; - closeBtn.style.fontWeight = 'bold'; - closeBtn.style.marginLeft = '10px'; - closeBtn.style.cursor = 'pointer'; - closeBtn.setAttribute('title', 'Click to dismiss'); - messageDiv.appendChild(closeBtn); - - container.appendChild(messageDiv); - - // Auto-remove after 5 seconds - setTimeout(() => { - if (messageDiv.parentNode) { - messageDiv.remove(); - } - }, 5000); -} - -// Escape HTML -function escapeHtml(text) { - if (text === null || text === undefined) { - return ''; - } - const div = document.createElement('div'); - div.textContent = String(text); - return div.innerHTML; -} - -// Debounce function for input events -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); +// Export dashboard function for module coordination +if (typeof window.adminDashboard === 'undefined') { + window.adminDashboard = { + loadDashboardData: loadDashboardData }; } - -// Add shift management functions -async function loadAdminShifts() { - const list = document.getElementById('admin-shifts-list'); - if (list) { - list.innerHTML = '

Loading shifts...

'; - } - - try { - console.log('Loading admin shifts...'); - const response = await fetch('/api/shifts/admin'); - const data = await response.json(); - - if (data.success) { - console.log('Successfully loaded', data.shifts.length, 'shifts'); - displayAdminShifts(data.shifts); - } else { - console.error('Failed to load shifts:', data.error); - if (list) { - list.innerHTML = '

Failed to load shifts

'; - } - showStatus('Failed to load shifts', 'error'); - } - } catch (error) { - console.error('Error loading admin shifts:', error); - if (list) { - list.innerHTML = '

Error loading shifts

'; - } - showStatus('Failed to load shifts', 'error'); - } -} - -function displayAdminShifts(shifts) { - const list = document.getElementById('admin-shifts-list'); - - if (!list) { - console.error('Admin shifts list element not found'); - return; - } - - if (shifts.length === 0) { - list.innerHTML = '

No shifts created yet.

'; - return; - } - - list.innerHTML = shifts.map(shift => { - const shiftDate = createLocalDate(shift.Date); - const signupCount = shift.signups ? shift.signups.length : 0; - const isPublic = shift['Is Public'] !== false; - - console.log(`Shift "${shift.Title}" (ID: ${shift.ID}) has ${signupCount} volunteers:`, shift.signups?.map(s => s['User Email']) || []); - - return ` -
-
-

${escapeHtml(shift.Title)}

-

📅 ${shiftDate.toLocaleDateString()} | ⏰ ${shift['Start Time']} - ${shift['End Time']}

-

📍 ${escapeHtml(shift.Location || 'TBD')}

-

👥 ${signupCount}/${shift['Max Volunteers']} volunteers

-

${shift.Status || 'Open'}

-

${isPublic ? '🌐 Public' : '🔒 Private'}

- ${isPublic ? ` - - ` : ''} -
-
- - - -
-
- `; - }).join(''); - - // Add event listeners using delegation - setupShiftActionListeners(); -} - -// Fix the setupShiftActionListeners function -function setupShiftActionListeners() { - const list = document.getElementById('admin-shifts-list'); - if (!list) return; - - // Remove any existing listeners to avoid duplicates - const newList = list.cloneNode(true); - list.parentNode.replaceChild(newList, list); - - // Get the updated reference - const updatedList = document.getElementById('admin-shifts-list'); - - updatedList.addEventListener('click', function(e) { - if (e.target.classList.contains('delete-shift-btn')) { - const shiftId = e.target.getAttribute('data-shift-id'); - console.log('Delete button clicked for shift:', shiftId); - deleteShift(shiftId); - } else if (e.target.classList.contains('edit-shift-btn')) { - const shiftId = e.target.getAttribute('data-shift-id'); - console.log('Edit button clicked for shift:', shiftId); - editShift(shiftId); - } else if (e.target.classList.contains('manage-volunteers-btn')) { - const shiftId = e.target.getAttribute('data-shift-id'); - const shiftData = JSON.parse(e.target.getAttribute('data-shift').replace(/'/g, "'")); - console.log('Manage volunteers clicked for shift:', shiftId); - showShiftUserModal(shiftId, shiftData); - } else if (e.target.classList.contains('copy-shift-link-btn')) { - const shiftId = e.target.getAttribute('data-shift-id'); - console.log('Copy link button clicked for shift:', shiftId); - copyShiftLink(shiftId); - } else if (e.target.classList.contains('open-shift-link-btn')) { - const shiftId = e.target.getAttribute('data-shift-id'); - console.log('Open link button clicked for shift:', shiftId); - openShiftLink(shiftId); - } - }); -} - -// Update the deleteShift function (remove window. prefix) -async function deleteShift(shiftId) { - if (!confirm('Are you sure you want to delete this shift? All signups will be cancelled.')) { - return; - } - - try { - const response = await fetch(`/api/shifts/admin/${shiftId}`, { - method: 'DELETE' - }); - - const data = await response.json(); - - if (data.success) { - showStatus('Shift deleted successfully', 'success'); - await loadAdminShifts(); - console.log('Refreshed shifts list after deleting shift'); - } else { - showStatus(data.error || 'Failed to delete shift', 'error'); - } - } catch (error) { - console.error('Error deleting shift:', error); - showStatus('Failed to delete shift', 'error'); - } -} - -// Update editShift function (remove window. prefix) -async function editShift(shiftId) { - try { - // Find the shift in the current data - const response = await fetch('/api/shifts/admin'); - const data = await response.json(); - - if (!data.success) { - showStatus('Failed to load shift data', 'error'); - return; - } - - const shift = data.shifts.find(s => s.ID === parseInt(shiftId)); - if (!shift) { - showStatus('Shift not found', 'error'); - return; - } - - // Set editing mode - editingShiftId = shiftId; - - // Populate the form - document.getElementById('shift-title').value = shift.Title || ''; - document.getElementById('shift-description').value = shift.Description || ''; - document.getElementById('shift-date').value = shift.Date || ''; - document.getElementById('shift-start').value = shift['Start Time'] || ''; - document.getElementById('shift-end').value = shift['End Time'] || ''; - document.getElementById('shift-location').value = shift.Location || ''; - document.getElementById('shift-max-volunteers').value = shift['Max Volunteers'] || ''; - - // Update public checkbox if it exists - const publicCheckbox = document.getElementById('shift-is-public'); - if (publicCheckbox) { - publicCheckbox.checked = shift['Is Public'] !== false; - } - - // Change submit button text - const submitBtn = document.querySelector('#shift-form button[type="submit"]'); - if (submitBtn) { - submitBtn.textContent = 'Update Shift'; - } - - // Remove editing class from any previous item - document.querySelectorAll('.shift-admin-item.editing').forEach(el => { - el.classList.remove('editing'); - }); - - // Add editing class to current item - const shiftElement = document.querySelector(`[data-shift-id="${shiftId}"]`); - if (shiftElement) { - const shiftItem = shiftElement.closest('.shift-admin-item'); - if (shiftItem) { - shiftItem.classList.add('editing'); - } - } - - // Scroll to form - document.getElementById('shift-form').scrollIntoView({ behavior: 'smooth' }); - - showStatus('Editing shift: ' + shift.Title, 'info'); - - } catch (error) { - console.error('Error loading shift for edit:', error); - showStatus('Failed to load shift for editing', 'error'); - } -} - -// Add function to create shift -async function createShift(e) { - e.preventDefault(); - - const title = document.getElementById('shift-title').value; - const description = document.getElementById('shift-description').value; - const date = document.getElementById('shift-date').value; - const startTime = document.getElementById('shift-start').value; - const endTime = document.getElementById('shift-end').value; - const location = document.getElementById('shift-location').value; - const maxVolunteers = document.getElementById('shift-max-volunteers').value; - - // Get public checkbox value - const isPublic = document.getElementById('shift-is-public')?.checked ?? true; - - const shiftData = { - title, - description, - date, - startTime, - endTime, - location, - maxVolunteers: parseInt(maxVolunteers), - isPublic - }; - - try { - let response; - if (editingShiftId) { - // Update existing shift - response = await fetch(`/api/shifts/admin/${editingShiftId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(shiftData) - }); - } else { - // Create new shift - response = await fetch('/api/shifts/admin', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(shiftData) - }); - } - - const data = await response.json(); - - if (data.success) { - showStatus(editingShiftId ? 'Shift updated successfully' : 'Shift created successfully', 'success'); - clearShiftForm(); - await loadAdminShifts(); - console.log('Refreshed shifts list after saving shift'); - } else { - showStatus(data.error || 'Failed to save shift', 'error'); - } - } catch (error) { - console.error('Error saving shift:', error); - showStatus('Failed to save shift', 'error'); - } -} - -function clearShiftForm() { - const form = document.getElementById('shift-form'); - if (form) { - form.reset(); - - // Reset editing state - editingShiftId = null; - - // Reset submit button text - const submitBtn = document.querySelector('#shift-form button[type="submit"]'); - if (submitBtn) { - submitBtn.textContent = 'Create Shift'; - } - - // Remove editing class from any shift items - document.querySelectorAll('.shift-admin-item.editing').forEach(el => { - el.classList.remove('editing'); - }); - - showStatus('Form cleared', 'info'); - } -} - -// User Management Functions -async function loadUsers() { - const loadingEl = document.getElementById('users-loading'); - const emptyEl = document.getElementById('users-empty'); - const tableBody = document.getElementById('users-table-body'); - - if (loadingEl) loadingEl.style.display = 'block'; - if (emptyEl) emptyEl.style.display = 'none'; - if (tableBody) tableBody.innerHTML = ''; - - try { - const response = await fetch('/api/users'); - const data = await response.json(); - - if (loadingEl) loadingEl.style.display = 'none'; - - if (data.success && data.users) { - displayUsers(data.users); - } else { - throw new Error(data.error || 'Failed to load users'); - } - - } catch (error) { - console.error('Error loading users:', error); - if (loadingEl) loadingEl.style.display = 'none'; - if (emptyEl) { - emptyEl.textContent = 'Failed to load users'; - emptyEl.style.display = 'block'; - } - showStatus('Failed to load users', 'error'); - } -} - -function displayUsers(users) { - const container = document.querySelector('.users-list'); - if (!container) return; - - // Find or create the users table container, preserving the header - let usersTableContainer = container.querySelector('.users-table-container'); - if (!usersTableContainer) { - // If container doesn't exist, create it after the header - const header = container.querySelector('.users-list-header'); - usersTableContainer = document.createElement('div'); - usersTableContainer.className = 'users-table-container'; - - if (header && header.nextSibling) { - container.insertBefore(usersTableContainer, header.nextSibling); - } else if (header) { - container.appendChild(usersTableContainer); - } else { - container.appendChild(usersTableContainer); - } - } - - if (!users || users.length === 0) { - usersTableContainer.innerHTML = '

No users found.

'; - return; - } - - const tableHtml = ` -
- - - - - - - - - - - - ${users.map(user => { - const createdDate = user.created_at || user['Created At'] || user.createdAt; - const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A'; - const isAdmin = user.admin || user.Admin || false; - const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user'); - const userId = user.Id || user.id || user.ID; - - // Handle expiration info - let expirationInfo = ''; - if (user.ExpiresAt) { - const expirationDate = new Date(user.ExpiresAt); - const now = new Date(); - const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24)); - - if (daysUntilExpiration < 0) { - expirationInfo = `Expired ${Math.abs(daysUntilExpiration)} days ago`; - } else if (daysUntilExpiration <= 3) { - expirationInfo = `Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}`; - } else { - expirationInfo = `Expires: ${expirationDate.toLocaleDateString()}`; - } - } - - return ` - - - - - - - - `; - }).join('')} - -
EmailNameRoleCreatedActions
${escapeHtml(user.email || user.Email || 'N/A')}${escapeHtml(user.name || user.Name || 'N/A')} - - ${userType.charAt(0).toUpperCase() + userType.slice(1)} - - ${expirationInfo} - ${formattedDate} - -
-
- - `; - - usersTableContainer.innerHTML = tableHtml; - setupUserActionListeners(); -} - -function setupUserActionListeners() { - const container = document.querySelector('.users-list'); - if (!container) return; - - // Remove existing event listeners by cloning the container - const newContainer = container.cloneNode(true); - container.parentNode.replaceChild(newContainer, container); - - // Get the updated reference - const updatedContainer = document.querySelector('.users-list'); - - updatedContainer.addEventListener('click', function(e) { - if (e.target.classList.contains('delete-user-btn')) { - const userId = e.target.getAttribute('data-user-id'); - const userEmail = e.target.getAttribute('data-user-email'); - console.log('Delete button clicked for user:', userId); - deleteUser(userId, userEmail); - } else if (e.target.classList.contains('send-login-btn')) { - const userId = e.target.getAttribute('data-user-id'); - const userEmail = e.target.getAttribute('data-user-email'); - console.log('Send login details button clicked for user:', userId); - sendLoginDetailsToUser(userId, userEmail); - } else if (e.target.id === 'email-all-users-btn') { - console.log('Email All Users button clicked'); - showEmailUsersModal(); - } - }); -} - -async function deleteUser(userId, userEmail) { - if (!confirm(`Are you sure you want to delete user "${userEmail}"? This action cannot be undone.`)) { - return; - } - - try { - const response = await fetch(`/api/users/${userId}`, { - method: 'DELETE' - }); - - const data = await response.json(); - - if (data.success) { - showStatus(`User "${userEmail}" deleted successfully`, 'success'); - loadUsers(); // Reload the users list - } else { - throw new Error(data.error || 'Failed to delete user'); - } - - } catch (error) { - console.error('Error deleting user:', error); - showStatus(`Failed to delete user: ${error.message}`, 'error'); - } -} - -async function sendLoginDetailsToUser(userId, userEmail) { - if (!confirm(`Send login details to "${userEmail}"?`)) { - return; - } - - try { - const response = await fetch(`/api/users/${userId}/send-login-details`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - const data = await response.json(); - - if (data.success) { - showStatus(`Login details sent to "${userEmail}" successfully`, 'success'); - } else { - throw new Error(data.error || 'Failed to send login details'); - } - - } catch (error) { - console.error('Error sending login details:', error); - showStatus(`Failed to send login details: ${error.message}`, 'error'); - } -} - -async function createUser(e) { - e.preventDefault(); - - const email = document.getElementById('user-email').value.trim(); - const password = document.getElementById('user-password').value; - const name = document.getElementById('user-name').value.trim(); - const userType = document.getElementById('user-type').value; - const expireDays = userType === 'temp' ? - parseInt(document.getElementById('user-expire-days').value) : null; - const admin = document.getElementById('user-is-admin').checked; - - if (!email || !password) { - showStatus('Email and password are required', 'error'); - return; - } - - if (password.length < 6) { - showStatus('Password must be at least 6 characters long', 'error'); - return; - } - - if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) { - showStatus('Expiration days must be between 1 and 365 for temporary users', 'error'); - return; - } - - try { - const userData = { - email, - password, - name: name || '', - isAdmin: userType === 'admin' || admin, - userType, - expireDays - }; - - const response = await fetch('/api/users', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(userData) - }); - - const data = await response.json(); - - if (data.success) { - showStatus('User created successfully', 'success'); - clearUserForm(); - loadUsers(); // Reload the users list - } else { - throw new Error(data.error || 'Failed to create user'); - } - - } catch (error) { - console.error('Error creating user:', error); - showStatus(`Failed to create user: ${error.message}`, 'error'); - } -} - -function clearUserForm() { - const form = document.getElementById('create-user-form'); - if (form) { - form.reset(); - - // Reset user type to default - const userTypeSelect = document.getElementById('user-type'); - if (userTypeSelect) { - userTypeSelect.value = 'user'; - } - - // Hide expiration group - const expirationGroup = document.getElementById('expiration-group'); - if (expirationGroup) { - expirationGroup.style.display = 'none'; - } - - // Re-enable admin checkbox - const isAdminCheckbox = document.getElementById('user-is-admin'); - if (isAdminCheckbox) { - isAdminCheckbox.disabled = false; - } - - showStatus('User form cleared', 'info'); - } -} - -// Email All Users Functions -let allUsersData = []; - -async function showEmailUsersModal() { - // Load current users data - try { - const response = await fetch('/api/users'); - const data = await response.json(); - - if (data.success && data.users) { - allUsersData = data.users; - - // Update recipients count - const recipientsCount = document.getElementById('recipients-count'); - if (recipientsCount) { - recipientsCount.textContent = `${allUsersData.length}`; - } - } - } catch (error) { - console.error('Error loading users for email:', error); - showStatus('Failed to load user data', 'error'); - return; - } - - // Show modal - const modal = document.getElementById('email-users-modal'); - if (modal) { - modal.style.display = 'flex'; - - // Clear previous content - document.getElementById('email-subject').value = ''; - document.getElementById('email-content').innerHTML = ''; - document.getElementById('show-preview').checked = false; - document.getElementById('email-preview').style.display = 'none'; - } -} - -function closeEmailUsersModal() { - const modal = document.getElementById('email-users-modal'); - if (modal) { - modal.style.display = 'none'; - } -} - -function setupRichTextEditor() { - const toolbar = document.querySelector('.rich-text-toolbar'); - const editor = document.getElementById('email-content'); - - if (!toolbar || !editor) return; - - // Handle toolbar button clicks - toolbar.addEventListener('click', (e) => { - if (e.target.classList.contains('toolbar-btn')) { - e.preventDefault(); - const command = e.target.getAttribute('data-command'); - - if (command === 'createLink') { - const url = prompt('Enter the URL:'); - if (url) { - document.execCommand(command, false, url); - } - } else { - document.execCommand(command, false, null); - } - - // Update preview if visible - updateEmailPreview(); - } - }); - - // Update preview on content change - editor.addEventListener('input', updateEmailPreview); - - // Handle preview toggle - const showPreviewCheckbox = document.getElementById('show-preview'); - if (showPreviewCheckbox) { - showPreviewCheckbox.addEventListener('change', togglePreview); - } - - // Update preview when subject changes - const subjectInput = document.getElementById('email-subject'); - if (subjectInput) { - subjectInput.addEventListener('input', updateEmailPreview); - } -} - -function togglePreview() { - const preview = document.getElementById('email-preview'); - const checkbox = document.getElementById('show-preview'); - - if (preview && checkbox) { - if (checkbox.checked) { - preview.style.display = 'block'; - updateEmailPreview(); - } else { - preview.style.display = 'none'; - } - } -} - -function updateEmailPreview() { - const previewSubject = document.getElementById('preview-subject'); - const previewBody = document.getElementById('preview-body'); - const subjectInput = document.getElementById('email-subject'); - const contentEditor = document.getElementById('email-content'); - - if (previewSubject && subjectInput) { - previewSubject.textContent = subjectInput.value || 'Your subject will appear here'; - } - - if (previewBody && contentEditor) { - const content = contentEditor.innerHTML || 'Your message will appear here'; - previewBody.innerHTML = content; - } -} - -async function sendEmailToAllUsers(e) { - e.preventDefault(); - - const subject = document.getElementById('email-subject').value.trim(); - const content = document.getElementById('email-content').innerHTML.trim(); - - if (!subject) { - showStatus('Please enter an email subject', 'error'); - return; - } - - if (!content || content === '
' || content === '') { - showStatus('Please enter email content', 'error'); - return; - } - - if (allUsersData.length === 0) { - showStatus('No users found to email', 'error'); - return; - } - - const confirmMessage = `Send this email to all ${allUsersData.length} users?`; - if (!confirm(confirmMessage)) { - return; - } - - // Initialize progress tracking - initializeEmailProgress(allUsersData.length); - - try { - const response = await fetch('/api/users/email-all', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - subject: subject, - content: content - }) - }); - - const data = await response.json(); - - if (data.success) { - // Display detailed results - updateEmailProgress(data.results); - showStatus(data.message, 'success'); - console.log('Email results:', data.results); - } else { - showEmailError(data.error || 'Failed to send emails'); - if (data.details) { - console.error('Failed email details:', data.details); - } - } - } catch (error) { - console.error('Error sending emails to all users:', error); - showEmailError('Failed to send emails - Network error'); - } -} - -// Initialize email progress display -function initializeEmailProgress(totalCount) { - const progressContainer = document.getElementById('email-progress-container'); - const statusList = document.getElementById('email-status-list'); - const pendingCountEl = document.getElementById('pending-count'); - const successCountEl = document.getElementById('success-count'); - const errorCountEl = document.getElementById('error-count'); - const progressBar = document.getElementById('email-progress-bar'); - const progressText = document.getElementById('progress-text'); - const closeBtn = document.getElementById('close-progress-btn'); - - // Show progress container - progressContainer.classList.add('show'); - - // Reset counters - pendingCountEl.textContent = totalCount; - successCountEl.textContent = '0'; - errorCountEl.textContent = '0'; - - // Reset progress bar - progressBar.style.width = '0%'; - progressBar.classList.remove('complete', 'error'); - progressText.textContent = '0%'; - - // Clear status list - statusList.innerHTML = ''; - - // Hide close button initially - closeBtn.style.display = 'none'; - - // Add status items for each user - allUsersData.forEach(user => { - const statusItem = document.createElement('div'); - statusItem.className = 'email-status-item'; - statusItem.innerHTML = ` -
${user.Name || user.Email}
-
-
- Sending... -
- `; - statusList.appendChild(statusItem); - }); -} - -// Update progress with results -function updateEmailProgress(results) { - const statusList = document.getElementById('email-status-list'); - const pendingCountEl = document.getElementById('pending-count'); - const successCountEl = document.getElementById('success-count'); - const errorCountEl = document.getElementById('error-count'); - const progressBar = document.getElementById('email-progress-bar'); - const progressText = document.getElementById('progress-text'); - const closeBtn = document.getElementById('close-progress-btn'); - - const successful = results.successful || []; - const failed = results.failed || []; - const total = results.total || (successful.length + failed.length); - - // Update counters - successCountEl.textContent = successful.length; - errorCountEl.textContent = failed.length; - pendingCountEl.textContent = '0'; - - // Update progress bar - const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); - progressBar.style.width = percentage + '%'; - progressText.textContent = percentage + '%'; - - if (failed.length > 0) { - progressBar.classList.add('error'); - } else { - progressBar.classList.add('complete'); - } - - // Update individual status items - const statusItems = statusList.children; - - // Update successful emails - successful.forEach(result => { - const statusItem = Array.from(statusItems).find(item => - item.querySelector('.email-status-recipient').textContent.includes(result.email) || - item.querySelector('.email-status-recipient').textContent.includes(result.name) - ); - if (statusItem) { - statusItem.querySelector('.email-status-result').innerHTML = ` - ✓ Sent - `; - } - }); - - // Update failed emails - failed.forEach(result => { - const statusItem = Array.from(statusItems).find(item => - item.querySelector('.email-status-recipient').textContent.includes(result.email) || - item.querySelector('.email-status-recipient').textContent.includes(result.name) - ); - if (statusItem) { - statusItem.querySelector('.email-status-result').innerHTML = ` - ✗ Failed - `; - } - }); - - // Show close button - closeBtn.style.display = 'block'; - closeBtn.onclick = () => { - document.getElementById('email-progress-container').classList.remove('show'); - closeEmailUsersModal(); - }; -} - -// Show email error -function showEmailError(message) { - const progressContainer = document.getElementById('email-progress-container'); - const progressBar = document.getElementById('email-progress-bar'); - const progressText = document.getElementById('progress-text'); - const closeBtn = document.getElementById('close-progress-btn'); - - // Show progress container if not visible - progressContainer.classList.add('show'); - - // Update progress bar to show error - progressBar.style.width = '100%'; - progressBar.classList.add('error'); - progressText.textContent = 'Error'; - - // Show close button - closeBtn.style.display = 'block'; - closeBtn.onclick = () => { - progressContainer.classList.remove('show'); - }; - - showStatus(message, 'error'); -} - -// Initialize NocoDB links in admin panel -async function initializeNocodbLinks() { - console.log('Starting NocoDB links initialization...'); - - try { - // Since we're in the admin panel, the user is already verified as admin - // by the requireAdmin middleware. Let's get the URLs from the server directly. - console.log('Fetching NocoDB URLs for admin panel...'); - const configResponse = await fetch('/api/admin/nocodb-urls'); - - if (!configResponse.ok) { - throw new Error(`NocoDB URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); - } - - const config = await configResponse.json(); - console.log('NocoDB URLs received:', config); - - if (config.success && config.nocodbUrls) { - console.log('Setting up NocoDB links with URLs:', config.nocodbUrls); - - // Set up admin dashboard NocoDB links - setAdminNocodbLink('admin-nocodb-view-link', config.nocodbUrls.viewUrl); - setAdminNocodbLink('admin-nocodb-login-link', config.nocodbUrls.loginSheet); - setAdminNocodbLink('admin-nocodb-settings-link', config.nocodbUrls.settingsSheet); - setAdminNocodbLink('admin-nocodb-shifts-link', config.nocodbUrls.shiftsSheet); - setAdminNocodbLink('admin-nocodb-signups-link', config.nocodbUrls.shiftSignupsSheet); - - console.log('NocoDB links initialized in admin panel'); - } else { - console.warn('No NocoDB URLs found in admin config response'); - // Hide the NocoDB section if no URLs are available - const nocodbSection = document.getElementById('nocodb-links'); - const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); - if (nocodbSection) { - nocodbSection.style.display = 'none'; - console.log('Hidden NocoDB section'); - } - if (nocodbNav) { - nocodbNav.style.display = 'none'; - console.log('Hidden NocoDB nav link'); - } - } - } catch (error) { - console.error('Error initializing NocoDB links in admin panel:', error); - // Hide the NocoDB section on error - const nocodbSection = document.getElementById('nocodb-links'); - const nocodbNav = document.querySelector('.admin-nav a[href="#nocodb-links"]'); - if (nocodbSection) { - nocodbSection.style.display = 'none'; - console.log('Hidden NocoDB section due to error'); - } - if (nocodbNav) { - nocodbNav.style.display = 'none'; - console.log('Hidden NocoDB nav link due to error'); - } - } -} - -// Helper function to set admin NocoDB link href -function setAdminNocodbLink(elementId, url) { - console.log(`Setting up NocoDB link: ${elementId} = ${url}`); - const element = document.getElementById(elementId); - - if (element && url) { - element.href = url; - element.style.display = 'inline-flex'; - // Remove any disabled state - element.classList.remove('btn-disabled'); - element.removeAttribute('disabled'); - console.log(`✓ Successfully set up ${elementId}`); - } else if (element) { - element.style.display = 'none'; - // Add disabled state if no URL - element.classList.add('btn-disabled'); - element.setAttribute('disabled', 'disabled'); - element.href = '#'; - console.log(`⚠ Disabled ${elementId} - no URL provided`); - } else { - console.error(`✗ Element not found: ${elementId}`); - } -} - -// Initialize Listmonk links in admin panel -async function initializeListmonkLinks() { - console.log('Starting Listmonk links initialization...'); - - try { - // Since we're in the admin panel, the user is already verified as admin - // by the requireAdmin middleware. Let's get the URLs from the server directly. - console.log('Fetching Listmonk URLs for admin panel...'); - const configResponse = await fetch('/api/admin/listmonk-urls'); - - if (!configResponse.ok) { - throw new Error(`Listmonk URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`); - } - - const config = await configResponse.json(); - console.log('Listmonk URLs received:', config); - - if (config.success && config.listmonkUrls) { - console.log('Setting up Listmonk links with URLs:', config.listmonkUrls); - - // Set up admin dashboard Listmonk links - setAdminListmonkLink('admin-listmonk-admin-link', config.listmonkUrls.adminUrl); - setAdminListmonkLink('admin-listmonk-lists-link', config.listmonkUrls.listsUrl); - setAdminListmonkLink('admin-listmonk-campaigns-link', config.listmonkUrls.campaignsUrl); - setAdminListmonkLink('admin-listmonk-subscribers-link', config.listmonkUrls.subscribersUrl); - setAdminListmonkLink('admin-listmonk-settings-link', config.listmonkUrls.settingsUrl); - - console.log('Listmonk links initialized in admin panel'); - } else { - console.warn('No Listmonk URLs found in admin config response'); - // Hide the Listmonk section if no URLs are available - const listmonkSection = document.getElementById('listmonk-links'); - const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); - if (listmonkSection) { - listmonkSection.style.display = 'none'; - console.log('Hidden Listmonk section'); - } - if (listmonkNav) { - listmonkNav.style.display = 'none'; - console.log('Hidden Listmonk nav link'); - } - } - } catch (error) { - console.error('Error initializing Listmonk links in admin panel:', error); - // Hide the Listmonk section on error - const listmonkSection = document.getElementById('listmonk-links'); - const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]'); - if (listmonkSection) { - listmonkSection.style.display = 'none'; - console.log('Hidden Listmonk section due to error'); - } - if (listmonkNav) { - listmonkNav.style.display = 'none'; - console.log('Hidden Listmonk nav link due to error'); - } - } -} - -// Helper function to set admin Listmonk link href -function setAdminListmonkLink(elementId, url) { - console.log(`Setting up Listmonk link: ${elementId} = ${url}`); - const element = document.getElementById(elementId); - - if (element && url) { - element.href = url; - element.style.display = 'inline-flex'; - // Remove any disabled state - element.classList.remove('btn-disabled'); - element.removeAttribute('disabled'); - console.log(`✓ Successfully set up ${elementId}`); - } else if (element) { - element.style.display = 'none'; - // Add disabled state if no URL - element.classList.add('btn-disabled'); - element.setAttribute('disabled', 'disabled'); - element.href = '#'; - console.log(`⚠ Disabled ${elementId} - no URL provided`); - } else { - console.error(`✗ Element not found: ${elementId}`); - } -} - -// Shift User Management Functions -let currentShiftData = null; -let allUsers = []; - -// Load all users for the dropdown -async function loadAllUsers() { - try { - const response = await fetch('/api/users'); - const data = await response.json(); - - if (data.success) { - allUsers = data.users; - populateUserSelect(); - } else { - console.error('Failed to load users:', data.error); - } - } catch (error) { - console.error('Error loading users:', error); - } -} - -// Populate user select dropdown -function populateUserSelect() { - const select = document.getElementById('user-select'); - if (!select) return; - - // Clear existing options except the first one - select.innerHTML = ''; - - allUsers.forEach(user => { - const option = document.createElement('option'); - option.value = user.email || user.Email; - option.textContent = `${user.name || user.Name || ''} (${user.email || user.Email})`; - select.appendChild(option); - }); -} - -// Show the shift user management modal -async function showShiftUserModal(shiftId, shiftData) { - currentShiftData = { ...shiftData, ID: shiftId }; - - // Update modal title and info - document.getElementById('modal-shift-title').textContent = shiftData.Title; - const shiftDate = createLocalDate(shiftData.Date); - document.getElementById('modal-shift-details').textContent = - `${shiftDate.toLocaleDateString()} | ${shiftData['Start Time']} - ${shiftData['End Time']} | ${shiftData.Location || 'TBD'}`; - - // Load users if not already loaded - if (allUsers.length === 0) { - await loadAllUsers(); - } - - // Display current volunteers - displayCurrentVolunteers(shiftData.signups || []); - - // Show modal - document.getElementById('shift-user-modal').style.display = 'flex'; -} - -// Display current volunteers in the modal -function displayCurrentVolunteers(volunteers) { - const container = document.getElementById('current-volunteers-list'); - - if (!volunteers || volunteers.length === 0) { - container.innerHTML = '
No volunteers signed up yet.
'; - return; - } - - container.innerHTML = volunteers.map(volunteer => ` -
-
-
${escapeHtml(volunteer['User Name'] || volunteer['User Email'] || 'Unknown')}
-
${escapeHtml(volunteer['User Email'])}
-
-
- -
-
- `).join(''); - - // Add event listeners for remove buttons - setupVolunteerActionListeners(); -} - -// Setup event listeners for volunteer actions -function setupVolunteerActionListeners() { - const container = document.getElementById('current-volunteers-list'); - - container.addEventListener('click', function(e) { - if (e.target.classList.contains('remove-volunteer-btn')) { - const volunteerId = e.target.getAttribute('data-volunteer-id'); - const volunteerEmail = e.target.getAttribute('data-volunteer-email'); - removeVolunteerFromShift(volunteerId, volunteerEmail); - } - }); -} - -// Add user to shift -async function addUserToShift() { - const userSelect = document.getElementById('user-select'); - const userEmail = userSelect.value; - - if (!userEmail) { - showStatus('Please select a user to add', 'error'); - return; - } - - if (!currentShiftData || !currentShiftData.ID) { - showStatus('No shift selected or invalid shift data', 'error'); - console.error('Invalid currentShiftData:', currentShiftData); - return; - } - - try { - const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/add-user`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ userEmail }) - }); - - const data = await response.json(); - - if (data.success) { - showStatus('User successfully added to shift', 'success'); - userSelect.value = ''; // Clear selection - - // Refresh the shift data and reload volunteers with better error handling - try { - await refreshCurrentShiftData(); - console.log('Refreshed shift data after adding user'); - } catch (refreshError) { - console.error('Error during refresh after adding user:', refreshError); - // Still show success since the add operation worked - } - } else { - showStatus(data.error || 'Failed to add user to shift', 'error'); - } - } catch (error) { - console.error('Error adding user to shift:', error); - showStatus('Failed to add user to shift', 'error'); - } -} - -// Remove volunteer from shift -async function removeVolunteerFromShift(volunteerId, volunteerEmail) { - if (!confirm(`Are you sure you want to remove ${volunteerEmail} from this shift?`)) { - return; - } - - if (!currentShiftData || !currentShiftData.ID) { - showStatus('No shift selected or invalid shift data', 'error'); - console.error('Invalid currentShiftData:', currentShiftData); - return; - } - - try { - const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/remove-user/${volunteerId}`, { - method: 'DELETE' - }); - - const data = await response.json(); - - if (data.success) { - showStatus('Volunteer successfully removed from shift', 'success'); - - // Refresh the shift data and reload volunteers with better error handling - try { - await refreshCurrentShiftData(); - console.log('Refreshed shift data after removing volunteer'); - } catch (refreshError) { - console.error('Error during refresh after removing volunteer:', refreshError); - // Still show success since the remove operation worked - } - } else { - showStatus(data.error || 'Failed to remove volunteer from shift', 'error'); - } - } catch (error) { - console.error('Error removing volunteer from shift:', error); - showStatus('Failed to remove volunteer from shift', 'error'); - } -} - -// Refresh current shift data -async function refreshCurrentShiftData() { - if (!currentShiftData || !currentShiftData.ID) { - console.warn('No current shift data or missing ID, skipping refresh'); - return; - } - - try { - console.log('Refreshing shift data for shift ID:', currentShiftData.ID); - - // Instead of reloading ALL admin shifts, just get this specific shift's signups - // This prevents the expensive backend call and reduces the refresh cascade - const response = await fetch(`/api/shifts/admin`); - const data = await response.json(); - - if (data.success && data.shifts && Array.isArray(data.shifts)) { - const updatedShift = data.shifts.find(s => s && s.ID === currentShiftData.ID); - if (updatedShift) { - console.log('Found updated shift with', updatedShift.signups?.length || 0, 'volunteers'); - currentShiftData = updatedShift; - displayCurrentVolunteers(updatedShift.signups || []); - - // Only update the specific shift in the main list, don't refresh everything - updateShiftInList(updatedShift); - } else { - console.warn('Could not find updated shift with ID:', currentShiftData.ID); - } - } else { - console.error('Failed to refresh shift data:', data.error || 'Invalid response format'); - } - } catch (error) { - console.error('Error refreshing shift data:', error); - } -} - -// New function to update a single shift in the list without full refresh -function updateShiftInList(updatedShift) { - const shiftElement = document.querySelector(`[data-shift-id="${updatedShift.ID}"]`); - if (shiftElement) { - const shiftItem = shiftElement.closest('.shift-admin-item'); - if (shiftItem) { - const signupCount = updatedShift.signups ? updatedShift.signups.length : 0; - - // Find the volunteer count paragraph (contains 👥) - const volunteerCountElement = Array.from(shiftItem.querySelectorAll('p')).find(p => - p.textContent.includes('👥') - ); - - if (volunteerCountElement) { - volunteerCountElement.textContent = `👥 ${signupCount}/${updatedShift['Max Volunteers']} volunteers`; - } - - // Update the data attribute with new shift data - const manageBtn = shiftItem.querySelector('.manage-volunteers-btn'); - if (manageBtn) { - manageBtn.setAttribute('data-shift', JSON.stringify(updatedShift).replace(/'/g, "'")); - } - } - } -} - -// Close modal -function closeShiftUserModal() { - document.getElementById('shift-user-modal').style.display = 'none'; - currentShiftData = null; - - // Don't refresh the entire shifts list when closing modal - // The shifts list should already be up to date from the individual updates - console.log('Modal closed - shifts list should already be current'); -} - -// Email shift details to all volunteers -async function emailShiftDetails() { - if (!currentShiftData) { - showStatus('No shift selected', 'error'); - return; - } - - // Check if there are volunteers to email - const volunteers = currentShiftData.signups || []; - if (volunteers.length === 0) { - showStatus('No volunteers signed up for this shift', 'error'); - return; - } - - // Confirm action - const confirmMessage = `Send shift details email to ${volunteers.length} volunteer${volunteers.length !== 1 ? 's' : ''}?`; - if (!confirm(confirmMessage)) { - return; - } - - // Initialize progress tracking for shift emails - initializeShiftEmailProgress(volunteers.length); - - try { - const response = await fetch(`/api/shifts/admin/${currentShiftData.ID}/email-details`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - const data = await response.json(); - - if (data.success) { - // Display detailed results - updateShiftEmailProgress(data.results); - showStatus(data.message, 'success'); - console.log('Email results:', data.results); - } else { - showShiftEmailError(data.error || 'Failed to send emails'); - if (data.details) { - console.error('Failed email details:', data.details); - } - } - } catch (error) { - console.error('Error sending shift details emails:', error); - showShiftEmailError('Failed to send emails - Network error'); - } -} - -// Initialize shift email progress display -function initializeShiftEmailProgress(totalCount) { - const progressContainer = document.getElementById('shift-email-progress-container'); - const statusList = document.getElementById('shift-email-status-list'); - const pendingCountEl = document.getElementById('shift-pending-count'); - const successCountEl = document.getElementById('shift-success-count'); - const errorCountEl = document.getElementById('shift-error-count'); - const progressBar = document.getElementById('shift-email-progress-bar'); - const progressText = document.getElementById('shift-progress-text'); - const closeBtn = document.getElementById('close-shift-progress-btn'); - - // Show progress container - progressContainer.classList.add('show'); - - // Reset counters - pendingCountEl.textContent = totalCount; - successCountEl.textContent = '0'; - errorCountEl.textContent = '0'; - - // Reset progress bar - progressBar.style.width = '0%'; - progressBar.classList.remove('complete', 'error'); - progressText.textContent = '0%'; - - // Clear status list - statusList.innerHTML = ''; - - // Hide close button initially - closeBtn.style.display = 'none'; - - // Add status items for each volunteer - const volunteers = currentShiftData.signups || []; - volunteers.forEach(volunteer => { - const statusItem = document.createElement('div'); - statusItem.className = 'email-status-item'; - statusItem.innerHTML = ` -
${volunteer['User Name'] || volunteer['User Email']}
-
-
- Sending... -
- `; - statusList.appendChild(statusItem); - }); -} - -// Update shift email progress with results -function updateShiftEmailProgress(results) { - const statusList = document.getElementById('shift-email-status-list'); - const pendingCountEl = document.getElementById('shift-pending-count'); - const successCountEl = document.getElementById('shift-success-count'); - const errorCountEl = document.getElementById('shift-error-count'); - const progressBar = document.getElementById('shift-email-progress-bar'); - const progressText = document.getElementById('shift-progress-text'); - const closeBtn = document.getElementById('close-shift-progress-btn'); - - const successful = results.successful || []; - const failed = results.failed || []; - const total = results.total || (successful.length + failed.length); - - // Update counters - successCountEl.textContent = successful.length; - errorCountEl.textContent = failed.length; - pendingCountEl.textContent = '0'; - - // Update progress bar - const percentage = ((successful.length + failed.length) / total * 100).toFixed(1); - progressBar.style.width = percentage + '%'; - progressText.textContent = percentage + '%'; - - if (failed.length > 0) { - progressBar.classList.add('error'); - } else { - progressBar.classList.add('complete'); - } - - // Update individual status items - const statusItems = statusList.children; - - // Update successful emails - successful.forEach(result => { - const statusItem = Array.from(statusItems).find(item => - item.querySelector('.email-status-recipient').textContent.includes(result.email) || - item.querySelector('.email-status-recipient').textContent.includes(result.name) - ); - if (statusItem) { - statusItem.querySelector('.email-status-result').innerHTML = ` - ✓ Sent - `; - } - }); - - // Update failed emails - failed.forEach(result => { - const statusItem = Array.from(statusItems).find(item => - item.querySelector('.email-status-recipient').textContent.includes(result.email) || - item.querySelector('.email-status-recipient').textContent.includes(result.name) - ); - if (statusItem) { - statusItem.querySelector('.email-status-result').innerHTML = ` - ✗ Failed - `; - } - }); - - // Show close button - closeBtn.style.display = 'block'; - closeBtn.onclick = () => { - document.getElementById('shift-email-progress-container').classList.remove('show'); - }; -} - -// Show shift email error -function showShiftEmailError(message) { - const progressContainer = document.getElementById('shift-email-progress-container'); - const progressBar = document.getElementById('shift-email-progress-bar'); - const progressText = document.getElementById('shift-progress-text'); - const closeBtn = document.getElementById('close-shift-progress-btn'); - - // Show progress container if not visible - progressContainer.classList.add('show'); - - // Update progress bar to show error - progressBar.style.width = '100%'; - progressBar.classList.add('error'); - progressText.textContent = 'Error'; - - // Show close button - closeBtn.style.display = 'block'; - closeBtn.onclick = () => { - progressContainer.classList.remove('show'); - }; - - showStatus(message, 'error'); -} - -// Setup modal event listeners when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { - const closeModalBtn = document.getElementById('close-user-modal'); - const addUserBtn = document.getElementById('add-user-btn'); - const emailShiftDetailsBtn = document.getElementById('email-shift-details-btn'); - const modal = document.getElementById('shift-user-modal'); - - if (closeModalBtn) { - closeModalBtn.addEventListener('click', closeShiftUserModal); - } - - if (addUserBtn) { - addUserBtn.addEventListener('click', addUserToShift); - } - - if (emailShiftDetailsBtn) { - emailShiftDetailsBtn.addEventListener('click', emailShiftDetails); - } - - // Close modal when clicking outside - if (modal) { - modal.addEventListener('click', function(e) { - if (e.target === modal) { - closeShiftUserModal(); - } - }); - } -}); - -// Setup email users modal event listeners when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { - // Email all users functionality - const closeEmailModalBtn = document.getElementById('close-email-modal'); - const cancelEmailBtn = document.getElementById('cancel-email-btn'); - const emailUsersForm = document.getElementById('email-users-form'); - const emailModal = document.getElementById('email-users-modal'); - - if (closeEmailModalBtn) { - closeEmailModalBtn.addEventListener('click', closeEmailUsersModal); - } - - if (cancelEmailBtn) { - cancelEmailBtn.addEventListener('click', closeEmailUsersModal); - } - - if (emailUsersForm) { - emailUsersForm.addEventListener('submit', sendEmailToAllUsers); - } - - // Close modal when clicking outside - if (emailModal) { - emailModal.addEventListener('click', function(e) { - if (e.target === emailModal) { - closeEmailUsersModal(); - } - }); - } - - // Setup rich text editor functionality - setupRichTextEditor(); -}); - -// Public Shifts Functions -function generateShiftPublicLink(shiftId) { - const baseUrl = window.location.origin; - return `${baseUrl}/public-shifts.html#shift-${shiftId}`; -} - -function copyShiftLink(shiftId) { - const link = generateShiftPublicLink(shiftId); - navigator.clipboard.writeText(link).then(() => { - showStatus('Public shift link copied to clipboard!', 'success'); - }).catch(() => { - // Fallback for older browsers - const textArea = document.createElement('textarea'); - textArea.value = link; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - showStatus('Public shift link copied to clipboard!', 'success'); - }); -} - -function openShiftLink(shiftId) { - const link = generateShiftPublicLink(shiftId); - window.open(link, '_blank'); -} - -// Update the shift form to include Is Public checkbox -function updateShiftFormWithPublicOption() { - const form = document.getElementById('shift-form'); - if (!form) return; - - // Check if public checkbox already exists - if (document.getElementById('shift-is-public')) return; - - const maxVolunteersGroup = form.querySelector('.form-group:has(#shift-max-volunteers)'); - if (maxVolunteersGroup) { - const publicGroup = document.createElement('div'); - publicGroup.className = 'form-group'; - publicGroup.innerHTML = ` - - `; - maxVolunteersGroup.insertAdjacentElement('afterend', publicGroup); - } -} - -// Call this when the shifts section is shown -function enhanceShiftsSection() { - updateShiftFormWithPublicOption(); -} - -// Update the showSection function to call enhanceShiftsSection when shifts section is shown -const originalShowSection = window.showSection || showSection; -window.showSection = function(sectionId) { - if (originalShowSection) { - originalShowSection(sectionId); - } - - if (sectionId === 'shifts') { - enhanceShiftsSection(); - } -}; diff --git a/map/app/public/js/admin/auth.js b/map/app/public/js/admin/auth.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/app/public/js/admin/navigation.js b/map/app/public/js/admin/navigation.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/app/public/js/admin/shifts.js b/map/app/public/js/admin/shifts.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/app/public/js/admin/startLocation.js b/map/app/public/js/admin/startLocation.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/app/public/js/admin/users.js b/map/app/public/js/admin/users.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/app/public/js/admin/utils.js b/map/app/public/js/admin/utils.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/app/public/js/admin/walkSheet.js b/map/app/public/js/admin/walkSheet.js deleted file mode 100644 index e69de29..0000000 diff --git a/map/files-explainer.md b/map/files-explainer.md index a214790..f580486 100644 --- a/map/files-explainer.md +++ b/map/files-explainer.md @@ -384,7 +384,43 @@ CSS styles for the user profile page and user management components in the admin # app/public/js/admin.js -JavaScript for admin panel functionality (map, start location, walk sheet, shift management, user management, and email broadcasting). Includes rich text editor for composing emails with live preview, shift volunteer management with modal interface, mass email broadcasting to all users, shift detail emailing to volunteers, comprehensive admin controls, and automatic initialization of both NocoDB database links and Listmonk email management links. +**Main Admin Panel Coordinator** - Coordinates all admin modules and handles initialization. Contains the primary DOM ready handler that initializes all admin functionality, manages section routing, and provides coordination between all modular components. Maintains backward compatibility and global function exports. + +# app/public/js/admin-core.js + +**Admin Core Utilities Module** - Contains essential admin panel utilities including viewport management, navigation between sections, mobile menu functionality, status messaging, HTML escaping, and debounce utilities. Provides the foundation for all other admin modules. + +# app/public/js/admin-auth.js + +**Admin Authentication Module** - Handles all authentication-related functionality including admin verification, user session checks, logout processes, and admin info display for both desktop and mobile interfaces. Manages security and access control. + +# app/public/js/admin-map.js + +**Admin Map Management Module** - Contains all map-related functionality including Leaflet map initialization, start location management, marker handling, coordinate input/output, crosshair display, and map interaction event handlers. Manages the campaign map configuration. + +# app/public/js/admin-walksheet.js + +**Walk Sheet Configuration Module** - Manages walk sheet template configuration, preview generation, QR code creation and display, printing functionality, and form field management. Handles the generation of printable canvassing materials. + +# app/public/js/admin-shifts.js + +**Shift Management Module** - Handles all shift-related operations including CRUD (Create, Read, Update, Delete) operations, shift display and formatting, public/private shift management, form handling, and shift status management. Core shift administration functionality. + +# app/public/js/admin-shift-volunteers.js + +**Shift Volunteer Management Module** - Manages volunteer assignment to shifts through modal interfaces, handles adding/removing volunteers from shifts, provides email functionality for sending shift details to volunteers, and manages volunteer-specific progress tracking and notifications. + +# app/public/js/admin-users.js + +**User Management Module** - Contains all user administration functionality including user CRUD operations, user display and table management, user role and type management (admin, user, temp), expiration tracking for temporary users, and individual user email functionality. + +# app/public/js/admin-email.js + +**Email Broadcasting Module** - Handles mass email functionality including rich text editor with toolbar, email composition with live preview, progress tracking for bulk email operations, HTML email template management, and broadcast email status monitoring with detailed results display. + +# app/public/js/admin-integration.js + +**External Integration Module** - Manages external service integrations including NocoDB database link initialization and management, Listmonk email service link configuration, admin-only integration controls, and dynamic link setup based on service availability. # app/public/js/dashboard.js