Compare commits

..

No commits in common. "e3bbd96d96e2327eea656dd5ea2eb78281275eae" and "3de1d3fca547af86ff1b2ff60de20266c40aa269" have entirely different histories.

2972 changed files with 614836 additions and 183718 deletions

View File

@ -1,350 +0,0 @@
# 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)

View File

@ -1,15 +0,0 @@
# 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

View File

@ -1,2 +0,0 @@
"filename", "language", "", "comment", "blank", "total"
"Total", "-", , 0, 0, 0
1 filename language comment blank total
2 Total - 0 0 0

View File

@ -1,19 +0,0 @@
# 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)

View File

@ -1,22 +0,0 @@
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 |
+----------+----------+------------+------------+------------+------------+

View File

@ -1,337 +0,0 @@
"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
1 filename language Shell Script Markdown Log YAML JSON Properties JavaScript HTML PostCSS Python Docker CSV XML comment blank total
2 /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
3 /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
4 /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
5 /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
6 /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
7 /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
8 /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
9 /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
10 /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
11 /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
12 /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
13 /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
14 /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
15 /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
16 /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
17 /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
18 /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
19 /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
20 /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
21 /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
22 /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
23 /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
24 /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
25 /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
26 /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
27 /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
28 /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
29 /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
30 /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
31 /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
32 /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
33 /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
34 /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
35 /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
36 /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
37 /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
38 /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
39 /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
40 /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
41 /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
42 /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
43 /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
44 /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
45 /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
46 /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
47 /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
48 /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
49 /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
50 /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
51 /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
52 /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
53 /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
54 /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
55 /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
56 /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
57 /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
58 /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
59 /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
60 /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
61 /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
62 /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
63 /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
64 /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
65 /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
66 /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
67 /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
68 /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
69 /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
70 /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
71 /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
72 /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
73 /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
74 /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
75 /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
76 /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
77 /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
78 /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
79 /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
80 /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
81 /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
82 /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
83 /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
84 /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
85 /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
86 /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
87 /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
88 /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
89 /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
90 /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
91 /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
92 /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
93 /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
94 /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
95 /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
96 /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
97 /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
98 /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
99 /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
100 /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
101 /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
102 /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
103 /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
104 /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
105 /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
106 /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
107 /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
108 /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
109 /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
110 /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
111 /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
112 /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
113 /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
114 /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
115 /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
116 /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
117 /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
118 /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
119 /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
120 /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
121 /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
122 /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
123 /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
124 /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
125 /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
126 /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
127 /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
128 /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
129 /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
130 /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
131 /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
132 /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
133 /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
134 /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
135 /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
136 /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
137 /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
138 /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
139 /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
140 /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
141 /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
142 /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
143 /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
144 /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
145 /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
146 /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
147 /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
148 /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
149 /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
150 /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
151 /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
152 /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
153 /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
154 /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
155 /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
156 /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
157 /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
158 /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
159 /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
160 /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
161 /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
162 /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
163 /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
164 /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
165 /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
166 /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
167 /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
168 /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
169 /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
170 /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
171 /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
172 /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
173 /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
174 /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
175 /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
176 /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
177 /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
178 /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
179 /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
180 /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
181 /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
182 /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
183 /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
184 /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
185 /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
186 /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
187 /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
188 /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
189 /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
190 /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
191 /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
192 /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
193 /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
194 /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
195 /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
196 /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
197 /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
198 /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
199 /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
200 /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
201 /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
202 /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
203 /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
204 /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
205 /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
206 /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
207 /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
208 /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
209 /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
210 /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
211 /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
212 /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
213 /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
214 /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
215 /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
216 /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
217 /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
218 /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
219 /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
220 /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
221 /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
222 /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
223 /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
224 /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
225 /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
226 /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
227 /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
228 /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
229 /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
230 /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
231 /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
232 /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
233 /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
234 /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
235 /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
236 /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
237 /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
238 /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
239 /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
240 /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
241 /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
242 /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
243 /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
244 /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
245 /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
246 /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
247 /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
248 /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
249 /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
250 /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
251 /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
252 /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
253 /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
254 /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
255 /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
256 /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
257 /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
258 /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
259 /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
260 /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
261 /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
262 /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
263 /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
264 /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
265 /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
266 /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
267 /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
268 /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
269 /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
270 /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
271 /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
272 /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
273 /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
274 /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
275 /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
276 /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
277 /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
278 /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
279 /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
280 /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
281 /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
282 /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
283 /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
284 /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
285 /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
286 /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
287 /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
288 /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
289 /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
290 /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
291 /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
292 /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
293 /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
294 /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
295 /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
296 /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
297 /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
298 /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
299 /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
300 /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
301 /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
302 /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
303 /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
304 /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
305 /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
306 /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
307 /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
308 /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
309 /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
310 /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
311 /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
312 /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
313 /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
314 /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
315 /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
316 /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
317 /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
318 /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
319 /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
320 /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
321 /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
322 /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
323 /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
324 /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
325 /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
326 /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
327 /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
328 /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
329 /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
330 /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
331 /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
332 /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
333 /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
334 /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
335 /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
336 /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
337 Total - 2480 6210 705 608 2634 33 27027 38504 10405 238 20 2913 147 4969 47542 144435

File diff suppressed because one or more lines are too long

View File

@ -1,148 +0,0 @@
# 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)

View File

@ -1,486 +0,0 @@
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 |
+---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+

View File

@ -0,0 +1,180 @@
---
name: foss-compliance-reviewer
description: "Use this agent when new dependencies, services, or tools are being added to the project, or when reviewing code changes that introduce third-party libraries, external services, or infrastructure components. It should verify that the project maintains its commitment to Free and Open Source Software (FOSS) principles.\\n\\nExamples:\\n\\n- Example 1:\\n user: \"Let's add Stripe for payment processing and Auth0 for authentication\"\\n assistant: \"Let me check these choices against our FOSS principles before proceeding.\"\\n <uses Task tool to launch foss-compliance-reviewer agent to evaluate Stripe and Auth0>\\n assistant: \"The FOSS compliance reviewer has flagged both Stripe and Auth0 as proprietary services. Here are the findings and suggested alternatives...\"\\n\\n- Example 2:\\n user: \"I just added chart.js and @sentry/node to the project\"\\n assistant: \"Let me verify these new dependencies align with our FOSS standards.\"\\n <uses Task tool to launch foss-compliance-reviewer agent to review chart.js and @sentry/node>\\n assistant: \"The reviewer found that chart.js is MIT-licensed (approved), but @sentry/node connects to a proprietary SaaS by default. Here are the details...\"\\n\\n- Example 3:\\n user: \"Can you set up monitoring with Datadog?\"\\n assistant: \"Before implementing this, let me check if Datadog aligns with our FOSS principles.\"\\n <uses Task tool to launch foss-compliance-reviewer agent to evaluate Datadog vs existing FOSS monitoring stack>\\n assistant: \"The FOSS compliance reviewer notes that we already have a Prometheus + Grafana monitoring stack which is fully open source. Datadog is proprietary SaaS. Recommendation is to continue using our existing FOSS stack.\"\\n\\n- Example 4:\\n user: \"Please install these packages: prisma, bullmq, axios, and algolia\"\\n assistant: \"Let me review these packages for FOSS compliance before installing.\"\\n <uses Task tool to launch foss-compliance-reviewer agent to review prisma, bullmq, axios, and algolia>\\n assistant: \"The reviewer approved prisma (Apache-2.0), bullmq (MIT), and axios (MIT). However, Algolia's search client connects to proprietary SaaS. Suggested alternatives include Meilisearch or Typesense.\""
model: sonnet
color: purple
memory: project
---
You are an expert Free and Open Source Software (FOSS) compliance reviewer with deep knowledge of open source licensing, the FOSS ecosystem, and self-hosted infrastructure. You have extensive experience evaluating software dependencies, services, and tools against FOSS principles. You understand the nuances between truly open source software, source-available software, open-core models, and proprietary systems.
## Your Mission
You review technology choices in the Changemaker Lite project to ensure the stack remains predominantly Free and Open Source. This is a self-hosted political campaign platform that values digital sovereignty, transparency, and community-driven software. The project already demonstrates strong FOSS alignment with its stack (PostgreSQL, Redis, Nginx, Prometheus, Grafana, Listmonk, Gitea, n8n, NocoDB, MkDocs, etc.).
## Review Process
When evaluating technology choices, follow this systematic approach:
### 1. Identify What's Being Evaluated
- New npm/Node.js dependencies
- Docker services or containers
- External APIs or SaaS platforms
- Development tools
- Infrastructure components
- Frontend libraries or frameworks
### 2. Check License Classification
For each item, determine its license and classify it:
**Approved FOSS Licenses (Green):**
- MIT, ISC, BSD-2-Clause, BSD-3-Clause
- Apache-2.0
- GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0
- MPL-2.0
- Unlicense, CC0-1.0
- PostgreSQL License
- Artistic-2.0
**Caution - Review Needed (Yellow):**
- AGPL-3.0 (fine for self-hosted, but review implications)
- SSPL (Server Side Public License - used by MongoDB, not OSI-approved)
- BSL (Business Source License - used by some HashiCorp tools, MariaDB)
- Elastic License 2.0
- Commons Clause additions
- Any "source-available" but not OSI-approved license
**Not FOSS (Red):**
- Proprietary/commercial licenses
- SaaS-only services with no self-hosted option
- Closed-source binaries
- Services requiring proprietary API keys with no open alternative
### 3. Evaluate the Full Picture
Beyond just the license, consider:
- **Governance**: Is the project community-governed or single-company controlled?
- **Self-hostability**: Can it be fully self-hosted without phoning home?
- **Data sovereignty**: Does data stay on your infrastructure?
- **Vendor lock-in risk**: How hard is it to migrate away?
- **Open-core concerns**: Is the open source version meaningfully usable, or is it crippled to upsell?
- **Transitive dependencies**: Do key dependencies have problematic licenses?
### 4. Provide Clear Recommendations
For each item reviewed, provide:
- **Status**: ✅ Approved, ⚠️ Caution, ❌ Not Recommended
- **License**: The specific license
- **Reasoning**: Why it passes or fails
- **Alternative** (if not recommended): A FOSS alternative that achieves the same goal
## Project Context
The Changemaker Lite project already uses these FOSS-aligned technologies (use as reference for what's acceptable):
| Component | License | Category |
|-----------|---------|----------|
| PostgreSQL | PostgreSQL License | Database |
| Redis | BSD-3-Clause (pre-7.4) / RSALv2+SSPLv1 (7.4+) | Cache/Queue |
| Nginx | BSD-2-Clause | Reverse Proxy |
| Node.js/Express | MIT | API Framework |
| Fastify | MIT | API Framework |
| React | MIT | Frontend |
| Vite | MIT | Build Tool |
| Ant Design | MIT | UI Library |
| Prisma | Apache-2.0 | ORM |
| BullMQ | MIT | Job Queue |
| Prometheus | Apache-2.0 | Monitoring |
| Grafana | AGPL-3.0 | Dashboards |
| Listmonk | AGPL-3.0 | Newsletter |
| NocoDB | AGPL-3.0 | Data Browser |
| Gitea | MIT | Git Hosting |
| n8n | Sustainable Use License (⚠️) | Workflow |
| MkDocs | BSD-2-Clause | Documentation |
| GrapesJS | BSD-3-Clause | Page Builder |
| Leaflet | BSD-2-Clause | Maps |
| Docker | Apache-2.0 | Containers |
**Note on Redis**: Redis changed to dual RSALv2+SSPLv1 in v7.4. The project may be using an older BSD-licensed version or a fork like Valkey (BSD-3-Clause). Flag this if relevant.
**Note on n8n**: n8n uses the Sustainable Use License which is NOT OSI-approved. It's already in the project but should be noted as an exception.
## Output Format
Structure your review as follows:
```
## FOSS Compliance Review
### Items Reviewed
| Item | License | Status | Notes |
|------|---------|--------|-------|
| ... | ... | ✅/⚠️/❌ | ... |
### Detailed Findings
[For each item that is ⚠️ or ❌, provide detailed analysis]
### FOSS Alternatives
[For each ❌ item, suggest FOSS replacements]
### Overall Assessment
[Summary: Is the project maintaining its FOSS commitment?]
```
## Important Guidelines
1. **Be pragmatic, not dogmatic.** A project that is 95% FOSS with a few pragmatic exceptions (like n8n) is still a strong FOSS project. Note exceptions but don't treat them as failures.
2. **Distinguish between dependencies and services.** An MIT-licensed npm package that only works with a proprietary API is effectively proprietary. Evaluate the full dependency chain.
3. **Consider the ecosystem.** Some packages are so standard (e.g., Express, React) that their FOSS status is well-established. Focus your detailed analysis on less common or newer additions.
4. **Check actual files when possible.** Use tools to read `package.json` files, `docker-compose.yml`, and other configuration to identify what's actually in use. Don't rely solely on what the user tells you.
5. **Flag copyleft implications.** If a GPL/AGPL dependency is being used, note any distribution or linking implications, especially for the API server.
6. **Acknowledge trade-offs.** Sometimes there's no good FOSS alternative for a specific need. In those cases, be honest about the trade-off rather than recommending an inferior FOSS option.
7. **When reviewing recently added code**, focus on new `import` statements, new entries in `package.json`, new services in `docker-compose.yml`, and any new external API integrations.
**Update your agent memory** as you discover licensing information about dependencies, services with licensing changes (like Redis's license change), FOSS alternatives that work well for specific use cases, and any exceptions or trade-offs the project has accepted. This builds institutional knowledge across conversations. Write concise notes about what you found.
Examples of what to record:
- License classifications for commonly used packages
- Known licensing changes in popular projects (e.g., Redis, Elasticsearch, Terraform)
- Verified FOSS alternatives that have been evaluated
- Project-specific exceptions and the reasoning behind them
- Transitive dependency issues discovered during reviews
# Persistent Agent Memory
You have a persistent Persistent Agent Memory directory at `/home/bunker-admin/changemaker.lite/.claude/agent-memory/foss-compliance-reviewer/`. Its contents persist across conversations.
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
Guidelines:
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
- Update or remove memories that turn out to be wrong or outdated
- Organize memory semantically by topic, not chronologically
- Use the Write and Edit tools to update your memory files
What to save:
- Stable patterns and conventions confirmed across multiple interactions
- Key architectural decisions, important file paths, and project structure
- User preferences for workflow, tools, and communication style
- Solutions to recurring problems and debugging insights
What NOT to save:
- Session-specific context (current task details, in-progress work, temporary state)
- Information that might be incomplete — verify against project docs before writing
- Anything that duplicates or contradicts existing CLAUDE.md instructions
- Speculative or unverified conclusions from reading a single file
Explicit user requests:
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
## MEMORY.md
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.

408
.env.example Normal file
View File

@ -0,0 +1,408 @@
# ==============================================================================
# Changemaker Lite v2 — Environment Variables
# Copy this file to .env and fill in the values
# Generate secrets with: openssl rand -hex 32
# ==============================================================================
#
# SECURITY WARNING:
# - All passwords marked REQUIRED_STRONG_PASSWORD_CHANGE_THIS MUST be changed
# - Use strong, unique passwords (20+ characters recommended)
# - Generate secrets with: openssl rand -hex 32
# - NEVER commit .env to version control
# ==============================================================================
# ==============================================================================
# MINIMUM VIABLE SETUP (required — change these before deploying)
# ==============================================================================
# 1. V2_POSTGRES_PASSWORD — database password (8+ chars)
# 2. REDIS_PASSWORD — cache password (8+ chars)
# 3. JWT_ACCESS_SECRET — openssl rand -hex 32
# 4. JWT_REFRESH_SECRET — openssl rand -hex 32 (different from above)
# 5. JWT_INVITE_SECRET — openssl rand -hex 32 (different from above)
# 6. ENCRYPTION_KEY — openssl rand -hex 32 (different from JWT secrets)
# 7. INITIAL_ADMIN_PASSWORD — 12+ chars, uppercase + lowercase + digit
# 8. DOMAIN — your deployment domain (default: cmlite.org)
#
# Everything below these 8 values works with defaults for development.
# ==============================================================================
# --- General ---
NODE_ENV=development
# Root domain serves MkDocs documentation site only
# All application routes (admin + public) accessible at app.${DOMAIN}
DOMAIN=cmlite.org
USER_ID=1000
GROUP_ID=1000
DOCKER_GROUP_ID=984
# --- V2 PostgreSQL ---
V2_POSTGRES_USER=changemaker
V2_POSTGRES_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
V2_POSTGRES_DB=changemaker_v2
V2_POSTGRES_PORT=5433
# --- JWT Auth ---
JWT_ACCESS_SECRET=GENERATE_WITH_openssl_rand_hex_32
JWT_REFRESH_SECRET=GENERATE_WITH_openssl_rand_hex_32
JWT_INVITE_SECRET=GENERATE_WITH_openssl_rand_hex_32
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d
# Encryption key for DB-stored secrets (SMTP password, etc.)
# REQUIRED in production — must NOT reuse JWT_ACCESS_SECRET
# Generate with: openssl rand -hex 32
ENCRYPTION_KEY=GENERATE_WITH_openssl_rand_hex_32
# --- Initial Super Admin User (auto-created during database seeding) ---
# These credentials are used to create the initial super admin account
# Change these before running the seed script in production
INITIAL_ADMIN_EMAIL=admin@cmlite.org
INITIAL_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
# --- API ---
API_PORT=4000
API_URL=http://localhost:4000
# Include docs/root domain for inline payment widgets on MkDocs pages
CORS_ORIGINS=http://localhost:3000,http://localhost,http://localhost:4003
# --- Admin GUI ---
ADMIN_PORT=3000
ADMIN_URL=http://localhost:3000
# --- Nginx ---
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
# --- Embed Proxy Ports ---
# Dedicated nginx ports for iframe embedding without DNS/subdomain.
# Change these to avoid port conflicts when running multiple instances on one host.
NOCODB_EMBED_PORT=8881
N8N_EMBED_PORT=8882
GITEA_EMBED_PORT=8883
MAILHOG_EMBED_PORT=8884
MINI_QR_EMBED_PORT=8885
EXCALIDRAW_EMBED_PORT=8886
HOMEPAGE_EMBED_PORT=8887
VAULTWARDEN_EMBED_PORT=8890
ROCKETCHAT_EMBED_PORT=8891
GANCIO_EMBED_PORT=8892
JITSI_EMBED_PORT=8893
GRAFANA_EMBED_PORT=8894
ALERTMANAGER_EMBED_PORT=8895
# --- Docker / Container Management ---
# Docker network name (used by dashboard to auto-discover containers)
DOCKER_NETWORK_NAME=changemaker-lite
# Docker socket proxy URL (read-only container inspection)
DOCKER_PROXY_URL=http://docker-socket-proxy:2375
# Newt tunnel container (for Pangolin restart/status checks)
NEWT_CONTAINER_NAME=newt-changemaker
NEWT_COMPOSE_SERVICE=newt
# --- SMTP / Email ---
SMTP_HOST=mailhog-changemaker
SMTP_PORT=1025
SMTP_USER=
SMTP_PASS=
SMTP_FROM=noreply@cmlite.org
SMTP_FROM_NAME=Changemaker Lite
EMAIL_TEST_MODE=true
TEST_EMAIL_RECIPIENT=admin@cmlite.org
# --- Listmonk ---
LISTMONK_PORT=9001
# Use 5434 to avoid conflict with main PostgreSQL (5432 internal / 5433 host)
LISTMONK_DB_PORT=5434
LISTMONK_DB_USER=listmonk
LISTMONK_DB_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
LISTMONK_DB_NAME=listmonk
# Web admin login (for the Listmonk dashboard at :9001)
LISTMONK_WEB_ADMIN_USER=admin
LISTMONK_WEB_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
# API user (auto-created by listmonk-init container, used by V2 API for sync)
# Generate token: openssl rand -hex 16
# NOTE: LISTMONK_ADMIN_USER/PASSWORD are what the V2 API uses to connect.
# They MUST match LISTMONK_API_USER/TOKEN (same credentials, different var names).
LISTMONK_API_USER=v2-api
LISTMONK_API_TOKEN=GENERATE_WITH_openssl_rand_hex_16
LISTMONK_ADMIN_USER=v2-api
LISTMONK_ADMIN_PASSWORD=SAME_AS_LISTMONK_API_TOKEN
LISTMONK_SYNC_ENABLED=false
LISTMONK_WEBHOOK_SECRET=
LISTMONK_PROXY_PORT=9002
# Listmonk SMTP — MailHog for development (production SMTP added as second provider if credentials set)
LISTMONK_SMTP_HOST=mailhog-changemaker
LISTMONK_SMTP_PORT=1025
LISTMONK_SMTP_USER=
LISTMONK_SMTP_PASSWORD=
LISTMONK_SMTP_TLS_TYPE=none
LISTMONK_SMTP_FROM=Changemaker Lite <noreply@cmlite.org>
# Production SMTP (uncomment and set for real email delivery):
# LISTMONK_SMTP_HOST=smtp.protonmail.ch
# LISTMONK_SMTP_PORT=587
# LISTMONK_SMTP_USER=your@email.com
# LISTMONK_SMTP_PASSWORD=your-password
# LISTMONK_SMTP_TLS_TYPE=STARTTLS
# --- Represent API (Canadian electoral data) ---
REPRESENT_API_URL=https://represent.opennorth.ca
# --- NocoDB v2 (read-only data browser) ---
# NocoDB uses its own database (nocodb_meta) to avoid conflicts with Prisma
# The database is auto-created by init-nocodb-db.sh on first PostgreSQL startup
# nocodb-init container auto-registers changemaker_v2 as a browsable data source
NOCODB_V2_PORT=8091
NOCODB_URL=http://changemaker-v2-nocodb:8080
NOCODB_PORT=8091
NC_ADMIN_EMAIL=admin@cmlite.org
NC_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
# --- Redis ---
# Shared Redis (v2 uses authenticated connection)
REDIS_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379
# --- Payments (Stripe) ---
# Enable payments feature (memberships, products, donations)
# Stripe API keys are stored encrypted in DB via admin settings page
ENABLE_PAYMENTS=false
# --- Media Management ---
ENABLE_MEDIA_FEATURES=false
MEDIA_API_PORT=4100
MEDIA_API_PUBLIC_URL=http://media-api:4100
# Used during admin Docker build to set the media API endpoint for Vite
VITE_MEDIA_API_URL=http://changemaker-media-api:4100
MEDIA_ROOT=/media/library
MEDIA_UPLOADS=/media/uploads
MAX_UPLOAD_SIZE_GB=10
PUBLIC_MEDIA_PORT=3100
VIDEO_PLAYER_DEBUG=false
# Video Analytics (Feb 2026)
VIDEO_ANALYTICS_RETENTION_DAYS=90
VIDEO_ANALYTICS_IP_HASHING_ENABLED=true
# Video Scheduling (Feb 2026)
VIDEO_SCHEDULE_DEFAULT_TIMEZONE=UTC
VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true
# Preview Links (Feb 2026)
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
# --- Container Registry ---
# Gitea registry for pre-built production images.
# Set IMAGE_TAG to a commit SHA (or 'latest') to pull pre-built images instead of building from source.
# Leave IMAGE_TAG blank/unset (defaults to 'local') to build locally from source.
GITEA_REGISTRY=gitea.bnkops.com/admin
IMAGE_TAG=
# Docker Compose profiles — set to 'monitoring' to include Prometheus/Grafana/Alertmanager
# in every 'docker compose up -d'. Leave blank to start monitoring separately.
COMPOSE_PROFILES=
# Credentials used by the registry status API endpoint (GET /api/registry/status)
# For docker push/pull, run: docker login gitea.bnkops.com
GITEA_REGISTRY_USER=admin
GITEA_REGISTRY_PASS=
# --- Gitea ---
GITEA_URL=http://gitea-changemaker:3000
GITEA_PORT=3030
GITEA_WEB_PORT=3030
GITEA_SSH_PORT=2222
GITEA_DB_TYPE=mysql
GITEA_DB_HOST=gitea-db:3306
GITEA_DB_NAME=gitea
GITEA_DB_USER=gitea
GITEA_DB_PASSWD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
GITEA_DB_ROOT_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
GITEA_ROOT_URL=https://git.cmlite.org
GITEA_DOMAIN=git.cmlite.org
# --- Gitea Docs Comments ---
# Enable comments on MkDocs pages (backed by Gitea Issues)
GITEA_COMMENTS_ENABLED=false
# Personal access token with repo write scope (create in Gitea → Settings → Applications)
GITEA_API_TOKEN=
# Repository owner (Gitea username that will own the docs-comments repo)
GITEA_COMMENTS_REPO_OWNER=
# Repository name (auto-created via admin setup button)
GITEA_COMMENTS_REPO_NAME=docs-comments
# OAuth2 Application credentials (create in Gitea → Settings → Applications → OAuth2)
# Redirect URIs: https://{DOMAIN}/comments/callback/ and http://localhost:4003/comments/callback/
GITEA_OAUTH_CLIENT_ID=
GITEA_OAUTH_CLIENT_SECRET=
# --- n8n ---
N8N_URL=http://n8n-changemaker:5678
N8N_PORT=5678
N8N_HOST=n8n.cmlite.org
N8N_ENCRYPTION_KEY=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
N8N_USER_EMAIL=admin@example.com
N8N_USER_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
GENERIC_TIMEZONE=UTC
# --- MkDocs ---
# Port mapping for MkDocs container (host:container)
# This also controls the Vite dev proxy in local development
# Change this port to use a different local port, and the admin dev server will automatically use it
MKDOCS_PORT=4003
MKDOCS_SITE_SERVER_PORT=4004
BASE_DOMAIN=https://cmlite.org
MKDOCS_PREVIEW_URL=http://mkdocs:8000
MKDOCS_DOCS_PATH=/mkdocs/docs
# --- Code Server ---
CODE_SERVER_PORT=8888
CODE_SERVER_URL=http://code-server-changemaker:8443
# --- Homepage ---
HOMEPAGE_PORT=3010
HOMEPAGE_VAR_BASE_URL=http://localhost
# --- Mini QR ---
MINI_QR_PORT=8089
MINI_QR_URL=http://mini-qr:8080
# --- Excalidraw (Collaborative Whiteboard) ---
EXCALIDRAW_PORT=8090
EXCALIDRAW_URL=http://excalidraw-changemaker:80
EXCALIDRAW_WS_URL=wss://draw.cmlite.org
# --- Vaultwarden (Password Manager) ---
VAULTWARDEN_PORT=8445
VAULTWARDEN_URL=http://vaultwarden-changemaker:80
# Admin panel token (access at /admin) — generate with: openssl rand -hex 32
VAULTWARDEN_ADMIN_TOKEN=
# MUST use HTTPS — Bitwarden web vault enforces HTTPS for account creation
# Set to your Pangolin tunnel URL (e.g., https://vault.yourdomain.org)
# Local access (browsing existing vault) works on HTTP, but signup/invite requires HTTPS
VAULTWARDEN_DOMAIN=https://vault.cmlite.org
VAULTWARDEN_SIGNUPS_ALLOWED=false
VAULTWARDEN_WEBSOCKET_ENABLED=true
# SMTP security: "off" for MailHog, "starttls" or "force_tls" for production
VAULTWARDEN_SMTP_SECURITY=off
# --- MailHog ---
MAILHOG_SMTP_PORT=1025
MAILHOG_WEB_PORT=8025
# --- NAR (National Address Register) ---
# Path to extracted NAR data (contains YYYYMM/Addresses/ and YYYYMM/Locations/)
# Download from: https://www150.statcan.gc.ca/n1/pub/46-26-0002/462600022022001-eng.htm
NAR_DATA_DIR=/data
# --- Overpass / Area Import ---
# OpenStreetMap Overpass API endpoint (use a private instance for heavy usage)
OVERPASS_API_URL=https://overpass-api.de/api/interpreter
# Minimum delay between Overpass requests (ms) — public API requires 30s
OVERPASS_MIN_DELAY_MS=30000
# Maximum reverse geocode grid points for area import fill-in
AREA_IMPORT_MAX_GRID_POINTS=500
# --- Geocoding ---
# Optional Mapbox API key for improved geocoding accuracy
# Free tier: 100,000 requests/month
# Sign up: https://www.mapbox.com/pricing
MAPBOX_API_KEY=
# Rate limit delay between provider requests (milliseconds)
GEOCODING_RATE_LIMIT_MS=1100
# Redis-backed persistent cache settings
GEOCODING_CACHE_ENABLED=true
GEOCODING_CACHE_TTL_HOURS=24
# Phase 2: Performance & Accuracy
# Google Maps API (optional, most accurate but costs $0.005/request after 100k/month)
GOOGLE_MAPS_API_KEY=
GOOGLE_MAPS_ENABLED=false
# Parallel geocoding for bulk imports (10x speedup)
GEOCODING_PARALLEL_ENABLED=true
GEOCODING_BATCH_SIZE=10
# Bulk Re-Geocoding (Phase 3)
BULK_GEOCODE_ENABLED=true
BULK_GEOCODE_MAX_BATCH=5000
# --- Pangolin Tunnel ---
# Server: self-hosted Pangolin instance
PANGOLIN_API_URL=https://api.bnkserve.org/v1
PANGOLIN_API_KEY=
PANGOLIN_ORG_ID=
# Populated after setup (via admin GUI or API)
PANGOLIN_SITE_ID=
PANGOLIN_ENDPOINT=https://pangolin.bnkserve.org
PANGOLIN_NEWT_ID=
PANGOLIN_NEWT_SECRET=
# --- Prisma CLI (host-side only, NOT used by Docker containers) ---
# Containers resolve the DB hostname internally via docker-compose environment
# This is used when running `npx prisma migrate dev` from the host machine
DATABASE_URL=postgresql://changemaker:YOUR_POSTGRES_PASSWORD@localhost:5433/changemaker_v2
# --- Rocket.Chat (Team Chat) ---
# ENABLE_CHAT is the initial default; once saved in admin Settings, the DB value is authoritative
ENABLE_CHAT=false
ROCKETCHAT_ADMIN_USER=rcadmin
ROCKETCHAT_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
ROCKETCHAT_URL=http://rocketchat-changemaker:3000
# MongoDB credentials for Rocket.Chat (required — MongoDB runs with --auth)
MONGO_ROOT_USER=rocketchat
MONGO_ROOT_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
# --- Gancio (Event Management) ---
# Uses shared PostgreSQL (database: gancio, auto-created by init-gancio-db.sh)
GANCIO_PORT=8092
GANCIO_URL=http://gancio-changemaker:13120
GANCIO_BASE_URL=https://events.cmlite.org
# Gancio admin credentials for shift-to-event sync (OAuth login)
GANCIO_ADMIN_USER=admin
GANCIO_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
# Enable automatic shift → Gancio event sync
GANCIO_SYNC_ENABLED=false
# --- Jitsi Meet (Video Conferencing) ---
# Self-hosted Jitsi with JWT auth — integrates with Rocket.Chat for channel video calls
# ENABLE_MEET is the initial default; once saved in admin Settings, the DB value is authoritative
ENABLE_MEET=false
# JWT authentication (shared between Jitsi Prosody, Rocket.Chat, and the API)
# Generate with: openssl rand -hex 32
JITSI_APP_ID=changemaker
JITSI_APP_SECRET=GENERATE_WITH_openssl_rand_hex_32
# Internal XMPP passwords (used between Jitsi containers, not exposed externally)
# Generate each with: openssl rand -hex 16
JITSI_JICOFO_AUTH_PASSWORD=GENERATE_WITH_openssl_rand_hex_16
JITSI_JVB_AUTH_PASSWORD=GENERATE_WITH_openssl_rand_hex_16
JITSI_URL=http://jitsi-web-changemaker:80
# JVB public IP (required for NAT traversal — set to server's public IP in production)
JVB_ADVERTISE_IP=
# JVB UDP port for media traffic (must be open in firewall)
JVB_PORT=10000
# --- SMS Campaigns (Termux Android Bridge) ---
# ENABLE_SMS is the initial default; once saved in admin Settings, the DB value is authoritative
# URL + API key are typically managed via admin Settings page (DB overrides env)
# Use Tailscale IP (100.x.x.x) for stable addressing across networks
ENABLE_SMS=false
TERMUX_API_URL=http://100.x.x.x:5001
TERMUX_API_KEY=
SMS_DELAY_BETWEEN_MS=3000
SMS_MAX_RETRIES=3
SMS_RESPONSE_SYNC_INTERVAL_MS=120000
SMS_DEVICE_MONITOR_INTERVAL_MS=300000
# --- Monitoring (only used with --profile monitoring) ---
PROMETHEUS_PORT=9090
GRAFANA_PORT=3005
GRAFANA_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
GRAFANA_ROOT_URL=http://localhost:3005
CADVISOR_PORT=8086
NODE_EXPORTER_PORT=9100
REDIS_EXPORTER_PORT=9121
ALERTMANAGER_PORT=9093
GOTIFY_PORT=8889
GOTIFY_ADMIN_USER=admin
GOTIFY_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
# --- Bunker Ops (Fleet Management) ---
INSTANCE_LABEL= # Unique label for this instance (defaults to DOMAIN)
BUNKER_OPS_ENABLED=false # Enable remote metrics push to central server
BUNKER_OPS_REMOTE_WRITE_URL= # VictoriaMetrics remote_write endpoint (e.g., https://ops.example.com/api/v1/write)

56
.gitignore vendored
View File

@ -1,14 +1,16 @@
# Node modules
node_modules/
*/node_modules/
**/node_modules/
/configs/code-server/.local/*
!/configs/code-server/.local/.gitkeep
/configs/code-server/.config/*
!/configs/code-server/.config/.gitkeep
# MkDocs cache and built site (created by containers)
/mkdocs/.cache/*
!/mkdocs/.cache/.gitkeep
/mkdocs/site/*
!/mkdocs/site/.gitkeep
# Root assets (generated by containers)
/assets/
# Homepage logs (created by container)
/configs/homepage/logs/*
@ -16,6 +18,7 @@
.env
.env*
!.env.example
/configs/cloudflare/*.json
/configs/cloudflare/*.yaml
@ -25,4 +28,45 @@
/.VSCodeCounter
/influence/app/public/uploads
/influence/app/public/uploadsdata/
# NAR data directory (large voter registry files)
/data/*
!/data/upgrade/
/data/upgrade/*.json
# Media files (managed by Docker volumes, not git)
/media/
# Nginx generated configs (built from *.template at container startup)
nginx/conf.d/*.conf
# Ansible per-instance override (generated by Bunker Ops)
docker-compose.override.yml
# Build output
/admin/dist/
# Core dumps
core.*
*/core.*
# MkDocs core binary and container-generated assets (owned by root, not stashable)
/mkdocs/core
/mkdocs/assets/
# Upgrade artifacts
/logs/
/backups/
.upgrade.lock
# Release tarballs (generated by build-release.sh)
/releases/
# API compiled output (generated by tsc, baked into Docker images)
/api/dist/
# Control Panel runtime data (managed deployments + backups)
/changemaker-control-panel/instances/
/changemaker-control-panel/backups/
logs/

18
.mcp.json Normal file
View File

@ -0,0 +1,18 @@
{
"mcpServers": {
"changemaker": {
"command": "npx",
"args": ["tsx", "mcp-server/src/index.ts"],
"cwd": "/home/bunker-admin/changemaker.lite",
"env": {
"CML_API_URL": "http://localhost:4002",
"CML_SERVICE_EMAIL": "admin@bnkops.ca",
"CML_SERVICE_PASSWORD": "ChangeMe2025!"
}
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
}
}
}

View File

@ -0,0 +1,2 @@
[ 288ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/:2287
[ 288ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,6 @@
[ 92ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/:0
[ 92ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 496039ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/:0
[ 496039ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 498038ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/:0
[ 498038ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,26 @@
[ 121ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/:885
[ 121ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 497669ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2201
[ 497669ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 499981ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2302
[ 499981ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 503949ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:0
[ 503949ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 506409ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2302
[ 506409ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 510957ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2302
[ 510957ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 523501ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2304
[ 523501ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 534339ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:891
[ 534339ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 536931ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:0
[ 536931ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 543415ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2312
[ 543415ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 545948ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2209
[ 545948ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 552080ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:0
[ 552080ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 554689ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/#use-the-platform:2313
[ 554689ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 101ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/:2313
[ 101ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1 @@
[ 287ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4004/favicon.ico:0

View File

@ -0,0 +1,2 @@
[ 118ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2272
[ 118ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 49ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2101
[ 49ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 52ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/getting-started/:2582
[ 52ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 59ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2313
[ 59ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 40ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2226
[ 40ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,6 @@
[ 269ms] ReferenceError: Missing element: expected "[data-md-component=header]" to be present
at j (http://localhost:4000/assets/javascripts/bundle.79ae519e.min.js:14:35799)
at Ce (http://localhost:4000/assets/javascripts/bundle.79ae519e.min.js:14:42721)
at http://localhost:4000/assets/javascripts/bundle.79ae519e.min.js:14:94068
at http://localhost:4000/assets/javascripts/bundle.79ae519e.min.js:14:95391
[ 418ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4000/favicon.ico:0

View File

@ -0,0 +1,2 @@
[ 339ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:511
[ 339ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 36ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2212
[ 36ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 64ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2315
[ 64ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 189ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:511
[ 189ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,2 @@
[ 150ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:2315
[ 151ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,14 @@
[ 64ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/:893
[ 65ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 926012ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/?_=1773266458361:933
[ 926012ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 1794181ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/?_=1773267326487:2359
[ 1794181ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 1857070ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/?v=1773267389387:2391
[ 1857070ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 2018066ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/?r=1773267550383:2406
[ 2018066ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 2115925ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/?final=1773267648297:571
[ 2115925ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0
[ 2810593ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4004' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4004/docs/?ff=1773268342997:961
[ 2810593ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,21 @@
[ 1411ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3002/favicon.ico:0
[ 11195ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/connectivity:0
[ 11196ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/services/status:0
[ 11197ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/weather:0
[ 11197ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/docs-analytics/summary?days=30:0
[ 11198ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/chat-summary:0
[ 11199ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/rocketchat-stats:0
[ 11199ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/upcoming-shifts:0
[ 11200ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/jitsi/meetings:0
[ 11201ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/influence/effectiveness/overview:0
[ 11201ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/top-videos:0
[ 11203ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/recent-signups:0
[ 11204ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/recent-comments:0
[ 11205ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/listmonk/stats:0
[ 11206ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/listmonk-campaigns:0
[ 11206ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/listmonk:0
[ 11207ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/observability/alerts:0
[ 11208ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/payments/admin/dashboard:0
[ 11209ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/gitea-activity:0
[ 11209ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/vaultwarden-adoption:0
[ 11210ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/map/canvass/analytics/cuts:0

View File

@ -0,0 +1,8 @@
[ 788ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/auth/me:0
[ 789ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/settings:0
[ 791ms] [ERROR] Unexpected auth error: AxiosError: Request failed with status code 500
at settle (http://localhost:3002/node_modules/.vite/deps/axios.js?v=c83de56d:1281:12)
at XMLHttpRequest.onloadend (http://localhost:3002/node_modules/.vite/deps/axios.js?v=c83de56d:1638:7)
at Axios.request (http://localhost:3002/node_modules/.vite/deps/axios.js?v=c83de56d:2255:41)
at async Object.fetchMe (http://localhost:3002/src/stores/auth.store.ts:101:28)
at async hydrate (http://localhost:3002/src/stores/auth.store.ts:118:11) @ http://localhost:3002/src/stores/auth.store.ts:105

View File

@ -0,0 +1,30 @@
[ 960624ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 1920622ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 2880624ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 3840624ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 4800623ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 5760623ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 6720616ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 7680622ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 8640625ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 9600615ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[10560615ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[11520625ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[12480623ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[13440615ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[14400616ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[15360616ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[16320615ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[17280618ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[18240616ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[19200622ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[20160621ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[21120618ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[22080623ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[23040622ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[24000616ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[24960616ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[25920615ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[26880613ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[27840614ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[28800615ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0

View File

@ -0,0 +1,2 @@
[ 92ms] [ERROR] Access to resource at 'http://localhost:4002/api/docs-analytics/track' from origin 'http://localhost:4003' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. @ http://localhost:4003/docs/admin/dashboard/:1574
[ 92ms] [ERROR] Failed to load resource: net::ERR_FAILED @ http://localhost:4002/api/docs-analytics/track:0

View File

@ -0,0 +1,44 @@
[ 1044ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3002/favicon.ico:0
[ 1045ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0
[ 957294ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 1915502ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 2875494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 3835503ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 4795505ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 5755494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 6715495ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 7675495ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 8635495ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 9595539ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[10555496ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[11515504ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[12475494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[13435504ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[14395501ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[15355503ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[16315505ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[17275496ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[18235494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[19195496ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[20155502ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[21115501ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[22075494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[23035502ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[23995496ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[24955494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[25915495ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[26875500ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[27835504ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[28795505ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[29755503ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[30715505ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[31675500ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[32635503ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[33595504ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[34555501ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[35515495ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[36475494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[37435493ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[38395495ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[39355494ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[40315488ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0

View File

@ -0,0 +1 @@
[ 915ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3002/favicon.ico:0

View File

@ -0,0 +1,442 @@
[ 719376ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0
[ 949197ms] [ERROR] ReferenceError: MeetingAgendaPage is not defined
at App (http://localhost:3002/src/App.tsx?t=1773363079750:663:127)
at renderWithHooks (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:3520:25)
at updateFunctionComponent (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:5151:19)
at beginWork (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:5762:18)
at performUnitOfWork (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:8567:18)
at workLoopSync (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:8465:41)
at renderRootSync (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:8449:11)
at performWorkOnRoot (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:8124:44)
at performSyncWorkOnRoot (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:9134:7)
at flushSyncWorkAcrossRoots_impl (http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:9042:153) @ http://localhost:3002/node_modules/.vite/deps/chunk-2NI7C5SJ.js?v=c83de56d:4778
[ 953711ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/src/App.tsx?t=1773363084913:0
[ 1676461ms] [ERROR] WebSocket connection to 'ws://localhost:3002/' failed: Error during WebSocket handshake: net::ERR_CONNECTION_RESET @ http://localhost:3002/@vite/client:1034
[ 1677465ms] [ERROR] WebSocket connection to 'ws://localhost:3002/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/@vite/client:1034
[ 1678466ms] [ERROR] WebSocket connection to 'ws://localhost:3002/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/@vite/client:1034
[ 1679810ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ListmonkPage.tsx:0
[ 1679810ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/LandingPagesPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/DocsPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MkDocsSettingsPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/CodeEditorPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/NocoDBPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/N8nPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/GiteaPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MailHogPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MiniQRPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ExcalidrawPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/VaultwardenPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/RocketChatPage.tsx:0
[ 1679815ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/GancioPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/JitsiMeetPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/SettingsPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/NavigationSettingsPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/PangolinPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ObservabilityPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/DocsAnalyticsPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/DocsCommentsPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/PaymentsDashboardPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/SubscribersPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/ProductsPage.tsx:0
[ 1679816ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/DonationsPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/DonationPagesPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/PlansPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/payments/PaymentSettingsPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/LibraryPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/AnalyticsDashboardPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/MediaJobsPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/CommentModerationPage.tsx:0
[ 1679818ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/GalleryAdsPage.tsx:0
[ 1679819ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/AdAnalyticsDashboardPage.tsx:0
[ 1679819ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/influence/CampaignModerationPage.tsx:0
[ 1679819ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/influence/CampaignEffectivenessPage.tsx:0
[ 1679819ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/LandingPage.tsx:0
[ 1679819ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PagesIndexPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/EventsPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/HomePage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/CampaignsListPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/CampaignPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/CreateCampaignPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MyCampaignsPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ResponseWallPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MapPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShiftsPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MediaGalleryPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShortsPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MediaViewerPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlaylistBrowsePage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlaylistViewerPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/PlaylistManagementPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MyStatsPage.tsx:0
[ 1679820ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MySettingsPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerChatPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PricingPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShopPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ProductDetailPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlanDetailPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/DonatePage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/DonationPagesListPage.tsx:0
[ 1679821ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PaymentSuccessPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyActivityPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerShiftsPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyRoutesPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerMapPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/FriendsPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SocialProfilePage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/NotificationsPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SocialFeedPage.tsx:0
[ 1679824ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/DiscoverPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/GroupDetailPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/AchievementsPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/api.ts:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/roles.ts:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/QuickJoinPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/VerifyEmailPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ResetPasswordPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsDashboardPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsContactsPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsCampaignsPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsConversationsPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsTemplatesPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsSetupPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/PeoplePage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ContactProfilePage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialDashboardPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialGraphPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialModerationPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/ReferralAdminPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SpotlightAdminPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/ChallengesAdminPage.tsx:0
[ 1679825ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/influence/ImpactStoriesPage.tsx:0
[ 1679826ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ReferralsPage.tsx:0
[ 1679826ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ChallengesPage.tsx:0
[ 1679826ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ChallengeDetailPage.tsx:0
[ 1679826ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/WallOfFamePage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MeetingJoinPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MeetingPlannerPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MeetingAgendaPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ActionItemsPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/SchedulingPollPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PollsListPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/JitsiAuthPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/SchedulingCalendarPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/AdminCalendarViewPage.tsx:0
[ 1679827ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/TicketedEventsPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/EventDetailPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/CheckInScannerPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/TicketedEventDetailPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/TicketConfirmationPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyTicketsPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyCalendarPage.tsx:0
[ 1679828ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SharedCalendarsPage.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SharedCalendarViewPage.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/FriendCalendarPage.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/NotFoundPage.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/command-palette/CommandPalette.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/api.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/VolunteerFooterNav.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/PublicNavBar.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useSSE.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useLocalStorage.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/service-url.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/nav-defaults.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/stores/command-palette.store.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/stores/favorites.store.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/menu-items.ts:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/chat/RocketChatWidget.tsx:0
[ 1679829ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/MediaSidebar.tsx:0
[ 1679830ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/MediaBottomNav.tsx:0
[ 1679830ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/ChatNotificationToast.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/chatbar/ChatBarContext.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/chatbar/ChatBar.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useChatNotifications.ts:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/color.ts:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/AuthModal.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/public/NewsletterSignup.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/CampaignEmailsDrawer.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/ExportContactsModal.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/QrCodeModal.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoPickerModal.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/SystemGauges.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/MiniDonutChart.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RequestTrafficChart.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/LatencyBandsChart.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ContainerPopover.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ContainerMemoryChart.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ActivityFeedCard.tsx:0
[ 1679831ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/TodayEventsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ChatNotifierCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/TopVideosCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RecentCommentsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/DocsAnalyticsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/UpcomingShiftsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/MyActionItemsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/CampaignEffectivenessCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RecentSignupsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/NewsletterStatsCard.tsx:0
[ 1679832ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/DonationSummaryCard.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/SystemAlertsCard.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/GiteaActivityCard.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/VaultwardenAdoptionCard.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/UpcomingMeetingsCard.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/CutCampaignAnalyticsCard.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/TestEmailModal.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/VersionHistoryDrawer.tsx:0
[ 1679833ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/EmailTemplateEditor.tsx:0
[ 1685249ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/settings:0
[ 1685251ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/auth/me:0
[ 1685252ms] [ERROR] Unexpected auth error: AxiosError: Request failed with status code 500
at settle (http://localhost:3002/node_modules/.vite/deps/axios.js?v=c83de56d:1281:12)
at XMLHttpRequest.onloadend (http://localhost:3002/node_modules/.vite/deps/axios.js?v=c83de56d:1638:7)
at Axios.request (http://localhost:3002/node_modules/.vite/deps/axios.js?v=c83de56d:2255:41)
at async Object.fetchMe (http://localhost:3002/src/stores/auth.store.ts:101:28)
at async hydrate (http://localhost:3002/src/stores/auth.store.ts:118:11) @ http://localhost:3002/src/stores/auth.store.ts:105
[ 1685344ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/payments/plans:0
[758179964ms] [ERROR] WebSocket connection to 'ws://localhost:3002/' failed: Connection closed before receiving a handshake response @ http://localhost:3002/@vite/client:1034
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MyCampaignsPage.tsx:0
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ResponseWallPage.tsx:0
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MapPage.tsx:0
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShiftsPage.tsx:0
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MediaGalleryPage.tsx:0
[758181441ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShortsPage.tsx:0
[758181441ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MediaViewerPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlaylistBrowsePage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlaylistViewerPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/PlaylistManagementPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MyStatsPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MySettingsPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerChatPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PricingPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShopPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ProductDetailPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlanDetailPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/DonatePage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/DonationPagesListPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PaymentSuccessPage.tsx:0
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyActivityPage.tsx:0
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerShiftsPage.tsx:0
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyRoutesPage.tsx:0
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerMapPage.tsx:0
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/FriendsPage.tsx:0
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SocialProfilePage.tsx:0
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/NotificationsPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SocialFeedPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/DiscoverPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/GroupDetailPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/AchievementsPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/api.ts:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/roles.ts:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/QuickJoinPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/VerifyEmailPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ResetPasswordPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsDashboardPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsContactsPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsCampaignsPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsConversationsPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsTemplatesPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsSetupPage.tsx:0
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/PeoplePage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ContactProfilePage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialDashboardPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialGraphPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialModerationPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/ReferralAdminPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SpotlightAdminPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/ChallengesAdminPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/influence/ImpactStoriesPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ReferralsPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ChallengesPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ChallengeDetailPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/WallOfFamePage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MeetingJoinPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MeetingPlannerPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MeetingAgendaPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ActionItemsPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/SchedulingPollPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PollsListPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/JitsiAuthPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/SchedulingCalendarPage.tsx:0
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/AdminCalendarViewPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/TicketedEventsPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/EventDetailPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/CheckInScannerPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/TicketedEventDetailPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/TicketConfirmationPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyTicketsPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyCalendarPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SharedCalendarsPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SharedCalendarViewPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/FriendCalendarPage.tsx:0
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/NotFoundPage.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/command-palette/CommandPalette.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/api.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/VolunteerFooterNav.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/PublicNavBar.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useSSE.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useLocalStorage.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/service-url.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/nav-defaults.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/stores/command-palette.store.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/stores/favorites.store.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/menu-items.ts:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/chat/RocketChatWidget.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/MediaSidebar.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/MediaBottomNav.tsx:0
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/ChatNotificationToast.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/chatbar/ChatBarContext.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/chatbar/ChatBar.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useChatNotifications.ts:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/color.ts:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/AuthModal.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/public/NewsletterSignup.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/CampaignEmailsDrawer.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/ExportContactsModal.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/QrCodeModal.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoPickerModal.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/TestEmailModal.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/VersionHistoryDrawer.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/EmailTemplateEditor.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/map/CutEditorMap.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/SystemGauges.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/MiniDonutChart.tsx:0
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RequestTrafficChart.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/LatencyBandsChart.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ContainerPopover.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ContainerMemoryChart.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ActivityFeedCard.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/TodayEventsCard.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ChatNotifierCard.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/TopVideosCard.tsx:0
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RecentCommentsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/DocsAnalyticsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/UpcomingShiftsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/MyActionItemsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/CampaignEffectivenessCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RecentSignupsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/NewsletterStatsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/DonationSummaryCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/SystemAlertsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/GiteaActivityCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/VaultwardenAdoptionCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/UpcomingMeetingsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/CutCampaignAnalyticsCard.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/shifts/EditModeModal.tsx:0
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/shifts/ShiftsCalendar.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/map/AdminMapView.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/map/AreaImportWizard.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/canvass.ts:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/AdminLiveMap.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/HistoricalRoutesDrawer.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/CanvassTrendsCard.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useMkDocsBuild.ts:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/landing-pages/LandingPageEditor.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDocsEditor.ts:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/docs/MobileDocsEditor.tsx:0
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoPickerModal.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoInsertModal.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/videoCardHtml.ts:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/photoCardHtml.ts:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/DonateInsertModal.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/ProductInsertModal.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AdPickerModal.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/scheduling/PollInsertModal.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDocsCollaboration.ts:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/docs/CollaboratorAvatars.tsx:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/wikiLinkCompletion.ts:0
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/docs/WikiLinkPickerModal.tsx:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/ServiceStatusCard.tsx:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/MetricsGrid.tsx:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/AlertsTable.tsx:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/IframeErrorBoundary.tsx:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/media-api.ts:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/standalone/browser/standalone-tokens.css:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/aria/aria.css:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/codeEditor/editor.css:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/scrollbar/media/scrollbars.css:0
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/blockDecorations/blockDecorations.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/decorations/decorations.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/indentGuides/indentGuides.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/mouseCursor/mouseCursor.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/viewLines/viewLines.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/linesDecorations/linesDecorations.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/margin/margin.css:0
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/marginDecorations/marginDecorations.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/minimap/minimap.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/rulers/rulers.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/selections/selections.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/viewCursors/viewCursors.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/whitespace/whitespace.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/gpu/css/media/decorationCssRuleExtractor.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/controller/editContext/native/nativeEditContext.css:0
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/gpuMark/gpuMark.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/platform/hover/browser/hover.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/hover/hoverWidget.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/contextview/contextview.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/selectBox/selectBox.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/list/list.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/dnd/dnd.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/selectBox/selectBoxCustom.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/actionbar/actionbar.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/dropdown/dropdown.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/platform/actions/browser/menuEntryActionViewItem.css:0
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/toggle/toggle.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/platform/quickinput/browser/media/quickInput.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/button/button.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/countBadge/countBadge.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/progressbar/progressbar.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/inputbox/inputBox.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/findinput/findInput.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/iconLabel/iconlabel.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/keybindingLabel/keybindingLabel.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/tree/media/tree.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/sash/sash.css:0
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/splitview/splitview.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/table/table.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/toolbar/toolbar.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/diffEditor/style.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/markdownRenderer/browser/renderedMarkdown.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/multiDiffEditor/style.css:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDebounce.ts:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoCard.tsx:0
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoCard.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AlbumCard.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/BulkActionsBar.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PublishModal.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/DeleteConfirmModal.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/UploadVideoDrawer.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/UploadPhotoDrawer.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoViewerModal.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoViewerModal.tsx:0
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/EditPhotoModal.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AlbumDetailDrawer.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/CreateAlbumModal.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/QuickAnalyticsModal.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/SchedulePublishModal.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/ScheduleCalendarDrawer.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/EditVideoModal.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/FetchVideosDrawer.tsx:0
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AddToPlaylistModal.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/BulkAddToPlaylistModal.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/BulkAccessLevelModal.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/gallery-ads.ts:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/GalleryAdCard.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoPlayer.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AdvancedVideoPlayer.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/DonationWidget.tsx:0
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/PricingWidget.tsx:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/ProductWidget.tsx:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/influence/CampaignFormWidget.tsx:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/scheduling/SchedulingPollWidget.tsx:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDocumentTitle.ts:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/usePageAds.ts:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/AdBanner.tsx:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/calendar/UnifiedCalendar.tsx:0
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/public/ShiftSignupModal.tsx:0

View File

@ -0,0 +1,50 @@
[ 840406ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 1800416ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 2760412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 3720412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 4680414ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 5640416ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 6600415ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 7560413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 8520411ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 9480406ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[10440412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[11400412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[12360413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[13320416ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[14280412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[15240418ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[16200413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[17160406ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[18120407ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[19080428ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[20040413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[21000417ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[21960412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[22920413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[23880411ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[24840409ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[25800407ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[26760411ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[27720411ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[28680412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[29640407ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[30600412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[31560405ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[32520418ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[33480412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[34440414ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[35400411ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[36360450ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[37320412ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[38280418ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[39240414ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[40200413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[41160456ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[42120417ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[43080416ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[44040414ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[45000413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[45960411ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[46920413ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[47880405ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0

View File

@ -0,0 +1 @@
[ 567ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0

View File

@ -0,0 +1,66 @@
[ 156566ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 159562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 162561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 165562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 168561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 171561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 174562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 177561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 180561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 183561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 186561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 189561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 192561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 195561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 198562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 201561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 204561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 207561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 210561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 213562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 216562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 219561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 222561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 225562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 228561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 231562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 234562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 237562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 240390ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/dashboard/summary:0
[ 240562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 243562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 246561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 249561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 252562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 255561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 258561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 261562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 264562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 267562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 270562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 273561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 276562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 279563ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 282561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 285562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 288562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 291562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 294562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 297561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 300562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 303561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 306562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 309561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 312562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 315561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 318561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 321561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 324561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 327561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 330562ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 333561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 336561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 339561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 342561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 345561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 348561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0

View File

@ -0,0 +1,2 @@
[ 480462ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 1440463ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0

View File

@ -0,0 +1,14 @@
[ 140214ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 143212ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 146211ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 149211ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/upgrade/status:0
[ 360382ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 960378ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/dashboard/summary:0
[ 1320377ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 1920379ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/dashboard/summary:0
[ 2040375ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:3002/api/dashboard/summary:0
[ 2280391ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 2400379ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/dashboard/summary:0
[ 3240382ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 4200393ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0
[ 5160392ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/dashboard/summary:0

View File

@ -0,0 +1,2 @@
[ 616ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3002/favicon.ico:0
[ 628ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0

View File

@ -0,0 +1 @@
[ 605ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0

View File

@ -0,0 +1 @@
[ 1538ms] [ERROR] Failed to load resource: the server responded with a status of 400 (Bad Request) @ http://localhost:8091/auth/token/refresh:0

View File

@ -0,0 +1 @@
[ 32ms] [WARNING] Manifest: property 'start_url' ignored, should be same origin as document. @ data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHRlYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cHM6Ly9naXQuY21saXRlLm9yZy8iLCJpY29ucyI6W3sic3JjIjoiaHR0cHM6Ly9naXQuY21saXRlLm9yZy9hc3NldHMvaW1nL2xvZ28ucG5nIiwidHlwZSI6ImltYWdlL3BuZyIsInNpemVzIjoiNTEyeDUxMiJ9LHsic3JjIjoiaHR0cHM6Ly9naXQuY21saXRlLm9yZy9hc3NldHMvaW1nL2xvZ28uc3ZnIiwidHlwZSI6ImltYWdlL3N2Zyt4bWwiLCJzaXplcyI6IjUxMng1MTIifV19:0

View File

@ -0,0 +1 @@
[ 871ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:5678/rest/login:0

View File

@ -0,0 +1 @@
[ 343ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/favicon.ico:0

View File

@ -0,0 +1 @@
[ 238ms] [WARNING] Simple Analytics: Set hostname on localhost:8090. See https://docs.simpleanalytics.com/overwrite-domain-name @ https://scripts.simpleanalyticscdn.com/latest.js:2

View File

@ -0,0 +1,5 @@
[ 967ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/node_modules/vsda/rust/web/vsda_bg.wasm:0
[ 969ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/node_modules/vsda/rust/web/vsda.js:0
[ 1271ms] [WARNING] The web worker extension host is started in a same-origin iframe! @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:4591
[ 1304ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html?&vscodeWebWorkerExtHostId=2ab26743-4428-4df3-944e-d603e9a82c44:0
[ 2145ms] [WARNING] AI generated workspace trust dialog contents not available. @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:4552

View File

@ -0,0 +1,11 @@
[ 5ms] [ERROR] %c ERR color: #f33 [lifecycle] Long running operations during shutdown are unsupported in the web (id: join.disconnectRemote) @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:37
[ 6ms] [ERROR] %c ERR color: #f33 [lifecycle] Long running operations during shutdown are unsupported in the web (id: join.chatSessionStore) @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:37
[ 7ms] [ERROR] %c ERR color: #f33 [lifecycle] Long running operations during shutdown are unsupported in the web (id: join.chatEditingSession) @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:37
[ 27ms] [ERROR] %c ERR color: #f33 Error creating chat editing session content folder vscode-remote:/home/coder/.local/share/code-server/User/workspaceStorage/4a334e63/chatEditingSessions/266a91b3-2e96-499a-bfc1-6d451b72bd57/contents Canceled: Canceled
at Object.call (http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:612:1374)
at http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:613:2181
at async vOt.W (http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:4587:115075)
at async vOt.createFolder (http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:4587:114875)
at async ice.storeState (http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:2978:15032)
at async Promise.all (index 0) @ http://localhost:8888/stable-13ca0e47310505f4ab1ac59e891e9a3c5e5eec04/static/out/vs/code/browser/workbench/workbench.js:37
[ 137ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:8025/api/v2/jim:0

View File

@ -0,0 +1 @@
[ 1014ms] [WARNING] GPS permission denied — enable location access in your browser settings @ http://localhost:3002/src/components/canvass/GPSTracker.tsx:32

View File

@ -0,0 +1,2 @@
[ 600748ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/social/notifications/count:0
[ 1560748ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/social/notifications/count:0

View File

@ -0,0 +1,12 @@
[ 127ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 127ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3242:41)
at async initSearch (http://localhost:4003/lander/:3307:9) @ http://localhost:4003/lander/:3254
[ 626262ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 626262ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3212:41)
at async initSearch (http://localhost:4003/lander/:3277:9) @ http://localhost:4003/lander/:3224
[ 628485ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 628485ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3212:41)
at async initSearch (http://localhost:4003/lander/:3277:9) @ http://localhost:4003/lander/:3224

View File

@ -0,0 +1,60 @@
[ 86ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 87ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3212:41)
at async initSearch (http://localhost:4003/lander/:3277:9) @ http://localhost:4003/lander/:3224
[ 426459ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 426459ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3213:41)
at async initSearch (http://localhost:4003/lander/:3278:9) @ http://localhost:4003/lander/:3225
[ 433706ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 433706ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3214:41)
at async initSearch (http://localhost:4003/lander/:3279:9) @ http://localhost:4003/lander/:3226
[ 436108ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 436108ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3214:41)
at async initSearch (http://localhost:4003/lander/:3279:9) @ http://localhost:4003/lander/:3226
[ 445396ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 445396ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3237:41)
at async initSearch (http://localhost:4003/lander/:3302:9) @ http://localhost:4003/lander/:3249
[ 447757ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 447757ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3237:41)
at async initSearch (http://localhost:4003/lander/:3302:9) @ http://localhost:4003/lander/:3249
[ 455113ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 455113ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3237:41)
at async initSearch (http://localhost:4003/lander/:3302:9) @ http://localhost:4003/lander/:3249
[ 457733ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 457733ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3237:41)
at async initSearch (http://localhost:4003/lander/:3302:9) @ http://localhost:4003/lander/:3249
[ 489124ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 489124ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3252:41)
at async initSearch (http://localhost:4003/lander/:3317:9) @ http://localhost:4003/lander/:3264
[ 491509ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 491509ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3252:41)
at async initSearch (http://localhost:4003/lander/:3317:9) @ http://localhost:4003/lander/:3264
[ 510185ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 510185ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3257:41)
at async initSearch (http://localhost:4003/lander/:3322:9) @ http://localhost:4003/lander/:3269
[ 528536ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 528536ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3262:41)
at async initSearch (http://localhost:4003/lander/:3327:9) @ http://localhost:4003/lander/:3274
[ 530976ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 530976ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3262:41)
at async initSearch (http://localhost:4003/lander/:3327:9) @ http://localhost:4003/lander/:3274
[ 541596ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 541596ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3322:41)
at async initSearch (http://localhost:4003/lander/:3387:9) @ http://localhost:4003/lander/:3334
[ 543950ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 543950ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3322:41)
at async initSearch (http://localhost:4003/lander/:3387:9) @ http://localhost:4003/lander/:3334

View File

@ -0,0 +1,28 @@
[ 96ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 97ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3322:41)
at async initSearch (http://localhost:4003/lander/:3387:9) @ http://localhost:4003/lander/:3334
[ 320202ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 320202ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3335:41)
at async initSearch (http://localhost:4003/lander/:3400:9) @ http://localhost:4003/lander/:3347
[ 329151ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 329151ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3337:41)
at async initSearch (http://localhost:4003/lander/:3402:9) @ http://localhost:4003/lander/:3349
[ 331533ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 331533ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3337:41)
at async initSearch (http://localhost:4003/lander/:3402:9) @ http://localhost:4003/lander/:3349
[ 628619ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 628619ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3337:41)
at async initSearch (http://localhost:4003/lander/:3402:9) @ http://localhost:4003/lander/:3349
[ 631005ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 631005ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3337:41)
at async initSearch (http://localhost:4003/lander/:3402:9) @ http://localhost:4003/lander/:3349
[ 633385ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/lander/search/search_index.json:0
[ 633385ms] [ERROR] Search init failed: Error: Failed to load search index
at MkDocsSearch.initialize (http://localhost:4003/lander/:3337:41)
at async initSearch (http://localhost:4003/lander/:3402:9) @ http://localhost:4003/lander/:3349

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

747
CLAUDE.md Normal file
View File

@ -0,0 +1,747 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Changemaker Lite is a self-hosted political campaign platform built with Docker Compose. It consolidates advocacy email campaigns, geographic mapping, volunteer management, and administration into a single TypeScript stack. The primary domain is `cmlite.org`.
**Current state:** V2 rebuild substantially complete on the `v2` branch. Core platform operational with Phases 1-14 complete. See `V2_PLAN.md` for the full roadmap.
**Status Summary:**
- ✅ Phases 1-14 Complete (Foundation through Monitoring + DevOps)
- ✅ Security Audit Complete (13 findings addressed, Feb 2026)
- ✅ NAR 2025 Server Import (Canadian electoral data)
- ✅ Media Manager Integration (dual API architecture)
- ✅ Email Templates System
- ✅ Data Quality Dashboard
- ✅ Observability Dashboard
- ✅ **Drizzle to Prisma Migration Complete** (Media API consolidated to single-ORM, Feb 2026)
- ✅ **Automated Pangolin Setup** (One-command tunnel deployment, Feb 2026)
- ✅ **Migration Drift Fixed** (Baseline catch-up migration, 14 migrations cover full schema, Feb 2026)
- 🚧 Phase 15 (Testing + Polish) - Next
---
## V2 Architecture
### Stack
- **Dual API Architecture**
- **Express.js API** (TypeScript, port 4000) — Main V2 features with Prisma ORM + PostgreSQL 16
- **Fastify Media API** (TypeScript, port 4100) — Video library with Prisma ORM (shared DB) ✅ **Migrated from Drizzle (Feb 2026)**
- **React Admin GUI** — Vite + Ant Design + Zustand, port 3000
- **Nginx reverse proxy** — subdomain routing (`*.cmlite.org`)
- **NocoDB v2** — read-only data browser on port 8091
- **Redis** — caching, rate limiting, BullMQ backend, geocoding queue (authenticated)
- **Monitoring Stack** (Docker profile: `monitoring`) — Prometheus, Grafana, Alertmanager, cAdvisor, exporters
### Authentication & Security
- **JWT-based auth:** access tokens (15min) + refresh tokens (7 days, stored in DB)
- **Password policy:** 12+ characters, uppercase, lowercase, digit (enforced at schema level)
- **Initial admin:** Configured via `INITIAL_ADMIN_EMAIL` and `INITIAL_ADMIN_PASSWORD` env vars (auto-created during database seeding)
- **Roles:** `SUPER_ADMIN`, `INFLUENCE_ADMIN`, `MAP_ADMIN`, `BROADCAST_ADMIN`, `CONTENT_ADMIN`, `MEDIA_ADMIN`, `PAYMENTS_ADMIN`, `EVENTS_ADMIN`, `SOCIAL_ADMIN`, `USER`, `TEMP`
- **RBAC:** `requireRole(...roles)`, `requireNonTemp`, `authenticate` middleware; `SUPER_ADMIN` implicitly bypasses all role checks
- **Module-specific role groups** (defined in `api/src/utils/roles.ts`): `INFLUENCE_ROLES`, `MAP_ROLES`, `BROADCAST_ROLES`, `CONTENT_ROLES`, `MEDIA_ROLES`, `PAYMENTS_ROLES`, `EVENTS_ROLES`, `SOCIAL_ROLES`, `SYSTEM_ROLES`, `SCHEDULING_ROLES`
- **User management:** `SUPER_ADMIN` always; other admins need `permissions.canManageUsers: true` for write operations
- **Security:** See Security & Configuration section below + `SECURITY_AUDIT_2025-02-11.md`
### Email Systems
- **BullMQ** — async advocacy email job queue with SMTP
- **Listmonk** — newsletter/marketing campaigns (opt-in sync via `LISTMONK_SYNC_ENABLED`)
- **MailHog** — dev email capture (`EMAIL_TEST_MODE=true`)
### Directory Structure (Annotated)
```
changemaker.lite/
├── api/ # Dual API servers (Express + Fastify)
│ ├── prisma/
│ │ ├── schema.prisma # 30+ models: User, Campaign, Location, Shift, etc.
│ │ ├── migrations/ # Prisma migration history
│ │ └── seed.ts # Admin user, settings, page blocks
│ ├── drizzle/ # Media tables (Drizzle ORM)
│ ├── Dockerfile.media # Fastify media server container
│ └── src/
│ ├── server.ts # Express API entry point (port 4000)
│ ├── media-server.ts # Fastify media API entry point (port 4100)
│ ├── config/
│ │ └── env.ts # Zod-validated environment config (100+ vars)
│ ├── middleware/ # auth, rbac, rate-limit, validate, error-handler
│ ├── modules/
│ │ ├── auth/ # JWT login, register, refresh, logout
│ │ ├── users/ # User CRUD + pagination + search
│ │ ├── settings/ # Site settings singleton
│ │ ├── services/ # Service health checks
│ │ ├── influence/
│ │ │ ├── campaigns/ # Campaign CRUD + public routes
│ │ │ ├── representatives/ # Represent API integration + cache
│ │ │ ├── responses/ # Response wall + moderation + upvoting
│ │ │ ├── postal-codes/ # Postal code cache service
│ │ │ ├── campaign-emails/ # Email tracking + stats
│ │ │ └── email-queue/ # BullMQ queue admin
│ │ ├── map/
│ │ │ ├── locations/ # Location CRUD + geocoding + NAR import
│ │ │ ├── geocoding/ # Multi-provider geocoding (6 providers)
│ │ │ ├── cuts/ # Polygon CRUD + spatial queries
│ │ │ ├── shifts/ # Shift CRUD + signups
│ │ │ ├── canvass/ # Canvassing sessions + visits + routes
│ │ │ ├── tracking/ # GPS tracking sessions (volunteer + admin routes)
│ │ │ └── settings/ # Map settings singleton
│ │ ├── pages/
│ │ │ ├── pages-admin.routes.ts # Landing page CRUD
│ │ │ ├── pages-public.routes.ts # Public page renderer
│ │ │ └── blocks.routes.ts # Block library API
│ │ ├── email-templates/ # Email template CRUD + rendering
│ │ ├── media/ # Fastify media API (videos, reactions, jobs)
│ │ ├── listmonk/ # Newsletter sync admin routes
│ │ ├── pangolin/ # Tunnel management (Newt integration)
│ │ ├── docs/ # MkDocs + Code Server health checks
│ │ ├── qr/ # QR code PNG generation (public)
│ │ └── observability/ # Prometheus/Grafana/Alertmanager integration
│ ├── services/ # email, email-queue, geocode-queue, listmonk, pangolin, docker
│ ├── types/ # express.d.ts (Request augmentation)
│ └── utils/ # logger (Winston), metrics (prom-client), spatial
├── admin/ # React Admin (Vite + Ant Design + Zustand)
│ └── src/
│ ├── App.tsx # Main router + route definitions
│ ├── components/
│ │ ├── AppLayout.tsx # Admin sidebar layout
│ │ ├── PublicLayout.tsx # Public dark theme layout
│ │ ├── VolunteerLayout.tsx # Volunteer portal layout
│ │ ├── MediaPublicLayout.tsx # Public media gallery layout
│ │ ├── GrapesJSEditor.tsx # Landing page editor wrapper (forwardRef, Ctrl+S)
│ │ ├── map/ # Leaflet map components + controls + drawing modes
│ │ ├── canvass/ # GPS tracking, markers, route, visit recording
│ │ ├── media/ # VideoCard, BulkActions, gallery components
│ │ ├── email-templates/ # Email template components
│ │ └── observability/ # Monitoring components
│ ├── pages/
│ │ ├── auth/ # LoginPage
│ │ ├── influence/ # CampaignsPage, ResponsesPage, RepresentativesPage, EmailQueuePage
│ │ ├── map/ # LocationsPage, CutsPage, ShiftsPage, MapSettingsPage, DataQualityDashboardPage
│ │ ├── volunteer/ # VolunteerMapPage, VolunteerShiftsPage, MyActivityPage, MyRoutesPage
│ │ ├── public/ # CampaignsListPage, CampaignPage, ResponseWallPage, MapPage, ShiftsPage, LandingPage, MediaGalleryPage, MediaViewerPage
│ │ ├── media/ # LibraryPage, SharedMediaPage, MediaJobsPage, AnalyticsDashboardPage
│ │ ├── services/ # MiniQRPage, MailHogPage, CodeEditorPage, N8nPage, GiteaPage, NocoDBPage
│ │ └── (root) # DashboardPage, UsersPage, SettingsPage, CanvassDashboardPage, WalkSheetPage, CutExportPage, LandingPagesPage, PageEditorPage, EmailTemplatesPage, ListmonkPage, PangolinPage, ObservabilityPage
│ ├── stores/ # auth.store.ts, canvass.store.ts (Zustand)
│ ├── lib/ # api.ts, media-api.ts, media-public-api.ts (axios)
│ ├── hooks/ # useDebounce, useLocalStorage
│ └── types/ # api.ts, canvass.ts, media.ts (TypeScript interfaces)
├── media-manager/ # Legacy media manager (reference)
├── nginx/ # Reverse proxy config (subdomain routing + CSP)
├── configs/ # Prometheus, Grafana, Alertmanager configs
├── scripts/ # Deployment, backup, upgrade, registry scripts
│ ├── install.sh # Curl-friendly installer (downloads tarball + runs config.sh)
│ ├── build-and-push.sh # Build production images → push to Gitea registry
│ ├── build-release.sh # Package runtime files into release tarball
│ ├── mirror-images.sh # Mirror third-party images to Gitea
│ ├── upgrade.sh # 6-phase upgrade (git or release-tarball mode)
│ ├── upgrade-check.sh # Check for updates (git or Gitea API)
│ ├── upgrade-watcher.sh # Systemd bridge for admin GUI upgrades
│ └── backup.sh # PostgreSQL + Listmonk + uploads backup
├── docker-compose.yml # V2 orchestration (20+ services)
├── docker-compose.v1.yml # V1 backup (reference)
├── .env.example # All required environment variables
└── V2_PLAN.md # Full 14-phase roadmap
```
---
## Quick Start Guide
### Pre-built Install (Production — Recommended)
The fastest way to deploy. No source code, no compilation:
```bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
```
This downloads a ~9MB release tarball, runs the config wizard, and sets `IMAGE_TAG=latest`. Then:
```bash
cd ~/changemaker.lite && docker compose up -d
```
Pre-built images are pulled from `gitea.bnkops.com/admin` (~2 min). Database migrations and seeding run automatically via the API entrypoint. Access the admin GUI at http://localhost:3000.
### Source Install (Development)
1. **Clone repository and checkout v2 branch:**
```bash
git clone <repo-url> changemaker.lite
cd changemaker.lite
git checkout v2
```
2. **Create environment file:**
```bash
cp .env.example .env
# Edit .env and set:
# - V2_POSTGRES_PASSWORD (strong password)
# - REDIS_PASSWORD (strong password)
# - JWT_ACCESS_SECRET (openssl rand -hex 32)
# - JWT_REFRESH_SECRET (openssl rand -hex 32)
# - ENCRYPTION_KEY (openssl rand -hex 32, must differ from JWT secrets)
```
3. **Start core services:**
```bash
docker compose up -d v2-postgres redis api admin
```
4. **Run database migrations:**
```bash
docker compose exec api npx prisma migrate deploy
docker compose exec api npx prisma db seed
```
5. **Access the application:**
- Admin GUI: http://localhost:3000 (see INITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORD in .env)
- API: http://localhost:4000
- **Change default password immediately**
### Development Workflow
**Starting services:**
```bash
# Core services
docker compose up -d v2-postgres redis api admin
# Include monitoring stack
docker compose --profile monitoring up -d
# Include media API
docker compose up -d media-api
```
**Local development (without Docker):**
```bash
# Terminal 1: API
cd api && npm install && npm run dev
# Terminal 2: Admin
cd admin && npm install && npm run dev
# Terminal 3 (optional): Media API
cd api && npm run dev:media
```
### Accessing Services
| Service | URL | Default Credentials |
|---------|-----|---------------------|
| Admin GUI | http://localhost:3000 | See INITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORD in .env |
| API | http://localhost:4000 | - |
| NocoDB | http://localhost:8091 | See `NC_ADMIN_EMAIL`/`NC_ADMIN_PASSWORD` in .env |
| MailHog | http://localhost:8025 | - |
| Grafana | http://localhost:3001 | admin / admin |
| Prometheus | http://localhost:9090 | - |
| Listmonk | http://localhost:9001 | See `LISTMONK_WEB_ADMIN_USER`/`PASSWORD` in .env |
### Feature Flags
Enable optional features in `.env`:
```bash
# Media Manager
ENABLE_MEDIA_FEATURES=true
# Listmonk Newsletter Sync
LISTMONK_SYNC_ENABLED=true
# Email Test Mode (sends to MailHog instead of SMTP)
EMAIL_TEST_MODE=true
```
---
## Development Commands
The user likes to use Docker - recereating services as if in production.
### API Development
```bash
cd api && npm run dev # Express dev server (port 4000)
cd api && npm run dev:media # Fastify media dev server (port 4100)
cd api && npx tsc --noEmit # Type-check
cd api && npx prisma migrate dev # Run/create Prisma migrations
cd api && npx prisma studio # Browse database
cd api && npx drizzle-kit push # Push Drizzle schema changes (media)
```
### Admin Development
```bash
cd admin && npm run dev # Vite dev server (port 3000)
cd admin && npx tsc --noEmit # Type-check
cd admin && npm run build # Production build
```
### Docker Operations
```bash
# Start services
docker compose up -d v2-postgres redis api admin
docker compose up -d media-api
docker compose --profile monitoring up -d
# View logs
docker compose logs -f api
docker compose logs -f media-api
# Database operations
docker compose exec api npx prisma migrate dev
docker compose exec api npx drizzle-kit push
# Stop services
docker compose down
```
### Registry & Release Operations
```bash
# Build production images and push to Gitea registry
./scripts/build-and-push.sh --services api,admin,media-api,nginx
./scripts/build-and-push.sh --no-push # Build only, no push (verify)
# Mirror third-party images to Gitea
./scripts/mirror-images.sh # Core images (postgres, redis, etc.)
./scripts/mirror-images.sh --all # Include heavy images (RC, Jitsi, n8n)
# Build release tarball (for pre-built installs — run AFTER build-and-push)
./scripts/build-release.sh --tag v2.1.0 # Creates releases/changemaker-lite-v2.1.0.tar.gz
./scripts/build-release.sh --tag v2.1.0 --upload # Also upload to Gitea Releases API
./scripts/build-release.sh --dry-run # Preview tarball contents
# Use registry images in upgrade (source installs)
./scripts/upgrade.sh --use-registry --force --skip-backup
# Install from tarball (end-user one-liner)
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
```
**Two compose files:**
- `docker-compose.yml` — Development: includes `build:` blocks and `./api:/app` source mounts
- `docker-compose.prod.yml` — Production: `image:` only, no source mounts, `IMAGE_TAG:-latest`
Release tarballs ship `docker-compose.prod.yml` as the compose file. Source installs use `docker-compose.yml`.
**Note:** gitea.bnkops.com must use Pangolin tunnel (not Cloudflare proxy) for large image layers (>100MB). See `docs/REGISTRY_GUIDE.md`.
### Testing & Backup
```bash
# Media API tests
cd api && ./test-media-api.sh
# Backup (PostgreSQL + Listmonk + uploads)
./scripts/backup.sh
# Type-check all projects
cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
```
### API Testing Credentials & Login
**Test admin account:** `admin@bnkops.ca` / `ChangeMe2025!` (SUPER_ADMIN role)
**Reliable login method (avoids shell `!` escaping issues):**
1. Write the JSON body to a file using the **Write tool** (NOT echo/printf — the `!` gets backslash-escaped by bash):
```
Write /tmp/login.json → {"email":"admin@bnkops.ca","password":"ChangeMe2025!"}
```
2. Use `curl -d @/tmp/login.json`:
```bash
curl -s -X POST http://localhost:4002/api/auth/login \
-H "Content-Type: application/json" -d @/tmp/login.json
```
3. Extract token and use for authenticated requests:
```bash
TOKEN=$(curl -s -X POST http://localhost:4002/api/auth/login \
-H "Content-Type: application/json" -d @/tmp/login.json \
| python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
curl -s http://localhost:4002/api/some-endpoint -H "Authorization: Bearer $TOKEN"
```
**Port mapping:** API container port 4000 → host port **4002**, Admin port 3000 → host port **3002**
**Important:** The `!` character in `ChangeMe2025!` triggers bash history expansion. NEVER pass this password directly in bash command strings. Always use the Write-tool-to-file approach above.
---
## Core Modules Reference
### Auth & Users
**Files:**
- `api/src/modules/auth/` — JWT login, register, refresh, logout
- `api/src/modules/users/` — User CRUD + pagination + search
- `api/src/middleware/auth.ts` — JWT verification + RBAC
- `admin/src/stores/auth.store.ts` — Zustand auth state + token persistence
- `admin/src/lib/api.ts` — Axios with 401 refresh interceptor
**Features:** JWT access/refresh tokens, bcrypt passwords (12+ chars), role-based access control, user enumeration prevention, rate limiting
### Influence Module (Advocacy Campaigns)
**Files:**
- `api/src/modules/influence/campaigns/` — Campaign CRUD + public routes
- `api/src/modules/influence/representatives/` — Represent API client + cache
- `api/src/modules/influence/responses/` — Response wall + moderation + upvoting
- `api/src/services/email-queue.service.ts` — BullMQ queue + worker
- `admin/src/pages/CampaignsPage.tsx` — Campaign management
- `admin/src/pages/public/CampaignPage.tsx` — Public campaign page
**Features:** Postal code → representative lookup, email campaigns, response wall with moderation, BullMQ async email queue
**Routes:**
- Admin: `/app/influence/campaigns`, `/app/influence/responses`, `/app/influence/email-queue`
- Public: `/campaigns`, `/campaigns/:id`, `/responses/:campaignId`
### Map Module (Locations & Canvassing)
**Files:**
- `api/src/modules/map/locations/` — Location CRUD + geocoding + NAR import
- `api/src/modules/map/geocoding/geocoding.service.ts` — Multi-provider geocoding (6 providers)
- `api/src/modules/map/cuts/` — Polygon CRUD + spatial queries
- `api/src/modules/map/shifts/` — Shift CRUD + signups
- `api/src/modules/map/canvass/` — Canvassing sessions + visits + routes
- `api/src/modules/map/tracking/` — GPS tracking sessions (volunteer + admin routes)
- `api/src/utils/spatial.ts` — Point-in-polygon, haversine, bounds, centroids
- `admin/src/pages/LocationsPage.tsx` — Location CRUD + CSV + geocoding
- `admin/src/pages/CutsPage.tsx` — Cut table + map drawing editor
- `admin/src/pages/CanvassDashboardPage.tsx` — Admin canvass overview
- `admin/src/pages/volunteer/VolunteerMapPage.tsx` — Full-screen GPS canvass map
**Features:** Multi-provider geocoding, NAR 2025 import (Canadian electoral data), polygon cuts, volunteer shifts, canvassing system with GPS tracking, walking route algorithm, printable walk sheets
**Routes:**
- Admin: `/app/map/locations`, `/app/map/cuts`, `/app/map/shifts`, `/app/canvass/dashboard`
- Public: `/map`, `/shifts`
- Volunteer: `/volunteer/canvass/:cutId`, `/volunteer/assignments`, `/volunteer/activity`
### Landing Pages & Email Templates
**Files:**
- `api/src/modules/pages/` — Landing page CRUD + block library (3 route files)
- `api/src/modules/email-templates/` — Email template CRUD + rendering
- `admin/src/components/GrapesJSEditor.tsx` — GrapesJS wrapper (forwardRef, Ctrl+S)
- `admin/src/pages/PageEditorPage.tsx` — Full-screen page editor
- `admin/src/pages/EmailTemplateEditorPage.tsx` — Email template editor
**Features:** GrapesJS WYSIWYG editor, page/template CRUD, MkDocs export (Jinja2 Material overrides), public renderer, desktop-only editor warning
**Routes:**
- Admin: `/app/pages`, `/app/pages/:id/edit`, `/app/email-templates`
- Public: `/p/:slug`
### Media Manager (Dual API)
**Files:**
- `api/src/modules/media/` — Fastify media API (videos, reactions, jobs, analytics)
- `api/src/modules/media/services/` — FFprobe, video analytics service
- `api/src/modules/media/routes/` — Video CRUD, actions, schedule, analytics, tracking, upload
- `api/src/services/video-schedule-queue.service.ts` — BullMQ queue for scheduled publishing
- `admin/src/lib/media-api.ts` — Dedicated axios instance for Media API
- `admin/src/pages/media/LibraryPage.tsx` — Video library with quick actions + calendar
- `admin/src/pages/media/AnalyticsDashboardPage.tsx` — Global analytics dashboard
- `admin/src/pages/media/SharedMediaPage.tsx` — Public gallery admin
- `admin/src/pages/public/MediaGalleryPage.tsx` — Public video gallery
- `admin/src/components/media/` — VideoCard, VideoActions, modals, charts
**Features:** Video CRUD with FFprobe metadata, quick actions, scheduled publishing (BullMQ + timezones), analytics (GDPR-compliant), public tracking endpoints, keyboard shortcuts
**Routes:**
- Admin: `/app/media/library`, `/app/media/analytics`, `/app/media/shared`, `/app/media/jobs`
- Public: `/gallery`, `/gallery/watch/:id`, `/media/:id` (legacy)
- Public gallery uses `MediaPublicLayout` (purple theme, optional auth)
### Services & Integrations
**Listmonk Newsletter Sync:**
- `api/src/services/listmonk.client.ts` — Listmonk REST API client (native fetch)
- `api/src/services/listmonk-sync.service.ts` — Sync participants/locations → lists
- `admin/src/pages/ListmonkPage.tsx` — Newsletter sync management
- Opt-in sync: `LISTMONK_SYNC_ENABLED=true`
**Pangolin Tunnel Management:**
- `api/src/services/pangolin.client.ts` — Pangolin Integration API client
- `api/src/modules/pangolin/pangolin.routes.ts` — Tunnel management routes (includes `/setup-automated`)
- `admin/src/pages/PangolinPage.tsx` — Setup wizard + status dashboard + automated setup button
- `scripts/pangolin-setup.sh` — CLI wrapper for automated setup
- `configs/pangolin/resources.yml` — Central resource definitions (12 services)
- Newt container integration (Cloudflare alternative)
- **Automated setup:** One-command deployment (creates site, updates .env, restarts Newt)
- **Continuous sync:** Hourly resource sync via nginx cron job
**MkDocs + Code Server:**
- `api/src/modules/docs/docs.routes.ts` — Health checks + export routes
- `admin/src/pages/DocsPage.tsx` — MkDocs export management
- `admin/src/pages/CodeEditorPage.tsx` — Code Server management
- Embedded iframes in admin (CSP `frame-ancestors` for embedding)
**Mini QR Service:**
- `api/src/modules/qr/qr.routes.ts` — QR code PNG generation (public, no auth)
- `admin/src/pages/MiniQRPage.tsx` — Mini QR iframe
- Used by walk sheets + cut exports
### Observability & Monitoring
**Files:**
- `api/src/modules/observability/observability.routes.ts` — Prometheus/Grafana/Alertmanager integration
- `api/src/utils/metrics.ts` — 12 custom `cm_*` Prometheus metrics
- `admin/src/pages/ObservabilityPage.tsx` — Monitoring dashboard (3 tabs)
- `admin/src/pages/DataQualityDashboardPage.tsx` — Geocoding quality metrics
- `configs/prometheus/` — Scrape targets, alert rules
- `configs/grafana/` — 3 pre-configured dashboards
**Features:** 12 custom `cm_*` metrics (API uptime, queue size, sessions, etc.), HTTP request metrics, external service health gauges, 3 Grafana dashboards, alert rules, auto-start banner
**Routes:**
- Admin: `/app/observability`, `/app/map/data-quality`
- Direct: `localhost:9090` (Prometheus), `localhost:3001` (Grafana)
---
## Port Reference
| Port | Service | Notes |
|------|---------|-------|
| **Core Services** | | |
| 3000 | Admin GUI | Vite dev / React production |
| 4000 | Express API | Main V2 API (Prisma) |
| 4100 | Fastify Media API | Video library (Drizzle) |
| 5433 | V2 PostgreSQL | Localhost (container: 5432) |
| 6379 | Redis | Cache, rate limit, BullMQ |
| **Supporting Services** | | |
| 3001 | Grafana | Metrics visualization |
| 3010 | Homepage | Service dashboard |
| 3030 | Gitea | Git hosting |
| 4001 | MkDocs Site | Served docs |
| 4003 | MkDocs Dev | Live preview |
| 5432 | Listmonk PostgreSQL | Listmonk DB |
| 5678 | n8n | Workflow automation |
| 8025 | MailHog | Email capture (dev) |
| 8089 | Mini QR | QR generator |
| 8091 | NocoDB | Data browser |
| 8885 | Mini QR Proxy | Iframe-friendly |
| 8888 | Code Server | Web IDE |
| 9001 | Listmonk | Newsletter platform |
| **Monitoring** (profile: `monitoring`) | | |
| 8080 | cAdvisor | Container metrics |
| 8889 | Gotify | Notifications |
| 9090 | Prometheus | Metrics collection |
| 9093 | Alertmanager | Alert routing |
| 9100 | Node Exporter | Host metrics |
| 9121 | Redis Exporter | Redis metrics |
---
## Nginx Routing
| Subdomain | Target | Purpose |
|-----------|--------|---------|
| `app.cmlite.org` | Admin (3000) | **All application routes** (admin + public pages, campaigns, map, shifts, media) |
| `api.cmlite.org` | Express (4000) | Main API |
| `media.cmlite.org` | Fastify (4100) | Media API |
| `db.cmlite.org` | NocoDB (8091) | Data browser |
| `docs.cmlite.org` | MkDocs (4003) | Docs site |
| `code.cmlite.org` | Code Server (8888) | Web IDE |
| `n8n.cmlite.org` | n8n (5678) | Workflow automation |
| `git.cmlite.org` | Gitea (3030) | Git hosting |
| `home.cmlite.org` | Homepage (3010) | Dashboard |
| `grafana.cmlite.org` | Grafana (3001) | Metrics viz |
| `listmonk.cmlite.org` | Listmonk (9001) | Newsletters |
| `qr.cmlite.org` | Mini QR (8089) | QR generator |
| `cmlite.org` | MkDocs Static (4004) | **Documentation/marketing site only** |
**Clean separation:** Root domain (`${DOMAIN}`) serves MkDocs documentation site. All application functionality (admin GUI, public campaigns, map, shifts, media gallery) is accessible via `app.${DOMAIN}` subdomain. This provides clear separation between public documentation and the application.
---
## Common Patterns
**Note:** See `MEMORY.md` for comprehensive development patterns, gotchas, and lessons learned. Below are V2-specific patterns only.
### API Router Structure
- Service layer (`*.service.ts`) — business logic, database queries
- Routes (`*.routes.ts`) — Express router, middleware, validation
- Schemas (`*.schemas.ts`) — Zod validation schemas
- Split admin/public routes when needed (e.g., `campaigns.routes.ts` + `campaigns-public.routes.ts`)
### Authentication Middleware
- `authenticate` — requires any logged-in user
- `requireRole(...roles)` — requires specific role(s)
- `requireNonTemp` — blocks TEMP users
- Login redirects: ADMIN_ROLES → `/app`, USER/TEMP → `/volunteer`
### Frontend Architecture
- Admin pages: `admin/src/pages/` (AppLayout)
- Public pages: `admin/src/pages/public/` (PublicLayout, dark theme)
- Volunteer pages: `admin/src/pages/volunteer/` (VolunteerLayout)
- Zustand stores: `auth.store.ts`, `canvass.store.ts`
- API clients: `{ api }` from `lib/api.ts`, `mediaApi` from `lib/media-api.ts`
### Database ORMs
- **Prisma** (main API): Use `UncheckedCreateInput`/`UncheckedUpdateInput` for foreign keys, `Prisma.InputJsonValue` for JSON arrays
- **Drizzle** (media API): Separate schema file, push with `npx drizzle-kit push`, no migrations generated
### Prisma Migration Workflow
- **Always use `prisma migrate dev`** for schema changes (not `prisma db push`) — `db push` applies changes directly but doesn't create migration files, causing drift
- **Migration history:** 14 migrations in `api/prisma/migrations/` fully cover the schema (baseline catch-up applied Feb 2026)
- **Fixing drift:** Use `prisma migrate diff --from-migrations ... --to-schema-datamodel ... --script` with a shadow DB to generate catch-up SQL, then `prisma migrate resolve --applied`. See MEMORY.md for detailed steps
- **Production deploys:** Use `prisma migrate deploy` (not `migrate dev`)
### V2-Specific Gotchas
- **Prisma migrations:** Never use `db push` — always `migrate dev` to keep history in sync
- Nginx media API block must come BEFORE general API block
- `IMAGE_TAG=local` (default) never pulls from registry; set to SHA or `latest` for pre-built images
- **Release vs source installs:** Detected by `VERSION` file + absence of `.git/`; release uses `docker-compose.prod.yml`, source uses `docker-compose.yml`
- **`api/dist/` is gitignored** — never commit; if root-owned from container builds, fix with `chown`
- See MEMORY.md "Common Gotchas" for additional gotchas (ports, volumes, media upload, registry, etc.)
---
## Security & Configuration
### Security Audit
Comprehensive security audit completed 2025-02-11, addressing 13 findings. See `SECURITY_AUDIT_2025-02-11.md` for full report.
**Key improvements:**
- Password policy: 12+ chars, uppercase, lowercase, digit (schema-enforced)
- Rate limits on auth endpoints (10/min per IP)
- Refresh token rotation (atomic transaction)
- User enumeration prevention (401 not 404)
- Redis authentication required
- XSS/injection prevention (HTML escaping)
- Path traversal protection
- Encryption key for DB secrets (`ENCRYPTION_KEY` required in all environments)
- Nginx security headers (HSTS, Permissions-Policy, CSP)
### Required Environment Variables
See `.env.example` for all 100+ variables. Critical ones:
- `V2_POSTGRES_PASSWORD`, `REDIS_PASSWORD`
- `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`
- `ENCRYPTION_KEY` (must differ from JWT secrets)
- `LISTMONK_SYNC_ENABLED` (opt-in newsletter sync)
- `EMAIL_TEST_MODE` (MailHog vs SMTP)
- `ENABLE_MEDIA_FEATURES` (media manager)
### Production Deployment
- **Tunneling:** Pangolin with Newt container (Cloudflare alternative)
- **SSL/TLS:** Handled by tunnel provider (Pangolin/Cloudflare)
- **Docker Networking:** All containers share `changemaker-lite` bridge network, reference by container name
- **Monitoring:** Enable with `docker compose --profile monitoring up -d`
- **Backups:** Run `./scripts/backup.sh` (PostgreSQL + Listmonk + uploads, optional S3 upload)
#### Production CORS Configuration
When deploying to a production domain via Pangolin tunnel, you MUST update the `.env` file to include the production domain in `CORS_ORIGINS`:
```bash
# Example for betteredmonton.org
CORS_ORIGINS=http://app.betteredmonton.org,https://app.betteredmonton.org,http://localhost:3000,http://localhost
# Also set production mode
NODE_ENV=production
```
Without this, API requests from the production domain will fail CORS validation. After updating `.env`, restart the API container:
```bash
docker compose restart api
```
---
## Troubleshooting
### Production 403/302 Errors - Pangolin Resources
**Symptom:** All API endpoints return 302 redirects to Pangolin authentication page, or 403 Forbidden errors.
**Root Cause:** Pangolin tunnel resources are configured with authentication enabled (default behavior).
**Fix:** Log in to your Pangolin dashboard and edit each resource:
1. Navigate to **Resources** → **Public**
2. For each resource (app, api, media, docs, etc.), click **Edit**
3. Change **Authentication** setting to **"Not Protected"** (or "Public Access"/"No Authentication")
4. Save changes
**Critical resources to fix first:**
- `api.betteredmonton.org` - Main API (all endpoints fail without this)
- `app.betteredmonton.org` - Admin GUI + public pages
- `media.betteredmonton.org` - Media API
**Verification:**
```bash
# Should return JSON, NOT a 302 redirect
curl https://api.betteredmonton.org/api/health
```
**See Also:** `PRODUCTION_403_FIX.md` for detailed step-by-step instructions.
### CORS Errors in Production
**Symptom:** Browser console shows CORS errors when accessing production domain.
**Fix:** Add production domain to `CORS_ORIGINS` in `.env` file (see Production CORS Configuration above).
### API Works Locally But Not Via Tunnel
Check in order:
1. **Newt container running:** `docker compose ps newt`
2. **Newt connected:** `docker compose logs newt --tail 50` (should show successful connection)
3. **Environment variables set:** `PANGOLIN_SITE_ID`, `PANGOLIN_NEWT_ID`, `PANGOLIN_NEWT_SECRET` in `.env`
4. **Pangolin resources configured:** All resources set to "Not Protected"
5. **Nginx running:** `docker compose ps nginx`
### Database/Redis Connection Failures
Check container status (`docker compose ps`), verify credentials in `.env`, check logs (`docker compose logs <service> --tail 50`). Test DB: `docker compose exec api npx prisma db execute --stdin <<< "SELECT 1"`. Test Redis: `docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping`.
---
## V1 Reference (Legacy)
V1 code archived in `influence/`, `map/`, and `docker-compose.v1.yml`. Two independent Express apps using NocoDB REST API. See individual README files for V1 documentation:
- `influence/README.MD` — Features, config, campaign management
- `map/README.md` — Features, config, setup instructions
- Both use session-based auth, bcryptjs passwords, Bull job queues
---
## Key Configuration Files
### Infrastructure
- `docker-compose.yml` — Development orchestration (build blocks + source mounts, 20+ services)
- `docker-compose.prod.yml` — Production orchestration (image-only, no source mounts, `IMAGE_TAG:-latest`)
- `.env` / `.env.example` — Environment variables (100+ vars)
- `config.sh` — Interactive setup wizard (14 steps, release-mode aware)
### Database
- `api/prisma/schema.prisma` — Main schema (30+ Prisma models)
- `api/prisma/migrations/` — 14 migration files (fully cover schema as of Feb 2026)
- `api/drizzle.config.ts` — Drizzle config for media tables
- `api/prisma/seed.ts` — Database seeding
### Nginx
- `nginx/nginx.conf` — Global config + security headers
- `nginx/conf.d/default.conf` — Subdomain routing (12+ subdomains)
- `nginx/conf.d/api.conf` — API reverse proxy (Express + Fastify)
- `nginx/conf.d/services.conf` — Service proxies
### Monitoring
- `configs/prometheus/prometheus.yml` — Scrape targets + global config
- `configs/prometheus/alerts.yml` — Alert rules (12 rules)
- `configs/grafana/` — 3 pre-configured dashboards
- `configs/alertmanager/alertmanager.yml` — Alert routing
### Documentation
- `CLAUDE.md` — Project-wide instructions (this file)
- `V2_PLAN.md` — Full 14-phase roadmap
- `SECURITY_AUDIT_2025-02-11.md` — Security audit report
- `MEMORY.md` — Development patterns and gotchas

288
DEV_WORKFLOW.md Normal file
View File

@ -0,0 +1,288 @@
# Development & Release Workflow
How code changes move from development to production deployments across all installation methods.
---
## Overview
There are **three ways** Changemaker Lite gets deployed:
| Method | Who uses it | Images from | Compose file |
|--------|------------|-------------|--------------|
| **Source install** | Developers, contributors | Built locally from source | `docker-compose.yml` |
| **Release install** | Production servers, evaluators | Gitea registry (pre-built) | `docker-compose.prod.yml` (ships as `docker-compose.yml` in tarball) |
| **CCP provisioned** | Fleet operators (Control Panel) | Gitea registry (pre-built) | Rendered from `templates/docker-compose.yml.hbs` |
All three methods share the same Gitea container registry at `gitea.bnkops.com/admin`.
---
## The Pipeline
```
┌──────────────────────────────────────────────────────────────────┐
│ DEVELOPMENT (your machine) │
│ │
│ Edit code → docker compose up -d → test locally │
│ Uses: docker-compose.yml (build: blocks + ./api:/app mounts) │
└──────────────────┬───────────────────────────────────────────────┘
│ git push
┌──────────────────────────────────────────────────────────────────┐
│ BUILD & PUBLISH │
│ │
│ Step 1: ./scripts/build-and-push.sh │
│ Builds 4 production images, pushes to Gitea registry │
│ (api, admin, media-api, nginx) tagged :SHA + :latest │
│ │
│ Step 2: ./scripts/mirror-images.sh (run once/rarely) │
│ Mirrors 36 third-party images to Gitea registry │
│ (postgres, redis, nocodb, jitsi, grafana, etc.) │
│ │
│ Step 3: ./scripts/build-release.sh --tag vX.Y.Z --upload │
│ Packages runtime files into ~9MB tarball, uploads to │
│ Gitea Releases │
└──────────────────┬───────────────────────────────────────────────┘
┌───────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ RELEASE INSTALL │ │ CCP PROVISIONED │
│ │ │ │
│ curl installer │ │ Control Panel │
│ or manual tarball│ │ creates instance │
│ → config.sh │ │ via web UI │
│ → docker compose │ │ → renders config │
│ up -d │ │ → docker compose │
│ │ │ up -d │
└─────────────────┘ └──────────────────┘
│ │
└───────────┬───────────┘
All images pulled from
gitea.bnkops.com/admin
(zero external dependencies)
```
---
## Step-by-Step
### 1. Local Development
Standard Docker Compose workflow with hot-reload:
```bash
# Start core services
docker compose up -d v2-postgres redis api admin
# API logs (watch for errors)
docker compose logs -f api
# Run with media API
docker compose up -d media-api
# Run with monitoring stack
docker compose --profile monitoring up -d
```
**Key:** `docker-compose.yml` uses `build:` blocks to compile TypeScript from source and mounts `./api:/app` for live code changes. This is the only compose file that builds from source.
### 2. Build & Push Production Images
After code changes are tested locally:
```bash
# Build production images and push to Gitea registry
./scripts/build-and-push.sh
```
This builds **4 services** with multi-stage Dockerfiles (production target, no dev dependencies), tags each image with `:SHA` and `:latest`, and pushes to `gitea.bnkops.com/admin/changemaker-{service}`:
| Service | Dockerfile | What it produces |
|---------|-----------|-----------------|
| `api` | `api/Dockerfile` | Express + Prisma (compiled JS, no TS) |
| `admin` | `admin/Dockerfile` | Nginx serving React build output |
| `media-api` | `api/Dockerfile.media` | Fastify + FFmpeg (compiled JS) |
| `nginx` | `nginx/Dockerfile` | Nginx with `envsubst` domain templating |
```bash
# Build specific services only
./scripts/build-and-push.sh --services api,admin
# Build without pushing (verify first)
./scripts/build-and-push.sh --no-push
# Include code-server (~9GB, only when Dockerfile changes)
./scripts/build-and-push.sh --include-code-server
```
### 3. Mirror Third-Party Images (Run Once / On Version Bumps)
Copies all third-party Docker images used by the platform to the Gitea registry, so deployments never depend on Docker Hub, GHCR, LSCR, or GCR:
```bash
# Mirror all 36 images (core + platform + comms + monitoring)
./scripts/mirror-images.sh
# Mirror only essential infrastructure (postgres, redis, alpine)
./scripts/mirror-images.sh --core-only
# Preview without executing
./scripts/mirror-images.sh --dry-run
```
**When to re-run:** Only when upgrading a third-party image version. The script has explicit version pins — update the version in `mirror-images.sh`, then re-run.
Images are organized into 4 groups:
| Group | Count | Examples |
|-------|-------|---------|
| Core Infrastructure | 5 | postgres:16-alpine, redis:7-alpine, alpine:3 |
| Platform Services | 16 | nocodb, listmonk, gitea, n8n, vaultwarden, nginx, code-server |
| Communication | 8 | rocket.chat, mongo, nats, gancio, jitsi (4 containers) |
| Monitoring | 7 | prometheus, grafana, alertmanager, cadvisor, exporters, gotify |
### 4. Build Release Tarball
Packages only runtime files (~9 MB) — no source code, no node_modules:
```bash
# Build tarball
./scripts/build-release.sh --tag v2.2.0
# Build and upload to Gitea Releases
./scripts/build-release.sh --tag v2.2.0 --upload
# Preview contents without creating tarball
./scripts/build-release.sh --dry-run
```
The tarball contains:
- `docker-compose.yml` (copy of `docker-compose.prod.yml` — image-only, no build blocks)
- `.env.example`, `config.sh` (configuration wizard)
- `scripts/` (init scripts, backup, upgrade, systemd units)
- `configs/` (prometheus, grafana, alertmanager, homepage, pangolin)
- `nginx/conf.d/` (templates for reference)
- `mkdocs/` (starter documentation)
- Empty data directories
### 5. Deploying
#### New Release Install (End Users)
```bash
# One-liner
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
# Or manual
curl -LO https://gitea.bnkops.com/admin/changemaker.lite/releases/latest/download/changemaker-lite-latest.tar.gz
tar xzf changemaker-lite-latest.tar.gz
cd changemaker-lite
bash config.sh
docker compose up -d
```
All images (custom + third-party) pull from `gitea.bnkops.com/admin`. No external registry access needed.
#### New CCP Instance (Fleet Operators)
The Control Panel provisions instances via its web UI:
1. Operator fills in the Create Instance wizard (domain, features, email, tunnel)
2. CCP copies source files, renders templates (Handlebars), generates secrets
3. With `USE_REGISTRY_IMAGES=true` (default): pulls pre-built images from Gitea (~2 min)
4. With `USE_REGISTRY_IMAGES=false`: builds from source (~10+ min)
5. Starts infrastructure → runs migrations → starts all services
CCP registry settings (in `changemaker-control-panel/.env`):
```bash
GITEA_REGISTRY=gitea.bnkops.com/admin # Registry URL for all images
USE_REGISTRY_IMAGES=true # true = pull pre-built, false = build from source
IMAGE_TAG=latest # Tag for custom images (api, admin, media-api)
```
### 6. Upgrading Existing Installations
#### Source Installs
```bash
./scripts/upgrade.sh # Standard: git pull + rebuild from source
./scripts/upgrade.sh --use-registry # Fast: pull pre-built images instead of rebuilding
./scripts/upgrade.sh --dry-run # Preview changes
```
#### Release Installs
```bash
./scripts/upgrade.sh # Auto-detects release mode, downloads latest tarball
```
Release installs are detected by the presence of a `VERSION` file and absence of `.git/`. The upgrade script automatically downloads the latest tarball from Gitea instead of running `git pull`.
---
## Image Naming Conventions
All images live under `gitea.bnkops.com/admin/`:
| Type | Naming Pattern | Example |
|------|---------------|---------|
| Custom services | `changemaker-{service}:{sha\|latest}` | `changemaker-api:latest` |
| Simple names | Same as upstream | `postgres:16-alpine`, `redis:7-alpine` |
| Namespaced → short | Org removed | `nocodb/nocodb``nocodb:0.301.3` |
| Conflict resolution | Explicit short name | `gotify/server``gotify`, `vaultwarden/server``vaultwarden` |
| Jitsi suite | `jitsi-{component}` | `jitsi-web:stable-9823`, `jitsi-prosody:stable-9823` |
| LinuxServer nginx | `ls-nginx` (avoids nginx conflict) | `ls-nginx:1.28.2` |
---
## Two Compose Files
| File | Purpose | Build? | Source mounts? | Image source |
|------|---------|--------|---------------|-------------|
| `docker-compose.yml` | Development | Yes (`build:` blocks) | Yes (`./api:/app`) | Built locally |
| `docker-compose.prod.yml` | Production | No | No | `${GITEA_REGISTRY:-gitea.bnkops.com/admin}/...` |
Release tarballs ship `docker-compose.prod.yml` renamed as `docker-compose.yml`.
The CCP template (`templates/docker-compose.yml.hbs`) generates a compose file that works like `docker-compose.prod.yml` when `USE_REGISTRY_IMAGES=true`, or like `docker-compose.yml` when `false`.
---
## Quick Reference
```bash
# ── Development ──
docker compose up -d v2-postgres redis api admin # Start dev stack
docker compose logs -f api # Watch API logs
docker compose exec api npx prisma migrate dev # Create migration
# ── Build & Publish ──
./scripts/build-and-push.sh # Build + push 4 images
./scripts/mirror-images.sh # Mirror 36 third-party images
./scripts/build-release.sh --tag v2.2.0 --upload # Package + upload release
# ── Deploy ──
curl -fsSL .../install.sh | bash # New install (release)
./scripts/upgrade.sh # Upgrade existing install
./scripts/upgrade.sh --use-registry # Fast upgrade (registry images)
# ── Verify ──
curl -s http://localhost:4000/api/health # API health check
docker compose ps # Container status
```
---
## Checklist: Cutting a New Release
1. [ ] All code changes committed and pushed to `v2` branch
2. [ ] `docker compose up -d` works locally (smoke test)
3. [ ] `./scripts/build-and-push.sh` — builds and pushes 4 production images
4. [ ] `./scripts/mirror-images.sh` — only if third-party versions changed
5. [ ] `./scripts/build-release.sh --tag vX.Y.Z --upload` — packages and uploads tarball
6. [ ] Test clean install: `tar xzf ... && cd changemaker-lite && bash config.sh && docker compose up -d`
7. [ ] Test upgrade: `./scripts/upgrade.sh` on an existing installation
8. [ ] Verify: `curl http://localhost:4000/api/health` returns `{"status":"ok"}`

View File

@ -2,16 +2,13 @@ FROM codercom/code-server:latest
USER root
# Install Node.js 18+ and npm
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs
# Install Node.js (for npm/claude-code — code-server bundles its own node but doesn't expose it)
RUN apt-get update && apt-get install -y nodejs npm --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install Claude Code globally as root
# Install Claude Code globally
RUN npm install -g @anthropic-ai/claude-code
# Install Ollama
RUN curl -fsSL https://ollama.com/install.sh | sh
# Install Python and dependencies
RUN apt-get update && apt-get install -y \
python3 \

257
FEDERATION_PLAN.md Normal file
View File

@ -0,0 +1,257 @@
# Phase 16: Federation — Instance-to-Instance Campaign Network
## Context
Changemaker Lite instances are currently isolated islands. This feature introduces a **federated discovery network** where any instance can act as a **hub** (accepting registrations, serving a directory) and/or a **spoke** (registering with hubs, sharing campaigns). The goal is organic, admin-to-admin networking with public campaign discoverability as a secondary benefit.
**Design principles:**
- Any instance can be a hub, spoke, or both — no central authority
- Medium-depth campaign sharing: enough metadata for discovery, click-through to source
- Per-campaign federation toggle — admins choose what's shared
- Strict privacy boundary: **never** share emails, participant data, queue data, addresses, volunteer/canvass data, or credentials
- Hub admins curate their own directories — organic > control
---
## Prisma Schema Changes
**File:** `api/prisma/schema.prisma`
### New enums
```
FederationPeerStatus: PENDING | ACTIVE | REJECTED | SUSPENDED | OFFLINE
FederationRole: HUB | SPOKE
```
### New models
**FederationIdentity** (singleton — this instance's federation profile)
- `enabled`, `hubEnabled`, `hubAutoApprove`
- Instance profile: `instanceName`, `instanceDescription`, `instanceUrl`, `instanceRegion`, `instanceTags` (Json), `instanceLogoUrl`
- Ed25519 keypair: `publicKey`, `privateKey` (encrypted at rest)
- Hub description, sync interval, last sync timestamp/error
**FederationPeer** (one record per connection, in either direction)
- `role` (HUB or SPOKE), `remoteUrl` (unique per role+url)
- Remote instance profile fields (name, description, region, tags, logo, publicKey)
- Auth: `apiKey` (ours for them), `remoteApiKey` (theirs for us) — both encrypted
- Status tracking: `status`, `statusMessage`, `lastSeenAt`, `lastSyncAt`, `failureCount`
- Stats: `campaignsShared`, `responsesShared`
- Relation to `FederatedCampaign[]`
**FederatedCampaign** (cached campaign metadata from peers)
- `peerId` → FederationPeer
- Remote identifiers: `remoteCampaignId`, `remoteCampaignSlug`
- Safe metadata: title, description, emailSubject (NOT body), callToAction, coverPhoto, status, targetGovernmentLevels, featureFlags (Json), createdByName
- Aggregate stats: `emailCount`, `responseCount`
- Source instance info (denormalized): `sourceInstanceName`, `sourceInstanceUrl`, `sourceInstanceRegion`
- Staleness tracking: `lastSyncedAt`, `isStale`
- Future adoption: `adoptedAsCampaignId` (nullable FK to local Campaign)
- Unique constraint: `[peerId, remoteCampaignId]`
### Modifications to existing models
**Campaign** — add `federated Boolean @default(false)` field
**SiteSettings** — add `enableFederation Boolean @default(false)` feature toggle
---
## API Module Structure
**New directory:** `api/src/modules/federation/`
| File | Purpose |
|------|---------|
| `federation.schemas.ts` | Zod schemas: identity update, peer registration, campaign sync, directory query, list filters |
| `federation.service.ts` | Core business logic: identity CRUD, peer management, `buildSafeCampaignPayload()`, campaign sync, directory serving |
| `federation-admin.routes.ts` | SUPER_ADMIN routes: identity management, peer approve/reject/suspend, manual sync trigger |
| `federation-peer.routes.ts` | Inter-instance routes: inbound registration, campaign sync, directory, heartbeat (API-key auth) |
| `federation-public.routes.ts` | Public browsing: federated campaigns list, instance directory (no auth) |
| `federation-crypto.service.ts` | Ed25519 keypair generation, request signing/verification |
**New file:** `api/src/services/federation-sync-queue.service.ts` — BullMQ repeatable job for periodic sync
### Route table
**Admin routes** (`/api/federation/...`, SUPER_ADMIN + JWT auth):
| Method | Path | Description |
|--------|------|-------------|
| GET | `/identity` | Get federation config |
| PUT | `/identity` | Update config/profile |
| POST | `/identity/generate-keypair` | Generate Ed25519 keypair |
| GET | `/peers` | List all peers |
| POST | `/peers/register` | Register with a remote hub |
| POST | `/peers/:id/approve` | Approve incoming spoke |
| POST | `/peers/:id/reject` | Reject incoming spoke |
| POST | `/peers/:id/suspend` | Suspend peer |
| DELETE | `/peers/:id` | Remove peer |
| POST | `/sync` | Trigger manual sync |
| GET | `/sync/status` | Sync status + history |
**Peer routes** (`/api/federation/peer/...`, API-key auth via `X-Federation-Key` header):
| Method | Path | Description |
|--------|------|-------------|
| POST | `/register` | Inbound spoke registration |
| POST | `/sync` | Inbound campaign metadata push |
| GET | `/directory` | Serve campaign directory |
| GET | `/profile` | Return instance profile |
| POST | `/heartbeat` | Liveness check |
**Public routes** (`/api/federation/...`, no auth):
| Method | Path | Description |
|--------|------|-------------|
| GET | `/campaigns` | Browse federated campaigns (paginated, searchable) |
| GET | `/campaigns/:id` | Single federated campaign detail |
| GET | `/instances` | List known network instances |
### Mounting in server.ts
```
app.use('/api/federation', federationPublicRouter); // No auth — first
app.use('/api/federation', federationPeerRouter); // API-key auth
app.use('/api/federation', federationAdminRouter); // SUPER_ADMIN JWT
```
---
## Federation Protocol
### Registration handshake
1. Spoke admin enters hub URL, clicks "Register"
2. Spoke sends `POST /api/federation/peer/register` to hub with instance profile + generated API key
3. Hub creates peer record (PENDING or ACTIVE if `hubAutoApprove`)
4. Hub responds with its own API key + peer ID
5. If approved (now or later), hub calls back to spoke's `/peer/register` to complete mutual registration
6. Both instances now have each other as peers (Spoke→HUB role, Hub→SPOKE role)
### Campaign sync
- Spokes push federated campaigns to hubs on schedule (BullMQ repeatable job)
- Payload: array of safe campaign metadata + array of un-federated campaign IDs (for removal)
- Hub stores/updates `FederatedCampaign` records
- Sync includes heartbeat (updates `lastSeenAt`)
### Privacy boundary enforcement
`buildSafeCampaignPayload()` in the service layer filters campaigns to only safe fields. **Never included:** emailBody, any email addresses, user IDs, participant data, moderation internals, custom recipients, calls data.
### Offline handling
- Increment `failureCount` on sync failure; after 5 consecutive failures → status `OFFLINE`
- Mark federated campaigns as `isStale` after 24h offline
- Keep checking with exponential backoff (max 24h)
- Auto-recover when heartbeat succeeds
---
## Security
- **API-key auth:** `crypto.randomBytes(32).toString('hex')`, encrypted at rest with existing `encrypt()`/`decrypt()` utility
- **Custom middleware:** `authenticatePeer` checks `X-Federation-Key` header, verifies peer exists + is ACTIVE
- **Request signing (optional):** Ed25519 signatures on `X-Federation-Signature` header for non-repudiation (configurable, not enforced in MVP)
- **Rate limiting:** 30 req/min for peer routes, 60 req/min for public routes (separate Redis prefixes)
- **CORS:** Peer routes need permissive CORS (cross-domain by nature)
- **Input validation:** All incoming peer data Zod-validated + HTML-escaped before storage
---
## Environment Variables
Add to `api/src/config/env.ts`:
```
ENABLE_FEDERATION: z.string().default('false')
FEDERATION_SYNC_INTERVAL_MINUTES: z.coerce.number().default(60)
FEDERATION_MAX_CAMPAIGNS_PER_SYNC: z.coerce.number().default(500)
FEDERATION_PEER_TIMEOUT_MS: z.coerce.number().default(15000)
FEDERATION_MAX_PEERS: z.coerce.number().default(50)
```
---
## Admin UI
### FederationPage (`admin/src/pages/FederationPage.tsx`)
4-tab page following PangolinPage pattern:
**Tab 1 — Identity & Settings:** Toggle federation, instance profile form, keypair management, hub/spoke settings
**Tab 2 — Connected Peers:** Table of peers (name, URL, role tag, status tag, campaigns shared, last sync, actions). "Register with Hub" button opens modal. Pending incoming registrations highlighted.
**Tab 3 — Federated Campaigns:** Card grid/table of federated campaigns with search + filter (region, tags, government level). Click-through links to source instances.
**Tab 4 — Sync Status:** Last/next sync, per-peer status, manual sync button, sync history.
### Sidebar
Add to `buildMenuItems()` in `AppLayout.tsx`, gated on `settings?.enableFederation`:
```typescript
{ key: '/app/federation', icon: <GlobalOutlined />, label: 'Federation' }
```
(Using `<GlobalOutlined />` since `<GlobalOutlined />` is already imported but used for Web submenu — may use `<ClusterOutlined />` or `<DeploymentUnitOutlined />` instead)
### Route in App.tsx
```tsx
<Route path="federation" element={<ProtectedRoute requiredRoles={['SUPER_ADMIN']}><FederationPage /></ProtectedRoute>} />
```
### Campaign form integration
Add `federated` checkbox to campaign create/edit form in CampaignsPage, visible only when `settings.enableFederation` is true.
### TypeScript types
Add `FederationIdentity`, `FederationPeer`, `FederatedCampaign`, `FederationSyncStatus` interfaces to `admin/src/types/api.ts`.
### Public network page (stretch goal in MVP)
`admin/src/pages/public/FederatedCampaignsPage.tsx` at `/network` route — card grid of federated campaigns with PublicLayout dark theme.
---
## Prometheus Metrics
Add to `api/src/utils/metrics.ts`:
- `cm_federation_peers_active` (Gauge)
- `cm_federation_campaigns_shared` (Gauge)
- `cm_federation_sync_duration_seconds` (Histogram)
- `cm_federation_sync_errors_total` (Counter with `peer_id` label)
---
## Implementation Order
| Step | Description | Files Created/Modified | Depends On |
|------|-------------|----------------------|------------|
| 1 | **Prisma schema** — Add enums, 3 new models, Campaign.federated, SiteSettings.enableFederation | `api/prisma/schema.prisma` | — |
| 2 | **Migration**`npx prisma migrate dev --name add-federation` | `api/prisma/migrations/` | Step 1 |
| 3 | **Env vars** — Add federation config to env.ts + .env.example | `api/src/config/env.ts`, `.env.example` | — |
| 4 | **Crypto service** — Ed25519 keypair, sign/verify | `api/src/modules/federation/federation-crypto.service.ts` | — |
| 5 | **Schemas** — Zod validation for all federation endpoints | `api/src/modules/federation/federation.schemas.ts` | Step 1 |
| 6 | **Core service** — Identity CRUD, peer management, buildSafeCampaignPayload, campaign sync logic | `api/src/modules/federation/federation.service.ts` | Steps 2, 4, 5 |
| 7 | **Admin routes** — SUPER_ADMIN federation management | `api/src/modules/federation/federation-admin.routes.ts` | Step 6 |
| 8 | **Peer routes** — Inter-instance API with authenticatePeer middleware | `api/src/modules/federation/federation-peer.routes.ts` | Step 6 |
| 9 | **Public routes** — Browsing federated campaigns | `api/src/modules/federation/federation-public.routes.ts` | Step 6 |
| 10 | **Rate limiting** — Add federation rate limiters | `api/src/middleware/rate-limit.ts` | — |
| 11 | **Server mounting** — Import + mount routers, start sync queue | `api/src/server.ts` | Steps 7-10 |
| 12 | **Sync queue** — BullMQ repeatable job for periodic sync | `api/src/services/federation-sync-queue.service.ts` | Step 6 |
| 13 | **Metrics** — Prometheus counters/gauges | `api/src/utils/metrics.ts` | — |
| 14 | **Campaign form** — Add `federated` to schemas + service + CampaignsPage checkbox | `api/src/modules/influence/campaigns/campaigns.schemas.ts`, `campaigns.service.ts`, `admin/src/pages/CampaignsPage.tsx` | Step 2 |
| 15 | **Frontend types** — Federation TypeScript interfaces | `admin/src/types/api.ts` | — |
| 16 | **FederationPage** — 4-tab admin page | `admin/src/pages/FederationPage.tsx` | Steps 7, 15 |
| 17 | **Sidebar + routing** — Menu item + route in AppLayout/App.tsx | `admin/src/components/AppLayout.tsx`, `admin/src/App.tsx` | Step 16 |
| 18 | **Public network page** (stretch) — Federated campaigns browse | `admin/src/pages/public/FederatedCampaignsPage.tsx` | Steps 9, 15 |
---
## Future Extensions (not in MVP, but models accommodate)
- **Campaign adoption** — "Fork" a federated campaign locally (`FederatedCampaign.adoptedAsCampaignId`)
- **Cross-instance response sharing** — New `FederatedResponse` model synced alongside campaigns
- **Named networks/coalitions**`FederationNetwork` + `FederationNetworkMember` models for named alliances
- **Hub-of-hubs discovery** — Hubs share known-hub lists for transitive discovery (gossip protocol)
---
## Verification
1. **Two-instance test:** Run two API instances on different ports, enable federation on both, register one with the other
2. **Campaign sync:** Create a federated campaign on spoke, verify it appears in hub's directory
3. **Privacy boundary:** Inspect sync payloads — verify no emails, user IDs, or email bodies leak
4. **Offline handling:** Stop one instance, verify the other marks it OFFLINE after 5 failed syncs, then recovers on restart
5. **Rate limiting:** Hit peer endpoints rapidly, verify 429 responses after threshold
6. **Feature gate:** Disable federation in settings, verify all routes return 403/hidden
7. **UI:** Verify sidebar item appears/hides with feature toggle, all 4 tabs functional

BIN
NARguide.pdf Normal file

Binary file not shown.

247
README.md
View File

@ -1,140 +1,157 @@
# Changemaker Lite
<p align="center">
<img src="mkdocs/docs/assets/logo.png" alt="Changemaker Lite" width="120" />
</p>
Changemaker Lite is a streamlined documentation and development platform featuring essential self-hosted services for creating, managing, and automating political campaign workflows.
<h1 align="center">Changemaker Lite</h1>
## Features
<p align="center">
A self-hosted campaign platform for community organizers who want to own their data.
</p>
- **Homepage**: Modern dashboard for accessing all services
- **Code Server**: VS Code in your browser for remote development
- **MkDocs Material**: Beautiful documentation with live preview
- **Static Site Server**: High-performance hosting for built sites
- **Listmonk**: Self-hosted newsletter and email campaign management
- **PostgreSQL**: Reliable database backend
- **n8n**: Workflow automation and service integration
- **NocoDB**: No-code database platform and smart spreadsheet interface
- **Map**: Interactive map visualization for geographic data with real-time geolocation, walk sheet generation, and QR code integration
- **Influence**: Campaign tool for connecting Alberta residents with elected representatives at all government levels
<p align="center">
<a href="https://cmlite.org/docs/getting-started/">Documentation</a> &middot;
<a href="https://cmlite.org">Website</a> &middot;
<a href="https://opensource.org/license/apache-2-0">Apache 2.0 License</a>
</p>
---
Changemaker Lite consolidates advocacy campaigns, geographic mapping, volunteer canvassing, media management, newsletters, and administration into a single Docker Compose stack. One `.env` file, one command to start, everything under your control.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/admin-dashboard.png" alt="Admin Dashboard" width="800" />
</p>
## Why Changemaker Lite?
Most campaign tools are SaaS platforms that lock you into monthly subscriptions, hold your data hostage, and disappear when funding dries up. Changemaker Lite is different:
- **Self-hosted** -- runs on any machine with Docker. Your server, your data.
- **All-in-one** -- replaces 5-10 separate tools with a single integrated platform.
- **Free and open source** -- Apache 2.0 licensed. Fork it, modify it, make it yours.
- **Privacy-first** -- no telemetry, no third-party analytics, no data leaving your server.
## What's Inside
### Advocacy Campaigns
Let supporters look up their elected representatives by postal code and send advocacy emails in a few clicks. Track responses, moderate a public response wall, and monitor email delivery.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-campaigns.png" alt="Public Campaign Page" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/influence-campaigns.png" alt="Campaign Management" width="800" />
</p>
### Interactive Map & Canvassing
Import thousands of addresses, draw canvassing areas, schedule volunteer shifts, and track door-to-door visits with GPS. Volunteers get a full-screen mobile map with real-time location tracking and visit recording.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-map.png" alt="Public Map" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/canvass-dashboard.png" alt="Canvass Dashboard" width="800" />
</p>
### Volunteer Portal
Volunteers get their own portal with shift sign-ups, canvassing assignments, activity tracking, a social calendar, and a friends system to stay connected with their team.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/volunteer-dashboard.png" alt="Volunteer Map" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/volunteer-calendar.png" alt="Volunteer Calendar" width="800" />
</p>
### Media Library & Public Gallery
Upload campaign videos, manage metadata, schedule publishing, and share them through a public gallery. Includes GDPR-compliant analytics.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/media-library.png" alt="Media Library" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-gallery.png" alt="Public Gallery" width="800" />
</p>
### Landing Pages & Email Templates
Build campaign microsites with a drag-and-drop GrapesJS editor. Design email templates for consistent campaign communications.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/landing-pages.png" alt="Landing Page Builder" width="800" />
</p>
### SMS Campaigns, Newsletters & More
Send SMS campaigns via an Android bridge, sync subscribers to Listmonk for newsletters, recognize volunteers on a Wall of Fame leaderboard, and monitor everything with built-in Prometheus + Grafana observability.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/sms-dashboard.png" alt="SMS Dashboard" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-wall-of-fame.png" alt="Wall of Fame" width="800" />
</p>
## Quick Start
The whole system can be set up in minutes using Docker Compose. It is recommended to run this on a server with at least 8GB of RAM and 4 CPU cores for optimal performance. Instructions to build to production are available in the mkdocs/docs/build directory, at cmlite.org, or in the site preview.
```bash
# Clone the repository
git clone https://gitea.bnkops.com/admin/changemaker.lite
cd changemaker.lite
# One-command install (downloads pre-built images, runs config wizard)
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
# Configure environment (creates .env file)
./config.sh
# Start all services
cd ~/changemaker.lite
docker compose up -d
```
## Map
Instructions on how to build the map are available in the map directory.
Instructions on how to build for production are available in the mkdocs/docs/build directory or in the site preview.
### Quick Start for Map
Update the .env file in the map directory with your NocoDB URLs, and then run:
Or clone and build from source:
```bash
cd map
docker compose up -d
git clone <repo-url> changemaker.lite
cd changemaker.lite && git checkout v2
cp .env.example .env
# Edit .env -- set passwords, JWT secrets, admin credentials
docker compose up -d v2-postgres redis api admin
docker compose exec api npx prisma migrate deploy
docker compose exec api npx prisma db seed
```
## Influence
The Influence Campaign Tool helps Alberta residents connect with elected representatives at federal, provincial, and municipal levels. Users can look up representatives by postal code and send advocacy emails through customizable campaigns.
Detailed setup and configuration instructions are available in the `influence/README.MD` file.
### Quick Start for Influence
Configure your environment and start the service:
```bash
cd influence
cp example.env .env
# Edit .env with your NocoDB and SMTP settings
./scripts/build-nocodb.sh # Set up database tables
docker compose up -d
```
## Service Access
After starting, access services at:
- **Homepage Dashboard**: http://localhost:3010
- **Documentation (Dev)**: http://localhost:4000
- **Documentation (Built)**: http://localhost:4001
- **Code Server**: http://localhost:8888
- **Listmonk**: http://localhost:9000
- **n8n**: http://localhost:5678
- **NocoDB**: http://localhost:8090
- **Map Viewer**: http://localhost:3000
- **Influence Campaign Tool**: http://localhost:3333
## Production Deployment
If you are deploying to production, using Cloudflare, you can use the included 'start-production.sh' script to set up a secure deployment with HTTPS. Ensure your domain and cloudflare settings are correctly configured in the root .env before running. More information on the required API tokens and settings can be found in the mkdocs/docs/build directory or at cmlite.org.
```bash
./start-production.sh
```
Then open **http://localhost:3000** and log in with the admin credentials from your `.env`.
## Documentation
Complete documentation is available in the MkDocs site, including:
**Full documentation is available at [cmlite.org/docs/getting-started](https://cmlite.org/docs/getting-started/).**
- Service configuration guides
- Integration examples
- Workflow automation tutorials
- Map application setup and usage
- Troubleshooting guides
The docs site covers installation, configuration, all features, architecture details, production deployment with Pangolin tunnels, and troubleshooting. It is the authoritative and up-to-date reference for Changemaker Lite.
Visit http://localhost:4000 after starting services to access the full documentation.
## Architecture at a Glance
## Licensing
| Layer | Technology |
|-------|-----------|
| API | Express.js + Prisma + PostgreSQL 16 |
| Media API | Fastify + Prisma (shared DB) |
| Frontend | React + Vite + Ant Design + Zustand |
| Reverse Proxy | Nginx (subdomain routing) |
| Cache & Queue | Redis + BullMQ |
| Newsletter | Listmonk |
| Monitoring | Prometheus + Grafana + Alertmanager |
| Tunneling | Pangolin (self-hosted Cloudflare alternative) |
This project is licensed under the Apache License 2.0 - https://opensource.org/license/apache-2-0
The entire stack runs on Docker Compose. Enable optional modules (media, newsletters, SMS, monitoring) with feature flags in `.env`.
## License
[Apache License 2.0](https://opensource.org/license/apache-2-0)
## AI Disclaimer
This project used AI tools to assist in its creation and large amounts of the boilerplate code was reviewed using AI. AI tools (although not activated or connected) are pre-installed in the Coder docker image. See `docker.code-server` for more details.
While these tools can help generate code and documentation, they may also introduce errors or inaccuracies. Users should review and test all content to ensure it meets their requirements and standards.
## Troubleshooting
### Permission Denied Errors (EACCES)
If you see errors like `EACCES: permission denied` when starting containers, run the included fix script:
```bash
./fix-permissions.sh
```
This fixes permissions on directories that containers need to write to, such as:
- `configs/code-server/.config` and `.local` (Code Server)
- `mkdocs/.cache` (MkDocs social cards plugin)
- `mkdocs/site` (MkDocs built output)
If the script can't fix some directories (owned by a different container UID), it will prompt to use `sudo`.
### Manual Permission Fix
If you prefer to fix manually:
```bash
# Fix all permissions at once
sudo chown -R $(id -u):$(id -g) .
chmod -R 755 .
# Or fix specific directories
chmod -R 777 configs/code-server/.config configs/code-server/.local
chmod -R 777 mkdocs/.cache mkdocs/site
```
AI tools were used to assist in the creation of this project. All generated code has been reviewed. Users should test all functionality to ensure it meets their requirements.

BIN
RNAguide.pdf Normal file

Binary file not shown.

6
admin/.dockerignore Normal file
View File

@ -0,0 +1,6 @@
node_modules
dist
.git
*.log
.env
.env.*

9
admin/.env.example Normal file
View File

@ -0,0 +1,9 @@
# API Configuration
# For Docker: http://changemaker-v2-api:4000
# For local dev: http://localhost:4000
VITE_API_URL=http://localhost:4000
# MkDocs Configuration
# For Docker: http://mkdocs-changemaker:8000
# For local dev: http://localhost:4003
VITE_MKDOCS_URL=http://localhost:4003

23
admin/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
# Development stage
FROM base AS development
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Build stage
FROM base AS build
COPY . .
RUN npm run build
# Production stage — serve with Nginx
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]

47
admin/index.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Changemaker Lite</title>
<!-- Default OG meta tags (overridden by nginx bot detection → /api/og/* for specific pages) -->
<meta property="og:title" content="Changemaker Lite" />
<meta property="og:description" content="Take action. Make a difference." />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Changemaker Lite" />
<meta name="twitter:description" content="Take action. Make a difference." />
</head>
<body style="margin:0;background:#1a1025">
<div id="root"></div>
<noscript>
<div style="display:flex;align-items:center;justify-content:center;height:100vh;color:#e0e0e0;font-family:sans-serif">
<p>JavaScript is required to run this application.</p>
</div>
</noscript>
<script>
// Fallback: if the main module fails to load entirely (stale deployment),
// show a reload prompt after a timeout. The main app replaces #root content on success.
setTimeout(function() {
var root = document.getElementById('root');
if (root && root.children.length === 0) {
var key = 'cm_chunk_reload';
var last = sessionStorage.getItem(key);
var now = Date.now();
if (!last || now - parseInt(last, 10) > 10000) {
sessionStorage.setItem(key, String(now));
window.location.reload();
} else {
root.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;color:#e0e0e0;font-family:sans-serif;text-align:center;padding:0 24px">'
+ '<div style="font-size:48px;margin-bottom:16px">&#x21BB;</div>'
+ '<h2 style="margin:0 0 8px;font-size:20px">Application Updated</h2>'
+ '<p style="margin:0 0 24px;color:#999;max-width:400px;line-height:1.5">A new version has been deployed. Please refresh to load the latest version.</p>'
+ '<button onclick="window.location.reload()" style="padding:10px 24px;font-size:14px;border:none;border-radius:6px;background:#9d4edd;color:#fff;cursor:pointer">Refresh Page</button>'
+ '</div>';
}
}
}, 5000);
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

17
admin/nginx.conf Normal file
View File

@ -0,0 +1,17 @@
server {
listen 3000;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://changemaker-v2-api:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

4055
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
admin/package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "changemaker-v2-admin",
"private": true,
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.6.0",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@dagrejs/dagre": "^2.0.4",
"@hocuspocus/provider": "^3.4.4",
"@monaco-editor/react": "^4.7.0",
"@types/d3-force": "^3.0.10",
"@types/dompurify": "^3.2.0",
"@xyflow/react": "^12.10.1",
"antd": "^5.23.0",
"axios": "^1.7.9",
"d3-force": "^3.0.0",
"dayjs": "^1.11.19",
"dompurify": "^3.3.1",
"grapesjs": "^0.22.14",
"grapesjs-blocks-basic": "^1.0.2",
"grapesjs-component-countdown": "^1.0.2",
"grapesjs-custom-code": "^1.0.2",
"grapesjs-navbar": "^1.0.2",
"grapesjs-plugin-export": "^1.0.12",
"grapesjs-plugin-forms": "^2.0.6",
"grapesjs-preset-webpage": "^1.0.3",
"grapesjs-style-gradient": "^3.0.3",
"grapesjs-tabs": "^1.0.6",
"grapesjs-touch": "^0.1.1",
"grapesjs-typed": "^2.0.1",
"html5-qrcode": "^2.3.8",
"jwt-decode": "^4.0.0",
"leaflet": "^1.9.4",
"minisearch": "^7.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"react-leaflet-cluster": "^4.0.0",
"react-router-dom": "^7.1.1",
"recharts": "^3.7.0",
"y-monaco": "^0.1.6",
"yaml": "^2.8.2",
"yjs": "^13.6.29",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/leaflet": "^1.9.21",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.3",
"vite": "^6.0.7"
}
}

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html><head><title>Auth Check</title></head>
<body>
<script>
// This page is loaded in a hidden iframe from the MkDocs header.
// It reads the auth state from this origin's localStorage and
// posts it back to the parent window via postMessage.
// The parent passes its origin as ?origin=... so we can target the reply.
(function() {
var authenticated = false;
try {
var stored = localStorage.getItem('cml-auth');
if (stored) {
var parsed = JSON.parse(stored);
if (parsed && parsed.state && parsed.state.accessToken) {
authenticated = true;
}
}
} catch(e) {}
// Only post back to the declared parent origin (prevents state disclosure to arbitrary embedders)
var params = new URLSearchParams(location.search);
var targetOrigin = params.get('origin');
if (!targetOrigin) return;
// Validate targetOrigin is a proper origin (protocol + host, no path)
try {
var url = new URL(targetOrigin);
targetOrigin = url.origin;
} catch(e) { return; }
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: 'cml-auth-status',
authenticated: authenticated
}, targetOrigin);
}
})();
</script>
</body>
</html>

1041
admin/src/App.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,89 @@
import GalleryAdCard from '@/components/media/GalleryAdCard';
import type { GalleryAd } from '@/types/gallery-ads';
import { getOrCreateSessionId } from '@/lib/media-public-api';
interface AdBannerProps {
ads: GalleryAd[];
/** Maximum number of ads to display (default: 1) */
maxAds?: number;
/** Placement identifier for session-stable rotation */
placement?: string;
}
/** Simple deterministic hash for session-stable selection */
function hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0; // Convert to 32-bit int
}
return Math.abs(hash);
}
/**
* Select ads using weighted session-stable rotation.
* Lower position = higher weight. Same session+placement always returns the same ads.
*/
function selectAds(ads: GalleryAd[], maxAds: number, placement: string): GalleryAd[] {
if (ads.length <= maxAds) return ads;
const sessionId = getOrCreateSessionId();
const seed = hashCode(sessionId + ':' + placement);
const maxPosition = Math.max(...ads.map((a) => a.position ?? 0));
const selected: GalleryAd[] = [];
const remaining = [...ads];
for (let i = 0; i < maxAds && remaining.length > 0; i++) {
// Build weights: lower position = higher weight
const weights = remaining.map((ad) => maxPosition - (ad.position ?? 0) + 1);
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
// Use a different hash offset per slot so multi-ad displays don't repeat
const pick = (seed + i * 7919) % totalWeight;
let cumulative = 0;
let chosenIdx = 0;
for (let j = 0; j < weights.length; j++) {
cumulative += (weights[j] ?? 0);
if (pick < cumulative) {
chosenIdx = j;
break;
}
}
const chosen = remaining[chosenIdx];
if (chosen) selected.push(chosen);
remaining.splice(chosenIdx, 1);
}
return selected;
}
/**
* Renders GalleryAdCard(s) in a centered container.
* Intended for use on public pages between content sections.
*/
export default function AdBanner({ ads, maxAds = 1, placement }: AdBannerProps) {
if (ads.length === 0) return null;
const displayAds = placement ? selectAds(ads, maxAds, placement) : ads.slice(0, maxAds);
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
gap: 16,
margin: '24px auto',
maxWidth: maxAds > 1 ? 800 : 400,
flexWrap: 'wrap',
}}
>
{displayAds.map((ad) => (
<div key={ad.id} style={{ flex: '1 1 300px', maxWidth: 400 }}>
<GalleryAdCard ad={ad} />
</div>
))}
</div>
);
}

View File

@ -0,0 +1,734 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
import { Layout, Menu, Dropdown, Button, Typography, Drawer, Grid, theme, Tooltip, Modal, Badge } from 'antd';
import {
DashboardOutlined,
SendOutlined,
IdcardOutlined,
MailOutlined,
MessageOutlined,
EnvironmentOutlined,
TeamOutlined,
SettingOutlined,
LogoutOutlined,
UserOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
MenuOutlined,
ScissorOutlined,
CalendarOutlined,
ScheduleOutlined,
FileTextOutlined,
NotificationOutlined,
BookOutlined,
GlobalOutlined,
CodeOutlined,
DatabaseOutlined,
ApiOutlined,
BranchesOutlined,
CloudServerOutlined,
QrcodeOutlined,
PlaySquareOutlined,
FolderOutlined,
HistoryOutlined,
LineChartOutlined,
BarChartOutlined,
SoundOutlined,
EditOutlined,
OrderedListOutlined,
DollarOutlined,
ShoppingOutlined,
HeartOutlined,
CrownOutlined,
PictureOutlined,
LockOutlined,
PhoneOutlined,
TagOutlined,
SearchOutlined,
ContactsOutlined,
VideoCameraOutlined,
ApartmentOutlined,
SafetyOutlined,
StarFilled,
StarOutlined,
TrophyOutlined,
FlagOutlined,
UserAddOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth.store';
import { useSettingsStore } from '@/stores/settings.store';
import { hasAnyRole } from '@/utils/roles';
import type { PageHeaderConfig, AppOutletContext, User } from '@/types/api';
import {
INFLUENCE_ROLES,
BROADCAST_ROLES,
CONTENT_ROLES,
MAP_ROLES,
SCHEDULING_ROLES,
MEDIA_ROLES,
PAYMENTS_ROLES,
SOCIAL_ROLES,
} from '@/types/api';
import { buildHomeUrl, resolveNavUrl } from '@/lib/service-url';
import type { NavItem } from '@/types/api';
import {
DEFAULT_NAV_ITEMS,
ICON_MAP,
mergeNavDefaults,
filterNavItems,
buildFeatureFlags,
applyAdminOverrides,
} from '@/lib/nav-defaults';
import { useCommandPaletteStore } from '@/stores/command-palette.store';
import { useFavoritesStore } from '@/stores/favorites.store';
import { useTourStore } from '@/stores/tour.store';
import { AdminTour } from './tour/AdminTour';
import { TourHub } from './tour/TourHub';
import { TourTriggerButton } from './tour/TourTriggerButton';
import { resolveValidFavorites, collectLeafKeys } from '@/utils/menu-items';
import RocketChatWidget from './chat/RocketChatWidget';
// Re-export for backward compatibility
export type { PageHeaderConfig, AppOutletContext };
/** Wrap a leaf menu item's label with a favorite star toggle */
function FavoriteLabel({ label, itemKey }: { label: React.ReactNode; itemKey: string }) {
const { isFavorite, toggleFavorite } = useFavoritesStore();
const starred = isFavorite(itemKey);
return (
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>
<span
className={`favorite-star${starred ? ' favorite-star--active' : ''}`}
onClick={(e) => { e.stopPropagation(); toggleFavorite(itemKey); }}
style={{ fontSize: 12, lineHeight: 1, cursor: 'pointer', flexShrink: 0 }}
>
{starred ? <StarFilled style={{ color: '#faad14' }} /> : <StarOutlined style={{ color: 'rgba(255,255,255,0.45)' }} />}
</span>
</span>
);
}
/** Recursively walk menu items, wrapping leaf labels with FavoriteLabel stars */
function addStarsToMenuItems(items: MenuProps['items'], leafKeys: Set<string>): MenuProps['items'] {
if (!items) return items;
return items.map(item => {
if (!item) return item;
if ('children' in item && item.children) {
return { ...item, children: addStarsToMenuItems(item.children as MenuProps['items'], leafKeys) } as typeof item;
}
if ('key' in item && 'label' in item && item.key && leafKeys.has(item.key as string)) {
return { ...item, label: <FavoriteLabel label={item.label} itemKey={item.key as string} /> } as typeof item;
}
return item;
});
}
const { Header, Sider, Content } = Layout;
const { Text } = Typography;
const { useBreakpoint } = Grid;
/** Admin icon overrides: some icons differ in the admin header context */
const ADMIN_ICON_OVERRIDES: Record<string, React.ReactNode> = {
SendOutlined: <SoundOutlined />,
PlayCircleOutlined: <PlaySquareOutlined />,
};
function buildMenuItems(settings: import('@/types/api').SiteSettings | null, user: User | null, badges?: { pendingResponses?: number; pendingEmails?: number; pendingCampaignReview?: number; pendingComments?: number }): MenuProps['items'] {
const isSuperAdmin = hasAnyRole(user, ['SUPER_ADMIN']);
const can = (roles: import('@/types/api').UserRole[]) => hasAnyRole(user, roles);
const items: MenuProps['items'] = [
{
key: '/app',
icon: <DashboardOutlined />,
label: 'Dashboard',
},
];
// People & Access submenu — Users visible to any admin, social sub-items gated by SOCIAL_ROLES
{
const communityChildren: MenuProps['items'] = [];
if (settings?.enablePeople) {
communityChildren.push({ key: '/app/people', icon: <ContactsOutlined />, label: 'People' });
}
communityChildren.push({ key: '/app/users', icon: <TeamOutlined />, label: 'Users' });
if (settings?.enableSocial && can(SOCIAL_ROLES)) {
communityChildren.push(
{ key: '/app/social', icon: <HeartOutlined />, label: 'Social Dashboard' },
{ key: '/app/social/graph', icon: <ApartmentOutlined />, label: 'Social Graph' },
{ key: '/app/social/moderation', icon: <SafetyOutlined />, label: 'Social Moderation' },
{ key: '/app/social/referrals', icon: <UserAddOutlined />, label: 'Referrals' },
{ key: '/app/social/spotlights', icon: <StarOutlined />, label: 'Spotlights' },
{ key: '/app/social/challenges', icon: <FlagOutlined />, label: 'Challenges' },
);
}
items.push({
key: 'community-submenu',
icon: <ContactsOutlined />,
label: 'People & Access',
children: communityChildren,
});
}
if (settings?.enableInfluence !== false && can(INFLUENCE_ROLES)) {
items.push({
key: 'influence-submenu',
icon: <SendOutlined />,
label: 'Advocacy',
children: [
{ key: '/app/campaigns', icon: <SendOutlined />, label: 'Campaigns' },
{ key: '/app/campaign-moderation', icon: <FileTextOutlined />, label: badges?.pendingCampaignReview ? <Badge count={badges.pendingCampaignReview} size="small" offset={[8, 0]}>Campaign Review</Badge> : 'Campaign Review' },
{ key: '/app/representatives', icon: <IdcardOutlined />, label: 'Representatives' },
{ key: '/app/email-queue', icon: <MailOutlined />, label: badges?.pendingEmails ? <Badge count={badges.pendingEmails} size="small" offset={[8, 0]}>Outgoing Emails</Badge> : 'Outgoing Emails' },
{ key: '/app/responses', icon: <MessageOutlined />, label: badges?.pendingResponses ? <Badge count={badges.pendingResponses} size="small" offset={[8, 0]}>Responses</Badge> : 'Responses' },
{ key: '/app/influence/effectiveness', icon: <LineChartOutlined />, label: 'Effectiveness' },
{ key: '/app/influence/stories', icon: <TrophyOutlined />, label: 'Impact Stories' },
],
});
}
if (settings?.enableNewsletter !== false && can(BROADCAST_ROLES)) {
const broadcastChildren: MenuProps['items'] = [
{ key: '/app/listmonk', icon: <MailOutlined />, label: 'Newsletter' },
{ key: '/app/email-templates', icon: <FileTextOutlined />, label: 'Email Templates' },
];
if (settings?.enableChat) {
broadcastChildren.push({ key: '/app/services/rocketchat', icon: <MessageOutlined />, label: 'Team Chat' });
}
if (settings?.enableSms || isSuperAdmin) {
broadcastChildren.push(
{ key: '/app/sms/setup', icon: <SettingOutlined />, label: 'SMS Setup' },
);
}
if (settings?.enableSms) {
broadcastChildren.push(
{ key: '/app/sms', icon: <PhoneOutlined />, label: 'SMS Dashboard' },
{ key: '/app/sms/contacts', icon: <TeamOutlined />, label: 'SMS Contacts' },
{ key: '/app/sms/campaigns', icon: <SendOutlined />, label: 'SMS Campaigns' },
{ key: '/app/sms/conversations', icon: <MessageOutlined />, label: 'SMS Threads' },
{ key: '/app/sms/templates', icon: <FileTextOutlined />, label: 'SMS Templates' },
);
}
items.push({
key: 'broadcast-submenu',
icon: <NotificationOutlined />,
label: 'Broadcast',
children: broadcastChildren,
});
}
// Web submenu — conditionally include Landing Pages
if (can(CONTENT_ROLES)) {
const webChildren: MenuProps['items'] = [];
if (settings?.enableLandingPages !== false) {
webChildren.push({ key: '/app/pages', icon: <FileTextOutlined />, label: 'Landing Pages' });
}
webChildren.push({ key: '/app/navigation', icon: <GlobalOutlined />, label: 'Navigation' });
webChildren.push({ key: '/app/docs', icon: <BookOutlined />, label: 'Documentation' });
webChildren.push({ key: '/app/docs/analytics', icon: <BarChartOutlined />, label: 'Analytics' });
webChildren.push({ key: '/app/docs/comments', icon: <MessageOutlined />, label: badges?.pendingComments ? <Badge count={badges.pendingComments} size="small" offset={[8, 0]}>Comments</Badge> : 'Comments' });
webChildren.push({ key: '/app/docs/metadata', icon: <DatabaseOutlined />, label: 'Metadata' });
webChildren.push({ key: '/app/docs/settings', icon: <SettingOutlined />, label: 'Docs Settings' });
webChildren.push({ key: '/app/code', icon: <CodeOutlined />, label: 'Code Editor' });
items.push({
key: 'web-submenu',
icon: <GlobalOutlined />,
label: 'Web',
children: webChildren,
});
}
if (settings?.enableMap !== false && can(MAP_ROLES)) {
items.push({
key: 'map-submenu',
icon: <EnvironmentOutlined />,
label: 'Map',
children: [
{ key: '/app/map', icon: <EnvironmentOutlined />, label: 'Locations' },
{ key: '/app/map/data-quality', icon: <BarChartOutlined />, label: 'Data Quality' },
{ key: '/app/map/cuts', icon: <ScissorOutlined />, label: 'Areas' },
{ key: '/app/map/canvass', icon: <TeamOutlined />, label: 'Canvassing' },
{ key: '/app/map/settings', icon: <SettingOutlined />, label: 'Settings' },
],
});
}
// Scheduling submenu — visible if relevant features are enabled AND user has SCHEDULING_ROLES
if (can(SCHEDULING_ROLES) && (settings?.enableMap !== false || settings?.enableMeetingPlanner || settings?.enableTicketedEvents || settings?.enableMeet || settings?.enableEvents)) {
const schedulingChildren: any[] = [];
if (settings?.enableMap !== false) {
schedulingChildren.push({ key: '/app/map/shifts', icon: <ScheduleOutlined />, label: 'Shifts' });
}
if (settings?.enableMeetingPlanner) {
schedulingChildren.push({ key: '/app/meeting-planner', icon: <ScheduleOutlined />, label: 'Meeting Planner' });
schedulingChildren.push({ key: '/app/meetings/agendas', icon: <FileTextOutlined />, label: 'Agendas' });
schedulingChildren.push({ key: '/app/meetings/action-items', icon: <OrderedListOutlined />, label: 'Action Items' });
}
if (settings?.enableTicketedEvents) {
schedulingChildren.push({ key: '/app/events', icon: <TagOutlined />, label: 'Events' });
}
if (settings?.enableMeet) {
schedulingChildren.push({ key: '/app/services/jitsi', icon: <VideoCameraOutlined />, label: 'Video Meet' });
}
if (settings?.enableEvents) {
schedulingChildren.push({ key: '/app/services/gancio', icon: <GlobalOutlined />, label: 'Gancio' });
}
// Always add Calendar as the last item in scheduling
schedulingChildren.push({ key: '/app/scheduling/calendar', icon: <CalendarOutlined />, label: 'Calendar' });
if (schedulingChildren.length > 0) {
items.push({
key: 'scheduling-submenu',
icon: <ScheduleOutlined />,
label: 'Scheduling',
children: schedulingChildren,
});
}
}
if (settings?.enableMediaFeatures !== false && can(MEDIA_ROLES)) {
items.push({
key: 'media-submenu',
icon: <PlaySquareOutlined />,
label: 'Media',
children: [
{ key: '/app/media/library', icon: <FolderOutlined />, label: 'Library' },
{ key: '/app/media/analytics', icon: <BarChartOutlined />, label: 'Analytics' },
{ key: '/app/media/curated', icon: <OrderedListOutlined />, label: 'Curated' },
{ key: '/app/media/moderation', icon: <MessageOutlined />, label: 'Moderation' },
{ key: '/app/media/jobs', icon: <HistoryOutlined />, label: 'Processing Jobs' },
],
});
}
if (settings?.enablePayments && can(PAYMENTS_ROLES)) {
items.push({
key: 'payments-submenu',
icon: <DollarOutlined />,
label: 'Payments',
children: [
{ key: '/app/payments', icon: <DashboardOutlined />, label: 'Dashboard' },
{ key: '/app/payments/plans', icon: <TagOutlined />, label: 'Plans' },
{ key: '/app/payments/subscribers', icon: <CrownOutlined />, label: 'Subscribers' },
{ key: '/app/payments/products', icon: <ShoppingOutlined />, label: 'Products' },
{ key: '/app/payments/donation-pages', icon: <HeartOutlined />, label: 'Donation Pages' },
{ key: '/app/payments/donations', icon: <DollarOutlined />, label: 'Donation Orders' },
{ key: '/app/payments/ads', icon: <PictureOutlined />, label: 'Gallery Ads' },
{ key: '/app/payments/ads/analytics', icon: <BarChartOutlined />, label: 'Ad Analytics' },
{ key: '/app/payments/settings', icon: <SettingOutlined />, label: 'Settings' },
],
});
}
if (isSuperAdmin) {
items.push({
key: 'services-submenu',
icon: <CloudServerOutlined />,
label: 'Services',
children: [
{ type: 'group', label: 'Infrastructure', children: [
{ key: '/app/tunnel', icon: <CloudServerOutlined />, label: 'Tunnel' },
{ key: '/app/observability', icon: <LineChartOutlined />, label: 'Monitoring' },
{ key: '/app/services/nocodb', icon: <DatabaseOutlined />, label: 'Database' },
{ key: '/app/services/vaultwarden', icon: <LockOutlined />, label: 'Vault' },
{ key: '/app/services/mailhog', icon: <MailOutlined />, label: 'MailHog' },
]},
{ type: 'group', label: 'Tools', children: [
{ key: '/app/services/n8n', icon: <ApiOutlined />, label: 'Workflows' },
{ key: '/app/services/gitea', icon: <BranchesOutlined />, label: 'Git' },
{ key: '/app/services/gitea/setup', icon: <SettingOutlined />, label: 'Gitea Setup' },
{ key: '/app/services/excalidraw', icon: <EditOutlined />, label: 'Whiteboard' },
{ key: '/app/services/miniqr', icon: <QrcodeOutlined />, label: 'QR Codes' },
]},
],
});
}
if (isSuperAdmin) {
items.push(
{
key: '/app/settings',
icon: <SettingOutlined />,
label: 'Settings',
},
);
}
return items;
}
export default function AppLayout() {
const [collapsed, setCollapsed] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [pageHeader, setPageHeader] = useState<PageHeaderConfig | null>(null);
const navigate = useNavigate();
const location = useLocation();
const { user, logout } = useAuthStore();
const { settings } = useSettingsStore();
const { token } = theme.useToken();
const screens = useBreakpoint();
const isMobile = !screens.md;
const [badgeCounts, setBadgeCounts] = useState<{ pendingResponses: number; pendingEmails: number; pendingCampaignReview: number; pendingComments: number }>({ pendingResponses: 0, pendingEmails: 0, pendingCampaignReview: 0, pendingComments: 0 });
const fetchBadges = useCallback(() => {
api.get('/dashboard/summary').then(({ data }) => {
setBadgeCounts({
pendingResponses: data?.responses?.pending ?? 0,
pendingEmails: data?.emails?.queued ?? 0,
pendingCampaignReview: data?.campaignModeration?.pendingReview ?? 0,
pendingComments: data?.docsComments?.pending ?? 0,
});
}).catch(() => {});
}, []);
useEffect(() => {
fetchBadges();
const interval = setInterval(fetchBadges, 120_000);
return () => clearInterval(interval);
}, [fetchBadges]);
const baseMenuItems = buildMenuItems(settings, user, badgeCounts);
const { favorites } = useFavoritesStore();
// Build final menu: resolve favorites, add stars, prepend favorites section
const menuItems = (() => {
const leafKeys = collectLeafKeys(baseMenuItems);
const starredItems = addStarsToMenuItems(baseMenuItems, leafKeys);
// Resolve favorites against current menu (handles feature-flag changes)
const validFavorites = resolveValidFavorites(baseMenuItems, favorites);
if (validFavorites.length === 0) return starredItems;
const favSection: NonNullable<MenuProps['items']> = [
{
type: 'group' as const,
label: 'Favorites',
children: validFavorites.map(fav => ({
key: `fav:${fav.key}`,
icon: fav.icon,
label: fav.label,
})),
},
{ type: 'divider' as const },
];
return [...favSection, ...(starredItems || [])];
})();
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
// Strip 'fav:' prefix from favorites section items
const route = key.startsWith('fav:') ? key.slice(4) : key;
navigate(route);
if (isMobile) setDrawerOpen(false);
};
const handleLogout = () => {
Modal.confirm({
title: 'Log out?',
icon: <LogoutOutlined />,
okText: 'Log out',
okType: 'danger',
onOk: async () => {
await logout();
navigate('/login', { replace: true });
},
});
};
const userMenuItems: MenuProps['items'] = [
{
key: 'tour',
icon: <QuestionCircleOutlined />,
label: 'Learning Tours',
onClick: () => {
useTourStore.getState().openHub();
},
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: 'Logout',
onClick: handleLogout,
},
];
// Match the current path to a menu key (supports submenus and item groups)
const selectedKey = (() => {
const path = location.pathname;
// Exact match first
if (path === '/app') return '/app';
// Check all items including children and group grandchildren — longest match wins
let best = '';
const checkKey = (k: string) => {
if (k.startsWith('/') && (path === k || path.startsWith(k + '/'))) {
if (k.length > best.length) best = k;
}
};
for (const item of menuItems || []) {
if (!item || !('key' in item)) continue;
if ('children' in item && item.children) {
for (const child of item.children) {
if (!child) continue;
// Handle item groups (type: 'group') — check their nested children
if ('type' in child && child.type === 'group' && 'children' in child && child.children) {
for (const grandchild of child.children) {
if (!grandchild || !('key' in grandchild)) continue;
checkKey(grandchild.key as string);
}
} else if ('key' in child) {
checkKey(child.key as string);
}
}
}
const k = item.key?.toString() || '';
if (k !== '/app') checkKey(k);
}
return best || '/app';
})();
// Also highlight the corresponding favorite item if present
const selectedKeys = favorites.includes(selectedKey)
? [selectedKey, `fav:${selectedKey}`]
: [selectedKey];
// Derive which submenus should be open based on active route
const defaultOpenKeys = (() => {
const path = location.pathname;
const keys: string[] = [];
for (const item of menuItems || []) {
if (!item || !('children' in item) || !item.children) continue;
let found = false;
for (const child of item.children) {
if (found) break;
if (!child) continue;
// Handle item groups (type: 'group') — check their nested children
if ('type' in child && child.type === 'group' && 'children' in child && child.children) {
for (const grandchild of child.children) {
if (!grandchild || !('key' in grandchild)) continue;
const k = grandchild.key as string;
if (path === k || path.startsWith(k + '/')) {
keys.push(item.key as string);
found = true;
break;
}
}
} else if ('key' in child) {
const k = child.key as string;
if (path === k || path.startsWith(k + '/')) {
keys.push(item.key as string);
found = true;
}
}
}
}
return keys;
})();
const fullBleed = pageHeader?.fullBleed === true;
const logoUrl = settings?.organizationLogoUrl;
const showLogo = logoUrl && !(collapsed && !isMobile);
const sidebarMenu = (
<>
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontWeight: 600,
fontSize: collapsed && !isMobile ? 14 : 16,
borderBottom: '1px solid rgba(255,255,255,0.1)',
padding: '0 8px',
gap: 8,
}}
>
{showLogo ? (
<img
src={logoUrl}
alt={settings?.organizationName ?? 'Logo'}
style={{ maxHeight: 36, maxWidth: collapsed && !isMobile ? 48 : 160, objectFit: 'contain' }}
/>
) : (
collapsed && !isMobile
? (settings?.organizationShortName ?? 'CML')
: (settings?.organizationName ?? 'Changemaker Lite')
)}
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={selectedKeys}
defaultOpenKeys={defaultOpenKeys}
items={menuItems}
onClick={handleMenuClick}
/>
</>
);
return (
<>
<style>{`
.favorite-star { opacity: 0; transition: opacity 0.15s; }
.favorite-star--active { opacity: 1; }
.ant-menu-item:hover .favorite-star { opacity: 1; }
`}</style>
<Layout style={{ minHeight: '100vh' }}>
{isMobile ? (
<Drawer
placement="left"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={256}
styles={{ body: { padding: 0, background: '#001529' }, header: { display: 'none' } }}
>
{sidebarMenu}
</Drawer>
) : (
<Sider
trigger={null}
collapsible
collapsed={collapsed}
data-tour="sidebar"
style={{ overflow: 'auto', height: '100vh', position: 'sticky', top: 0, left: 0 }}
>
{sidebarMenu}
</Sider>
)}
<Layout>
<Header
style={{
padding: '0 16px',
background: 'transparent',
display: 'flex',
alignItems: 'center',
gap: 2,
borderBottom: `1px solid ${token.colorBorderSecondary}`,
}}
>
<Button
type="text"
icon={
isMobile
? <MenuOutlined />
: collapsed
? <MenuUnfoldOutlined />
: <MenuFoldOutlined />
}
onClick={() => (isMobile ? setDrawerOpen(true) : setCollapsed(!collapsed))}
/>
{pageHeader?.title && (
<Text strong style={{ fontSize: 16, whiteSpace: 'nowrap' }}>
{pageHeader.title}
</Text>
)}
<div style={{ flex: 1 }} />
<Tooltip title={navigator.platform?.toLowerCase().includes('mac') ? 'Search (⌘K)' : 'Search (Ctrl+K)'}>
<Button
type="text"
icon={<SearchOutlined />}
data-tour="search-button"
onClick={() => useCommandPaletteStore.getState().open()}
/>
</Tooltip>
{pageHeader?.actions}
{(() => {
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
const withOverrides = applyAdminOverrides(merged);
const flags = buildFeatureFlags(settings);
const filtered = filterNavItems(withOverrides, flags);
const getIcon = (iconName: string) => ADMIN_ICON_OVERRIDES[iconName] ?? ICON_MAP[iconName] ?? <GlobalOutlined />;
const handleItemClick = (item: NavItem) => {
if (item.path.startsWith('$')) {
window.open(resolveNavUrl(item.path), '_blank');
} else if (item.external && item.id === 'home') {
window.open(buildHomeUrl(), '_blank');
} else if (item.external) {
window.open(item.path, '_blank');
} else {
navigate(item.path);
}
};
return filtered.map(item => {
if (item.type === 'group' && item.children) {
return (
<Dropdown
key={item.id}
menu={{
items: item.children.map(child => ({
key: child.id,
icon: getIcon(child.icon),
label: child.label,
onClick: () => handleItemClick(child),
})),
}}
placement="bottomRight"
>
<Button type="text" size="small" icon={getIcon(item.icon)}>
{!isMobile && !collapsed && item.label}
</Button>
</Dropdown>
);
}
return (
<Tooltip key={item.id} title={item.label}>
<Button
type="text"
size="small"
icon={getIcon(item.icon)}
onClick={() => handleItemClick(item)}
>
{!isMobile && !collapsed && item.label}
</Button>
</Tooltip>
);
});
})()}
{/* Volunteer Portal button — always visible for quick switching */}
<Tooltip title="Switch to Volunteer Portal">
<Button
type="text"
size="small"
icon={<TeamOutlined />}
onClick={() => navigate('/volunteer')}
>
{!isMobile && !collapsed && 'Volunteer'}
</Button>
</Tooltip>
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<Button type="text" icon={<UserOutlined />} data-tour="user-menu">
{!isMobile && !collapsed && (
<Text style={{ marginLeft: 8 }}>
{user?.name || user?.email || 'User'}
</Text>
)}
</Button>
</Dropdown>
</Header>
<Content
style={{
margin: fullBleed ? 0 : (isMobile ? 12 : 24),
padding: fullBleed ? 0 : (isMobile ? 16 : 24),
background: 'transparent',
borderRadius: fullBleed ? 0 : token.borderRadiusLG,
minHeight: 280,
overflow: fullBleed ? 'hidden' : undefined,
}}
>
<Outlet context={{ setPageHeader } satisfies AppOutletContext} />
</Content>
</Layout>
</Layout>
<AdminTour />
<TourHub />
<TourTriggerButton />
<RocketChatWidget />
</>
);
}

View File

@ -0,0 +1,242 @@
import { useState, useEffect } from 'react';
import { Modal, Form, Input, Button, Alert, Segmented, Typography } from 'antd';
import { MailOutlined, LockOutlined, UserOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { useAuthStore } from '@/stores/auth.store';
import axios from 'axios';
const { Text } = Typography;
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';
type AuthMode = 'signin' | 'register';
interface AuthModalProps {
open: boolean;
onCancel: () => void;
onSuccess: () => void;
title?: string;
subtitle?: string;
}
export default function AuthModal({ open, onCancel, onSuccess, title, subtitle }: AuthModalProps) {
const { login, register, isLoading, error, errorCode, registrationMessage, clearError } = useAuthStore();
const [mode, setMode] = useState<AuthMode>('signin');
const [loginForm] = Form.useForm();
const [registerForm] = Form.useForm();
const [resendLoading, setResendLoading] = useState(false);
const [resendSent, setResendSent] = useState(false);
// Clear errors when switching modes
useEffect(() => {
clearError();
setResendSent(false);
}, [mode]); // eslint-disable-line react-hooks/exhaustive-deps
const handleLogin = async (values: { email: string; password: string }) => {
try {
await login(values.email, values.password);
loginForm.resetFields();
onSuccess();
} catch {
// Error is set in store
}
};
const handleRegister = async (values: { name: string; email: string; password: string }) => {
try {
const result = await register(values.name, values.email, values.password);
if (result?.requiresVerification) {
// Stay open to show verification message — don't call onSuccess
registerForm.resetFields();
return;
}
registerForm.resetFields();
onSuccess();
} catch {
// Error is set in store
}
};
const handleResendVerification = async () => {
const email = loginForm.getFieldValue('email');
if (!email) return;
setResendLoading(true);
try {
await axios.post(`${API_URL}/api/auth/resend-verification`, { email });
setResendSent(true);
} catch {
// Ignore — always show success for security
setResendSent(true);
} finally {
setResendLoading(false);
}
};
const handleCancel = () => {
loginForm.resetFields();
registerForm.resetFields();
clearError();
onCancel();
};
return (
<Modal
open={open}
onCancel={handleCancel}
footer={null}
destroyOnHidden
width={420}
>
{title && (
<div style={{ textAlign: 'center', marginBottom: 4 }}>
<Text strong style={{ fontSize: 18 }}>{title}</Text>
</div>
)}
{subtitle && (
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<Text type="secondary" style={{ fontSize: 13 }}>{subtitle}</Text>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 20 }}>
<Segmented
options={[
{ label: 'Sign In', value: 'signin' },
{ label: 'Register', value: 'register' },
]}
value={mode}
onChange={(val) => setMode(val as AuthMode)}
/>
</div>
{/* Registration success — verification required */}
{registrationMessage && (
<Alert
message="Check Your Email"
description={registrationMessage}
type="success"
showIcon
icon={<CheckCircleOutlined />}
closable
onClose={() => clearError()}
style={{ marginBottom: 16 }}
/>
)}
{error && (
<Alert
message={error}
type="error"
showIcon
closable
onClose={() => clearError()}
description={
errorCode === 'EMAIL_NOT_VERIFIED' ? (
resendSent ? (
<Text type="success" style={{ fontSize: 12 }}>Verification email sent! Check your inbox.</Text>
) : (
<Button
type="link"
size="small"
loading={resendLoading}
onClick={handleResendVerification}
style={{ padding: 0 }}
>
Resend verification email
</Button>
)
) : errorCode === 'ACCOUNT_PENDING' ? (
<Text type="secondary" style={{ fontSize: 12 }}>
An admin will review your account shortly.
</Text>
) : undefined
}
style={{ marginBottom: 16 }}
/>
)}
{mode === 'signin' ? (
<Form form={loginForm} onFinish={handleLogin} layout="vertical" size="large">
<Form.Item
name="email"
rules={[
{ required: true, message: 'Please enter your email' },
{ type: 'email', message: 'Please enter a valid email' },
]}
>
<Input prefix={<MailOutlined />} placeholder="Email" autoFocus />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please enter your password' }]}
>
<Input.Password prefix={<LockOutlined />} placeholder="Password" />
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button type="primary" htmlType="submit" loading={isLoading} block>
Sign In
</Button>
</Form.Item>
</Form>
) : (
<Form form={registerForm} onFinish={handleRegister} layout="vertical" size="large">
<Form.Item
name="name"
rules={[{ required: true, message: 'Please enter your name' }]}
>
<Input prefix={<UserOutlined />} placeholder="Full Name" autoFocus />
</Form.Item>
<Form.Item
name="email"
rules={[
{ required: true, message: 'Please enter your email' },
{ type: 'email', message: 'Please enter a valid email' },
]}
>
<Input prefix={<MailOutlined />} placeholder="Email" />
</Form.Item>
<Form.Item
name="password"
rules={[
{ required: true, message: 'Please enter a password' },
{ min: 12, message: 'Password must be at least 12 characters' },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
message: 'Must contain uppercase, lowercase, and a digit',
},
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="Password (12+ chars)" />
</Form.Item>
<Form.Item
name="confirmPassword"
dependencies={['password']}
rules={[
{ required: true, message: 'Please confirm your password' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('Passwords do not match'));
},
}),
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="Confirm Password" />
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button type="primary" htmlType="submit" loading={isLoading} block>
Create Account
</Button>
</Form.Item>
</Form>
)}
</Modal>
);
}

View File

@ -0,0 +1,113 @@
import { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
isChunkError: boolean;
}
/**
* Global error boundary that catches React rendering crashes.
* Detects stale chunk errors (after redeployment) and offers auto-reload.
*/
export default class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, isChunkError: false };
static getDerivedStateFromError(error: Error): State {
const isChunkError = isChunkLoadError(error);
return { hasError: true, isChunkError };
}
componentDidCatch(error: Error) {
// If it's a stale chunk error, auto-reload once (avoid infinite loop via sessionStorage flag)
if (isChunkLoadError(error)) {
const reloadKey = 'cm_chunk_reload';
const lastReload = sessionStorage.getItem(reloadKey);
const now = Date.now();
// Only auto-reload if we haven't done so in the last 10 seconds
if (!lastReload || now - parseInt(lastReload, 10) > 10000) {
sessionStorage.setItem(reloadKey, String(now));
window.location.reload();
return;
}
}
}
handleReload = () => {
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
background: '#1a1025',
color: '#e0e0e0',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
textAlign: 'center',
padding: '0 24px',
}}
>
<div style={{ fontSize: 48, marginBottom: 16 }}>
{this.state.isChunkError ? '\u21BB' : '\u26A0'}
</div>
<h2 style={{ margin: '0 0 8px', fontSize: 20, fontWeight: 600 }}>
{this.state.isChunkError
? 'Application Updated'
: 'Something went wrong'}
</h2>
<p style={{ margin: '0 0 24px', color: '#999', maxWidth: 400, lineHeight: 1.5 }}>
{this.state.isChunkError
? 'A new version has been deployed. Please refresh to load the latest version.'
: 'An unexpected error occurred. A page refresh usually fixes this.'}
</p>
<button
onClick={this.handleReload}
style={{
padding: '10px 24px',
fontSize: 14,
fontWeight: 500,
border: 'none',
borderRadius: 6,
background: '#9d4edd',
color: '#fff',
cursor: 'pointer',
transition: 'opacity 0.2s',
}}
onMouseOver={(e) => (e.currentTarget.style.opacity = '0.85')}
onMouseOut={(e) => (e.currentTarget.style.opacity = '1')}
>
Refresh Page
</button>
</div>
);
}
return this.props.children;
}
}
/** Detect chunk/module load errors (stale deployments) */
function isChunkLoadError(error: Error): boolean {
const msg = error.message || '';
return (
msg.includes('Failed to fetch dynamically imported module') ||
msg.includes('Loading chunk') ||
msg.includes('Loading CSS chunk') ||
msg.includes('error loading dynamically imported module') ||
msg.includes('Importing a module script failed') ||
// Vite-specific: chunk not found
msg.includes('is not a valid JavaScript MIME type') ||
// Generic syntax errors from loading wrong content (e.g., HTML 404 page as JS)
(error.name === 'SyntaxError' && msg.includes('Unexpected token'))
);
}

View File

@ -0,0 +1,66 @@
import type { ReactNode } from 'react';
import { Result, Button, Skeleton } from 'antd';
import { useNavigate } from 'react-router-dom';
import { SettingOutlined } from '@ant-design/icons';
import { useSettingsStore } from '@/stores/settings.store';
import { useAuthStore } from '@/stores/auth.store';
import { hasAnyRole } from '@/utils/roles';
import type { SiteSettings } from '@/types/api';
const FEATURE_LABELS: Record<string, string> = {
enableInfluence: 'Influence (Advocacy Campaigns)',
enableMap: 'Map & Canvassing',
enableLandingPages: 'Landing Pages',
enableNewsletter: 'Newsletter',
enableMediaFeatures: 'Media Library',
enablePayments: 'Payments',
enableGalleryAds: 'Gallery Ads',
enablePeople: 'People CRM',
enableEvents: 'Events',
enableSocial: 'Social Connections',
enableMeet: 'Video Meetings',
enableMeetingPlanner: 'Meeting Planner',
enableTicketedEvents: 'Ticketed Events',
enableSocialCalendar: 'Social Calendar',
};
interface FeatureGateProps {
feature: keyof Pick<SiteSettings, 'enableInfluence' | 'enableMap' | 'enableLandingPages' | 'enableNewsletter' | 'enableMediaFeatures' | 'enablePayments' | 'enableGalleryAds' | 'enablePeople' | 'enableEvents' | 'enableSocial' | 'enableMeet' | 'enableMeetingPlanner' | 'enableTicketedEvents' | 'enableSocialCalendar'>;
children: ReactNode;
}
export default function FeatureGate({ feature, children }: FeatureGateProps) {
const { settings, loading } = useSettingsStore();
const { user } = useAuthStore();
const navigate = useNavigate();
const isSuperAdmin = hasAnyRole(user, ['SUPER_ADMIN']);
const featureName = FEATURE_LABELS[feature] || feature;
// Show skeleton while settings are loading to prevent briefly showing disabled features
if (loading || !settings) return <Skeleton active style={{ padding: 24 }} />;
if (settings[feature] === false) {
return (
<Result
status="info"
title="Feature Not Enabled"
subTitle={isSuperAdmin
? `${featureName} is currently disabled. You can enable it in Settings.`
: `${featureName} has not been enabled for this site.`
}
style={{ paddingTop: 80 }}
extra={isSuperAdmin && (
<Button
type="primary"
icon={<SettingOutlined />}
onClick={() => navigate('/app/settings', { state: { activeTab: 'features' } })}
>
Go to Settings
</Button>
)}
/>
);
}
return <>{children}</>;
}

View File

@ -0,0 +1,579 @@
import { useEffect, useRef, useImperativeHandle, forwardRef, useState } from 'react';
import grapesjs, { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
import blocksBasicPlugin from 'grapesjs-blocks-basic';
import presetWebpagePlugin from 'grapesjs-preset-webpage';
import formsPlugin from 'grapesjs-plugin-forms';
import navbarPlugin from 'grapesjs-navbar';
import countdownPlugin from 'grapesjs-component-countdown';
import tabsPlugin from 'grapesjs-tabs';
import typedPlugin from 'grapesjs-typed';
import customCodePlugin from 'grapesjs-custom-code';
import exportPlugin from 'grapesjs-plugin-export';
import styleGradientPlugin from 'grapesjs-style-gradient';
import touchPlugin from 'grapesjs-touch';
import type { PageBlock } from '@/types/api';
import { generateVideoCardHtml } from '@/utils/videoCardHtml';
import { generatePhotoCardHtml } from '@/utils/photoCardHtml';
interface GrapesJSEditorProps {
initialData?: Record<string, unknown>;
onSave: (data: { projectData: Record<string, unknown>; html: string; css: string }) => void;
customBlocks?: PageBlock[];
}
export interface GrapesJSEditorHandle {
triggerSave: () => void;
}
const GrapesJSEditor = forwardRef<GrapesJSEditorHandle, GrapesJSEditorProps>(
function GrapesJSEditor({ initialData, onSave, customBlocks }, ref) {
const editorRef = useRef<Editor | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const onSaveRef = useRef(onSave);
const [error, setError] = useState<string | null>(null);
onSaveRef.current = onSave;
useImperativeHandle(ref, () => ({
triggerSave() {
editorRef.current?.runCommand('save-page');
},
}));
useEffect(() => {
if (!containerRef.current) return;
let editor: Editor;
try {
editor = grapesjs.init({
container: containerRef.current,
height: '100%',
width: 'auto',
storageManager: false,
plugins: [
blocksBasicPlugin,
presetWebpagePlugin,
formsPlugin,
navbarPlugin,
countdownPlugin,
tabsPlugin,
typedPlugin,
customCodePlugin,
exportPlugin,
styleGradientPlugin,
touchPlugin,
],
pluginsOpts: {
[blocksBasicPlugin as unknown as string]: {
flexGrid: true,
},
[presetWebpagePlugin as unknown as string]: {},
[formsPlugin as unknown as string]: {},
[navbarPlugin as unknown as string]: {},
[countdownPlugin as unknown as string]: {},
[tabsPlugin as unknown as string]: {},
[typedPlugin as unknown as string]: {},
[customCodePlugin as unknown as string]: {},
[exportPlugin as unknown as string]: {},
[styleGradientPlugin as unknown as string]: {},
[touchPlugin as unknown as string]: {},
},
canvas: {
styles: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
],
},
});
} catch (err) {
console.error('GrapesJS init error:', err);
setError('Failed to initialize the page editor. Please refresh the page.');
return;
}
// Register custom blocks from PageBlock library
if (customBlocks && customBlocks.length > 0) {
const blockManager = editor.Blocks;
for (const block of customBlocks) {
const defaults = block.defaults as Record<string, unknown>;
const html = generateBlockHtml(block.type, defaults);
blockManager.add(`custom-${block.type}`, {
label: block.label,
category: block.category || 'Campaign',
content: html,
});
}
}
// Register ad blocks
{
const bm = editor.Blocks;
bm.add('ad-specific', {
label: 'Specific Ad',
category: 'Ads',
content: generateBlockHtml('ad-specific', { adId: 0 }),
});
bm.add('ad-slot', {
label: 'Ad Slot (Dynamic)',
category: 'Ads',
content: generateBlockHtml('ad-slot', { variant: 'standard' }),
});
}
// Register save command
editor.Commands.add('save-page', {
run(ed: Editor) {
const projectData = ed.getProjectData() as Record<string, unknown>;
const html = ed.getHtml();
const css = ed.getCss() || '';
onSaveRef.current({ projectData, html, css });
},
});
// Keyboard shortcut: Ctrl+S / Cmd+S
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
editor.runCommand('save-page');
}
};
document.addEventListener('keydown', handleKeyDown);
// Load initial project data
if (initialData && Object.keys(initialData).length > 0) {
editor.loadProjectData(initialData);
}
editorRef.current = editor;
return () => {
document.removeEventListener('keydown', handleKeyDown);
editor.destroy();
editorRef.current = null;
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (error) {
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ff4d4f' }}>
{error}
</div>
);
}
return (
<div
ref={containerRef}
role="application"
aria-label="Page builder editor"
style={{ flex: 1, overflow: 'hidden' }}
/>
);
});
function generateBlockHtml(type: string, defaults: Record<string, unknown>): string {
switch (type) {
case 'hero':
return `
<section style="padding: 80px 40px; text-align: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff;">
<h1 style="font-size: 2.5rem; margin-bottom: 16px;">${defaults.title || 'Hero Title'}</h1>
<p style="font-size: 1.25rem; opacity: 0.85; margin-bottom: 32px;">${defaults.subtitle || 'Subtitle text here'}</p>
<a href="${defaults.ctaUrl || '#'}" style="display: inline-block; padding: 12px 32px; background: #9d4edd; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600;">${defaults.ctaText || 'Get Started'}</a>
</section>`;
case 'text':
return `
<section style="padding: 60px 40px; max-width: 800px; margin: 0 auto;">
<h2 style="font-size: 1.75rem; margin-bottom: 16px;">${defaults.heading || 'Heading'}</h2>
<p style="font-size: 1rem; line-height: 1.7; opacity: 0.85;">${defaults.body || 'Body text goes here.'}</p>
</section>`;
case 'features': {
const features = (defaults.features as Array<{ title: string; description: string }>) || [];
const featureHtml = features.map(f => `
<div style="flex: 1; min-width: 250px; padding: 24px; text-align: center;">
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">${f.title}</h3>
<p style="opacity: 0.8;">${f.description}</p>
</div>`).join('');
return `
<section style="padding: 60px 40px;">
<div style="display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
${featureHtml}
</div>
</section>`;
}
case 'cta':
return `
<section style="padding: 60px 40px; text-align: center; background: linear-gradient(135deg, #9d4edd 0%, #7b2cbf 100%); color: #fff;">
<h2 style="font-size: 2rem; margin-bottom: 12px;">${defaults.heading || 'Call to Action'}</h2>
<p style="font-size: 1.1rem; margin-bottom: 24px; opacity: 0.9;">${defaults.description || 'Description here'}</p>
<a href="${defaults.buttonUrl || '#'}" style="display: inline-block; padding: 12px 32px; background: #fff; color: #9d4edd; text-decoration: none; border-radius: 6px; font-weight: 600;">${defaults.buttonText || 'Click Here'}</a>
</section>`;
case 'testimonials': {
const quotes = (defaults.quotes as Array<{ text: string; author: string; role: string }>) || [];
const quoteHtml = quotes.map(q => `
<div style="flex: 1; min-width: 280px; padding: 24px; background: rgba(255,255,255,0.05); border-radius: 8px;">
<p style="font-style: italic; margin-bottom: 12px;">"${q.text}"</p>
<p style="font-weight: 600; margin-bottom: 2px;">${q.author}</p>
<p style="font-size: 0.85rem; opacity: 0.7;">${q.role}</p>
</div>`).join('');
return `
<section style="padding: 60px 40px;">
<div style="display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
${quoteHtml}
</div>
</section>`;
}
case 'contact-form':
return `
<section style="padding: 60px 40px; max-width: 600px; margin: 0 auto;">
<h2 style="text-align: center; margin-bottom: 24px;">${defaults.heading || 'Contact Us'}</h2>
<form style="display: flex; flex-direction: column; gap: 16px;">
<input type="text" placeholder="Name" style="padding: 10px 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.05); color: inherit;" />
<input type="email" placeholder="Email" style="padding: 10px 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.05); color: inherit;" />
<textarea placeholder="Message" rows="4" style="padding: 10px 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.05); color: inherit; resize: vertical;"></textarea>
<button type="submit" style="padding: 12px 24px; background: #9d4edd; color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer;">Send Message</button>
</form>
</section>`;
case 'video': {
const videoId = defaults.videoId || 'PLACEHOLDER';
const playerType = defaults.playerType || 'standard';
const width = defaults.width || '100%';
const height = defaults.height || 'auto';
// Generate placeholder with data attributes for hydration
return `
<section style="padding: 60px 40px;">
<div class="video-block"
data-video-id="${videoId}"
data-player-type="${playerType}"
data-width="${width}"
data-height="${height}"
data-autoplay="${defaults.autoplay || false}"
data-controls="${defaults.controls !== false}"
data-show-reactions="${defaults.showReactions !== false}"
style="max-width: ${width}; margin: 0 auto;">
<div class="video-placeholder" style="aspect-ratio: 16/9; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<div style="text-align: center; color: #fff; padding: 24px;">
<svg style="width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.9;" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" />
</svg>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">Video Player</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.8;">ID: ${videoId}</p>
<p style="margin: 4px 0 0; font-size: 0.85rem; opacity: 0.7;">${playerType === 'advanced' ? 'Advanced Player (with reactions)' : 'Standard HTML5 Player'}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Video will render on published page</p>
</div>
</div>
</div>
</section>`;
}
case 'video-card': {
const videoId = defaults.videoId;
const title = (defaults.title as string) || 'Video Title';
const durationSeconds = (defaults.durationSeconds as number) || 0;
const quality = (defaults.quality as string) || '';
const viewCount = (defaults.viewCount as number) || 0;
const mediaApiUrl = 'http://localhost:4100';
if (!videoId || videoId === 'PLACEHOLDER') {
return `
<section style="padding: 40px 20px;">
<div class="video-card-block" style="max-width: 480px; margin: 0 auto; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="padding-bottom: 56.25%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); position: relative;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #fff;">
<svg style="width: 48px; height: 48px; margin-bottom: 8px; opacity: 0.9;" fill="currentColor" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"/></svg>
<p style="margin: 0; font-size: 14px; font-weight: 600;">Video Card</p>
<p style="margin: 4px 0 0; font-size: 12px; opacity: 0.7;">Select a video to display</p>
</div>
</div>
<div style="padding: 12px 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600;">Video Title</div>
<div style="color: #8899aa; font-size: 13px; margin-top: 6px;">Card will render on published page</div>
</div>
</div>
</section>`;
}
const cardHtml = generateVideoCardHtml({
id: videoId as number,
title,
durationSeconds,
quality,
viewCount,
thumbnailUrl: `${mediaApiUrl}/api/videos/${videoId}/thumbnail`,
});
return `<section style="padding: 40px 20px;">${cardHtml}</section>`;
}
case 'donate-button': {
const heading = (defaults.heading as string) || 'Support Our Cause';
const description = (defaults.description as string) || 'Your contribution helps us create lasting change in our community.';
const buttonText = (defaults.buttonText as string) || 'Donate Now';
const showAmounts = defaults.showAmounts !== false;
return `
<section class="payment-block" data-payment-type="donate" data-button-text="${buttonText}" data-show-amounts="${showAmounts}" style="padding: 60px 40px; text-align: center; background: linear-gradient(135deg, #2d1b69 0%, #1a1a2e 100%); color: #fff;">
<div style="max-width: 600px; margin: 0 auto;">
<div style="font-size: 48px; margin-bottom: 16px;">&#x2764;</div>
<h2 style="font-size: 2rem; margin-bottom: 12px;">${heading}</h2>
<p style="font-size: 1.1rem; margin-bottom: 24px; opacity: 0.85;">${description}</p>
<a href="/donate" style="display: inline-block; padding: 14px 36px; background: #eb2f96; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 1.1rem;">${buttonText}</a>
</div>
</section>`;
}
case 'pricing-table': {
const heading = (defaults.heading as string) || 'Choose Your Plan';
const description = (defaults.description as string) || 'Get access to exclusive content and features.';
const showYearly = defaults.showYearly !== false;
return `
<section class="payment-block" data-payment-type="pricing" data-show-yearly="${showYearly}" style="padding: 60px 40px; text-align: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff;">
<div style="max-width: 900px; margin: 0 auto;">
<div style="font-size: 48px; margin-bottom: 16px;">&#x1F451;</div>
<h2 style="font-size: 2rem; margin-bottom: 12px;">${heading}</h2>
<p style="font-size: 1.1rem; margin-bottom: 32px; opacity: 0.85;">${description}</p>
<div style="display: flex; gap: 24px; justify-content: center; flex-wrap: wrap;">
<div style="flex: 1; min-width: 220px; max-width: 280px; padding: 24px; background: rgba(255,255,255,0.08); border-radius: 12px;">
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">Free</h3>
<p style="font-size: 2rem; font-weight: 700; margin: 8px 0;">$0</p>
<p style="opacity: 0.7; font-size: 0.9rem;">Access public content</p>
</div>
<div style="flex: 1; min-width: 220px; max-width: 280px; padding: 24px; background: rgba(114,46,209,0.2); border: 2px solid #722ed1; border-radius: 12px;">
<h3 style="font-size: 1.25rem; margin-bottom: 8px;">Premium</h3>
<p style="font-size: 2rem; font-weight: 700; margin: 8px 0;">$XX/mo</p>
<p style="opacity: 0.7; font-size: 0.9rem;">Plans load from API</p>
</div>
</div>
<p style="margin-top: 16px; font-size: 0.8rem; opacity: 0.5; font-style: italic;">Plans will load dynamically on published page</p>
</div>
</section>`;
}
case 'product-card': {
const productSlug = (defaults.productSlug as string) || '';
const buttonText = (defaults.buttonText as string) || 'Buy Now';
return `
<section class="payment-block" data-payment-type="product" data-product-slug="${productSlug}" data-button-text="${buttonText}" style="padding: 40px 20px;">
<div style="max-width: 380px; margin: 0 auto; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="padding-bottom: 56.25%; background: linear-gradient(135deg, #9d4edd 0%, #722ed1 100%); position: relative;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #fff;">
<div style="font-size: 48px; margin-bottom: 8px;">&#x1F6D2;</div>
<p style="margin: 0; font-size: 14px; font-weight: 600;">Product Card</p>
<p style="margin: 4px 0 0; font-size: 12px; opacity: 0.7;">${productSlug || 'Set product slug'}</p>
</div>
</div>
<div style="padding: 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600;">${productSlug || 'Product Name'}</div>
<div style="color: #8899aa; font-size: 13px; margin-top: 6px;">Product details load on published page</div>
<a href="/shop" style="display: inline-block; margin-top: 12px; padding: 8px 20px; background: #9d4edd; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 0.9rem;">${buttonText}</a>
</div>
</div>
</section>`;
}
case 'campaign-form': {
const campaignSlug = (defaults.campaignSlug as string) || '';
const compact = defaults.compact === true;
return `
<section class="campaign-form-block"
data-campaign-slug="${campaignSlug}"
data-compact="${compact}"
style="padding: 60px 40px;">
<div style="max-width: 600px; margin: 0 auto; border-radius: 12px; overflow: hidden; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="padding: 32px; text-align: center; color: #fff;">
<svg style="width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.9;" fill="currentColor" viewBox="0 0 1024 1024">
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5zM833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 356.9 277.7c11.7 9.1 28.4 9.1 40.1 0L889.7 270.8l27.6-21.5-39.3-50.5-44.4 33.2z"/>
</svg>
<p style="margin: 0; font-size: 1.2rem; font-weight: 600;">Campaign Email Form</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.8;">${campaignSlug || 'Set campaign slug in block properties'}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Interactive form will render on published page</p>
</div>
</div>
</section>`;
}
case 'gancio-events': {
const maxlength = defaults.maxlength || 10;
const evTheme = (defaults.theme as string) || 'dark';
const tags = (defaults.tags as string) || '';
const title = (defaults.title as string) || 'Upcoming Events';
return `
<section style="padding: 60px 40px;">
<div class="gancio-events-block"
data-maxlength="${maxlength}"
data-theme="${evTheme}"
data-tags="${tags}"
data-title="${title}"
style="max-width: 800px; margin: 0 auto;">
<div style="aspect-ratio: 16/9; background: linear-gradient(135deg, #1a472a 0%, #2d5a27 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<div style="text-align: center; color: #fff; padding: 24px;">
<svg style="width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.9;" fill="currentColor" viewBox="0 0 1024 1024">
<path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32z"/>
</svg>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">${title}</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.8;">Theme: ${evTheme} | Max: ${maxlength} events</p>
${tags ? `<p style="margin: 4px 0 0; font-size: 0.85rem; opacity: 0.7;">Tags: ${tags}</p>` : ''}
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Events will render on published page</p>
</div>
</div>
</div>
</section>`;
}
case 'photo': {
const photoId = defaults.photoId || 'PLACEHOLDER';
const size = defaults.size || 'large';
const caption = (defaults.caption as string) || '';
const linkToGallery = defaults.linkToGallery !== false;
const alignment = (defaults.alignment as string) || 'center';
const maxWidth = (defaults.maxWidth as string) || '100%';
return `
<section style="padding: 60px 40px;">
<div class="photo-block"
data-photo-id="${photoId}"
data-size="${size}"
data-caption="${caption}"
data-link-to-gallery="${linkToGallery}"
data-alignment="${alignment}"
style="max-width: ${maxWidth}; margin: 0 ${{ left: 'auto 0 0', center: 'auto', right: '0 0 auto' }[alignment as string] || 'auto'}; text-align: ${alignment};">
<div class="photo-placeholder" style="aspect-ratio: 3/2; background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<div style="text-align: center; color: #fff; padding: 24px;">
<svg style="width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.9;" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="m21 15-5-5L5 21"/>
</svg>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">Photo</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.8;">ID: ${photoId}</p>
<p style="margin: 4px 0 0; font-size: 0.85rem; opacity: 0.7;">Size: ${size}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Photo will render on published page</p>
</div>
</div>
</div>
</section>`;
}
case 'photo-card': {
const photoId = defaults.photoId;
const title = (defaults.title as string) || 'Photo Title';
const description = (defaults.description as string) || '';
const showMetadata = defaults.showMetadata !== false;
const mediaApiUrl = 'http://localhost:4100';
if (!photoId || photoId === 'PLACEHOLDER') {
return `
<section style="padding: 40px 20px;">
<div class="photo-card-block" style="max-width: 480px; margin: 0 auto; border-radius: 12px; overflow: hidden; background: #1b2838; box-shadow: 0 4px 12px rgba(0,0,0,0.3);">
<div style="padding-bottom: 66.67%; background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%); position: relative;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #fff;">
<svg style="width: 48px; height: 48px; margin-bottom: 8px; opacity: 0.9;" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
<p style="margin: 0; font-size: 14px; font-weight: 600;">Photo Card</p>
<p style="margin: 4px 0 0; font-size: 12px; opacity: 0.7;">Select a photo to display</p>
</div>
</div>
<div style="padding: 12px 16px;">
<div style="color: #fff; font-size: 15px; font-weight: 600;">Photo Title</div>
<div style="color: #8899aa; font-size: 13px; margin-top: 6px;">Card will render on published page</div>
</div>
</div>
</section>`;
}
const cardHtml = generatePhotoCardHtml({
id: photoId as number,
title,
description,
showMetadata,
viewCount: 0,
thumbnailUrl: `${mediaApiUrl}/api/public/photos/${photoId}/thumbnail`,
});
return `<section style="padding: 40px 20px;">${cardHtml}</section>`;
}
case 'photo-album': {
const albumId = defaults.albumId || 'PLACEHOLDER';
const columns = defaults.columns || '3';
const maxPhotos = defaults.maxPhotos || 12;
const showTitle = defaults.showTitle !== false;
return `
<section style="padding: 60px 40px;">
<div class="photo-album-block"
data-album-id="${albumId}"
data-columns="${columns}"
data-max-photos="${maxPhotos}"
data-show-title="${showTitle}"
style="max-width: 900px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%); border-radius: 12px; padding: 32px; position: relative; overflow: hidden;">
<div style="text-align: center; color: #fff; margin-bottom: 24px;">
<svg style="width: 48px; height: 48px; margin-bottom: 12px; opacity: 0.9;" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<rect x="2" y="2" width="20" height="20" rx="2"/>
<rect x="6" y="6" width="12" height="12" rx="1" opacity="0.6"/>
<rect x="9" y="9" width="6" height="6" rx="0.5" opacity="0.4"/>
</svg>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">Photo Album</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.8;">Album ID: ${albumId} | ${columns} columns | Max ${maxPhotos} photos</p>
</div>
<div style="display: grid; grid-template-columns: repeat(${columns}, 1fr); gap: 8px;">
${Array.from({ length: Math.min(Number(columns) * 2, 6) }, () => `
<div style="aspect-ratio: 1; background: rgba(255,255,255,0.15); border-radius: 6px; display: flex; align-items: center; justify-content: center;">
<svg style="width: 24px; height: 24px; opacity: 0.5;" fill="none" stroke="#fff" stroke-width="1.5" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</div>`).join('')}
</div>
<p style="text-align: center; margin: 16px 0 0; font-size: 0.75rem; opacity: 0.6; color: #fff; font-style: italic;">Album will render on published page</p>
</div>
</div>
</section>`;
}
case 'ad-specific': {
const adId = defaults.adId || 0;
return `
<section style="padding: 40px 20px;">
<div class="ad-specific-block"
data-ad-id="${adId}"
style="max-width: 400px; margin: 0 auto;">
<div style="border-radius: 12px; overflow: hidden; background: linear-gradient(135deg, #e65100 0%, #bf360c 100%); padding: 32px; text-align: center; color: #fff;">
<div style="font-size: 36px; margin-bottom: 12px;">&#x1F4E2;</div>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">Specific Ad</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.85;">ID: ${adId || 'Not set'}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Ad will render on published page</p>
</div>
</div>
</section>`;
}
case 'ad-slot': {
const variant = (defaults.variant as string) || 'standard';
return `
<section style="padding: 40px 20px;">
<div class="ad-slot-block"
data-placement="landing_page"
data-variant="${variant}"
style="max-width: 400px; margin: 0 auto;">
<div style="border-radius: 12px; overflow: hidden; background: linear-gradient(135deg, #2e7d32 0%, #1b5e20 100%); padding: 32px; text-align: center; color: #fff;">
<div style="font-size: 36px; margin-bottom: 12px;">&#x1F500;</div>
<p style="margin: 0; font-size: 1.1rem; font-weight: 600;">Ad Slot (Dynamic)</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.85;">Variant: ${variant}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Rotating ad will render on published page</p>
</div>
</div>
</section>`;
}
case 'scheduling-poll': {
const pollSlug = (defaults.pollSlug as string) || '';
const showComments = defaults.showComments !== false;
const title = (defaults.title as string) || 'Vote on a Meeting Time';
return `
<section style="padding: 60px 40px;">
<div class="scheduling-poll-block"
data-poll-slug="${pollSlug}"
data-show-comments="${showComments}"
data-title="${title}"
style="max-width: 700px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #fa8c16 0%, #d46b08 100%); border-radius: 12px; padding: 32px; text-align: center; color: #fff;">
<svg style="width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.9;" fill="currentColor" viewBox="0 0 1024 1024">
<path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32z"/>
</svg>
<p style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</p>
<p style="margin: 8px 0 0; font-size: 0.9rem; opacity: 0.85;">${pollSlug || 'Set poll slug in block properties'}</p>
<p style="margin: 12px 0 0; font-size: 0.75rem; opacity: 0.6; font-style: italic;">Poll will render on published page</p>
</div>
</div>
</section>`;
}
default:
return `<section style="padding: 40px; text-align: center;"><p>Custom block: ${type}</p></section>`;
}
}
export default GrapesJSEditor;

View File

@ -0,0 +1,125 @@
import { useEffect, useState } from 'react';
import { ConfigProvider, Layout, theme, Grid } from 'antd';
import { Outlet } from 'react-router-dom';
import MediaSidebar from '@/components/media/MediaSidebar';
import MediaBottomNav from '@/components/media/MediaBottomNav';
import ChatNotificationToast from '@/components/media/ChatNotificationToast';
import { ChatBarProvider } from '@/components/media/chatbar/ChatBarContext';
import ChatBar from '@/components/media/chatbar/ChatBar';
import { useChatNotifications } from '@/hooks/useChatNotifications';
import { useSettingsStore } from '@/stores/settings.store';
import { hexToRgba } from '@/utils/color';
import PublicNavBar from '@/components/PublicNavBar';
const { useBreakpoint } = Grid;
export default function MediaPublicLayout() {
const { settings } = useSettingsStore();
const { notifications, clearNotification } = useChatNotifications();
// Read colors from site settings (same source as PublicLayout)
const colorPrimary = settings?.publicColorPrimary ?? '#3498db';
const colorBgBase = settings?.publicColorBgBase ?? '#0d1b2a';
const colorBgContainer = settings?.publicColorBgContainer ?? '#1b2838';
const orgName = settings?.organizationName ?? 'Changemaker Lite';
const screens = useBreakpoint();
const isMobile = !screens.md; // < 768px
// Get sidebar collapse state from localStorage
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
const saved = localStorage.getItem('media_sidebar_collapsed');
return saved ? JSON.parse(saved) : false;
});
// Listen for sidebar collapse state changes
useEffect(() => {
const handleStorage = () => {
const saved = localStorage.getItem('media_sidebar_collapsed');
if (saved) {
setSidebarCollapsed(JSON.parse(saved));
}
};
window.addEventListener('storage', handleStorage);
// Also poll localStorage every 100ms to catch same-window changes
const interval = setInterval(handleStorage, 100);
return () => {
window.removeEventListener('storage', handleStorage);
clearInterval(interval);
};
}, []);
// Set document title for media pages
useEffect(() => {
document.title = `Media Gallery | ${orgName}`;
}, [orgName]);
// Calculate main content left margin based on sidebar state and screen size
const mainContentMarginLeft = isMobile ? 0 : sidebarCollapsed ? 64 : 256;
return (
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorPrimary,
colorBgBase,
colorBgContainer,
colorBgElevated: colorBgContainer,
colorBorder: hexToRgba(colorPrimary, 0.2),
colorBorderSecondary: 'rgba(255,255,255,0.06)',
borderRadius: 12,
colorLink: colorPrimary,
colorText: 'rgba(255, 255, 255, 0.85)',
colorTextSecondary: 'rgba(255, 255, 255, 0.65)',
colorTextTertiary: 'rgba(255, 255, 255, 0.45)',
},
}}
>
<ChatBarProvider>
<Layout style={{ minHeight: '100vh', background: colorBgBase }}>
<PublicNavBar activePath="/gallery" />
{/* Desktop: Show sidebar, Mobile: Hide */}
{!isMobile && <MediaSidebar />}
{/* Main content area */}
<main
style={{
marginLeft: mainContentMarginLeft,
minHeight: 'calc(100vh - 56px)',
overflowY: 'auto',
paddingBottom: 'calc(48px + env(safe-area-inset-bottom, 0px))', // Space for bottom search bar + iOS safe area
transition: 'margin-left 0.3s ease',
background: colorBgBase,
}}
>
<div
style={{
width: '100%',
margin: '0 auto',
padding: isMobile ? '8px 12px' : '12px 12px',
}}
>
<Outlet />
</div>
</main>
{/* Mobile: Show bottom nav, Desktop: Hide */}
<MediaBottomNav />
{/* Chat reply notifications */}
<ChatNotificationToast
notifications={notifications}
clearNotification={clearNotification}
/>
{/* Messenger-style chat bar */}
<ChatBar />
</Layout>
</ChatBarProvider>
</ConfigProvider>
);
}

View File

@ -0,0 +1,48 @@
import { Navigate } from 'react-router-dom';
import { Spin, Result } from 'antd';
import { useAuthStore } from '@/stores/auth.store';
import { hasAnyRole } from '@/utils/roles';
import type { UserRole } from '@/types/api';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRoles?: UserRole[];
}
export default function ProtectedRoute({
children,
requiredRoles,
}: ProtectedRouteProps) {
const { isAuthenticated, isLoading, user } = useAuthStore();
if (isLoading) {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
}}
>
<Spin size="large" />
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (requiredRoles && user && !hasAnyRole(user, requiredRoles)) {
return (
<Result
status="403"
title="403"
subTitle="You do not have permission to access this page."
/>
);
}
return <>{children}</>;
}

View File

@ -0,0 +1,130 @@
import { useState, useEffect, useMemo } from 'react';
import { ConfigProvider, Layout, theme } from 'antd';
import { Outlet, Link, useNavigate } from 'react-router-dom';
import { useSettingsStore } from '@/stores/settings.store';
import AuthModal from '@/components/AuthModal';
import PublicNavBar from '@/components/PublicNavBar';
import NewsletterSignup from '@/components/public/NewsletterSignup';
import {
DEFAULT_NAV_ITEMS,
mergeNavDefaults,
filterNavItems,
flattenNavItems,
buildFeatureFlags,
} from '@/lib/nav-defaults';
const { Content, Footer } = Layout;
export default function PublicLayout() {
const { settings } = useSettingsStore();
const navigate = useNavigate();
const [authModalOpen, setAuthModalOpen] = useState(false);
const [authModalContext, setAuthModalContext] = useState<'generic' | 'campaign'>('generic');
const colorPrimary = settings?.publicColorPrimary ?? '#3498db';
const colorBgBase = settings?.publicColorBgBase ?? '#0d1b2a';
const colorBgContainer = settings?.publicColorBgContainer ?? '#1b2838';
const footerText = settings?.footerText ?? 'Powered by Changemaker Lite';
// Build footer links from navConfig (or defaults) — flatten groups for flat footer
const footerLinks = useMemo(() => {
const featureFlags = buildFeatureFlags(settings);
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
const filtered = filterNavItems(merged, featureFlags);
const flat = flattenNavItems(filtered);
return flat
.filter(item => item.id !== 'home')
.map(item => ({ label: item.label, path: item.path, external: item.external }));
}, [settings]);
// Dynamic document title + favicon for public pages
useEffect(() => {
if (!settings) return;
document.title = settings.organizationName || 'Changemaker Lite';
if (settings.organizationFaviconUrl) {
let link = document.querySelector<HTMLLinkElement>("link[rel~='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = settings.organizationFaviconUrl;
}
}, [settings]);
return (
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorPrimary,
colorBgBase,
colorBgContainer,
colorBgElevated: colorBgContainer,
colorBorder: 'rgba(255,255,255,0.1)',
colorBorderSecondary: 'rgba(255,255,255,0.06)',
borderRadius: 8,
colorLink: colorPrimary,
},
}}
>
<Layout style={{ minHeight: '100vh', background: colorBgBase }}>
<PublicNavBar
showAuth
onSignInClick={() => { setAuthModalContext('generic'); setAuthModalOpen(true); }}
/>
<Content
style={{
maxWidth: 960,
width: '100%',
margin: '0 auto',
padding: '24px 16px',
}}
>
<Outlet />
</Content>
<Footer
style={{
textAlign: 'center',
background: 'transparent',
color: 'rgba(255,255,255,0.35)',
fontSize: 13,
borderTop: '1px solid rgba(255,255,255,0.06)',
}}
>
<NewsletterSignup />
<div>{footerText}</div>
<div style={{ marginTop: 8 }}>
{footerLinks.map((link, idx) => (
<span key={link.path}>
{idx > 0 && ' \u2022 '}
{link.external ? (
<a href={link.path} target="_blank" rel="noopener noreferrer" style={{ color: 'rgba(255,255,255,0.5)', fontSize: 12 }}>
{link.label}
</a>
) : (
<Link to={link.path} style={{ color: 'rgba(255,255,255,0.5)', fontSize: 12 }}>
{link.label}
</Link>
)}
</span>
))}
</div>
</Footer>
<AuthModal
open={authModalOpen}
onCancel={() => setAuthModalOpen(false)}
onSuccess={() => {
setAuthModalOpen(false);
if (authModalContext === 'campaign') {
navigate('/campaigns/create');
}
}}
title={authModalContext === 'campaign' ? 'Sign in to Create a Campaign' : 'Sign in to your account'}
subtitle={authModalContext === 'campaign' ? 'Sign in or create an account to submit your own campaign' : 'Sign in or create an account to get involved'}
/>
</Layout>
</ConfigProvider>
);
}

View File

@ -0,0 +1,612 @@
import { useState, useEffect, useMemo } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Typography, Space, Grid, Drawer, Dropdown, Button, Tooltip, message } from 'antd';
import {
MenuOutlined,
CloseOutlined,
LoginOutlined,
LogoutOutlined,
AppstoreOutlined,
TeamOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
EllipsisOutlined,
SearchOutlined,
UserOutlined,
DownOutlined,
UpOutlined,
} from '@ant-design/icons';
import { useSettingsStore } from '@/stores/settings.store';
import { useAuthStore } from '@/stores/auth.store';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import PublicSearchModal from '@/components/PublicSearchModal';
import NotificationBell from '@/components/social/NotificationBell';
import { api } from '@/lib/api';
import { resolveNavUrl } from '@/lib/service-url';
import {
DEFAULT_NAV_ITEMS,
ICON_MAP,
mergeNavDefaults,
filterNavItems,
buildFeatureFlags,
isItemActive,
} from '@/lib/nav-defaults';
import type { NavItem } from '@/types/api';
import { isAdmin as checkIsAdmin } from '@/utils/roles';
const navItemStyle: React.CSSProperties = {
color: 'rgba(255, 255, 255, 0.85)',
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 14,
transition: 'color 0.2s',
whiteSpace: 'nowrap',
cursor: 'pointer',
background: 'none',
border: 'none',
padding: 0,
font: 'inherit',
};
/** Resolve external URLs for builtin items (including $token paths) */
function resolveItemUrl(item: NavItem): string {
if (item.path.startsWith('$')) return resolveNavUrl(item.path);
if (item.external && item.path.startsWith('http')) return item.path;
return item.path;
}
interface PublicNavBarProps {
activePath?: string;
showAuth?: boolean;
onSignInClick?: () => void;
}
export default function PublicNavBar({ activePath, showAuth = true, onSignInClick }: PublicNavBarProps) {
const { settings } = useSettingsStore();
const { isAuthenticated, logout, user } = useAuthStore();
const isAdmin = isAuthenticated && user ? checkIsAdmin(user) : false;
const location = useLocation();
const navigate = useNavigate();
const [drawerOpen, setDrawerOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const [navCollapsed, setNavCollapsed] = useLocalStorage('public_nav_collapsed', false);
const [profileLoading, setProfileLoading] = useState(false);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const handleSignIn = onSignInClick ?? (() => navigate('/login'));
const handleMyProfile = async () => {
if (profileLoading) return;
setProfileLoading(true);
try {
const { data } = await api.get<{ token: string }>('/auth/me/profile-token');
navigate(`/profile/${data.token}`);
} catch {
message.error('Unable to load profile');
} finally {
setProfileLoading(false);
}
};
const screens = Grid.useBreakpoint();
const isMobile = !screens.md;
// Global Ctrl+K / Cmd+K to open search
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setSearchOpen(true);
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
/** Animated label that collapses to zero-width when navCollapsed is true */
const NavLabel = ({ label }: { label: string }) => (
<span style={{
display: 'inline-block',
maxWidth: navCollapsed ? 0 : 200,
opacity: navCollapsed ? 0 : 1,
overflow: 'hidden',
transition: 'max-width 0.25s ease, opacity 0.2s ease',
whiteSpace: 'nowrap',
}}>
{label}
</span>
);
const headerGradient = settings?.publicHeaderGradient ?? 'linear-gradient(135deg, #005a9c 0%, #007acc 100%)';
const orgName = settings?.organizationName ?? 'Changemaker Lite';
const logoUrl = settings?.organizationLogoUrl;
const colorBgBase = settings?.publicColorBgBase ?? '#0d1b2a';
const colorBgContainer = settings?.publicColorBgContainer ?? '#1b2838';
// Determine active route for nav highlight
const currentActive = activePath ?? location.pathname;
const featureFlags = useMemo(() => buildFeatureFlags(settings), [settings]);
// Get filtered, sorted nav items (with group support)
const navItems = useMemo(() => {
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
return filterNavItems(merged, featureFlags);
}, [settings?.navConfig, featureFlags]);
// Desktop overflow: group items beyond MAX_VISIBLE into "More" dropdown
const MAX_VISIBLE_NAV = 7;
const visibleNavItems = navCollapsed ? navItems : navItems.slice(0, MAX_VISIBLE_NAV);
const overflowNavItems = navCollapsed ? [] : navItems.slice(MAX_VISIBLE_NAV);
const toggleGroup = (groupId: string) => {
setExpandedGroups(prev => {
const next = new Set(prev);
if (next.has(groupId)) next.delete(groupId);
else next.add(groupId);
return next;
});
};
const renderDesktopLink = (item: NavItem) => {
const isActive = isItemActive(item, currentActive);
const icon = ICON_MAP[item.icon] ?? null;
const linkStyle: React.CSSProperties = {
...navItemStyle,
gap: navCollapsed ? 0 : 6,
color: isActive ? '#fff' : 'rgba(255, 255, 255, 0.85)',
fontWeight: isActive ? 600 : undefined,
borderBottom: isActive ? '2px solid #fff' : '2px solid transparent',
paddingBottom: 2,
};
// Group item: render as Dropdown trigger
if (item.type === 'group' && item.children) {
const menuItems = item.children.map(child => ({
key: child.id,
icon: ICON_MAP[child.icon],
label: child.external ? (
<a href={resolveItemUrl(child)} target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>{child.label}</a>
) : child.label,
onClick: child.external ? undefined : () => navigate(child.path),
}));
return (
<Dropdown
key={item.id}
menu={{ items: menuItems }}
placement="bottomRight"
>
<Tooltip title={navCollapsed ? item.label : ''}>
<span
style={linkStyle}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
{icon}
<NavLabel label={item.label} />
{!navCollapsed && <DownOutlined style={{ fontSize: 10, marginLeft: -2 }} />}
</span>
</Tooltip>
</Dropdown>
);
}
if (item.external) {
return (
<Tooltip key={item.id} title={navCollapsed ? item.label : ''}>
<a
href={resolveItemUrl(item)}
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
{icon}
<NavLabel label={item.label} />
</a>
</Tooltip>
);
}
return (
<Tooltip key={item.id} title={navCollapsed ? item.label : ''}>
<Link
to={item.path}
style={linkStyle}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
{icon}
<NavLabel label={item.label} />
</Link>
</Tooltip>
);
};
const renderMobileLink = (item: NavItem, indent = false) => {
const isActive = isItemActive(item, currentActive);
const icon = ICON_MAP[item.icon] ?? null;
const style: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 10,
padding: indent ? '10px 24px 10px 44px' : '12px 24px',
color: isActive ? '#fff' : 'rgba(255,255,255,0.85)',
textDecoration: 'none',
fontSize: indent ? 14 : 15,
fontWeight: isActive ? 600 : 400,
background: isActive ? 'rgba(255,255,255,0.1)' : 'transparent',
borderRadius: 4,
};
if (item.external) {
return (
<a
key={item.id}
href={resolveItemUrl(item)}
target="_blank"
rel="noopener noreferrer"
onClick={() => setDrawerOpen(false)}
style={style}
>
{icon}
<span>{item.label}</span>
</a>
);
}
return (
<Link
key={item.id}
to={item.path}
onClick={() => setDrawerOpen(false)}
style={style}
>
{icon}
<span>{item.label}</span>
</Link>
);
};
const renderMobileGroup = (item: NavItem) => {
const isActive = isItemActive(item, currentActive);
const icon = ICON_MAP[item.icon] ?? null;
const expanded = expandedGroups.has(item.id);
return (
<div key={item.id}>
<span
role="button"
tabIndex={0}
onClick={() => toggleGroup(item.id)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(item.id); } }}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '12px 24px',
color: isActive ? '#fff' : 'rgba(255,255,255,0.85)',
fontSize: 15,
fontWeight: isActive ? 600 : 400,
cursor: 'pointer',
background: 'none',
border: 'none',
font: 'inherit',
width: '100%',
textAlign: 'left',
}}
>
{icon}
<span style={{ flex: 1 }}>{item.label}</span>
{expanded ? <UpOutlined style={{ fontSize: 10 }} /> : <DownOutlined style={{ fontSize: 10 }} />}
</span>
{expanded && item.children?.map(child => renderMobileLink(child, true))}
</div>
);
};
// Build overflow menu items with group support (nested children)
const overflowMenuItems = overflowNavItems.map(item => {
if (item.type === 'group' && item.children) {
return {
key: item.id,
icon: ICON_MAP[item.icon],
label: item.label,
children: item.children.map(child => ({
key: child.id,
icon: ICON_MAP[child.icon],
label: child.external ? (
<a href={resolveItemUrl(child)} target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>{child.label}</a>
) : child.label,
onClick: child.external ? undefined : () => navigate(child.path),
})),
};
}
return {
key: item.id,
icon: ICON_MAP[item.icon],
label: item.external ? (
<a href={resolveItemUrl(item)} target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>{item.label}</a>
) : item.label,
onClick: item.external ? undefined : () => navigate(item.path),
};
});
return (
<>
<div
style={{
background: headerGradient,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
height: 56,
overflow: 'hidden',
flexShrink: 0,
}}
>
{/* Left: Logo + Brand */}
<Link to="/home" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 10 }}>
{logoUrl && (
<img
src={logoUrl}
alt={orgName}
style={{ maxHeight: 32, objectFit: 'contain' }}
/>
)}
<Typography.Text strong style={{ fontSize: 18, color: '#fff' }}>
{orgName}
</Typography.Text>
</Link>
{/* Right: Navigation */}
{isMobile ? (
<Space size={4}>
{isAuthenticated && settings?.enableSocial && <NotificationBell />}
<Button
type="text"
icon={<MenuOutlined style={{ color: '#fff', fontSize: 20 }} />}
onClick={() => setDrawerOpen(true)}
aria-label="Open navigation menu"
style={{ padding: '4px 8px' }}
/>
</Space>
) : (
<Space size={navCollapsed ? 8 : 16} style={{ flexWrap: 'nowrap', overflow: 'hidden' }}>
{visibleNavItems.map(renderDesktopLink)}
{overflowMenuItems.length > 0 && (
<Dropdown
menu={{ items: overflowMenuItems }}
placement="bottomRight"
>
<span
style={{ ...navItemStyle, cursor: 'pointer' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
<EllipsisOutlined />
<NavLabel label="More" />
</span>
</Dropdown>
)}
{/* Search button */}
<Tooltip title={navCollapsed ? 'Search (Ctrl+K)' : 'Search'}>
<span
role="button"
tabIndex={0}
onClick={() => setSearchOpen(true)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSearchOpen(true); } }}
style={{ ...navItemStyle, gap: navCollapsed ? 0 : 6 }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
<SearchOutlined />
<NavLabel label="Search" />
</span>
</Tooltip>
{/* Collapse toggle */}
<Tooltip title={navCollapsed ? 'Expand navigation' : 'Collapse navigation'}>
<span
role="button"
tabIndex={0}
onClick={() => setNavCollapsed(!navCollapsed)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setNavCollapsed(!navCollapsed); } }}
style={{
...navItemStyle,
color: 'rgba(255, 255, 255, 0.5)',
borderLeft: '1px solid rgba(255, 255, 255, 0.2)',
paddingLeft: 12,
marginLeft: 4,
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'rgba(255, 255, 255, 0.5)'; }}
>
{navCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
</Tooltip>
{/* Notification bell (authenticated + social enabled) */}
{isAuthenticated && settings?.enableSocial && <NotificationBell />}
{/* Auth: user dropdown when logged in, Sign In when not */}
{isAuthenticated ? (
<Dropdown
menu={{
items: [
...(isAdmin ? [{
key: 'admin',
icon: <AppstoreOutlined />,
label: 'Admin Panel',
onClick: () => navigate('/app'),
style: { fontWeight: 600 },
}] : []),
{
key: 'volunteer',
icon: <TeamOutlined />,
label: 'Volunteer Portal',
onClick: () => navigate('/volunteer'),
style: isAdmin ? undefined : { fontWeight: 600 },
},
{ type: 'divider' as const },
{
key: 'profile',
icon: <UserOutlined />,
label: 'My Profile',
disabled: profileLoading,
onClick: handleMyProfile,
},
{ type: 'divider' as const },
{
key: 'logout',
icon: <LogoutOutlined />,
label: 'Logout',
onClick: () => logout(),
},
],
}}
placement="bottomRight"
trigger={['click']}
>
<span
style={{
...navItemStyle,
gap: 6,
cursor: 'pointer',
borderLeft: '1px solid rgba(255,255,255,0.2)',
paddingLeft: 12,
marginLeft: 4,
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
<UserOutlined />
<span style={{
maxWidth: navCollapsed ? 0 : 120,
opacity: navCollapsed ? 0 : 1,
overflow: 'hidden',
transition: 'max-width 0.25s ease, opacity 0.2s ease',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
{user?.name || user?.email || 'Account'}
</span>
</span>
</Dropdown>
) : showAuth && (
<Tooltip title={navCollapsed ? 'Sign In' : ''}>
<span
role="button"
tabIndex={0}
onClick={handleSignIn}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleSignIn(); } }}
style={{ ...navItemStyle, gap: navCollapsed ? 0 : 6 }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'rgba(255, 255, 255, 0.85)'; }}
>
<LoginOutlined /><NavLabel label="Sign In" />
</span>
</Tooltip>
)}
</Space>
)}
</div>
{/* Mobile Navigation Drawer */}
<Drawer
title={orgName}
placement="right"
onClose={() => setDrawerOpen(false)}
open={drawerOpen}
width={280}
closeIcon={<CloseOutlined style={{ color: 'rgba(255,255,255,0.85)' }} />}
styles={{
header: { background: colorBgContainer, borderBottom: '1px solid rgba(255,255,255,0.1)' },
body: { background: colorBgBase, padding: '16px 0' },
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{/* Highlighted portal/admin link at top when authenticated */}
{isAuthenticated && (
<>
<Link
to={isAdmin ? '/app' : '/volunteer'}
onClick={() => setDrawerOpen(false)}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '12px 24px',
color: '#fff',
textDecoration: 'none', fontSize: 15,
fontWeight: 600,
borderRadius: 4,
margin: '0 8px 4px',
background: 'rgba(52,152,219,0.15)',
}}
>
{isAdmin ? <AppstoreOutlined /> : <TeamOutlined />}
<span>{isAdmin ? 'Open Admin Panel' : 'Open Volunteer Portal'}</span>
</Link>
</>
)}
{navItems.map(item =>
item.type === 'group' && item.children
? renderMobileGroup(item)
: renderMobileLink(item)
)}
<div style={{ borderTop: '1px solid rgba(255,255,255,0.1)', margin: '8px 24px' }} />
{isAuthenticated ? (
<>
<Link
to="/volunteer"
onClick={() => setDrawerOpen(false)}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '12px 24px',
color: 'rgba(255,255,255,0.85)',
textDecoration: 'none', fontSize: 15,
borderRadius: 4,
}}
>
<TeamOutlined /> <span>Volunteer Portal</span>
</Link>
<span
role="button"
tabIndex={0}
onClick={() => { handleMyProfile(); setDrawerOpen(false); }}
onKeyDown={(e) => { if (e.key === 'Enter') { handleMyProfile(); setDrawerOpen(false); } }}
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 24px', color: 'rgba(255,255,255,0.85)', cursor: 'pointer', fontSize: 15, background: 'none', border: 'none', font: 'inherit', opacity: profileLoading ? 0.5 : 1 }}
>
<UserOutlined /> <span>My Profile</span>
</span>
<span
role="button"
tabIndex={0}
onClick={() => { logout(); setDrawerOpen(false); }}
onKeyDown={(e) => { if (e.key === 'Enter') { logout(); setDrawerOpen(false); } }}
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 24px', color: 'rgba(255,255,255,0.85)', cursor: 'pointer', fontSize: 15, background: 'none', border: 'none', font: 'inherit' }}
>
<LogoutOutlined /> <span>Logout</span>
</span>
</>
) : showAuth && (
<span
role="button"
tabIndex={0}
onClick={() => { handleSignIn(); setDrawerOpen(false); }}
onKeyDown={(e) => { if (e.key === 'Enter') { handleSignIn(); setDrawerOpen(false); } }}
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 24px', color: 'rgba(255,255,255,0.85)', cursor: 'pointer', fontSize: 15, background: 'none', border: 'none', font: 'inherit' }}
>
<LoginOutlined /> <span>Sign In</span>
</span>
)}
</div>
</Drawer>
<PublicSearchModal open={searchOpen} onClose={() => setSearchOpen(false)} />
</>
);
}

View File

@ -0,0 +1,208 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Modal, Input, Typography, Tag, Empty, Spin, Grid } from 'antd';
import {
SearchOutlined,
SendOutlined,
CalendarOutlined,
ScheduleOutlined,
FileTextOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import axios from 'axios';
const { Text } = Typography;
interface SearchResult {
type: 'campaign' | 'shift' | 'page' | 'video' | 'event';
id: string;
title: string;
description: string | null;
link: string;
}
const TYPE_ICONS: Record<string, React.ReactNode> = {
campaign: <SendOutlined />,
shift: <ScheduleOutlined />,
page: <FileTextOutlined />,
video: <PlayCircleOutlined />,
event: <CalendarOutlined />,
};
const TYPE_COLORS: Record<string, string> = {
campaign: 'blue',
shift: 'green',
page: 'purple',
video: 'magenta',
event: 'orange',
};
interface PublicSearchModalProps {
open: boolean;
onClose: () => void;
}
export default function PublicSearchModal({ open, onClose }: PublicSearchModalProps) {
const navigate = useNavigate();
const screens = Grid.useBreakpoint();
const isMobile = !screens.md;
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const inputRef = useRef<any>(null);
// Focus input on open
useEffect(() => {
if (open) {
setQuery('');
setResults([]);
setActiveIndex(0);
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [open]);
// Debounced search
useEffect(() => {
clearTimeout(timerRef.current);
if (!query || query.length < 2) {
setResults([]);
return;
}
timerRef.current = setTimeout(async () => {
setLoading(true);
try {
const { data } = await axios.get<SearchResult[]>('/api/search', {
params: { q: query, limit: 10 },
});
setResults(data);
setActiveIndex(0);
} catch {
setResults([]);
} finally {
setLoading(false);
}
}, 300);
return () => clearTimeout(timerRef.current);
}, [query]);
const handleSelect = useCallback((result: SearchResult) => {
onClose();
navigate(result.link);
}, [navigate, onClose]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, results.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
} else if (e.key === 'Enter' && results[activeIndex]) {
e.preventDefault();
handleSelect(results[activeIndex]);
}
};
// Global Ctrl+K handler
useEffect(() => {
// Only open from outside — parent manages the `open` state
}, []);
// Group results by type
const grouped = results.reduce((acc, r) => {
if (!acc[r.type]) acc[r.type] = [];
acc[r.type]!.push(r);
return acc;
}, {} as Record<string, SearchResult[]>);
let flatIndex = 0;
return (
<Modal
open={open}
onCancel={onClose}
footer={null}
closable={false}
width={isMobile ? '100%' : 520}
style={{ top: isMobile ? 0 : 80 }}
styles={{ body: { padding: 0 } }}
>
<div style={{ padding: '12px 16px', borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<Input
ref={inputRef}
prefix={<SearchOutlined style={{ color: 'rgba(255,255,255,0.4)' }} />}
placeholder="Search campaigns, shifts, pages..."
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
variant="borderless"
size="large"
suffix={loading ? <Spin size="small" /> : <Text type="secondary" style={{ fontSize: 11 }}>ESC</Text>}
/>
</div>
<div style={{ maxHeight: 400, overflowY: 'auto', padding: '8px 0' }}>
{query.length >= 2 && !loading && results.length === 0 && (
<Empty
description={<Text type="secondary">No results found</Text>}
style={{ padding: 32 }}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
{Object.entries(grouped).map(([type, items]) => (
<div key={type}>
<div style={{ padding: '8px 16px 4px', fontSize: 11, textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', letterSpacing: 1 }}>
{type}s
</div>
{items.map((item) => {
const idx = flatIndex++;
return (
<div
key={`${item.type}-${item.id}`}
onClick={() => handleSelect(item)}
onMouseEnter={() => setActiveIndex(idx)}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '10px 16px',
cursor: 'pointer',
background: idx === activeIndex ? 'rgba(255,255,255,0.06)' : 'transparent',
transition: 'background 0.1s',
}}
>
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 16 }}>
{TYPE_ICONS[item.type]}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{item.title}
</div>
{item.description && (
<Text type="secondary" style={{ fontSize: 12, display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{item.description}
</Text>
)}
</div>
<Tag color={TYPE_COLORS[item.type]} style={{ margin: 0, fontSize: 10 }}>
{item.type}
</Tag>
</div>
);
})}
</div>
))}
</div>
{query.length < 2 && (
<div style={{ padding: '16px', textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: 13 }}>
Type at least 2 characters to search
</Text>
</div>
)}
</Modal>
);
}

View File

@ -0,0 +1,89 @@
import { useState, useRef } from 'react';
import { Modal, Button, Space, Input, message, Spin } from 'antd';
import { DownloadOutlined, CopyOutlined, CheckOutlined } from '@ant-design/icons';
interface QrCodeModalProps {
open: boolean;
onClose: () => void;
url: string;
title: string;
}
export default function QrCodeModal({ open, onClose, url, title }: QrCodeModalProps) {
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(true);
const imgRef = useRef<HTMLImageElement>(null);
const qrSrc = `/api/qr?text=${encodeURIComponent(url)}&size=300`;
const handleDownload = () => {
const img = imgRef.current;
if (!img) return;
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.drawImage(img, 0, 0);
const link = document.createElement('a');
link.download = `qr-${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
};
const handleCopyUrl = () => {
navigator.clipboard.writeText(url);
setCopied(true);
message.success('URL copied');
setTimeout(() => setCopied(false), 2000);
};
return (
<Modal
title={`QR Code: ${title}`}
open={open}
onCancel={onClose}
footer={null}
width={400}
destroyOnHidden
>
<div style={{ textAlign: 'center', padding: '16px 0' }}>
{loading && <Spin style={{ display: 'block', marginBottom: 16 }} />}
<img
ref={imgRef}
src={qrSrc}
alt={`QR code for ${title}`}
crossOrigin="anonymous"
style={{ width: 300, height: 300, display: loading ? 'none' : 'block', margin: '0 auto' }}
onLoad={() => setLoading(false)}
onError={() => { setLoading(false); message.error('Failed to generate QR code'); }}
/>
<div style={{ marginTop: 16 }}>
<Input
value={url}
readOnly
style={{ marginBottom: 12, textAlign: 'center' }}
addonAfter={
<Button
type="text"
size="small"
icon={copied ? <CheckOutlined /> : <CopyOutlined />}
onClick={handleCopyUrl}
/>
}
/>
<Space>
<Button icon={<DownloadOutlined />} type="primary" onClick={handleDownload}>
Download PNG
</Button>
<Button icon={copied ? <CheckOutlined /> : <CopyOutlined />} onClick={handleCopyUrl}>
Copy URL
</Button>
</Space>
</div>
</div>
</Modal>
);
}

View File

@ -0,0 +1,119 @@
import { useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { theme } from 'antd';
import {
EnvironmentOutlined,
ScheduleOutlined,
HistoryOutlined,
NodeIndexOutlined,
MessageOutlined,
TeamOutlined,
TagOutlined,
CalendarOutlined,
MenuOutlined,
} from '@ant-design/icons';
import { useSettingsStore } from '@/stores/settings.store';
const BASE_NAV_ITEMS = [
{ key: '/volunteer', icon: EnvironmentOutlined, label: 'Map' },
{ key: '/volunteer/shifts', icon: ScheduleOutlined, label: 'Shifts' },
{ key: '/volunteer/activity', icon: HistoryOutlined, label: 'Activity' },
{ key: '/volunteer/routes', icon: NodeIndexOutlined, label: 'Routes' },
];
interface VolunteerFooterNavProps {
style?: React.CSSProperties;
onMenuOpen?: () => void;
menuActive?: boolean;
}
export default function VolunteerFooterNav({ style, onMenuOpen, menuActive = false }: VolunteerFooterNavProps) {
const navigate = useNavigate();
const location = useLocation();
const { token } = theme.useToken();
const { settings } = useSettingsStore();
const NAV_ITEMS = useMemo(() => {
const items = [...BASE_NAV_ITEMS];
if (settings?.enableSocialCalendar) {
items.push({ key: '/volunteer/calendar', icon: CalendarOutlined, label: 'Calendar' });
}
if (settings?.enableTicketedEvents) {
items.push({ key: '/volunteer/tickets', icon: TagOutlined, label: 'Tickets' });
}
if (settings?.enableSocial) {
items.push({ key: '/volunteer/feed', icon: TeamOutlined, label: 'Social' });
}
if (settings?.enableChat) {
items.push({ key: '/volunteer/chat', icon: MessageOutlined, label: 'Chat' });
}
return items;
}, [settings?.enableChat, settings?.enableSocial, settings?.enableSocialCalendar, settings?.enableTicketedEvents]);
const activeKey = (() => {
const path = location.pathname;
if (path === '/volunteer') return '/volunteer';
for (const item of NAV_ITEMS) {
if (item.key !== '/volunteer' && path.startsWith(item.key)) return item.key;
}
return '/volunteer';
})();
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-around',
minHeight: 44,
background: 'rgba(13, 27, 42, 0.95)',
borderTop: '1px solid rgba(255,255,255,0.1)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
paddingBottom: 'env(safe-area-inset-bottom)',
...style,
}}
>
{/* Menu button */}
{onMenuOpen && (
<div
onClick={onMenuOpen}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
cursor: 'pointer',
padding: '10px 0',
color: menuActive ? token.colorPrimary : 'rgba(255,255,255,0.5)',
transition: 'color 0.2s',
}}
>
<MenuOutlined style={{ fontSize: 22 }} />
</div>
)}
{NAV_ITEMS.map(({ key, icon: Icon }) => {
const isActive = activeKey === key;
return (
<div
key={key}
onClick={() => navigate(key)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
cursor: 'pointer',
padding: '10px 0',
color: isActive ? token.colorPrimary : 'rgba(255,255,255,0.5)',
transition: 'color 0.2s',
}}
>
<Icon style={{ fontSize: 22 }} />
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1,270 @@
import { useState, useMemo } from 'react';
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
import { ConfigProvider, Layout, Typography, theme, Drawer, Divider, Alert, Tag } from 'antd';
import {
LogoutOutlined,
UserOutlined,
GlobalOutlined,
AppstoreOutlined,
EnvironmentOutlined,
ScheduleOutlined,
HistoryOutlined,
NodeIndexOutlined,
CalendarOutlined,
TagOutlined,
TeamOutlined,
MessageOutlined,
} from '@ant-design/icons';
import { useAuthStore } from '@/stores/auth.store';
import { useSettingsStore } from '@/stores/settings.store';
import VolunteerFooterNav from '@/components/VolunteerFooterNav';
import PublicNavBar from '@/components/PublicNavBar';
import { useSSE } from '@/hooks/useSSE';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { isAdmin as checkIsAdmin } from '@/utils/roles';
const { Content, Footer } = Layout;
export default function VolunteerLayout() {
const navigate = useNavigate();
const location = useLocation();
const { user, logout } = useAuthStore();
const { settings } = useSettingsStore();
const [menuOpen, setMenuOpen] = useState(false);
const [welcomeDismissed, setWelcomeDismissed] = useLocalStorage('volunteer_welcome_dismissed', false);
// Initialize SSE connection for real-time notifications + online presence
useSSE();
const isAdmin = user ? checkIsAdmin(user) : false;
const colorBgBase = settings?.publicColorBgBase ?? '#0d1b2a';
const colorBgContainer = settings?.publicColorBgContainer ?? '#1b2838';
const handleLogout = async () => {
await logout();
navigate('/login', { replace: true });
};
// Build nav items list (mirrors VolunteerFooterNav logic)
const navItems = useMemo(() => {
const items: { key: string; icon: React.ReactNode; label: string }[] = [
{ key: '/volunteer', icon: <EnvironmentOutlined />, label: 'Map' },
{ key: '/volunteer/shifts', icon: <ScheduleOutlined />, label: 'Shifts' },
{ key: '/volunteer/activity', icon: <HistoryOutlined />, label: 'Activity' },
{ key: '/volunteer/routes', icon: <NodeIndexOutlined />, label: 'Routes' },
];
if (settings?.enableSocialCalendar) {
items.push({ key: '/volunteer/calendar', icon: <CalendarOutlined />, label: 'Calendar' });
}
if (settings?.enableTicketedEvents) {
items.push({ key: '/volunteer/tickets', icon: <TagOutlined />, label: 'Tickets' });
}
if (settings?.enableSocial) {
items.push({ key: '/volunteer/feed', icon: <TeamOutlined />, label: 'Social' });
}
if (settings?.enableChat) {
items.push({ key: '/volunteer/chat', icon: <MessageOutlined />, label: 'Chat' });
}
return items;
}, [settings?.enableSocialCalendar, settings?.enableTicketedEvents, settings?.enableSocial, settings?.enableChat]);
const activeKey = (() => {
const path = location.pathname;
if (path === '/volunteer') return '/volunteer';
for (const item of navItems) {
if (item.key !== '/volunteer' && path.startsWith(item.key)) return item.key;
}
return '/volunteer';
})();
return (
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: settings?.publicColorPrimary ?? '#3498db',
colorBgBase: settings?.publicColorBgBase ?? '#0d1b2a',
colorBgContainer: settings?.publicColorBgContainer ?? '#1b2838',
colorBgElevated: settings?.publicColorBgContainer ?? '#1b2838',
colorBorder: 'rgba(255,255,255,0.1)',
colorBorderSecondary: 'rgba(255,255,255,0.06)',
borderRadius: 8,
},
}}
>
<Layout style={{ minHeight: '100dvh', background: settings?.publicColorBgBase ?? '#0d1b2a' }}>
<PublicNavBar />
<Content
style={{
maxWidth: 800,
width: '100%',
margin: '0 auto',
padding: '16px 12px max(60px, calc(44px + 16px + env(safe-area-inset-bottom))) 12px',
}}
>
{!welcomeDismissed && (
<Alert
message="Welcome to the Volunteer Portal!"
description="Here you can view your shifts, canvass your area, track your activity, and connect with your team."
type="info"
closable
onClose={() => setWelcomeDismissed(true)}
style={{ marginBottom: 16 }}
/>
)}
<Outlet />
</Content>
<Footer
style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
padding: 0,
zIndex: 100,
}}
>
<VolunteerFooterNav
onMenuOpen={() => setMenuOpen(true)}
menuActive={menuOpen}
/>
</Footer>
</Layout>
{/* Navigation Menu Drawer */}
<Drawer
title={null}
placement="left"
onClose={() => setMenuOpen(false)}
open={menuOpen}
width={280}
styles={{
header: { display: 'none' },
body: { background: colorBgBase, padding: 0 },
}}
>
{/* User profile section */}
<div style={{
padding: '20px 20px 16px',
background: colorBgContainer,
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 40,
height: 40,
borderRadius: '50%',
background: 'rgba(52,152,219,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<UserOutlined style={{ fontSize: 18, color: 'rgba(255,255,255,0.85)' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Typography.Text strong style={{ color: '#fff', display: 'block', fontSize: 14 }}>
{user?.name || 'Volunteer'}
</Typography.Text>
<Typography.Text style={{ color: 'rgba(255,255,255,0.45)', display: 'block', fontSize: 12 }}>
{user?.email}
</Typography.Text>
</div>
</div>
<Tag color="blue" style={{ marginTop: 8, fontSize: 11 }}>
{user?.role === 'USER' ? 'Volunteer' : user?.role?.replace('_', ' ') ?? 'Volunteer'}
</Tag>
</div>
{/* Navigation items */}
<div style={{ padding: '8px 0' }}>
{navItems.map(({ key, icon, label }) => {
const isActive = activeKey === key;
return (
<div
key={key}
onClick={() => { navigate(key); setMenuOpen(false); }}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 20px',
cursor: 'pointer',
color: isActive ? '#fff' : 'rgba(255,255,255,0.7)',
fontWeight: isActive ? 600 : 400,
fontSize: 14,
background: isActive ? 'rgba(52,152,219,0.15)' : 'transparent',
borderRight: isActive ? '3px solid #3498db' : '3px solid transparent',
transition: 'all 0.2s',
}}
>
{icon}
<span>{label}</span>
</div>
);
})}
</div>
<Divider style={{ margin: '4px 20px', borderColor: 'rgba(255,255,255,0.1)' }} />
{/* Cross-navigation links */}
<div style={{ padding: '4px 0' }}>
<div
onClick={() => { navigate('/home'); setMenuOpen(false); }}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 20px',
cursor: 'pointer',
color: 'rgba(255,255,255,0.7)',
fontSize: 14,
}}
>
<GlobalOutlined />
<span>Public Website</span>
</div>
{isAdmin && (
<div
onClick={() => { navigate('/app'); setMenuOpen(false); }}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 20px',
cursor: 'pointer',
color: 'rgba(255,255,255,0.7)',
fontSize: 14,
}}
>
<AppstoreOutlined />
<span>Admin Panel</span>
</div>
)}
</div>
<Divider style={{ margin: '4px 20px', borderColor: 'rgba(255,255,255,0.1)' }} />
{/* Logout */}
<div style={{ padding: '4px 0' }}>
<div
onClick={() => { handleLogout(); setMenuOpen(false); }}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 20px',
cursor: 'pointer',
color: 'rgba(255,255,255,0.7)',
fontSize: 14,
}}
>
<LogoutOutlined />
<span>Logout</span>
</div>
</div>
</Drawer>
</ConfigProvider>
);
}

View File

@ -0,0 +1,252 @@
import { useState, useEffect, useCallback } from 'react';
import {
DatePicker,
Select,
Space,
Typography,
Skeleton,
Empty,
Tooltip,
theme,
} from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import { api } from '@/lib/api';
import type { AvailabilityResponse } from '@/types/api';
const { Text } = Typography;
const { RangePicker } = DatePicker;
interface AvailabilityFinderProps {
viewId: string;
onSlotClick?: (date: string, time: string) => void;
}
const DURATION_OPTIONS = [
{ value: 15, label: '15 min' },
{ value: 30, label: '30 min' },
{ value: 60, label: '1 hour' },
];
export default function AvailabilityFinder({ viewId, onSlotClick }: AvailabilityFinderProps) {
const { token } = theme.useToken();
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
dayjs(),
dayjs().add(6, 'day'),
]);
const [dayStart, setDayStart] = useState(9);
const [dayEnd, setDayEnd] = useState(18);
const [slotDuration, setSlotDuration] = useState(30);
const [availability, setAvailability] = useState<AvailabilityResponse | null>(null);
const [loading, setLoading] = useState(false);
const hourOptions = Array.from({ length: 24 }, (_, i) => ({
value: i,
label: `${String(i).padStart(2, '0')}:00`,
}));
const fetchAvailability = useCallback(async () => {
setLoading(true);
try {
const { data } = await api.get<AvailabilityResponse>(
`/calendar/shared/${viewId}/availability`,
{
params: {
startDate: dateRange[0].format('YYYY-MM-DD'),
endDate: dateRange[1].format('YYYY-MM-DD'),
dayStart,
dayEnd,
slotMinutes: slotDuration,
},
},
);
setAvailability(data);
} catch {
setAvailability(null);
} finally {
setLoading(false);
}
}, [viewId, dateRange, dayStart, dayEnd, slotDuration]);
useEffect(() => {
fetchAvailability();
}, [fetchAvailability]);
// Build time slots
const timeSlots: string[] = [];
for (let h = dayStart; h < dayEnd; h++) {
for (let m = 0; m < 60; m += slotDuration) {
if (h === dayEnd - 1 && m + slotDuration > 60) break;
timeSlots.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
}
}
// Build date columns
const dates: string[] = [];
let d = dateRange[0];
while (d.isBefore(dateRange[1]) || d.isSame(dateRange[1], 'day')) {
dates.push(d.format('YYYY-MM-DD'));
d = d.add(1, 'day');
}
const statusColors: Record<string, string> = {
free: '#52c41a',
busy: '#ff4d4f',
tentative: '#faad14',
};
return (
<div>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12 }}>
Find Availability
</Text>
<Space wrap style={{ marginBottom: 12 }}>
<RangePicker
size="small"
value={dateRange}
onChange={(vals) => {
if (vals && vals[0] && vals[1]) {
setDateRange([vals[0], vals[1]]);
}
}}
/>
<Select
size="small"
value={dayStart}
options={hourOptions}
onChange={setDayStart}
style={{ width: 90 }}
placeholder="Start"
/>
<Select
size="small"
value={dayEnd}
options={hourOptions}
onChange={setDayEnd}
style={{ width: 90 }}
placeholder="End"
/>
<Select
size="small"
value={slotDuration}
options={DURATION_OPTIONS}
onChange={setSlotDuration}
style={{ width: 90 }}
/>
</Space>
{loading ? (
<Skeleton active paragraph={{ rows: 6 }} />
) : !availability || dates.length === 0 ? (
<Empty description="Select a date range" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ borderCollapse: 'collapse', width: '100%', fontSize: 11 }}>
<thead>
<tr>
<th
style={{
padding: '4px 8px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
position: 'sticky',
left: 0,
background: token.colorBgContainer,
zIndex: 1,
}}
/>
{dates.map((date) => (
<th
key={date}
style={{
padding: '4px 8px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
whiteSpace: 'nowrap',
fontWeight: 500,
}}
>
{dayjs(date).format('ddd M/D')}
</th>
))}
</tr>
</thead>
<tbody>
{timeSlots.map((time) => (
<tr key={time}>
<td
style={{
padding: '3px 8px',
borderRight: `1px solid ${token.colorBorderSecondary}`,
whiteSpace: 'nowrap',
position: 'sticky',
left: 0,
background: token.colorBgContainer,
zIndex: 1,
color: 'rgba(255,255,255,0.6)',
}}
>
{time}
</td>
{dates.map((date) => {
const dayData = availability.dates[date];
const slot = dayData?.slots.find((s) => s.time === time);
const allFree = slot?.allFree;
return (
<td
key={date}
onClick={() => allFree && onSlotClick?.(date, time)}
style={{
padding: '3px 6px',
border: `1px solid ${token.colorBorderSecondary}`,
background: allFree
? 'rgba(82, 196, 26, 0.15)'
: 'transparent',
cursor: allFree ? 'pointer' : 'default',
textAlign: 'center',
}}
>
{slot && (
<Space size={2}>
{slot.members.map((m) => (
<Tooltip key={m.userId} title={`${m.userName}: ${m.status}`}>
<div
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: statusColors[m.status] || '#999',
display: 'inline-block',
}}
/>
</Tooltip>
))}
</Space>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
<Space style={{ marginTop: 8 }}>
<Space size={4}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#52c41a' }} />
<Text style={{ fontSize: 11 }}>Free</Text>
</Space>
<Space size={4}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#ff4d4f' }} />
<Text style={{ fontSize: 11 }}>Busy</Text>
</Space>
<Space size={4}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#faad14' }} />
<Text style={{ fontSize: 11 }}>Tentative</Text>
</Space>
</Space>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,156 @@
import { useState, useEffect, useCallback } from 'react';
import { List, Input, Button, Typography, Space, Popconfirm, message, Empty, Skeleton } from 'antd';
import { DeleteOutlined, SendOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { api } from '@/lib/api';
import type { SharedViewComment } from '@/types/api';
dayjs.extend(relativeTime);
const { Text } = Typography;
interface CalendarCommentsProps {
viewId: string;
date: string;
currentUserId: string;
}
export default function CalendarComments({ viewId, date, currentUserId }: CalendarCommentsProps) {
const [comments, setComments] = useState<SharedViewComment[]>([]);
const [loading, setLoading] = useState(true);
const [newComment, setNewComment] = useState('');
const [submitting, setSubmitting] = useState(false);
const fetchComments = useCallback(async () => {
try {
const { data } = await api.get<SharedViewComment[]>(
`/calendar/shared/${viewId}/comments`,
{ params: { date } },
);
setComments(data);
} catch {
// ignore
} finally {
setLoading(false);
}
}, [viewId, date]);
useEffect(() => {
setLoading(true);
fetchComments();
}, [fetchComments]);
const handleSubmit = async () => {
if (!newComment.trim()) return;
setSubmitting(true);
try {
await api.post(`/calendar/shared/${viewId}/comments`, {
itemDate: date,
content: newComment.trim(),
});
setNewComment('');
await fetchComments();
} catch {
message.error('Failed to post comment');
} finally {
setSubmitting(false);
}
};
const handleDelete = async (commentId: string) => {
try {
await api.delete(`/calendar/shared/${viewId}/comments/${commentId}`);
setComments((prev) => prev.filter((c) => c.id !== commentId));
} catch {
message.error('Failed to delete comment');
}
};
if (loading) return <Skeleton active paragraph={{ rows: 2 }} />;
return (
<div>
<Text strong style={{ fontSize: 13, marginBottom: 8, display: 'block' }}>
Comments
</Text>
{comments.length === 0 ? (
<Empty description="No comments yet" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<List
size="small"
dataSource={comments}
renderItem={(comment) => (
<List.Item
style={{ padding: '6px 0', alignItems: 'flex-start' }}
actions={
comment.userId === currentUserId
? [
<Popconfirm
key="delete"
title="Delete comment?"
onConfirm={() => handleDelete(comment.id)}
okText="Delete"
okType="danger"
>
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>,
]
: undefined
}
>
<div>
<Space size={4}>
<div
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: 'rgba(157, 78, 221, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 11,
flexShrink: 0,
}}
>
{(comment.user.name || comment.user.email)[0]?.toUpperCase()}
</div>
<Text strong style={{ fontSize: 12 }}>
{comment.user.name || comment.user.email}
</Text>
<Text type="secondary" style={{ fontSize: 11 }}>
{dayjs(comment.createdAt).fromNow()}
</Text>
</Space>
<div style={{ marginLeft: 28, marginTop: 2, fontSize: 13 }}>
{comment.content}
</div>
</div>
</List.Item>
)}
/>
)}
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<Input
size="small"
placeholder="Add a comment..."
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
onPressEnter={handleSubmit}
disabled={submitting}
/>
<Button
size="small"
type="primary"
icon={<SendOutlined />}
onClick={handleSubmit}
loading={submitting}
disabled={!newComment.trim()}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,172 @@
import { useState, useEffect, useCallback } from 'react';
import {
List,
Button,
Modal,
Form,
Checkbox,
Select,
Typography,
message,
} from 'antd';
import {
PlusOutlined,
CopyOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@/lib/api';
import type { CalendarExportToken, CalendarLayer } from '@/types/api';
const { Text } = Typography;
interface Props {
layers: CalendarLayer[];
}
export default function CalendarExportPanel({ layers }: Props) {
const [tokens, setTokens] = useState<CalendarExportToken[]>([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const fetchTokens = useCallback(async () => {
try {
const { data } = await api.get<{ tokens: CalendarExportToken[] }>('/calendar/export/tokens');
setTokens(data.tokens);
} catch {
// ignore
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchTokens();
}, [fetchTokens]);
const handleCreate = async () => {
try {
const values = await form.validateFields();
await api.post('/calendar/export/tokens', {
includePersonal: values.includePersonal ?? true,
includeLayers: values.includeLayers?.length ? values.includeLayers : null,
});
message.success('Export link created');
setModalOpen(false);
form.resetFields();
await fetchTokens();
} catch {
message.error('Failed to create export link');
}
};
const handleRevoke = (token: CalendarExportToken) => {
Modal.confirm({
title: 'Revoke export link?',
content: 'Anyone using this link will no longer be able to access your calendar.',
okText: 'Revoke',
okType: 'danger',
onOk: async () => {
try {
await api.delete(`/calendar/export/tokens/${token.id}`);
message.success('Export link revoked');
await fetchTokens();
} catch {
message.error('Failed to revoke link');
}
},
});
};
const copyUrl = (token: string) => {
const url = `${window.location.origin}/api/calendar/feed/${token}.ics`;
navigator.clipboard.writeText(url).then(
() => message.success('URL copied'),
() => message.error('Failed to copy'),
);
};
const describeScope = (t: CalendarExportToken) => {
const parts: string[] = [];
if (t.includePersonal) parts.push('Personal events');
if (t.includeLayers?.length) {
const names = t.includeLayers
.map((id) => layers.find((l) => l.id === id)?.name)
.filter(Boolean);
if (names.length) parts.push(names.join(', '));
}
return parts.length ? parts.join(' + ') : 'All layers';
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Text strong>Export Calendar</Text>
<Button size="small" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setModalOpen(true); }}>
Create Export Link
</Button>
</div>
<List
size="small"
loading={loading}
dataSource={tokens}
locale={{ emptyText: 'No export links' }}
renderItem={(t) => (
<List.Item
actions={[
<Button
key="copy"
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyUrl(t.token)}
/>,
<Button
key="revoke"
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleRevoke(t)}
/>,
]}
>
<List.Item.Meta
title={<Text style={{ fontSize: 13 }}>{describeScope(t)}</Text>}
description={
<Text style={{ fontSize: 11 }} type="secondary">
Created {dayjs(t.createdAt).format('MMM D, YYYY')}
</Text>
}
/>
</List.Item>
)}
/>
<Modal
title="Create Export Link"
open={modalOpen}
onOk={handleCreate}
onCancel={() => { setModalOpen(false); form.resetFields(); }}
okText="Create"
destroyOnClose
>
<Form form={form} layout="vertical" initialValues={{ includePersonal: true }}>
<Form.Item name="includePersonal" valuePropName="checked">
<Checkbox>Include personal events</Checkbox>
</Form.Item>
<Form.Item name="includeLayers" label="Include specific layers (optional)">
<Select
mode="multiple"
allowClear
placeholder="All layers"
options={layers.map((l) => ({ value: l.id, label: l.name }))}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@ -0,0 +1,233 @@
import { useState, useEffect, useCallback } from 'react';
import {
List,
Button,
Tag,
Modal,
Form,
Input,
Select,
Tooltip,
Space,
message,
Typography,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
SyncOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { api } from '@/lib/api';
import type { CalendarFeed, CalendarFeedInterval } from '@/types/api';
dayjs.extend(relativeTime);
const { Text } = Typography;
const INTERVAL_OPTIONS: { value: CalendarFeedInterval; label: string }[] = [
{ value: 'FIFTEEN_MIN', label: 'Every 15 minutes' },
{ value: 'HOURLY', label: 'Hourly' },
{ value: 'SIX_HOUR', label: 'Every 6 hours' },
{ value: 'DAILY', label: 'Daily' },
];
export default function CalendarFeedsPanel() {
const [feeds, setFeeds] = useState<CalendarFeed[]>([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editingFeed, setEditingFeed] = useState<CalendarFeed | null>(null);
const [refreshingId, setRefreshingId] = useState<string | null>(null);
const [form] = Form.useForm();
const fetchFeeds = useCallback(async () => {
try {
const { data } = await api.get<{ feeds: CalendarFeed[] }>('/calendar/feeds');
setFeeds(data.feeds);
} catch {
// ignore
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchFeeds();
}, [fetchFeeds]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingFeed) {
await api.patch(`/calendar/feeds/${editingFeed.id}`, values);
message.success('Feed updated');
} else {
await api.post('/calendar/feeds', values);
message.success('Feed added');
}
setModalOpen(false);
setEditingFeed(null);
form.resetFields();
await fetchFeeds();
} catch {
message.error('Failed to save feed');
}
};
const handleDelete = (feed: CalendarFeed) => {
Modal.confirm({
title: 'Delete feed?',
content: `Remove "${feed.name}" and all its imported events?`,
okText: 'Delete',
okType: 'danger',
onOk: async () => {
try {
await api.delete(`/calendar/feeds/${feed.id}`);
message.success('Feed deleted');
await fetchFeeds();
} catch {
message.error('Failed to delete feed');
}
},
});
};
const handleRefresh = async (feed: CalendarFeed) => {
setRefreshingId(feed.id);
try {
await api.post(`/calendar/feeds/${feed.id}/refresh`);
message.success('Feed refreshed');
await fetchFeeds();
} catch {
message.error('Failed to refresh feed');
} finally {
setRefreshingId(null);
}
};
const openEdit = (feed: CalendarFeed) => {
setEditingFeed(feed);
form.setFieldsValue({
name: feed.name,
url: feed.url,
refreshInterval: feed.refreshInterval,
});
setModalOpen(true);
};
const openAdd = () => {
setEditingFeed(null);
form.resetFields();
setModalOpen(true);
};
const statusTag = (feed: CalendarFeed) => {
const colorMap: Record<string, string> = { OK: 'green', ERROR: 'red', PENDING: 'gold' };
const tag = (
<Tag color={colorMap[feed.lastStatus] ?? 'default'}>
{feed.lastStatus}
</Tag>
);
if (feed.lastStatus === 'ERROR' && feed.lastError) {
return <Tooltip title={feed.lastError}>{tag}</Tooltip>;
}
return tag;
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Text strong>External Feeds</Text>
<Button size="small" icon={<PlusOutlined />} onClick={openAdd}>
Add Feed
</Button>
</div>
<List
size="small"
loading={loading}
dataSource={feeds}
locale={{ emptyText: 'No external feeds' }}
renderItem={(feed) => (
<List.Item
actions={[
<Button
key="edit"
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(feed)}
/>,
<Button
key="refresh"
type="text"
size="small"
icon={<SyncOutlined spin={refreshingId === feed.id} />}
onClick={() => handleRefresh(feed)}
/>,
<Button
key="delete"
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(feed)}
/>,
]}
>
<List.Item.Meta
title={
<Space size={8}>
<Text style={{ fontSize: 13 }}>{feed.name}</Text>
{statusTag(feed)}
</Space>
}
description={
<Space size={4} style={{ fontSize: 11 }}>
<span>{feed.itemCount} events</span>
{feed.lastFetchedAt && (
<span>· {dayjs(feed.lastFetchedAt).fromNow()}</span>
)}
</Space>
}
/>
</List.Item>
)}
/>
<Modal
title={editingFeed ? 'Edit Feed' : 'Add External Feed'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => {
setModalOpen(false);
setEditingFeed(null);
form.resetFields();
}}
okText={editingFeed ? 'Save' : 'Add'}
destroyOnClose
>
<Form form={form} layout="vertical" initialValues={{ refreshInterval: 'HOURLY' }}>
<Form.Item name="name" label="Name" rules={[{ required: true, message: 'Enter a name' }]}>
<Input placeholder="e.g. Work Calendar" />
</Form.Item>
<Form.Item
name="url"
label="ICS URL"
rules={[
{ required: true, message: 'Enter an ICS URL' },
{ type: 'url', message: 'Enter a valid URL' },
]}
>
<Input placeholder="https://example.com/calendar.ics" />
</Form.Item>
<Form.Item name="refreshInterval" label="Refresh Interval">
<Select options={INTERVAL_OPTIONS} />
</Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@ -0,0 +1,192 @@
import { Drawer, Button, Space, Tag, Typography, Divider, theme } from 'antd';
import {
ClockCircleOutlined,
EnvironmentOutlined,
CalendarOutlined,
EditOutlined,
DeleteOutlined,
BellOutlined,
LockOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import type { PersonalCalendarItem } from '@/types/api';
import { hexToRgba, formatTimeShort } from './calendarUtils';
const { Text, Title } = Typography;
interface CalendarItemDetailProps {
item: PersonalCalendarItem | null;
open: boolean;
onClose: () => void;
onEdit: (item: PersonalCalendarItem) => void;
onDelete: (item: PersonalCalendarItem) => void;
}
export default function CalendarItemDetail({
item,
open,
onClose,
onEdit,
onDelete,
}: CalendarItemDetailProps) {
const { token } = theme.useToken();
if (!item) return null;
const isPersonal = item.type === 'personal';
const isTimeBlock = item.itemType === 'TIME_BLOCK';
const isReminder = item.itemType === 'REMINDER';
const color = item.color || token.colorPrimary;
return (
<Drawer
open={open}
onClose={onClose}
width={360}
title={null}
closable={false}
styles={{
body: { padding: '16px 20px', display: 'flex', flexDirection: 'column' },
}}
>
{/* Color bar + title */}
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start', marginBottom: 16 }}>
<div
style={{
width: 4,
height: 40,
borderRadius: 2,
background: color,
flexShrink: 0,
marginTop: 2,
}}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<Title level={5} style={{ margin: 0 }}>
{isReminder && <BellOutlined style={{ marginRight: 6, fontSize: 14 }} />}
{isTimeBlock && item.showDetailsTo === 'NOBODY' ? (
<>
<LockOutlined style={{ marginRight: 6, fontSize: 14 }} />
Busy
</>
) : (
item.title
)}
</Title>
{item.type !== 'personal' && (
<Tag
color="blue"
style={{ marginTop: 4, fontSize: 11, textTransform: 'capitalize' }}
>
{item.type}
</Tag>
)}
{isTimeBlock && (
<Tag style={{ marginTop: 4, fontSize: 11 }}>Time Block</Tag>
)}
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
{/* Date & time */}
<DetailRow icon={<CalendarOutlined />}>
{dayjs(item.date).format('dddd, MMMM D, YYYY')}
</DetailRow>
<DetailRow icon={<ClockCircleOutlined />}>
{item.isAllDay
? 'All day'
: `${formatTimeShort(item.startTime)} - ${formatTimeShort(item.endTime)}`}
</DetailRow>
{/* Location */}
{item.location && (
<DetailRow icon={<EnvironmentOutlined />}>
{item.location}
</DetailRow>
)}
{/* Busy status */}
{item.busyStatus && item.busyStatus !== 'BUSY' && (
<DetailRow icon={null}>
<Tag style={{ fontSize: 11, textTransform: 'capitalize' }}>
{item.busyStatus.toLowerCase().replace('_', ' ')}
</Tag>
</DetailRow>
)}
{/* Recurrence */}
{item.isRecurring && (
<DetailRow icon={null}>
<Tag color="purple" style={{ fontSize: 11 }}>Recurring</Tag>
</DetailRow>
)}
{/* Layer color indicator */}
<div style={{ marginTop: 16 }}>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
padding: '4px 10px',
borderRadius: 12,
background: hexToRgba(color, 0.12),
border: `1px solid ${hexToRgba(color, 0.25)}`,
fontSize: 12,
}}
>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: color }} />
<Text style={{ fontSize: 12, color: 'rgba(255,255,255,0.65)' }}>
{item.type === 'personal' ? 'Personal' : item.type}
</Text>
</div>
</div>
{/* Bottom actions: Close on left, Edit/Delete on right */}
<div style={{ marginTop: 'auto', paddingTop: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button onClick={onClose}>
Close
</Button>
{isPersonal && (
<Space>
<Button
danger
icon={<DeleteOutlined />}
onClick={() => {
onClose();
onDelete(item);
}}
>
Delete
</Button>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => {
onClose();
onEdit(item);
}}
>
Edit
</Button>
</Space>
)}
</div>
</Drawer>
);
}
function DetailRow({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
{icon && (
<span style={{ fontSize: 14, color: 'rgba(255,255,255,0.45)', width: 16, textAlign: 'center' }}>
{icon}
</span>
)}
<Text style={{ fontSize: 14 }}>{children}</Text>
</div>
);
}

View File

@ -0,0 +1,479 @@
import { useState, useEffect, useMemo } from 'react';
import {
Modal,
Form,
Input,
DatePicker,
TimePicker,
Select,
Switch,
Collapse,
Radio,
Button,
Typography,
Space,
} from 'antd';
import {
CalendarOutlined,
ClockCircleOutlined,
BellOutlined,
BlockOutlined,
EnvironmentOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import RecurrenceEditor from './RecurrenceEditor';
import type {
CalendarLayer,
PersonalCalendarItem,
CalendarItemType,
CalendarVisibility,
CalendarBusyStatus,
CalendarShowDetailsTo,
CalendarRecurrenceRule,
SeriesEditScope,
} from '@/types/api';
const { TextArea } = Input;
const { Text } = Typography;
const PRESET_COLORS = [
'#1890ff', '#52c41a', '#fa8c16', '#722ed1', '#eb2f96',
'#13c2c2', '#f5222d', '#faad14', '#2f54eb', '#a0d911',
];
const ITEM_TYPE_OPTIONS: { value: CalendarItemType; label: string; icon: React.ReactNode }[] = [
{ value: 'EVENT', label: 'Event', icon: <CalendarOutlined /> },
{ value: 'TIME_BLOCK', label: 'Time Block', icon: <BlockOutlined /> },
{ value: 'REMINDER', label: 'Reminder', icon: <BellOutlined /> },
];
export interface CalendarItemFormData {
layerId: string;
title: string;
description?: string;
date: string;
startTime: string;
endTime: string;
isAllDay: boolean;
itemType: CalendarItemType;
location?: string;
color?: string;
visibility?: CalendarVisibility | null;
busyStatus: CalendarBusyStatus;
showDetailsTo: CalendarShowDetailsTo;
recurrenceRule?: CalendarRecurrenceRule | null;
recurrenceEnd?: string | null;
}
interface CalendarItemModalProps {
open: boolean;
onCancel: () => void;
onSave: (data: CalendarItemFormData, scope?: SeriesEditScope) => void;
onDelete?: () => void;
item?: PersonalCalendarItem | null;
defaultDate?: string | null;
layers: CalendarLayer[];
loading?: boolean;
}
export default function CalendarItemModal({
open,
onCancel,
onSave,
onDelete,
item,
defaultDate,
layers,
loading,
}: CalendarItemModalProps) {
const [form] = Form.useForm();
const [itemType, setItemType] = useState<CalendarItemType>('EVENT');
const [isAllDay, setIsAllDay] = useState(false);
const [colorOverride, setColorOverride] = useState<string | undefined>(undefined);
const [recurrenceRule, setRecurrenceRule] = useState<CalendarRecurrenceRule | null>(null);
const [recurrenceEnd, setRecurrenceEnd] = useState<string | null>(null);
const [editScope, setEditScope] = useState<SeriesEditScope>('THIS_ONLY');
const isEditing = !!item;
const isRecurringEdit = isEditing && !!item?.seriesId;
const userLayers = useMemo(
() => layers.filter((l) => l.layerType === 'USER'),
[layers],
);
// Reset form when modal opens
useEffect(() => {
if (!open) return;
if (item) {
form.setFieldsValue({
title: item.title ?? '',
layerId: item.layerId ?? userLayers[0]?.id,
date: item.date ? dayjs(item.date) : dayjs(),
startTime: item.startTime ? dayjs(item.startTime, 'HH:mm') : dayjs('09:00', 'HH:mm'),
endTime: item.endTime ? dayjs(item.endTime, 'HH:mm') : dayjs('10:00', 'HH:mm'),
description: '',
location: item.location ?? '',
visibility: null,
busyStatus: item.busyStatus ?? 'BUSY',
showDetailsTo: item.showDetailsTo ?? 'FRIENDS',
});
setItemType(item.itemType ?? 'EVENT');
setIsAllDay(item.isAllDay ?? false);
setColorOverride(item.color !== '#1890ff' ? item.color : undefined);
setRecurrenceRule(null);
setRecurrenceEnd(null);
} else {
form.resetFields();
const date = defaultDate ? dayjs(defaultDate) : dayjs();
form.setFieldsValue({
date,
startTime: dayjs('09:00', 'HH:mm'),
endTime: dayjs('10:00', 'HH:mm'),
layerId: userLayers[0]?.id,
busyStatus: 'BUSY',
showDetailsTo: 'FRIENDS',
visibility: null,
});
setItemType('EVENT');
setIsAllDay(false);
setColorOverride(undefined);
setRecurrenceRule(null);
setRecurrenceEnd(null);
}
setEditScope('THIS_ONLY');
}, [open, item, defaultDate, userLayers, form]);
const handleFinish = (values: Record<string, unknown>) => {
const data: CalendarItemFormData = {
layerId: values.layerId as string,
title: values.title as string,
description: (values.description as string) || undefined,
date: (values.date as dayjs.Dayjs).format('YYYY-MM-DD'),
startTime: isAllDay ? '00:00' : (values.startTime as dayjs.Dayjs).format('HH:mm'),
endTime: isAllDay ? '23:59' : (values.endTime as dayjs.Dayjs).format('HH:mm'),
isAllDay,
itemType,
location: (values.location as string) || undefined,
color: colorOverride,
visibility: (values.visibility as CalendarVisibility | null) ?? null,
busyStatus: (values.busyStatus as CalendarBusyStatus) ?? 'BUSY',
showDetailsTo: (values.showDetailsTo as CalendarShowDetailsTo) ?? 'FRIENDS',
recurrenceRule: recurrenceRule ?? undefined,
recurrenceEnd: recurrenceEnd ?? undefined,
};
onSave(data, isRecurringEdit ? editScope : undefined);
};
return (
<Modal
open={open}
onCancel={onCancel}
title={isEditing ? 'Edit Calendar Item' : 'New Calendar Item'}
footer={null}
width={520}
destroyOnClose
>
<Form
form={form}
layout="vertical"
onFinish={handleFinish}
style={{ marginTop: 16 }}
>
{/* Item type selector */}
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{ITEM_TYPE_OPTIONS.map((opt) => {
const selected = itemType === opt.value;
return (
<div
key={opt.value}
onClick={() => setItemType(opt.value)}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: '8px 12px',
borderRadius: 8,
cursor: 'pointer',
userSelect: 'none',
fontSize: 13,
fontWeight: selected ? 600 : 400,
background: selected ? 'rgba(24, 144, 255, 0.15)' : 'rgba(255,255,255,0.04)',
border: selected
? '1px solid rgba(24, 144, 255, 0.4)'
: '1px solid rgba(255,255,255,0.1)',
color: selected ? '#1890ff' : 'rgba(255,255,255,0.65)',
transition: 'all 0.2s',
}}
>
{opt.icon}
{opt.label}
</div>
);
})}
</div>
<Form.Item
name="title"
rules={[{ required: true, message: 'Title is required' }]}
>
<Input
placeholder={
itemType === 'REMINDER'
? 'Reminder title...'
: itemType === 'TIME_BLOCK'
? 'Block title (e.g. "Focus time")...'
: 'Event title...'
}
size="large"
style={{ fontSize: 16 }}
/>
</Form.Item>
<Form.Item
name="layerId"
label="Layer"
rules={[{ required: true, message: 'Select a layer' }]}
>
<Select
options={userLayers.map((l) => ({
value: l.id,
label: (
<Space>
<span
style={{
width: 10,
height: 10,
borderRadius: '50%',
background: l.color,
display: 'inline-block',
}}
/>
{l.name}
</Space>
),
}))}
placeholder="Select layer"
/>
</Form.Item>
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
<Form.Item
name="date"
label="Date"
rules={[{ required: true, message: 'Required' }]}
style={{ flex: 1 }}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item label=" " style={{ paddingTop: 4 }}>
<Space>
<Switch
checked={isAllDay}
onChange={setIsAllDay}
size="small"
/>
<Text style={{ fontSize: 13, color: 'rgba(255,255,255,0.65)' }}>All day</Text>
</Space>
</Form.Item>
</div>
{!isAllDay && (
<div style={{ display: 'flex', gap: 12 }}>
<Form.Item
name="startTime"
label="Start"
rules={[{ required: true, message: 'Required' }]}
style={{ flex: 1 }}
>
<TimePicker format="HH:mm" minuteStep={5} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="endTime"
label="End"
rules={[{ required: true, message: 'Required' }]}
style={{ flex: 1 }}
>
<TimePicker format="HH:mm" minuteStep={5} style={{ width: '100%' }} />
</Form.Item>
</div>
)}
{itemType === 'EVENT' && (
<Form.Item name="location" label="Location">
<Input prefix={<EnvironmentOutlined />} placeholder="Optional location" />
</Form.Item>
)}
<Form.Item name="description" label="Description">
<TextArea rows={2} placeholder="Optional description" />
</Form.Item>
{/* Time block specific fields */}
{itemType === 'TIME_BLOCK' && (
<div style={{ display: 'flex', gap: 12 }}>
<Form.Item name="busyStatus" label="Status" style={{ flex: 1 }}>
<Select
options={[
{ value: 'BUSY', label: 'Busy' },
{ value: 'TENTATIVE', label: 'Tentative' },
{ value: 'FREE', label: 'Free' },
]}
/>
</Form.Item>
<Form.Item name="showDetailsTo" label="Show details to" style={{ flex: 1 }}>
<Select
options={[
{ value: 'NOBODY', label: 'Nobody' },
{ value: 'FRIENDS', label: 'Friends' },
{ value: 'EVERYONE', label: 'Everyone' },
]}
/>
</Form.Item>
</div>
)}
{/* Visibility override */}
<Form.Item name="visibility" label="Visibility override">
<Select
allowClear
placeholder="Inherit from layer"
options={[
{ value: 'PRIVATE', label: 'Private' },
{ value: 'FRIENDS', label: 'Friends only' },
{ value: 'PUBLIC', label: 'Public' },
]}
/>
</Form.Item>
{/* Color override */}
<div style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 6 }}>
Color override
</Text>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<div
onClick={() => setColorOverride(undefined)}
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: 'rgba(255,255,255,0.08)',
cursor: 'pointer',
border: !colorOverride ? '2px solid #1890ff' : '2px solid transparent',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 10,
color: 'rgba(255,255,255,0.45)',
}}
title="Use layer color"
>
Auto
</div>
{PRESET_COLORS.map((c) => (
<div
key={c}
onClick={() => setColorOverride(c)}
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: c,
cursor: 'pointer',
border: colorOverride === c ? '2px solid #fff' : '2px solid transparent',
}}
/>
))}
</div>
</div>
{/* Recurrence */}
<Collapse
ghost
items={[
{
key: 'recurrence',
label: (
<Text style={{ fontSize: 13, color: 'rgba(255,255,255,0.65)' }}>
<ClockCircleOutlined style={{ marginRight: 6 }} />
Recurrence
{recurrenceRule && (
<span style={{ color: '#1890ff', marginLeft: 8, fontSize: 12 }}>
(configured)
</span>
)}
</Text>
),
children: (
<RecurrenceEditor
value={recurrenceRule}
endDate={recurrenceEnd}
onChange={(rule, end) => {
setRecurrenceRule(rule);
setRecurrenceEnd(end);
}}
/>
),
},
]}
style={{ marginBottom: 16, background: 'rgba(255,255,255,0.02)', borderRadius: 8 }}
/>
{/* Edit scope for recurring items */}
{isRecurringEdit && (
<div
style={{
marginBottom: 16,
padding: 12,
background: 'rgba(250, 140, 22, 0.08)',
border: '1px solid rgba(250, 140, 22, 0.2)',
borderRadius: 8,
}}
>
<Text style={{ fontSize: 13, color: 'rgba(255,255,255,0.85)', display: 'block', marginBottom: 8 }}>
This is a recurring event. Apply changes to:
</Text>
<Radio.Group
value={editScope}
onChange={(e) => setEditScope(e.target.value)}
>
<Space direction="vertical" size={4}>
<Radio value="THIS_ONLY">This event only</Radio>
<Radio value="THIS_AND_FUTURE">This and future events</Radio>
<Radio value="ALL">All events in the series</Radio>
</Space>
</Radio.Group>
</div>
)}
{/* Actions */}
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginTop: 8 }}>
<div>
{isEditing && onDelete && (
<Button
danger
icon={<DeleteOutlined />}
onClick={onDelete}
>
Delete
</Button>
)}
</div>
<Space>
<Button onClick={onCancel}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={loading}>
{isEditing ? 'Save Changes' : 'Create'}
</Button>
</Space>
</div>
</Form>
</Modal>
);
}

View File

@ -0,0 +1,435 @@
import { useState } from 'react';
import {
Typography,
Switch,
Button,
Input,
Popconfirm,
Tooltip,
Space,
Divider,
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
TeamOutlined,
GlobalOutlined,
LockOutlined,
CheckOutlined,
CloseOutlined,
ThunderboltOutlined,
UserOutlined,
CloudOutlined,
} from '@ant-design/icons';
import type {
CalendarLayer,
CalendarLayerType,
CalendarVisibility,
} from '@/types/api';
const { Text } = Typography;
const PRESET_COLORS = [
'#1890ff', '#52c41a', '#fa8c16', '#722ed1', '#eb2f96',
'#13c2c2', '#f5222d', '#faad14', '#2f54eb', '#a0d911',
];
const VISIBILITY_ICONS: Record<CalendarVisibility, React.ReactNode> = {
PRIVATE: <LockOutlined />,
FRIENDS: <TeamOutlined />,
PUBLIC: <GlobalOutlined />,
};
const VISIBILITY_LABELS: Record<CalendarVisibility, string> = {
PRIVATE: 'Private',
FRIENDS: 'Friends only',
PUBLIC: 'Public',
};
const GROUP_ICONS: Record<CalendarLayerType, React.ReactNode> = {
SYSTEM: <ThunderboltOutlined />,
USER: <UserOutlined />,
EXTERNAL: <CloudOutlined />,
};
const GROUP_LABELS: Record<CalendarLayerType, string> = {
SYSTEM: 'System',
USER: 'Personal',
EXTERNAL: 'External',
};
interface CalendarLayerPanelProps {
layers: CalendarLayer[];
compact?: boolean;
onToggle: (layerId: string, enabled: boolean) => void;
onCreate: (name: string, color: string) => void;
onUpdate: (layerId: string, data: Partial<CalendarLayer>) => void;
onDelete: (layerId: string) => void;
loading?: boolean;
}
export default function CalendarLayerPanel({
layers,
compact,
onToggle,
onCreate,
onUpdate,
onDelete,
loading,
}: CalendarLayerPanelProps) {
const [showAddForm, setShowAddForm] = useState(false);
const [newName, setNewName] = useState('');
const [newColor, setNewColor] = useState(PRESET_COLORS[0]!);
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
const [colorPickerLayerId, setColorPickerLayerId] = useState<string | null>(null);
const grouped = (['SYSTEM', 'USER', 'EXTERNAL'] as CalendarLayerType[]).map((type) => ({
type,
layers: layers.filter((l) => l.layerType === type).sort((a, b) => a.sortOrder - b.sortOrder),
})).filter((g) => g.layers.length > 0 || g.type === 'USER');
const handleCreate = () => {
if (!newName.trim()) return;
onCreate(newName.trim(), newColor);
setNewName('');
setNewColor(PRESET_COLORS[0]!);
setShowAddForm(false);
};
const handleEditSubmit = (layerId: string) => {
if (editName.trim()) {
onUpdate(layerId, { name: editName.trim() });
}
setEditingId(null);
};
const handleColorChange = (layerId: string, color: string) => {
onUpdate(layerId, { color });
setColorPickerLayerId(null);
};
// Compact mode: horizontal strip with just color dots + toggle
if (compact) {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 8,
padding: '8px 0',
marginBottom: 8,
borderBottom: '1px solid rgba(255,255,255,0.08)',
}}
>
{layers.map((layer) => (
<Tooltip key={layer.id} title={layer.name}>
<div
onClick={() => onToggle(layer.id, !layer.isEnabled)}
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
padding: '4px 8px',
borderRadius: 12,
cursor: 'pointer',
userSelect: 'none',
background: layer.isEnabled ? 'rgba(255,255,255,0.06)' : 'transparent',
opacity: layer.isEnabled ? 1 : 0.4,
border: `1px solid ${layer.isEnabled ? layer.color : 'rgba(255,255,255,0.1)'}`,
transition: 'all 0.2s',
}}
>
<div
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: layer.color,
flexShrink: 0,
}}
/>
<Text style={{ fontSize: 11, color: 'rgba(255,255,255,0.75)' }}>
{layer.name}
</Text>
</div>
</Tooltip>
))}
</div>
);
}
const renderColorDot = (layer: CalendarLayer) => {
const isOpen = colorPickerLayerId === layer.id;
return (
<div style={{ position: 'relative' }}>
<div
onClick={() => {
if (layer.layerType === 'SYSTEM') return;
setColorPickerLayerId(isOpen ? null : layer.id);
}}
style={{
width: 14,
height: 14,
borderRadius: '50%',
background: layer.color,
cursor: layer.layerType === 'SYSTEM' ? 'default' : 'pointer',
flexShrink: 0,
border: '2px solid rgba(255,255,255,0.15)',
transition: 'transform 0.15s',
}}
/>
{isOpen && (
<div
style={{
position: 'absolute',
top: 20,
left: 0,
zIndex: 10,
background: '#1b2838',
border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 8,
padding: 8,
display: 'flex',
flexWrap: 'wrap',
gap: 6,
width: 160,
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
}}
>
{PRESET_COLORS.map((c) => (
<div
key={c}
onClick={() => handleColorChange(layer.id, c)}
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: c,
cursor: 'pointer',
border: c === layer.color ? '2px solid #fff' : '2px solid transparent',
transition: 'transform 0.15s',
}}
/>
))}
</div>
)}
</div>
);
};
const renderLayerRow = (layer: CalendarLayer) => {
const isEditing = editingId === layer.id;
const vis = layer.visibility;
return (
<div
key={layer.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '6px 0',
opacity: layer.isEnabled ? 1 : 0.5,
transition: 'opacity 0.2s',
}}
>
{renderColorDot(layer)}
{isEditing ? (
<Input
size="small"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onPressEnter={() => handleEditSubmit(layer.id)}
onBlur={() => handleEditSubmit(layer.id)}
autoFocus
style={{ flex: 1, fontSize: 13 }}
/>
) : (
<Text
style={{
flex: 1,
fontSize: 13,
color: 'rgba(255,255,255,0.85)',
cursor: layer.layerType !== 'SYSTEM' ? 'pointer' : 'default',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
onClick={() => {
if (layer.layerType !== 'SYSTEM') {
setEditingId(layer.id);
setEditName(layer.name);
}
}}
>
{layer.name}
</Text>
)}
<Tooltip title={VISIBILITY_LABELS[vis]}>
<span
style={{
fontSize: 12,
color: vis === 'PUBLIC' ? 'rgba(82,196,26,0.7)' : 'rgba(255,255,255,0.35)',
cursor: layer.layerType !== 'SYSTEM' ? 'pointer' : 'default',
}}
onClick={() => {
if (layer.layerType === 'SYSTEM') return;
const cycle: CalendarVisibility[] = ['PRIVATE', 'FRIENDS', 'PUBLIC'];
const next = cycle[(cycle.indexOf(vis) + 1) % cycle.length];
onUpdate(layer.id, { visibility: next });
}}
>
{VISIBILITY_ICONS[vis]}
</span>
</Tooltip>
<Switch
size="small"
checked={layer.isEnabled}
onChange={(checked) => onToggle(layer.id, checked)}
/>
{layer.layerType !== 'SYSTEM' && (
<Popconfirm
title="Delete this layer?"
description="All items in this layer will be deleted."
onConfirm={() => onDelete(layer.id)}
okText="Delete"
okButtonProps={{ danger: true }}
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
style={{ color: 'rgba(255,255,255,0.25)', padding: '0 4px' }}
/>
</Popconfirm>
)}
</div>
);
};
return (
<div
style={{
background: 'rgba(255,255,255,0.03)',
borderRadius: 8,
border: '1px solid rgba(255,255,255,0.08)',
padding: '12px 16px',
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<Text strong style={{ color: 'rgba(255,255,255,0.85)', fontSize: 14, marginBottom: 4 }}>
Layers
</Text>
{loading && (
<Text type="secondary" style={{ fontSize: 12, padding: '8px 0' }}>
Loading layers...
</Text>
)}
{grouped.map((group) => (
<div key={group.type}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '6px 0 2px',
}}
>
<span style={{ color: 'rgba(255,255,255,0.35)', fontSize: 11 }}>
{GROUP_ICONS[group.type]}
</span>
<Text
type="secondary"
style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.5 }}
>
{GROUP_LABELS[group.type]}
</Text>
</div>
{group.layers.map(renderLayerRow)}
{group.layers.length === 0 && (
<Text type="secondary" style={{ fontSize: 12, padding: '4px 0 4px 22px', display: 'block' }}>
No layers
</Text>
)}
<Divider style={{ margin: '6px 0' }} />
</div>
))}
{showAddForm ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: '8px 0',
}}
>
<Input
size="small"
placeholder="Layer name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
onPressEnter={handleCreate}
autoFocus
/>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{PRESET_COLORS.map((c) => (
<div
key={c}
onClick={() => setNewColor(c)}
style={{
width: 20,
height: 20,
borderRadius: '50%',
background: c,
cursor: 'pointer',
border: c === newColor ? '2px solid #fff' : '2px solid transparent',
}}
/>
))}
</div>
<Space>
<Button
size="small"
type="primary"
icon={<CheckOutlined />}
onClick={handleCreate}
disabled={!newName.trim()}
>
Add
</Button>
<Button
size="small"
icon={<CloseOutlined />}
onClick={() => setShowAddForm(false)}
>
Cancel
</Button>
</Space>
</div>
) : (
<Button
type="dashed"
size="small"
icon={<PlusOutlined />}
onClick={() => setShowAddForm(true)}
block
style={{ marginTop: 4 }}
>
Add Layer
</Button>
)}
</div>
);
}

View File

@ -0,0 +1,97 @@
import { useState } from 'react';
import { Button, Popover, Space, Tooltip, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { api } from '@/lib/api';
import type { SharedViewReactionGroup } from '@/types/api';
const EMOJI_PALETTE = ['👍', '❤️', '🎉', '😄', '🤔', '👀', '🔥', '⭐', '💪', '📅'];
interface CalendarReactionsProps {
viewId: string;
itemId: string;
reactions: SharedViewReactionGroup[];
currentUserId: string;
onUpdate: () => void;
}
export default function CalendarReactions({
viewId,
itemId,
reactions,
currentUserId,
onUpdate,
}: CalendarReactionsProps) {
const [paletteOpen, setPaletteOpen] = useState(false);
const toggleReaction = async (emoji: string) => {
try {
await api.post(`/calendar/shared/${viewId}/reactions`, { itemId, emoji });
onUpdate();
} catch {
message.error('Failed to update reaction');
}
setPaletteOpen(false);
};
return (
<Space size={4} wrap style={{ marginTop: 4 }}>
{reactions.map((r) => {
const hasReacted = r.users.some((u) => u.id === currentUserId);
const tooltip = r.users.map((u) => u.name || 'Someone').join(', ');
return (
<Tooltip key={r.emoji} title={tooltip}>
<Button
size="small"
type={hasReacted ? 'primary' : 'default'}
style={{
fontSize: 13,
padding: '0 6px',
height: 24,
borderRadius: 12,
opacity: hasReacted ? 1 : 0.7,
}}
onClick={() => toggleReaction(r.emoji)}
>
{r.emoji} {r.count}
</Button>
</Tooltip>
);
})}
<Popover
open={paletteOpen}
onOpenChange={setPaletteOpen}
trigger="click"
placement="bottom"
content={
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, maxWidth: 200 }}>
{EMOJI_PALETTE.map((emoji) => (
<Button
key={emoji}
size="small"
type="text"
style={{ fontSize: 18, width: 36, height: 36 }}
onClick={() => toggleReaction(emoji)}
>
{emoji}
</Button>
))}
</div>
}
>
<Button
size="small"
type="text"
icon={<PlusOutlined />}
style={{
fontSize: 12,
height: 24,
width: 24,
borderRadius: 12,
opacity: 0.5,
}}
/>
</Popover>
</Space>
);
}

View File

@ -0,0 +1,525 @@
import { useMemo, useRef, useEffect, useCallback } from 'react';
import { Typography, theme } from 'antd';
import {
BellOutlined,
EnvironmentOutlined,
LockOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import type { PersonalCalendarItem } from '@/types/api';
import {
hexToRgba,
assignLanes,
formatHourLabel,
getViewDates,
formatTimeShort,
START_HOUR,
END_HOUR,
SLOT_HEIGHT,
} from './calendarUtils';
const { Text } = Typography;
interface CalendarTimeGridProps {
items: PersonalCalendarItem[];
viewMode: 'day' | '3day' | 'week';
currentDate: dayjs.Dayjs;
onDateSelect: (date: string) => void;
onItemClick: (item: PersonalCalendarItem) => void;
onNavigate: (direction: 'prev' | 'next') => void;
}
const GUTTER_WIDTH = 44;
const totalHeight = (END_HOUR - START_HOUR) * SLOT_HEIGHT;
const SWIPE_THRESHOLD = 50;
export default function CalendarTimeGrid({
items,
viewMode,
currentDate,
onDateSelect,
onItemClick,
onNavigate,
}: CalendarTimeGridProps) {
const { token } = theme.useToken();
const scrollRef = useRef<HTMLDivElement>(null);
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
const dates = useMemo(() => getViewDates(viewMode, currentDate), [viewMode, currentDate]);
const todayKey = dayjs().format('YYYY-MM-DD');
// Scroll to current time on mount
useEffect(() => {
if (scrollRef.current) {
const now = dayjs();
const minutes = now.hour() * 60 + now.minute();
const scrollTo = ((minutes - START_HOUR * 60) / 60) * SLOT_HEIGHT - 100;
scrollRef.current.scrollTop = Math.max(0, scrollTo);
}
}, []);
// Group items by date
const itemsByDate = useMemo(() => {
const map: Record<string, PersonalCalendarItem[]> = {};
for (const item of items) {
if (!map[item.date]) map[item.date] = [];
map[item.date]!.push(item);
}
return map;
}, [items]);
// Touch swipe handling
const handleTouchStart = useCallback((e: React.TouchEvent) => {
const touch = e.touches[0];
if (touch) {
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
}
}, []);
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
if (!touchStartRef.current) return;
const touch = e.changedTouches[0];
if (!touch) return;
const dx = touch.clientX - touchStartRef.current.x;
const dy = touch.clientY - touchStartRef.current.y;
touchStartRef.current = null;
// Only trigger swipe if horizontal movement is dominant
if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.5) {
onNavigate(dx < 0 ? 'next' : 'prev');
}
}, [onNavigate]);
// Current time indicator
const nowIndicatorTop = useMemo(() => {
const now = dayjs();
const minutes = now.hour() * 60 + now.minute();
const top = ((minutes - START_HOUR * 60) / 60) * SLOT_HEIGHT;
if (top < 0 || top > totalHeight) return null;
return top;
}, []);
const columnCount = dates.length;
const isNarrow = viewMode === 'week';
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 180px)',
background: 'rgba(255,255,255,0.02)',
borderRadius: 8,
border: '1px solid rgba(255,255,255,0.08)',
overflow: 'hidden',
}}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Column headers */}
<div
style={{
display: 'flex',
borderBottom: '1px solid rgba(255,255,255,0.08)',
background: 'rgba(255,255,255,0.03)',
flexShrink: 0,
}}
>
{/* Gutter spacer */}
<div style={{ width: GUTTER_WIDTH, flexShrink: 0 }} />
{dates.map((date) => {
const dateKey = date.format('YYYY-MM-DD');
const isToday = dateKey === todayKey;
return (
<div
key={dateKey}
onClick={() => onDateSelect(dateKey)}
style={{
flex: 1,
textAlign: 'center',
padding: '8px 2px',
cursor: 'pointer',
borderLeft: '1px solid rgba(255,255,255,0.06)',
}}
>
<Text
style={{
fontSize: isNarrow ? 11 : 12,
color: 'rgba(255,255,255,0.45)',
display: 'block',
textTransform: 'uppercase',
letterSpacing: 0.5,
}}
>
{isNarrow ? date.format('dd') : date.format('ddd')}
</Text>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: isNarrow ? 24 : 28,
height: isNarrow ? 24 : 28,
borderRadius: '50%',
background: isToday ? token.colorPrimary : 'transparent',
color: isToday ? '#fff' : 'rgba(255,255,255,0.85)',
fontSize: isNarrow ? 13 : 15,
fontWeight: isToday ? 700 : 500,
marginTop: 2,
}}
>
{date.format('D')}
</div>
</div>
);
})}
</div>
{/* All-day section */}
<AllDaySection
dates={dates}
itemsByDate={itemsByDate}
onItemClick={onItemClick}
isNarrow={isNarrow}
/>
{/* Scrollable time grid */}
<div
ref={scrollRef}
style={{
flex: 1,
overflowY: 'auto',
overflowX: 'hidden',
position: 'relative',
}}
>
<div style={{ position: 'relative', height: totalHeight }}>
{/* Hour lines */}
{Array.from({ length: END_HOUR - START_HOUR + 1 }, (_, i) => {
const hour = START_HOUR + i;
const top = i * SLOT_HEIGHT;
return (
<div
key={hour}
style={{
position: 'absolute',
top,
left: 0,
right: 0,
display: 'flex',
alignItems: 'flex-start',
}}
>
<Text
style={{
width: GUTTER_WIDTH,
fontSize: 11,
color: 'rgba(255,255,255,0.3)',
textAlign: 'right',
paddingRight: 8,
marginTop: -7,
flexShrink: 0,
}}
>
{formatHourLabel(hour)}
</Text>
<div
style={{
flex: 1,
borderTop: '1px solid rgba(255,255,255,0.06)',
height: 0,
}}
/>
</div>
);
})}
{/* Column dividers */}
{dates.map((date, i) => {
if (i === 0) return null;
return (
<div
key={`div-${date.format('YYYY-MM-DD')}`}
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: `calc(${GUTTER_WIDTH}px + ${(i / columnCount) * 100}% * (1 - ${GUTTER_WIDTH}px / 100%))`,
width: 1,
background: 'rgba(255,255,255,0.06)',
}}
/>
);
})}
{/* Column click zones (for creating events on empty space) */}
{dates.map((date, i) => {
const dateKey = date.format('YYYY-MM-DD');
return (
<div
key={`zone-${dateKey}`}
onClick={() => onDateSelect(dateKey)}
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: `calc(${GUTTER_WIDTH}px + ${(i / columnCount)} * (100% - ${GUTTER_WIDTH}px))`,
width: `calc((100% - ${GUTTER_WIDTH}px) / ${columnCount})`,
cursor: 'pointer',
}}
/>
);
})}
{/* Current time indicator */}
{nowIndicatorTop !== null && dates.some((d) => d.format('YYYY-MM-DD') === todayKey) && (
<div
style={{
position: 'absolute',
top: nowIndicatorTop,
left: GUTTER_WIDTH,
right: 0,
height: 2,
background: token.colorError,
zIndex: 5,
borderRadius: 1,
pointerEvents: 'none',
}}
>
<div
style={{
position: 'absolute',
left: -4,
top: -3,
width: 8,
height: 8,
borderRadius: '50%',
background: token.colorError,
}}
/>
</div>
)}
{/* Events per column */}
{dates.map((date, colIndex) => {
const dateKey = date.format('YYYY-MM-DD');
const dayItems = (itemsByDate[dateKey] ?? []).filter((it) => !it.isAllDay);
const lanedItems = assignLanes(dayItems);
return lanedItems.map(({ item, top, height, lane, totalLanes }) => {
const colWidth = `calc((100% - ${GUTTER_WIDTH}px) / ${columnCount})`;
const colLeft = `calc(${GUTTER_WIDTH}px + ${colIndex} * (100% - ${GUTTER_WIDTH}px) / ${columnCount})`;
const laneWidth = totalLanes > 1 ? `calc(${100 / totalLanes}% - 2px)` : 'calc(100% - 6px)';
const laneOffset = totalLanes > 1 ? `calc(${(lane / totalLanes) * 100}% + 1px)` : '3px';
return (
<EventBlock
key={item.id}
item={item}
top={top}
height={height}
colLeft={colLeft}
colWidth={colWidth}
laneWidth={laneWidth}
laneOffset={laneOffset}
isNarrow={isNarrow}
onClick={() => onItemClick(item)}
/>
);
});
})}
</div>
</div>
</div>
);
}
/** All-day events section spanning across columns */
function AllDaySection({
dates,
itemsByDate,
onItemClick,
isNarrow,
}: {
dates: dayjs.Dayjs[];
itemsByDate: Record<string, PersonalCalendarItem[]>;
onItemClick: (item: PersonalCalendarItem) => void;
isNarrow: boolean;
}) {
const hasAllDay = dates.some((d) => {
const items = itemsByDate[d.format('YYYY-MM-DD')];
return items?.some((it) => it.isAllDay);
});
if (!hasAllDay) return null;
return (
<div
style={{
display: 'flex',
borderBottom: '1px solid rgba(255,255,255,0.08)',
flexShrink: 0,
minHeight: 28,
}}
>
<div
style={{
width: GUTTER_WIDTH,
flexShrink: 0,
fontSize: 10,
color: 'rgba(255,255,255,0.3)',
textAlign: 'right',
paddingRight: 8,
paddingTop: 4,
}}
>
all-day
</div>
{dates.map((date) => {
const dateKey = date.format('YYYY-MM-DD');
const allDay = (itemsByDate[dateKey] ?? []).filter((it) => it.isAllDay);
return (
<div
key={dateKey}
style={{
flex: 1,
borderLeft: '1px solid rgba(255,255,255,0.06)',
padding: '2px 2px',
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
{allDay.map((item) => (
<div
key={item.id}
onClick={() => onItemClick(item)}
style={{
background: hexToRgba(item.color, 0.2),
borderLeft: `3px solid ${item.color}`,
borderRadius: 4,
padding: '2px 4px',
fontSize: isNarrow ? 10 : 11,
color: 'rgba(255,255,255,0.85)',
cursor: 'pointer',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{item.title}
</div>
))}
</div>
);
})}
</div>
);
}
/** Individual event block positioned within a column */
function EventBlock({
item,
top,
height,
colLeft,
colWidth,
laneWidth,
laneOffset,
isNarrow,
onClick,
}: {
item: PersonalCalendarItem;
top: number;
height: number;
colLeft: string;
colWidth: string;
laneWidth: string;
laneOffset: string;
isNarrow: boolean;
onClick: () => void;
}) {
const { token } = theme.useToken();
const isTimeBlock = item.itemType === 'TIME_BLOCK';
const isReminder = item.itemType === 'REMINDER';
const color = item.color || token.colorPrimary;
const isBusyHidden = isTimeBlock && item.showDetailsTo === 'NOBODY';
return (
<div
onClick={(e) => {
e.stopPropagation();
onClick();
}}
style={{
position: 'absolute',
top: top + 1,
left: `calc(${colLeft} + ${laneOffset})`,
width: `calc(min(${colWidth}, ${laneWidth}))`,
height: height - 2,
background: isTimeBlock ? hexToRgba(color, 0.1) : hexToRgba(color, 0.2),
border: isTimeBlock
? `1px dashed ${hexToRgba(color, 0.35)}`
: `1px solid ${hexToRgba(color, 0.4)}`,
borderLeft: `3px solid ${color}`,
borderRadius: 6,
padding: isNarrow ? '2px 3px' : '4px 8px',
cursor: 'pointer',
overflow: 'hidden',
zIndex: 2,
transition: 'background 0.15s',
}}
>
{isBusyHidden ? (
<Text style={{ fontSize: isNarrow ? 10 : 12, color: 'rgba(255,255,255,0.45)' }}>
<LockOutlined style={{ marginRight: 4, fontSize: 10 }} />
{!isNarrow && 'Busy'}
</Text>
) : (
<>
<Text
strong
style={{
fontSize: isNarrow ? 10 : 12,
color: 'rgba(255,255,255,0.85)',
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{isReminder && <BellOutlined style={{ marginRight: 3, fontSize: 10 }} />}
{item.title}
</Text>
{!isNarrow && (
<Text style={{ fontSize: 11, color: 'rgba(255,255,255,0.5)' }}>
{formatTimeShort(item.startTime)} - {formatTimeShort(item.endTime)}
</Text>
)}
{isNarrow && height > 30 && (
<Text style={{ fontSize: 9, color: 'rgba(255,255,255,0.45)', display: 'block' }}>
{formatTimeShort(item.startTime)}
</Text>
)}
{item.location && height > 45 && !isNarrow && (
<Text
style={{
fontSize: 10,
color: 'rgba(255,255,255,0.4)',
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
<EnvironmentOutlined style={{ marginRight: 3 }} />
{item.location}
</Text>
)}
</>
)}
</div>
);
}

View File

@ -0,0 +1,236 @@
import { useState } from 'react';
import {
Card,
Form,
Input,
Button,
DatePicker,
TimePicker,
Select,
Result,
Switch,
Typography,
message,
} from 'antd';
import { PlusOutlined, CheckCircleOutlined, VideoCameraOutlined, CopyOutlined } from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
import { api } from '@/lib/api';
import { getErrorMessage } from '@/utils/getErrorMessage';
const { TextArea } = Input;
const { Text } = Typography;
interface EventSubmissionFormProps {
/** Pre-fill the date picker with this date (YYYY-MM-DD) */
initialDate?: string;
onSuccess?: () => void;
gancioUrl?: string;
/** User is authenticated and non-TEMP */
canCreateMeeting?: boolean;
/** Site setting: enableMeet */
meetEnabled?: boolean;
}
export default function EventSubmissionForm({ initialDate, onSuccess, gancioUrl, canCreateMeeting, meetEnabled }: EventSubmissionFormProps) {
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const [success, setSuccess] = useState(false);
const [addVideo, setAddVideo] = useState(false);
const [createdMeetingUrl, setCreatedMeetingUrl] = useState<string | null>(null);
const handleSubmit = async (values: any) => {
setSubmitting(true);
try {
let description = values.description || '';
let tags: string[] = values.tags || [];
// Step 1: Create Jitsi meeting if toggle is on
if (addVideo) {
const dateStr = values.date.format('YYYY-MM-DD');
const startISO = dayjs(`${dateStr}T${values.startTime.format('HH:mm')}`).toISOString();
const endISO = dayjs(`${dateStr}T${values.endTime.format('HH:mm')}`).toISOString();
const { data: meeting } = await api.post('/jitsi/meetings', {
title: values.title,
startTime: startISO,
endTime: endISO,
});
const meetingUrl = `${window.location.origin}/meet/${meeting.slug}`;
description = description
? `${description}\n\n---\nJoin Video Meeting: ${meetingUrl}`
: `Join Video Meeting: ${meetingUrl}`;
if (!tags.includes('video-meeting')) {
tags = [...tags, 'video-meeting'];
}
setCreatedMeetingUrl(meetingUrl);
}
// Step 2: Submit event to Gancio
await axios.post('/api/events/submit', {
title: values.title,
description: description || undefined,
date: values.date.format('YYYY-MM-DD'),
startTime: values.startTime.format('HH:mm'),
endTime: values.endTime.format('HH:mm'),
location: values.location || undefined,
tags: tags.length > 0 ? tags : undefined,
});
setSuccess(true);
form.resetFields();
onSuccess?.();
} catch (err: unknown) {
message.error(getErrorMessage(err, 'Failed to submit event'));
} finally {
setSubmitting(false);
}
};
const handleReset = () => {
setSuccess(false);
setAddVideo(false);
setCreatedMeetingUrl(null);
form.resetFields();
};
if (success) {
return (
<Card size="small" style={{ borderLeft: '4px solid #52c41a', marginBottom: 8 }}>
<Result
icon={<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 32 }} />}
title="Event Submitted!"
subTitle="Your event has been added to the community calendar."
style={{ padding: '12px 0' }}
extra={
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
{createdMeetingUrl && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', background: 'rgba(82,196,26,0.1)', borderRadius: 6 }}>
<VideoCameraOutlined style={{ color: '#52c41a' }} />
<Text style={{ fontSize: 12, color: 'rgba(255,255,255,0.75)' }} copyable={{ text: createdMeetingUrl, icon: <CopyOutlined style={{ fontSize: 12 }} /> }}>
{createdMeetingUrl}
</Text>
</div>
)}
{gancioUrl && (
<Button type="link" size="small" href={gancioUrl} target="_blank" rel="noopener noreferrer">
View on Community Calendar
</Button>
)}
<Button size="small" onClick={handleReset}>
Submit Another
</Button>
</div>
}
/>
</Card>
);
}
const defaultDate = initialDate ? dayjs(initialDate) : dayjs().add(1, 'day');
return (
<Card
size="small"
title={
<span style={{ fontSize: 13 }}>
<PlusOutlined style={{ marginRight: 6 }} />
Submit a Community Event
</span>
}
style={{ borderLeft: '4px solid #52c41a', marginBottom: 8 }}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
size="small"
initialValues={{
date: defaultDate,
startTime: dayjs().hour(18).minute(0),
endTime: dayjs().hour(20).minute(0),
}}
>
<Form.Item
name="title"
label="Event Title"
rules={[{ required: true, message: 'Title is required' }]}
style={{ marginBottom: 8 }}
>
<Input placeholder="Community Town Hall" maxLength={200} />
</Form.Item>
<Form.Item name="description" label="Description" style={{ marginBottom: 8 }}>
<TextArea
placeholder="Tell people what this event is about..."
rows={2}
maxLength={2000}
showCount
/>
</Form.Item>
<Form.Item
name="date"
label="Date"
rules={[{ required: true, message: 'Date is required' }]}
style={{ marginBottom: 8 }}
>
<DatePicker
style={{ width: '100%' }}
disabledDate={(d) => d.isBefore(dayjs(), 'day')}
/>
</Form.Item>
<div style={{ display: 'flex', gap: 8 }}>
<Form.Item
name="startTime"
label="Start"
rules={[{ required: true, message: 'Required' }]}
style={{ flex: 1, marginBottom: 8 }}
>
<TimePicker format="HH:mm" minuteStep={5} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="endTime"
label="End"
rules={[{ required: true, message: 'Required' }]}
style={{ flex: 1, marginBottom: 8 }}
>
<TimePicker format="HH:mm" minuteStep={5} style={{ width: '100%' }} />
</Form.Item>
</div>
<Form.Item name="location" label="Location" style={{ marginBottom: 8 }}>
<Input placeholder="City Hall, 1 Sir Winston Churchill Square" maxLength={500} />
</Form.Item>
<Form.Item name="tags" label="Tags" style={{ marginBottom: 12 }}>
<Select
mode="tags"
placeholder="Add tags (e.g. meeting, workshop)"
maxCount={10}
style={{ width: '100%' }}
/>
</Form.Item>
{canCreateMeeting && meetEnabled && (
<Form.Item style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch checked={addVideo} onChange={setAddVideo} size="small" />
<Text type="secondary" style={{ fontSize: 13 }}>
<VideoCameraOutlined style={{ marginRight: 4 }} />
Add Video Meeting
</Text>
</div>
</Form.Item>
)}
<div style={{ textAlign: 'right' }}>
<Button type="primary" htmlType="submit" loading={submitting} icon={<PlusOutlined />}>
Submit Event
</Button>
</div>
</Form>
</Card>
);
}

View File

@ -0,0 +1,185 @@
import { useMemo } from 'react';
import { Calendar, Spin, Empty, theme } from 'antd';
import { BellOutlined, EnvironmentOutlined } from '@ant-design/icons';
import type { Dayjs } from 'dayjs';
import type { PersonalCalendarItem, CalendarLayer } from '@/types/api';
import { hexToRgba, formatTimeShort } from './calendarUtils';
const { useToken } = theme;
interface PersonalCalendarViewProps {
items: PersonalCalendarItem[];
layers?: CalendarLayer[];
loading?: boolean;
currentMonth: Dayjs;
selectedDate: string | null;
onDateSelect: (date: string) => void;
onItemClick: (item: PersonalCalendarItem) => void;
onMonthChange: (month: Dayjs) => void;
}
const MAX_CELL_ITEMS = 3;
export default function PersonalCalendarView({
items,
loading,
currentMonth,
onDateSelect,
onItemClick,
onMonthChange,
}: PersonalCalendarViewProps) {
useToken();
// Group items by date
const itemsByDate = useMemo(() => {
const map: Record<string, PersonalCalendarItem[]> = {};
for (const item of items) {
const arr = map[item.date];
if (arr) {
arr.push(item);
} else {
map[item.date] = [item];
}
}
for (const key of Object.keys(map)) {
map[key]!.sort((a, b) => a.startTime.localeCompare(b.startTime));
}
return map;
}, [items]);
const handleSelect = (date: Dayjs) => {
onDateSelect(date.format('YYYY-MM-DD'));
};
const handlePanelChange = (date: Dayjs) => {
onMonthChange(date);
};
const cellRender = (date: Dayjs) => {
const dateKey = date.format('YYYY-MM-DD');
const dayItems = itemsByDate[dateKey];
if (!dayItems || dayItems.length === 0) return null;
const visible = dayItems.slice(0, MAX_CELL_ITEMS);
const overflow = dayItems.length - MAX_CELL_ITEMS;
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 3,
padding: '0 2px',
}}
>
{visible.map((item) => {
const isTimeBlock = item.itemType === 'TIME_BLOCK';
const isReminder = item.itemType === 'REMINDER';
const color = item.color || '#1890ff';
const bgAlpha = isTimeBlock ? 0.1 : 0.2;
const borderAlpha = isTimeBlock ? 0.3 : 0.5;
const isSystemType = item.type !== 'personal';
return (
<div
key={item.id}
onClick={(e) => {
e.stopPropagation();
onItemClick(item);
}}
style={{
background: hexToRgba(color, bgAlpha),
border: isTimeBlock
? `1px dashed ${hexToRgba(color, borderAlpha)}`
: `1px solid ${hexToRgba(color, borderAlpha)}`,
borderLeft: `3px solid ${color}`,
borderRadius: 4,
padding: '2px 5px',
fontSize: 12,
lineHeight: '16px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
color: 'rgba(255,255,255,0.85)',
cursor: 'pointer',
opacity: isTimeBlock ? 0.75 : 1,
}}
>
{!item.isAllDay && (
<span style={{ color: 'rgba(255,255,255,0.5)', marginRight: 3, fontSize: 10 }}>
{formatTimeShort(item.startTime)}-{formatTimeShort(item.endTime)}
</span>
)}
{isReminder && (
<BellOutlined style={{ fontSize: 9, marginRight: 3, color: 'rgba(255,255,255,0.5)' }} />
)}
{isSystemType && (
<span
style={{
fontSize: 9,
color: 'rgba(255,255,255,0.5)',
marginRight: 3,
textTransform: 'uppercase',
}}
>
[{item.type}]
</span>
)}
{item.title}
{item.location && (
<span style={{ color: 'rgba(255,255,255,0.4)', marginLeft: 4, fontSize: 10 }}>
<EnvironmentOutlined style={{ fontSize: 9, marginRight: 2 }} />
{item.location}
</span>
)}
</div>
);
})}
{overflow > 0 && (
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.45)', textAlign: 'center' }}>
+{overflow} more
</div>
)}
</div>
);
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: 40 }}>
<Spin size="large" />
</div>
);
}
return (
<div style={{ position: 'relative' }}>
{loading && (
<div style={{ textAlign: 'center', padding: 12 }}>
<Spin />
</div>
)}
<Calendar
fullscreen
value={currentMonth}
cellRender={(date) => cellRender(date)}
onSelect={handleSelect}
onPanelChange={handlePanelChange}
headerRender={() => null}
/>
{items.length === 0 && (
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
}}
>
<Empty description="No calendar items. Click a date to add one." />
</div>
)}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More