Compare commits
No commits in common. "3de1d3fca547af86ff1b2ff60de20266c40aa269" and "e3bbd96d96e2327eea656dd5ea2eb78281275eae" have entirely different histories.
3de1d3fca5
...
e3bbd96d96
350
.VSCodeCounter/2025-09-05_12-42-08/details.md
Normal file
350
.VSCodeCounter/2025-09-05_12-42-08/details.md
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
# Details
|
||||||
|
|
||||||
|
Date : 2025-09-05 12:42:08
|
||||||
|
|
||||||
|
Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite
|
||||||
|
|
||||||
|
Total : 335 files, 91924 codes, 4969 comments, 47542 blanks, all 144435 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [README.md](/README.md) | Markdown | 55 | 0 | 24 | 79 |
|
||||||
|
| [combined.log](/combined.log) | Log | 13 | 0 | 3 | 16 |
|
||||||
|
| [config.sh](/config.sh) | Shell Script | 810 | 177 | 224 | 1,211 |
|
||||||
|
| [configs/homepage/bookmarks.yaml](/configs/homepage/bookmarks.yaml) | YAML | 47 | 2 | 5 | 54 |
|
||||||
|
| [configs/homepage/custom.css](/configs/homepage/custom.css) | PostCSS | 0 | 0 | 1 | 1 |
|
||||||
|
| [configs/homepage/custom.js](/configs/homepage/custom.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [configs/homepage/docker.yaml](/configs/homepage/docker.yaml) | YAML | 3 | 2 | 2 | 7 |
|
||||||
|
| [configs/homepage/kubernetes.yaml](/configs/homepage/kubernetes.yaml) | YAML | 1 | 1 | 1 | 3 |
|
||||||
|
| [configs/homepage/logs/homepage.log](/configs/homepage/logs/homepage.log) | Log | 692 | 0 | 14 | 706 |
|
||||||
|
| [configs/homepage/services.yaml](/configs/homepage/services.yaml) | YAML | 59 | 1 | 15 | 75 |
|
||||||
|
| [configs/homepage/settings.yaml](/configs/homepage/settings.yaml) | YAML | 36 | 4 | 10 | 50 |
|
||||||
|
| [configs/homepage/widgets.yaml](/configs/homepage/widgets.yaml) | YAML | 26 | 2 | 5 | 33 |
|
||||||
|
| [configs/mkdocs-site/default.conf](/configs/mkdocs-site/default.conf) | Properties | 33 | 7 | 9 | 49 |
|
||||||
|
| [docker-compose.yml](/docker-compose.yml) | YAML | 238 | 2 | 13 | 253 |
|
||||||
|
| [map/Instuctions.md](/map/Instuctions.md) | Markdown | 56 | 0 | 21 | 77 |
|
||||||
|
| [map/README.md](/map/README.md) | Markdown | 851 | 0 | 252 | 1,103 |
|
||||||
|
| [map/app/Dockerfile](/map/app/Dockerfile) | Docker | 20 | 7 | 10 | 37 |
|
||||||
|
| [map/app/config/index.js](/map/app/config/index.js) | JavaScript | 118 | 15 | 18 | 151 |
|
||||||
|
| [map/app/controllers/authController.js](/map/app/controllers/authController.js) | JavaScript | 172 | 13 | 25 | 210 |
|
||||||
|
| [map/app/controllers/cutsController.js](/map/app/controllers/cutsController.js) | JavaScript | 276 | 41 | 47 | 364 |
|
||||||
|
| [map/app/controllers/dashboardController.js](/map/app/controllers/dashboardController.js) | JavaScript | 70 | 9 | 15 | 94 |
|
||||||
|
| [map/app/controllers/dataConvertController.js](/map/app/controllers/dataConvertController.js) | JavaScript | 352 | 50 | 76 | 478 |
|
||||||
|
| [map/app/controllers/externalDataController.js](/map/app/controllers/externalDataController.js) | JavaScript | 101 | 11 | 21 | 133 |
|
||||||
|
| [map/app/controllers/listmonkController.js](/map/app/controllers/listmonkController.js) | JavaScript | 212 | 12 | 29 | 253 |
|
||||||
|
| [map/app/controllers/locationsController.js](/map/app/controllers/locationsController.js) | JavaScript | 333 | 23 | 58 | 414 |
|
||||||
|
| [map/app/controllers/passwordRecoveryController.js](/map/app/controllers/passwordRecoveryController.js) | JavaScript | 45 | 4 | 12 | 61 |
|
||||||
|
| [map/app/controllers/publicShiftsController.js](/map/app/controllers/publicShiftsController.js) | JavaScript | 212 | 21 | 38 | 271 |
|
||||||
|
| [map/app/controllers/settingsController.js](/map/app/controllers/settingsController.js) | JavaScript | 303 | 17 | 50 | 370 |
|
||||||
|
| [map/app/controllers/shiftsController.js](/map/app/controllers/shiftsController.js) | JavaScript | 643 | 57 | 123 | 823 |
|
||||||
|
| [map/app/controllers/usersController.js](/map/app/controllers/usersController.js) | JavaScript | 341 | 18 | 54 | 413 |
|
||||||
|
| [map/app/middleware/auth.js](/map/app/middleware/auth.js) | JavaScript | 170 | 13 | 25 | 208 |
|
||||||
|
| [map/app/middleware/rateLimiter.js](/map/app/middleware/rateLimiter.js) | JavaScript | 85 | 10 | 9 | 104 |
|
||||||
|
| [map/app/package-lock.json](/map/app/package-lock.json) | JSON | 2,203 | 0 | 1 | 2,204 |
|
||||||
|
| [map/app/package.json](/map/app/package.json) | JSON | 43 | 0 | 1 | 44 |
|
||||||
|
| [map/app/public/admin.html](/map/app/public/admin.html) | HTML | 1,080 | 46 | 135 | 1,261 |
|
||||||
|
| [map/app/public/css/REFACTORING\_SUMMARY.md](/map/app/public/css/REFACTORING_SUMMARY.md) | Markdown | 51 | 0 | 13 | 64 |
|
||||||
|
| [map/app/public/css/admin.css](/map/app/public/css/admin.css) | PostCSS | 12 | 4 | 3 | 19 |
|
||||||
|
| [map/app/public/css/admin/cuts-shifts.css](/map/app/public/css/admin/cuts-shifts.css) | PostCSS | 225 | 13 | 44 | 282 |
|
||||||
|
| [map/app/public/css/admin/data-convert.css](/map/app/public/css/admin/data-convert.css) | PostCSS | 176 | 7 | 33 | 216 |
|
||||||
|
| [map/app/public/css/admin/forms.css](/map/app/public/css/admin/forms.css) | PostCSS | 207 | 9 | 36 | 252 |
|
||||||
|
| [map/app/public/css/admin/layout.css](/map/app/public/css/admin/layout.css) | PostCSS | 202 | 7 | 36 | 245 |
|
||||||
|
| [map/app/public/css/admin/modals.css](/map/app/public/css/admin/modals.css) | PostCSS | 259 | 6 | 46 | 311 |
|
||||||
|
| [map/app/public/css/admin/nocodb-links.css](/map/app/public/css/admin/nocodb-links.css) | PostCSS | 109 | 4 | 23 | 136 |
|
||||||
|
| [map/app/public/css/admin/responsive.css](/map/app/public/css/admin/responsive.css) | PostCSS | 552 | 28 | 112 | 692 |
|
||||||
|
| [map/app/public/css/admin/status-messages.css](/map/app/public/css/admin/status-messages.css) | PostCSS | 154 | 9 | 27 | 190 |
|
||||||
|
| [map/app/public/css/admin/user-management.css](/map/app/public/css/admin/user-management.css) | PostCSS | 179 | 10 | 31 | 220 |
|
||||||
|
| [map/app/public/css/admin/variables.css](/map/app/public/css/admin/variables.css) | PostCSS | 48 | 9 | 10 | 67 |
|
||||||
|
| [map/app/public/css/admin/walk-sheet.css](/map/app/public/css/admin/walk-sheet.css) | PostCSS | 252 | 12 | 39 | 303 |
|
||||||
|
| [map/app/public/css/modules/apartment-marker.css](/map/app/public/css/modules/apartment-marker.css) | PostCSS | 35 | 2 | 5 | 42 |
|
||||||
|
| [map/app/public/css/modules/apartment-popup.css](/map/app/public/css/modules/apartment-popup.css) | PostCSS | 197 | 9 | 30 | 236 |
|
||||||
|
| [map/app/public/css/modules/base.css](/map/app/public/css/modules/base.css) | PostCSS | 68 | 22 | 12 | 102 |
|
||||||
|
| [map/app/public/css/modules/buttons.css](/map/app/public/css/modules/buttons.css) | PostCSS | 70 | 2 | 16 | 88 |
|
||||||
|
| [map/app/public/css/modules/cache-busting.css](/map/app/public/css/modules/cache-busting.css) | PostCSS | 99 | 2 | 13 | 114 |
|
||||||
|
| [map/app/public/css/modules/cuts.css](/map/app/public/css/modules/cuts.css) | PostCSS | 829 | 22 | 146 | 997 |
|
||||||
|
| [map/app/public/css/modules/dashboard.css](/map/app/public/css/modules/dashboard.css) | PostCSS | 129 | 3 | 29 | 161 |
|
||||||
|
| [map/app/public/css/modules/doc-search.css](/map/app/public/css/modules/doc-search.css) | PostCSS | 123 | 2 | 20 | 145 |
|
||||||
|
| [map/app/public/css/modules/forms.css](/map/app/public/css/modules/forms.css) | PostCSS | 105 | 2 | 18 | 125 |
|
||||||
|
| [map/app/public/css/modules/layout.css](/map/app/public/css/modules/layout.css) | PostCSS | 83 | 5 | 11 | 99 |
|
||||||
|
| [map/app/public/css/modules/leaflet-custom.css](/map/app/public/css/modules/leaflet-custom.css) | PostCSS | 147 | 20 | 32 | 199 |
|
||||||
|
| [map/app/public/css/modules/listmonk.css](/map/app/public/css/modules/listmonk.css) | PostCSS | 295 | 4 | 55 | 354 |
|
||||||
|
| [map/app/public/css/modules/map-controls.css](/map/app/public/css/modules/map-controls.css) | PostCSS | 281 | 13 | 45 | 339 |
|
||||||
|
| [map/app/public/css/modules/mobile-ui.css](/map/app/public/css/modules/mobile-ui.css) | PostCSS | 205 | 8 | 36 | 249 |
|
||||||
|
| [map/app/public/css/modules/modal.css](/map/app/public/css/modules/modal.css) | PostCSS | 73 | 1 | 10 | 84 |
|
||||||
|
| [map/app/public/css/modules/nocodb-dropdown.css](/map/app/public/css/modules/nocodb-dropdown.css) | PostCSS | 134 | 4 | 24 | 162 |
|
||||||
|
| [map/app/public/css/modules/notifications.css](/map/app/public/css/modules/notifications.css) | PostCSS | 105 | 5 | 16 | 126 |
|
||||||
|
| [map/app/public/css/modules/print.css](/map/app/public/css/modules/print.css) | PostCSS | 11 | 1 | 2 | 14 |
|
||||||
|
| [map/app/public/css/modules/qr-code.css](/map/app/public/css/modules/qr-code.css) | PostCSS | 59 | 3 | 13 | 75 |
|
||||||
|
| [map/app/public/css/modules/responsive.css](/map/app/public/css/modules/responsive.css) | PostCSS | 150 | 12 | 29 | 191 |
|
||||||
|
| [map/app/public/css/modules/start-location-marker.css](/map/app/public/css/modules/start-location-marker.css) | PostCSS | 65 | 3 | 8 | 76 |
|
||||||
|
| [map/app/public/css/modules/temp-user.css](/map/app/public/css/modules/temp-user.css) | PostCSS | 46 | 6 | 5 | 57 |
|
||||||
|
| [map/app/public/css/modules/unified-search.css](/map/app/public/css/modules/unified-search.css) | PostCSS | 580 | 30 | 99 | 709 |
|
||||||
|
| [map/app/public/css/public-shifts.css](/map/app/public/css/public-shifts.css) | PostCSS | 418 | 24 | 83 | 525 |
|
||||||
|
| [map/app/public/css/shifts.css](/map/app/public/css/shifts.css) | PostCSS | 608 | 21 | 118 | 747 |
|
||||||
|
| [map/app/public/css/style.css](/map/app/public/css/style.css) | PostCSS | 21 | 0 | 1 | 22 |
|
||||||
|
| [map/app/public/css/user.css](/map/app/public/css/user.css) | PostCSS | 364 | 13 | 70 | 447 |
|
||||||
|
| [map/app/public/index.html](/map/app/public/index.html) | HTML | 384 | 28 | 45 | 457 |
|
||||||
|
| [map/app/public/js/admin-auth.js](/map/app/public/js/admin-auth.js) | JavaScript | 63 | 11 | 14 | 88 |
|
||||||
|
| [map/app/public/js/admin-core.js](/map/app/public/js/admin-core.js) | JavaScript | 239 | 46 | 40 | 325 |
|
||||||
|
| [map/app/public/js/admin-cuts.js](/map/app/public/js/admin-cuts.js) | JavaScript | 1,505 | 184 | 302 | 1,991 |
|
||||||
|
| [map/app/public/js/admin-email.js](/map/app/public/js/admin-email.js) | JavaScript | 319 | 29 | 47 | 395 |
|
||||||
|
| [map/app/public/js/admin-map.js](/map/app/public/js/admin-map.js) | JavaScript | 178 | 26 | 46 | 250 |
|
||||||
|
| [map/app/public/js/admin-shift-volunteers.js](/map/app/public/js/admin-shift-volunteers.js) | JavaScript | 416 | 49 | 66 | 531 |
|
||||||
|
| [map/app/public/js/admin-shifts.js](/map/app/public/js/admin-shifts.js) | JavaScript | 330 | 35 | 55 | 420 |
|
||||||
|
| [map/app/public/js/admin-users.js](/map/app/public/js/admin-users.js) | JavaScript | 305 | 16 | 45 | 366 |
|
||||||
|
| [map/app/public/js/admin-walksheet.js](/map/app/public/js/admin-walksheet.js) | JavaScript | 387 | 35 | 50 | 472 |
|
||||||
|
| [map/app/public/js/admin.js](/map/app/public/js/admin.js) | JavaScript | 2,309 | 178 | 288 | 2,775 |
|
||||||
|
| [map/app/public/js/admin/auth.js](/map/app/public/js/admin/auth.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/admin/navigation.js](/map/app/public/js/admin/navigation.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/admin/shifts.js](/map/app/public/js/admin/shifts.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/admin/startLocation.js](/map/app/public/js/admin/startLocation.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/admin/users.js](/map/app/public/js/admin/users.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/admin/utils.js](/map/app/public/js/admin/utils.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/admin/walkSheet.js](/map/app/public/js/admin/walkSheet.js) | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| [map/app/public/js/auth.js](/map/app/public/js/auth.js) | JavaScript | 181 | 28 | 34 | 243 |
|
||||||
|
| [map/app/public/js/cache-manager.js](/map/app/public/js/cache-manager.js) | JavaScript | 219 | 71 | 28 | 318 |
|
||||||
|
| [map/app/public/js/config.js](/map/app/public/js/config.js) | JavaScript | 32 | 3 | 3 | 38 |
|
||||||
|
| [map/app/public/js/cut-controls.js](/map/app/public/js/cut-controls.js) | JavaScript | 982 | 163 | 145 | 1,290 |
|
||||||
|
| [map/app/public/js/cut-drawing.js](/map/app/public/js/cut-drawing.js) | JavaScript | 206 | 72 | 59 | 337 |
|
||||||
|
| [map/app/public/js/cut-manager.js](/map/app/public/js/cut-manager.js) | JavaScript | 359 | 96 | 79 | 534 |
|
||||||
|
| [map/app/public/js/dashboard.js](/map/app/public/js/dashboard.js) | JavaScript | 333 | 23 | 33 | 389 |
|
||||||
|
| [map/app/public/js/data-convert.js](/map/app/public/js/data-convert.js) | JavaScript | 510 | 64 | 118 | 692 |
|
||||||
|
| [map/app/public/js/database-search.js](/map/app/public/js/database-search.js) | JavaScript | 186 | 76 | 51 | 313 |
|
||||||
|
| [map/app/public/js/external-layers.js](/map/app/public/js/external-layers.js) | JavaScript | 513 | 39 | 45 | 597 |
|
||||||
|
| [map/app/public/js/listmonk-admin.js](/map/app/public/js/listmonk-admin.js) | JavaScript | 410 | 76 | 85 | 571 |
|
||||||
|
| [map/app/public/js/listmonk-status.js](/map/app/public/js/listmonk-status.js) | JavaScript | 169 | 25 | 32 | 226 |
|
||||||
|
| [map/app/public/js/location-manager.js](/map/app/public/js/location-manager.js) | JavaScript | 998 | 56 | 91 | 1,145 |
|
||||||
|
| [map/app/public/js/main.js](/map/app/public/js/main.js) | JavaScript | 178 | 40 | 36 | 254 |
|
||||||
|
| [map/app/public/js/map-manager.js](/map/app/public/js/map-manager.js) | JavaScript | 82 | 12 | 19 | 113 |
|
||||||
|
| [map/app/public/js/map-search.js](/map/app/public/js/map-search.js) | JavaScript | 120 | 44 | 36 | 200 |
|
||||||
|
| [map/app/public/js/mkdocs-search.js](/map/app/public/js/mkdocs-search.js) | JavaScript | 263 | 36 | 52 | 351 |
|
||||||
|
| [map/app/public/js/public-shifts.js](/map/app/public/js/public-shifts.js) | JavaScript | 518 | 15 | 25 | 558 |
|
||||||
|
| [map/app/public/js/search-manager.js](/map/app/public/js/search-manager.js) | JavaScript | 407 | 138 | 100 | 645 |
|
||||||
|
| [map/app/public/js/shifts.js](/map/app/public/js/shifts.js) | JavaScript | 1,059 | 35 | 55 | 1,149 |
|
||||||
|
| [map/app/public/js/ui-controls.js](/map/app/public/js/ui-controls.js) | JavaScript | 572 | 74 | 101 | 747 |
|
||||||
|
| [map/app/public/js/user.js](/map/app/public/js/user.js) | JavaScript | 81 | 12 | 19 | 112 |
|
||||||
|
| [map/app/public/js/utils.js](/map/app/public/js/utils.js) | JavaScript | 96 | 21 | 25 | 142 |
|
||||||
|
| [map/app/public/login.html](/map/app/public/login.html) | HTML | 459 | 3 | 76 | 538 |
|
||||||
|
| [map/app/public/public-shifts.html](/map/app/public/public-shifts.html) | HTML | 88 | 4 | 7 | 99 |
|
||||||
|
| [map/app/public/shifts.html](/map/app/public/shifts.html) | HTML | 73 | 5 | 8 | 86 |
|
||||||
|
| [map/app/public/user.html](/map/app/public/user.html) | HTML | 47 | 7 | 9 | 63 |
|
||||||
|
| [map/app/routes/admin.js](/map/app/routes/admin.js) | JavaScript | 80 | 15 | 16 | 111 |
|
||||||
|
| [map/app/routes/auth.js](/map/app/routes/auth.js) | JavaScript | 14 | 5 | 6 | 25 |
|
||||||
|
| [map/app/routes/cuts.js](/map/app/routes/cuts.js) | JavaScript | 18 | 6 | 7 | 31 |
|
||||||
|
| [map/app/routes/dashboard.js](/map/app/routes/dashboard.js) | JavaScript | 5 | 0 | 3 | 8 |
|
||||||
|
| [map/app/routes/dataConvert.js](/map/app/routes/dataConvert.js) | JavaScript | 21 | 4 | 6 | 31 |
|
||||||
|
| [map/app/routes/debug.js](/map/app/routes/debug.js) | JavaScript | 236 | 10 | 36 | 282 |
|
||||||
|
| [map/app/routes/external.js](/map/app/routes/external.js) | JavaScript | 7 | 2 | 4 | 13 |
|
||||||
|
| [map/app/routes/geocoding.js](/map/app/routes/geocoding.js) | JavaScript | 120 | 24 | 31 | 175 |
|
||||||
|
| [map/app/routes/index.js](/map/app/routes/index.js) | JavaScript | 153 | 29 | 36 | 218 |
|
||||||
|
| [map/app/routes/listmonk.js](/map/app/routes/listmonk.js) | JavaScript | 12 | 5 | 9 | 26 |
|
||||||
|
| [map/app/routes/locations.js](/map/app/routes/locations.js) | JavaScript | 11 | 5 | 6 | 22 |
|
||||||
|
| [map/app/routes/public.js](/map/app/routes/public.js) | JavaScript | 8 | 1 | 3 | 12 |
|
||||||
|
| [map/app/routes/publicShifts.js](/map/app/routes/publicShifts.js) | JavaScript | 8 | 1 | 2 | 11 |
|
||||||
|
| [map/app/routes/qr.js](/map/app/routes/qr.js) | JavaScript | 39 | 1 | 8 | 48 |
|
||||||
|
| [map/app/routes/settings.js](/map/app/routes/settings.js) | JavaScript | 8 | 2 | 3 | 13 |
|
||||||
|
| [map/app/routes/shifts.js](/map/app/routes/shifts.js) | JavaScript | 16 | 4 | 5 | 25 |
|
||||||
|
| [map/app/routes/users.js](/map/app/routes/users.js) | JavaScript | 11 | 6 | 7 | 24 |
|
||||||
|
| [map/app/server.js](/map/app/server.js) | JavaScript | 249 | 43 | 49 | 341 |
|
||||||
|
| [map/app/services/accountExpiration.js](/map/app/services/accountExpiration.js) | JavaScript | 59 | 11 | 19 | 89 |
|
||||||
|
| [map/app/services/email.js](/map/app/services/email.js) | JavaScript | 109 | 4 | 19 | 132 |
|
||||||
|
| [map/app/services/emailTemplates.js](/map/app/services/emailTemplates.js) | JavaScript | 59 | 10 | 16 | 85 |
|
||||||
|
| [map/app/services/geocoding.js](/map/app/services/geocoding.js) | JavaScript | 219 | 51 | 44 | 314 |
|
||||||
|
| [map/app/services/listmonk.js](/map/app/services/listmonk.js) | JavaScript | 412 | 36 | 66 | 514 |
|
||||||
|
| [map/app/services/nocodb.js](/map/app/services/nocodb.js) | JavaScript | 172 | 22 | 36 | 230 |
|
||||||
|
| [map/app/services/qrcode.js](/map/app/services/qrcode.js) | JavaScript | 110 | 33 | 20 | 163 |
|
||||||
|
| [map/app/services/socrata.js](/map/app/services/socrata.js) | JavaScript | 49 | 9 | 10 | 68 |
|
||||||
|
| [map/app/templates/email/login-details.html](/map/app/templates/email/login-details.html) | HTML | 113 | 0 | 4 | 117 |
|
||||||
|
| [map/app/templates/email/password-recovery.html](/map/app/templates/email/password-recovery.html) | HTML | 79 | 0 | 1 | 80 |
|
||||||
|
| [map/app/templates/email/public-shift-signup-existing.html](/map/app/templates/email/public-shift-signup-existing.html) | HTML | 161 | 0 | 22 | 183 |
|
||||||
|
| [map/app/templates/email/public-shift-signup-new.html](/map/app/templates/email/public-shift-signup-new.html) | HTML | 31 | 0 | 5 | 36 |
|
||||||
|
| [map/app/templates/email/shift-details.html](/map/app/templates/email/shift-details.html) | HTML | 152 | 0 | 5 | 157 |
|
||||||
|
| [map/app/templates/email/user-broadcast.html](/map/app/templates/email/user-broadcast.html) | HTML | 108 | 0 | 2 | 110 |
|
||||||
|
| [map/app/utils/cacheBusting.js](/map/app/utils/cacheBusting.js) | JavaScript | 105 | 51 | 24 | 180 |
|
||||||
|
| [map/app/utils/helpers.js](/map/app/utils/helpers.js) | JavaScript | 149 | 25 | 28 | 202 |
|
||||||
|
| [map/app/utils/logger.js](/map/app/utils/logger.js) | JavaScript | 38 | 2 | 3 | 43 |
|
||||||
|
| [map/build-nocodb.sh](/map/build-nocodb.sh) | Shell Script | 925 | 60 | 93 | 1,078 |
|
||||||
|
| [map/data/City of Edmonton - Neighbourhoods (Centroid Point)\_20250807.geojson](/map/data/City%20of%20Edmonton%20-%20Neighbourhoods%20(Centroid%20Point)_20250807.geojson) | JSON | 3 | 0 | 1 | 4 |
|
||||||
|
| [map/data/City of Edmonton - Neighbourhoods\_20250807.geojson](/map/data/City%20of%20Edmonton%20-%20Neighbourhoods_20250807.geojson) | JSON | 5 | 0 | 0 | 5 |
|
||||||
|
| [map/data/City of Edmonton Ward Boundary and Council Composition\_Current\_20250807.geojson](/map/data/City%20of%20Edmonton%20Ward%20Boundary%20and%20Council%20Composition_Current_20250807.geojson) | JSON | 5 | 0 | 0 | 5 |
|
||||||
|
| [map/data/City\_of\_Edmonton\_-\_Neighbourhoods\_20250807.csv](/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv) | CSV | 689 | 0 | 1 | 690 |
|
||||||
|
| [map/data/City\_of\_Edmonton\_-\_Neighbourhoods\_\_Centroid\_Point\_\_20250807.csv](/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv) | CSV | 407 | 0 | 1 | 408 |
|
||||||
|
| [map/data/City\_of\_Edmonton\_Ward\_Boundary\_and\_Council\_Composition\_Current\_20250807.csv](/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv) | CSV | 13 | 0 | 1 | 14 |
|
||||||
|
| [map/data/convert\_edmonton\_optimized.js](/map/data/convert_edmonton_optimized.js) | JavaScript | 185 | 21 | 43 | 249 |
|
||||||
|
| [map/data/convert\_ward\_boundaries\_optimized.js](/map/data/convert_ward_boundaries_optimized.js) | JavaScript | 173 | 24 | 45 | 242 |
|
||||||
|
| [map/data/edmonton\_neighborhoods\_nocodb\_optimized.csv](/map/data/edmonton_neighborhoods_nocodb_optimized.csv) | CSV | 689 | 0 | 0 | 689 |
|
||||||
|
| [map/data/edmonton\_nocodb\_optimized.csv](/map/data/edmonton_nocodb_optimized.csv) | CSV | 1,095 | 0 | 406 | 1,501 |
|
||||||
|
| [map/data/edmonton\_ward\_boundaries\_optimized.csv](/map/data/edmonton_ward_boundaries_optimized.csv) | CSV | 13 | 0 | 0 | 13 |
|
||||||
|
| [map/data/exampledata.csv](/map/data/exampledata.csv) | CSV | 7 | 0 | 0 | 7 |
|
||||||
|
| [map/docker-compose.yml](/map/docker-compose.yml) | YAML | 31 | 0 | 3 | 34 |
|
||||||
|
| [map/files-explainer.md](/map/files-explainer.md) | Markdown | 292 | 0 | 293 | 585 |
|
||||||
|
| [map/instruct/ADMIN\_IMPLEMENTATION.md](/map/instruct/ADMIN_IMPLEMENTATION.md) | Markdown | 102 | 0 | 28 | 130 |
|
||||||
|
| [map/instruct/CUT\_IMPLEMENTATION\_SUMMARY.md](/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md) | Markdown | 156 | 0 | 34 | 190 |
|
||||||
|
| [map/instruct/CUT\_PUBLIC\_IMPLEMENTATION.md](/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md) | Markdown | 124 | 5 | 31 | 160 |
|
||||||
|
| [map/instruct/CUT\_SIMPLIFICATION\_SUMMARY.md](/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md) | Markdown | 65 | 0 | 21 | 86 |
|
||||||
|
| [map/instruct/LISTMONK\_INTEGRATION\_GUIDE.md](/map/instruct/LISTMONK_INTEGRATION_GUIDE.md) | Markdown | 249 | 0 | 70 | 319 |
|
||||||
|
| [map/instruct/SHIFT\_PERFORMANCE\_FIX.md](/map/instruct/SHIFT_PERFORMANCE_FIX.md) | Markdown | 71 | 0 | 22 | 93 |
|
||||||
|
| [map/instruct/TEMP\_USER\_IMPLEMENTATION.md](/map/instruct/TEMP_USER_IMPLEMENTATION.md) | Markdown | 88 | 0 | 28 | 116 |
|
||||||
|
| [map/instruct/TEMP\_USER\_TEST.md](/map/instruct/TEMP_USER_TEST.md) | Markdown | 113 | 0 | 34 | 147 |
|
||||||
|
| [map/instruct/build-nocodb.md](/map/instruct/build-nocodb.md) | Markdown | 374 | 0 | 67 | 441 |
|
||||||
|
| [map/package-lock.json](/map/package-lock.json) | JSON | 17 | 0 | 1 | 18 |
|
||||||
|
| [map/package.json](/map/package.json) | JSON | 5 | 0 | 1 | 6 |
|
||||||
|
| [mkdocs/docs/adv/ansible.md](/mkdocs/docs/adv/ansible.md) | Markdown | 373 | 0 | 152 | 525 |
|
||||||
|
| [mkdocs/docs/adv/index.md](/mkdocs/docs/adv/index.md) | Markdown | 2 | 0 | 1 | 3 |
|
||||||
|
| [mkdocs/docs/adv/vscode-ssh.md](/mkdocs/docs/adv/vscode-ssh.md) | Markdown | 511 | 0 | 174 | 685 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/admin-changemaker.lite.json](/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/anthropics-claude-code.json](/mkdocs/docs/assets/repo-data/anthropics-claude-code.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/coder-code-server.json](/mkdocs/docs/assets/repo-data/coder-code-server.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/gethomepage-homepage.json](/mkdocs/docs/assets/repo-data/gethomepage-homepage.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/go-gitea-gitea.json](/mkdocs/docs/assets/repo-data/go-gitea-gitea.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/knadh-listmonk.json](/mkdocs/docs/assets/repo-data/knadh-listmonk.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/lyqht-mini-qr.json](/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/n8n-io-n8n.json](/mkdocs/docs/assets/repo-data/n8n-io-n8n.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/nocodb-nocodb.json](/mkdocs/docs/assets/repo-data/nocodb-nocodb.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/ollama-ollama.json](/mkdocs/docs/assets/repo-data/ollama-ollama.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json](/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/docs/blog/index.md](/mkdocs/docs/blog/index.md) | Markdown | 0 | 0 | 1 | 1 |
|
||||||
|
| [mkdocs/docs/blog/posts/1.md](/mkdocs/docs/blog/posts/1.md) | Markdown | 6 | 0 | 3 | 9 |
|
||||||
|
| [mkdocs/docs/blog/posts/2.md](/mkdocs/docs/blog/posts/2.md) | Markdown | 10 | 0 | 4 | 14 |
|
||||||
|
| [mkdocs/docs/blog/posts/3.md](/mkdocs/docs/blog/posts/3.md) | Markdown | 54 | 0 | 21 | 75 |
|
||||||
|
| [mkdocs/docs/build/index.md](/mkdocs/docs/build/index.md) | Markdown | 336 | 0 | 150 | 486 |
|
||||||
|
| [mkdocs/docs/build/map.md](/mkdocs/docs/build/map.md) | Markdown | 155 | 0 | 62 | 217 |
|
||||||
|
| [mkdocs/docs/build/server.md](/mkdocs/docs/build/server.md) | Markdown | 122 | 0 | 27 | 149 |
|
||||||
|
| [mkdocs/docs/build/site.md](/mkdocs/docs/build/site.md) | Markdown | 74 | 0 | 34 | 108 |
|
||||||
|
| [mkdocs/docs/config/cloudflare-config.md](/mkdocs/docs/config/cloudflare-config.md) | Markdown | 45 | 0 | 16 | 61 |
|
||||||
|
| [mkdocs/docs/config/coder.md](/mkdocs/docs/config/coder.md) | Markdown | 139 | 0 | 77 | 216 |
|
||||||
|
| [mkdocs/docs/config/index.md](/mkdocs/docs/config/index.md) | Markdown | 3 | 0 | 4 | 7 |
|
||||||
|
| [mkdocs/docs/config/map.md](/mkdocs/docs/config/map.md) | Markdown | 299 | 0 | 91 | 390 |
|
||||||
|
| [mkdocs/docs/config/mkdocs.md](/mkdocs/docs/config/mkdocs.md) | Markdown | 102 | 0 | 42 | 144 |
|
||||||
|
| [mkdocs/docs/hooks/repo\_widget\_hook.py](/mkdocs/docs/hooks/repo_widget_hook.py) | Python | 119 | 24 | 16 | 159 |
|
||||||
|
| [mkdocs/docs/how to/canvass.md](/mkdocs/docs/how%20to/canvass.md) | Markdown | 2 | 0 | 3 | 5 |
|
||||||
|
| [mkdocs/docs/index.md](/mkdocs/docs/index.md) | Markdown | 85 | 0 | 29 | 114 |
|
||||||
|
| [mkdocs/docs/javascripts/gitea-widget.js](/mkdocs/docs/javascripts/gitea-widget.js) | JavaScript | 121 | 3 | 8 | 132 |
|
||||||
|
| [mkdocs/docs/javascripts/github-widget.js](/mkdocs/docs/javascripts/github-widget.js) | JavaScript | 144 | 10 | 14 | 168 |
|
||||||
|
| [mkdocs/docs/javascripts/home.js](/mkdocs/docs/javascripts/home.js) | JavaScript | 271 | 30 | 37 | 338 |
|
||||||
|
| [mkdocs/docs/manual/index.md](/mkdocs/docs/manual/index.md) | Markdown | 2 | 0 | 1 | 3 |
|
||||||
|
| [mkdocs/docs/manual/map.md](/mkdocs/docs/manual/map.md) | Markdown | 91 | 0 | 48 | 139 |
|
||||||
|
| [mkdocs/docs/overrides/lander.html](/mkdocs/docs/overrides/lander.html) | HTML | 1,872 | 11 | 259 | 2,142 |
|
||||||
|
| [mkdocs/docs/overrides/main.html](/mkdocs/docs/overrides/main.html) | HTML | 8 | 1 | 3 | 12 |
|
||||||
|
| [mkdocs/docs/phil/cost-comparison.md](/mkdocs/docs/phil/cost-comparison.md) | Markdown | 153 | 0 | 51 | 204 |
|
||||||
|
| [mkdocs/docs/phil/index.md](/mkdocs/docs/phil/index.md) | Markdown | 95 | 0 | 69 | 164 |
|
||||||
|
| [mkdocs/docs/services/code-server.md](/mkdocs/docs/services/code-server.md) | Markdown | 41 | 0 | 21 | 62 |
|
||||||
|
| [mkdocs/docs/services/gitea.md](/mkdocs/docs/services/gitea.md) | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| [mkdocs/docs/services/homepage.md](/mkdocs/docs/services/homepage.md) | Markdown | 146 | 0 | 66 | 212 |
|
||||||
|
| [mkdocs/docs/services/index.md](/mkdocs/docs/services/index.md) | Markdown | 101 | 0 | 17 | 118 |
|
||||||
|
| [mkdocs/docs/services/listmonk.md](/mkdocs/docs/services/listmonk.md) | Markdown | 47 | 0 | 22 | 69 |
|
||||||
|
| [mkdocs/docs/services/map.md](/mkdocs/docs/services/map.md) | Markdown | 70 | 0 | 27 | 97 |
|
||||||
|
| [mkdocs/docs/services/mini-qr.md](/mkdocs/docs/services/mini-qr.md) | Markdown | 23 | 0 | 15 | 38 |
|
||||||
|
| [mkdocs/docs/services/mkdocs.md](/mkdocs/docs/services/mkdocs.md) | Markdown | 86 | 0 | 47 | 133 |
|
||||||
|
| [mkdocs/docs/services/n8n.md](/mkdocs/docs/services/n8n.md) | Markdown | 109 | 0 | 49 | 158 |
|
||||||
|
| [mkdocs/docs/services/nocodb.md](/mkdocs/docs/services/nocodb.md) | Markdown | 108 | 0 | 55 | 163 |
|
||||||
|
| [mkdocs/docs/services/postgresql.md](/mkdocs/docs/services/postgresql.md) | Markdown | 59 | 0 | 32 | 91 |
|
||||||
|
| [mkdocs/docs/services/static-server.md](/mkdocs/docs/services/static-server.md) | Markdown | 69 | 0 | 32 | 101 |
|
||||||
|
| [mkdocs/docs/stylesheets/extra.css](/mkdocs/docs/stylesheets/extra.css) | PostCSS | 498 | 18 | 82 | 598 |
|
||||||
|
| [mkdocs/docs/stylesheets/home.css](/mkdocs/docs/stylesheets/home.css) | PostCSS | 866 | 54 | 153 | 1,073 |
|
||||||
|
| [mkdocs/docs/test.md](/mkdocs/docs/test.md) | Markdown | 5 | 0 | 6 | 11 |
|
||||||
|
| [mkdocs/mkdocs.yml](/mkdocs/mkdocs.yml) | YAML | 167 | 11 | 11 | 189 |
|
||||||
|
| [mkdocs/site/404.html](/mkdocs/site/404.html) | HTML | 251 | 1 | 437 | 689 |
|
||||||
|
| [mkdocs/site/adv/ansible/index.html](/mkdocs/site/adv/ansible/index.html) | HTML | 1,653 | 1 | 1,326 | 2,980 |
|
||||||
|
| [mkdocs/site/adv/index.html](/mkdocs/site/adv/index.html) | HTML | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| [mkdocs/site/adv/vscode-ssh/index.html](/mkdocs/site/adv/vscode-ssh/index.html) | HTML | 1,926 | 1 | 1,358 | 3,285 |
|
||||||
|
| [mkdocs/site/assets/javascripts/bundle.50899def.min.js](/mkdocs/site/assets/javascripts/bundle.50899def.min.js) | JavaScript | 14 | 1 | 2 | 17 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js) | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js](/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js) | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/tinyseg.js](/mkdocs/site/assets/javascripts/lunr/tinyseg.js) | JavaScript | 176 | 21 | 9 | 206 |
|
||||||
|
| [mkdocs/site/assets/javascripts/lunr/wordcut.js](/mkdocs/site/assets/javascripts/lunr/wordcut.js) | JavaScript | 4,882 | 915 | 911 | 6,708 |
|
||||||
|
| [mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js](/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js) | JavaScript | 38 | 3 | 2 | 43 |
|
||||||
|
| [mkdocs/site/assets/repo-data/admin-changemaker.lite.json](/mkdocs/site/assets/repo-data/admin-changemaker.lite.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/anthropics-claude-code.json](/mkdocs/site/assets/repo-data/anthropics-claude-code.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/coder-code-server.json](/mkdocs/site/assets/repo-data/coder-code-server.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/gethomepage-homepage.json](/mkdocs/site/assets/repo-data/gethomepage-homepage.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/go-gitea-gitea.json](/mkdocs/site/assets/repo-data/go-gitea-gitea.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/knadh-listmonk.json](/mkdocs/site/assets/repo-data/knadh-listmonk.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/lyqht-mini-qr.json](/mkdocs/site/assets/repo-data/lyqht-mini-qr.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/n8n-io-n8n.json](/mkdocs/site/assets/repo-data/n8n-io-n8n.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/nocodb-nocodb.json](/mkdocs/site/assets/repo-data/nocodb-nocodb.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/ollama-ollama.json](/mkdocs/site/assets/repo-data/ollama-ollama.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json](/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json) | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| [mkdocs/site/assets/stylesheets/main.7e37652d.min.css](/mkdocs/site/assets/stylesheets/main.7e37652d.min.css) | PostCSS | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/assets/stylesheets/palette.06af60db.min.css](/mkdocs/site/assets/stylesheets/palette.06af60db.min.css) | PostCSS | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/blog/2025/07/03/blog-1/index.html](/mkdocs/site/blog/2025/07/03/blog-1/index.html) | HTML | 374 | 1 | 582 | 957 |
|
||||||
|
| [mkdocs/site/blog/2025/07/10/2/index.html](/mkdocs/site/blog/2025/07/10/2/index.html) | HTML | 392 | 1 | 583 | 976 |
|
||||||
|
| [mkdocs/site/blog/2025/08/01/3/index.html](/mkdocs/site/blog/2025/08/01/3/index.html) | HTML | 455 | 1 | 590 | 1,046 |
|
||||||
|
| [mkdocs/site/blog/archive/2025/index.html](/mkdocs/site/blog/archive/2025/index.html) | HTML | 442 | 1 | 582 | 1,025 |
|
||||||
|
| [mkdocs/site/blog/index.html](/mkdocs/site/blog/index.html) | HTML | 451 | 1 | 572 | 1,024 |
|
||||||
|
| [mkdocs/site/build/index.html](/mkdocs/site/build/index.html) | HTML | 1,312 | 1 | 1,199 | 2,512 |
|
||||||
|
| [mkdocs/site/build/map/index.html](/mkdocs/site/build/map/index.html) | HTML | 983 | 1 | 1,182 | 2,166 |
|
||||||
|
| [mkdocs/site/build/server/index.html](/mkdocs/site/build/server/index.html) | HTML | 1,045 | 1 | 1,234 | 2,280 |
|
||||||
|
| [mkdocs/site/build/site/index.html](/mkdocs/site/build/site/index.html) | HTML | 748 | 1 | 1,138 | 1,887 |
|
||||||
|
| [mkdocs/site/config/cloudflare-config/index.html](/mkdocs/site/config/cloudflare-config/index.html) | HTML | 740 | 1 | 1,146 | 1,887 |
|
||||||
|
| [mkdocs/site/config/coder/index.html](/mkdocs/site/config/coder/index.html) | HTML | 1,117 | 1 | 1,240 | 2,358 |
|
||||||
|
| [mkdocs/site/config/index.html](/mkdocs/site/config/index.html) | HTML | 550 | 1 | 1,083 | 1,634 |
|
||||||
|
| [mkdocs/site/config/map/index.html](/mkdocs/site/config/map/index.html) | HTML | 1,672 | 1 | 1,346 | 3,019 |
|
||||||
|
| [mkdocs/site/config/mkdocs/index.html](/mkdocs/site/config/mkdocs/index.html) | HTML | 833 | 1 | 1,151 | 1,985 |
|
||||||
|
| [mkdocs/site/hooks/repo\_widget\_hook.py](/mkdocs/site/hooks/repo_widget_hook.py) | Python | 119 | 24 | 16 | 159 |
|
||||||
|
| [mkdocs/site/how to/canvass/index.html](/mkdocs/site/how%20to/canvass/index.html) | HTML | 275 | 1 | 484 | 760 |
|
||||||
|
| [mkdocs/site/index.html](/mkdocs/site/index.html) | HTML | 1,872 | 11 | 259 | 2,142 |
|
||||||
|
| [mkdocs/site/javascripts/gitea-widget.js](/mkdocs/site/javascripts/gitea-widget.js) | JavaScript | 121 | 3 | 8 | 132 |
|
||||||
|
| [mkdocs/site/javascripts/github-widget.js](/mkdocs/site/javascripts/github-widget.js) | JavaScript | 144 | 10 | 14 | 168 |
|
||||||
|
| [mkdocs/site/javascripts/home.js](/mkdocs/site/javascripts/home.js) | JavaScript | 271 | 30 | 37 | 338 |
|
||||||
|
| [mkdocs/site/manual/index.html](/mkdocs/site/manual/index.html) | HTML | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| [mkdocs/site/manual/map/index.html](/mkdocs/site/manual/map/index.html) | HTML | 987 | 1 | 1,202 | 2,190 |
|
||||||
|
| [mkdocs/site/overrides/lander.html](/mkdocs/site/overrides/lander.html) | HTML | 1,872 | 11 | 259 | 2,142 |
|
||||||
|
| [mkdocs/site/overrides/main.html](/mkdocs/site/overrides/main.html) | HTML | 8 | 1 | 3 | 12 |
|
||||||
|
| [mkdocs/site/phil/cost-comparison/index.html](/mkdocs/site/phil/cost-comparison/index.html) | HTML | 1,300 | 1 | 766 | 2,067 |
|
||||||
|
| [mkdocs/site/phil/index.html](/mkdocs/site/phil/index.html) | HTML | 717 | 1 | 661 | 1,379 |
|
||||||
|
| [mkdocs/site/search/search\_index.json](/mkdocs/site/search/search_index.json) | JSON | 1 | 0 | 0 | 1 |
|
||||||
|
| [mkdocs/site/services/code-server/index.html](/mkdocs/site/services/code-server/index.html) | HTML | 755 | 1 | 1,155 | 1,911 |
|
||||||
|
| [mkdocs/site/services/gitea/index.html](/mkdocs/site/services/gitea/index.html) | HTML | 737 | 1 | 1,151 | 1,889 |
|
||||||
|
| [mkdocs/site/services/homepage/index.html](/mkdocs/site/services/homepage/index.html) | HTML | 1,119 | 1 | 1,235 | 2,355 |
|
||||||
|
| [mkdocs/site/services/index.html](/mkdocs/site/services/index.html) | HTML | 769 | 1 | 1,113 | 1,883 |
|
||||||
|
| [mkdocs/site/services/listmonk/index.html](/mkdocs/site/services/listmonk/index.html) | HTML | 775 | 1 | 1,159 | 1,935 |
|
||||||
|
| [mkdocs/site/services/map/index.html](/mkdocs/site/services/map/index.html) | HTML | 877 | 1 | 1,170 | 2,048 |
|
||||||
|
| [mkdocs/site/services/mini-qr/index.html](/mkdocs/site/services/mini-qr/index.html) | HTML | 707 | 1 | 1,147 | 1,855 |
|
||||||
|
| [mkdocs/site/services/mkdocs/index.html](/mkdocs/site/services/mkdocs/index.html) | HTML | 905 | 1 | 1,187 | 2,093 |
|
||||||
|
| [mkdocs/site/services/n8n/index.html](/mkdocs/site/services/n8n/index.html) | HTML | 1,057 | 1 | 1,223 | 2,281 |
|
||||||
|
| [mkdocs/site/services/nocodb/index.html](/mkdocs/site/services/nocodb/index.html) | HTML | 1,040 | 1 | 1,219 | 2,260 |
|
||||||
|
| [mkdocs/site/services/postgresql/index.html](/mkdocs/site/services/postgresql/index.html) | HTML | 880 | 1 | 1,190 | 2,071 |
|
||||||
|
| [mkdocs/site/services/static-server/index.html](/mkdocs/site/services/static-server/index.html) | HTML | 877 | 1 | 1,182 | 2,060 |
|
||||||
|
| [mkdocs/site/sitemap.xml](/mkdocs/site/sitemap.xml) | XML | 147 | 0 | 0 | 147 |
|
||||||
|
| [mkdocs/site/stylesheets/extra.css](/mkdocs/site/stylesheets/extra.css) | PostCSS | 498 | 18 | 82 | 598 |
|
||||||
|
| [mkdocs/site/stylesheets/home.css](/mkdocs/site/stylesheets/home.css) | PostCSS | 866 | 54 | 153 | 1,073 |
|
||||||
|
| [mkdocs/site/test/index.html](/mkdocs/site/test/index.html) | HTML | 278 | 1 | 484 | 763 |
|
||||||
|
| [reset-site.sh](/reset-site.sh) | Shell Script | 258 | 28 | 73 | 359 |
|
||||||
|
| [start-production.sh](/start-production.sh) | Shell Script | 487 | 77 | 98 | 662 |
|
||||||
|
| [test.md](/test.md) | Markdown | 1 | 0 | 0 | 1 |
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
15
.VSCodeCounter/2025-09-05_12-42-08/diff-details.md
Normal file
15
.VSCodeCounter/2025-09-05_12-42-08/diff-details.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Diff Details
|
||||||
|
|
||||||
|
Date : 2025-09-05 12:42:08
|
||||||
|
|
||||||
|
Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite
|
||||||
|
|
||||||
|
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
2
.VSCodeCounter/2025-09-05_12-42-08/diff.csv
Normal file
2
.VSCodeCounter/2025-09-05_12-42-08/diff.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
"filename", "language", "", "comment", "blank", "total"
|
||||||
|
"Total", "-", , 0, 0, 0
|
||||||
|
19
.VSCodeCounter/2025-09-05_12-42-08/diff.md
Normal file
19
.VSCodeCounter/2025-09-05_12-42-08/diff.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Diff Summary
|
||||||
|
|
||||||
|
Date : 2025-09-05 12:42:08
|
||||||
|
|
||||||
|
Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite
|
||||||
|
|
||||||
|
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
22
.VSCodeCounter/2025-09-05_12-42-08/diff.txt
Normal file
22
.VSCodeCounter/2025-09-05_12-42-08/diff.txt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
Date : 2025-09-05 12:42:08
|
||||||
|
Directory : /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite
|
||||||
|
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------+------------+------------+------------+------------+------------+
|
||||||
|
+----------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+------+------------+------------+------------+------------+------------+
|
||||||
|
+------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+----------+----------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+----------+----------+------------+------------+------------+------------+
|
||||||
|
| Total | | 0 | 0 | 0 | 0 |
|
||||||
|
+----------+----------+------------+------------+------------+------------+
|
||||||
337
.VSCodeCounter/2025-09-05_12-42-08/results.csv
Normal file
337
.VSCodeCounter/2025-09-05_12-42-08/results.csv
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
"filename", "language", "Shell Script", "Markdown", "Log", "YAML", "JSON", "Properties", "JavaScript", "HTML", "PostCSS", "Python", "Docker", "CSV", "XML", "comment", "blank", "total"
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/README.md", "Markdown", 0, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 79
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/combined.log", "Log", 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/config.sh", "Shell Script", 810, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 177, 224, 1211
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/bookmarks.yaml", "YAML", 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 5, 54
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/docker.yaml", "YAML", 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 7
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/kubernetes.yaml", "YAML", 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/logs/homepage.log", "Log", 0, 0, 692, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 706
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/services.yaml", "YAML", 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 15, 75
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/settings.yaml", "YAML", 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 50
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/widgets.yaml", "YAML", 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 5, 33
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/mkdocs-site/default.conf", "Properties", 0, 0, 0, 0, 0, 33, 0, 0, 0, 0, 0, 0, 0, 7, 9, 49
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/docker-compose.yml", "YAML", 0, 0, 0, 238, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 13, 253
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/Instuctions.md", "Markdown", 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 77
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/README.md", "Markdown", 0, 851, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 1103
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/Dockerfile", "Docker", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 7, 10, 37
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/config/index.js", "JavaScript", 0, 0, 0, 0, 0, 0, 118, 0, 0, 0, 0, 0, 0, 15, 18, 151
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/authController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 0, 0, 0, 13, 25, 210
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/cutsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 276, 0, 0, 0, 0, 0, 0, 41, 47, 364
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dashboardController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 9, 15, 94
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dataConvertController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 352, 0, 0, 0, 0, 0, 0, 50, 76, 478
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/externalDataController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 11, 21, 133
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/listmonkController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 212, 0, 0, 0, 0, 0, 0, 12, 29, 253
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/locationsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 333, 0, 0, 0, 0, 0, 0, 23, 58, 414
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/passwordRecoveryController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 4, 12, 61
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/publicShiftsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 212, 0, 0, 0, 0, 0, 0, 21, 38, 271
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/settingsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 303, 0, 0, 0, 0, 0, 0, 17, 50, 370
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/shiftsController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 643, 0, 0, 0, 0, 0, 0, 57, 123, 823
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/usersController.js", "JavaScript", 0, 0, 0, 0, 0, 0, 341, 0, 0, 0, 0, 0, 0, 18, 54, 413
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 170, 0, 0, 0, 0, 0, 0, 13, 25, 208
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/rateLimiter.js", "JavaScript", 0, 0, 0, 0, 0, 0, 85, 0, 0, 0, 0, 0, 0, 10, 9, 104
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package-lock.json", "JSON", 0, 0, 0, 0, 2203, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2204
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package.json", "JSON", 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 44
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/admin.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1080, 0, 0, 0, 0, 0, 46, 135, 1261
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/REFACTORING_SUMMARY.md", "Markdown", 0, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 64
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 4, 3, 19
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/cuts-shifts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 225, 0, 0, 0, 0, 13, 44, 282
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/data-convert.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 176, 0, 0, 0, 0, 7, 33, 216
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/forms.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 207, 0, 0, 0, 0, 9, 36, 252
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/layout.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 202, 0, 0, 0, 0, 7, 36, 245
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/modals.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 259, 0, 0, 0, 0, 6, 46, 311
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/nocodb-links.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 4, 23, 136
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/responsive.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 552, 0, 0, 0, 0, 28, 112, 692
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/status-messages.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 154, 0, 0, 0, 0, 9, 27, 190
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/user-management.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 179, 0, 0, 0, 0, 10, 31, 220
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/variables.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 9, 10, 67
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/walk-sheet.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 12, 39, 303
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-marker.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 2, 5, 42
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-popup.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 197, 0, 0, 0, 0, 9, 30, 236
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/base.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 0, 22, 12, 102
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/buttons.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 2, 16, 88
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cache-busting.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 99, 0, 0, 0, 0, 2, 13, 114
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cuts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 829, 0, 0, 0, 0, 22, 146, 997
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/dashboard.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 129, 0, 0, 0, 0, 3, 29, 161
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/doc-search.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 0, 0, 2, 20, 145
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/forms.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 2, 18, 125
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/layout.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 83, 0, 0, 0, 0, 5, 11, 99
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/leaflet-custom.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 147, 0, 0, 0, 0, 20, 32, 199
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/listmonk.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 295, 0, 0, 0, 0, 4, 55, 354
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/map-controls.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 281, 0, 0, 0, 0, 13, 45, 339
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/mobile-ui.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 205, 0, 0, 0, 0, 8, 36, 249
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/modal.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 0, 1, 10, 84
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/nocodb-dropdown.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 134, 0, 0, 0, 0, 4, 24, 162
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/notifications.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 5, 16, 126
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/print.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 1, 2, 14
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/qr-code.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 3, 13, 75
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/responsive.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 150, 0, 0, 0, 0, 12, 29, 191
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/start-location-marker.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 0, 3, 8, 76
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/temp-user.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 46, 0, 0, 0, 0, 6, 5, 57
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/unified-search.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 580, 0, 0, 0, 0, 30, 99, 709
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/public-shifts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 418, 0, 0, 0, 0, 24, 83, 525
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/shifts.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 608, 0, 0, 0, 0, 21, 118, 747
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/style.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 1, 22
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/user.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 364, 0, 0, 0, 0, 13, 70, 447
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 384, 0, 0, 0, 0, 0, 28, 45, 457
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 63, 0, 0, 0, 0, 0, 0, 11, 14, 88
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-core.js", "JavaScript", 0, 0, 0, 0, 0, 0, 239, 0, 0, 0, 0, 0, 0, 46, 40, 325
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-cuts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1505, 0, 0, 0, 0, 0, 0, 184, 302, 1991
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-email.js", "JavaScript", 0, 0, 0, 0, 0, 0, 319, 0, 0, 0, 0, 0, 0, 29, 47, 395
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-map.js", "JavaScript", 0, 0, 0, 0, 0, 0, 178, 0, 0, 0, 0, 0, 0, 26, 46, 250
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shift-volunteers.js", "JavaScript", 0, 0, 0, 0, 0, 0, 416, 0, 0, 0, 0, 0, 0, 49, 66, 531
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 330, 0, 0, 0, 0, 0, 0, 35, 55, 420
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-users.js", "JavaScript", 0, 0, 0, 0, 0, 0, 305, 0, 0, 0, 0, 0, 0, 16, 45, 366
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-walksheet.js", "JavaScript", 0, 0, 0, 0, 0, 0, 387, 0, 0, 0, 0, 0, 0, 35, 50, 472
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin.js", "JavaScript", 0, 0, 0, 0, 0, 0, 2309, 0, 0, 0, 0, 0, 0, 178, 288, 2775
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/navigation.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/startLocation.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/users.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/utils.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/walkSheet.js", "JavaScript", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 181, 0, 0, 0, 0, 0, 0, 28, 34, 243
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cache-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 219, 0, 0, 0, 0, 0, 0, 71, 28, 318
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/config.js", "JavaScript", 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 3, 3, 38
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-controls.js", "JavaScript", 0, 0, 0, 0, 0, 0, 982, 0, 0, 0, 0, 0, 0, 163, 145, 1290
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-drawing.js", "JavaScript", 0, 0, 0, 0, 0, 0, 206, 0, 0, 0, 0, 0, 0, 72, 59, 337
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 359, 0, 0, 0, 0, 0, 0, 96, 79, 534
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/dashboard.js", "JavaScript", 0, 0, 0, 0, 0, 0, 333, 0, 0, 0, 0, 0, 0, 23, 33, 389
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/data-convert.js", "JavaScript", 0, 0, 0, 0, 0, 0, 510, 0, 0, 0, 0, 0, 0, 64, 118, 692
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/database-search.js", "JavaScript", 0, 0, 0, 0, 0, 0, 186, 0, 0, 0, 0, 0, 0, 76, 51, 313
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/external-layers.js", "JavaScript", 0, 0, 0, 0, 0, 0, 513, 0, 0, 0, 0, 0, 0, 39, 45, 597
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-admin.js", "JavaScript", 0, 0, 0, 0, 0, 0, 410, 0, 0, 0, 0, 0, 0, 76, 85, 571
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-status.js", "JavaScript", 0, 0, 0, 0, 0, 0, 169, 0, 0, 0, 0, 0, 0, 25, 32, 226
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/location-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 998, 0, 0, 0, 0, 0, 0, 56, 91, 1145
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/main.js", "JavaScript", 0, 0, 0, 0, 0, 0, 178, 0, 0, 0, 0, 0, 0, 40, 36, 254
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 82, 0, 0, 0, 0, 0, 0, 12, 19, 113
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-search.js", "JavaScript", 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 44, 36, 200
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/mkdocs-search.js", "JavaScript", 0, 0, 0, 0, 0, 0, 263, 0, 0, 0, 0, 0, 0, 36, 52, 351
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/public-shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 518, 0, 0, 0, 0, 0, 0, 15, 25, 558
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/search-manager.js", "JavaScript", 0, 0, 0, 0, 0, 0, 407, 0, 0, 0, 0, 0, 0, 138, 100, 645
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1059, 0, 0, 0, 0, 0, 0, 35, 55, 1149
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/ui-controls.js", "JavaScript", 0, 0, 0, 0, 0, 0, 572, 0, 0, 0, 0, 0, 0, 74, 101, 747
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/user.js", "JavaScript", 0, 0, 0, 0, 0, 0, 81, 0, 0, 0, 0, 0, 0, 12, 19, 112
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/utils.js", "JavaScript", 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 21, 25, 142
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/login.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 459, 0, 0, 0, 0, 0, 3, 76, 538
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/public-shifts.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 88, 0, 0, 0, 0, 0, 4, 7, 99
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/shifts.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 73, 0, 0, 0, 0, 0, 5, 8, 86
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/user.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 0, 0, 7, 9, 63
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/admin.js", "JavaScript", 0, 0, 0, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 15, 16, 111
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/auth.js", "JavaScript", 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 5, 6, 25
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/cuts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 6, 7, 31
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dashboard.js", "JavaScript", 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3, 8
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dataConvert.js", "JavaScript", 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 4, 6, 31
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/debug.js", "JavaScript", 0, 0, 0, 0, 0, 0, 236, 0, 0, 0, 0, 0, 0, 10, 36, 282
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/external.js", "JavaScript", 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 2, 4, 13
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/geocoding.js", "JavaScript", 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 24, 31, 175
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/index.js", "JavaScript", 0, 0, 0, 0, 0, 0, 153, 0, 0, 0, 0, 0, 0, 29, 36, 218
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/listmonk.js", "JavaScript", 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 5, 9, 26
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/locations.js", "JavaScript", 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 5, 6, 22
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/public.js", "JavaScript", 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 3, 12
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/publicShifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 2, 11
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/qr.js", "JavaScript", 0, 0, 0, 0, 0, 0, 39, 0, 0, 0, 0, 0, 0, 1, 8, 48
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/settings.js", "JavaScript", 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 2, 3, 13
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/shifts.js", "JavaScript", 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 4, 5, 25
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/users.js", "JavaScript", 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 6, 7, 24
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/server.js", "JavaScript", 0, 0, 0, 0, 0, 0, 249, 0, 0, 0, 0, 0, 0, 43, 49, 341
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/accountExpiration.js", "JavaScript", 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 11, 19, 89
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/email.js", "JavaScript", 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 0, 0, 4, 19, 132
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/emailTemplates.js", "JavaScript", 0, 0, 0, 0, 0, 0, 59, 0, 0, 0, 0, 0, 0, 10, 16, 85
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/geocoding.js", "JavaScript", 0, 0, 0, 0, 0, 0, 219, 0, 0, 0, 0, 0, 0, 51, 44, 314
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/listmonk.js", "JavaScript", 0, 0, 0, 0, 0, 0, 412, 0, 0, 0, 0, 0, 0, 36, 66, 514
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/nocodb.js", "JavaScript", 0, 0, 0, 0, 0, 0, 172, 0, 0, 0, 0, 0, 0, 22, 36, 230
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/qrcode.js", "JavaScript", 0, 0, 0, 0, 0, 0, 110, 0, 0, 0, 0, 0, 0, 33, 20, 163
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/socrata.js", "JavaScript", 0, 0, 0, 0, 0, 0, 49, 0, 0, 0, 0, 0, 0, 9, 10, 68
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/login-details.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 113, 0, 0, 0, 0, 0, 0, 4, 117
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/password-recovery.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 0, 0, 0, 1, 80
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-existing.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 161, 0, 0, 0, 0, 0, 0, 22, 183
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-new.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 5, 36
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/shift-details.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 152, 0, 0, 0, 0, 0, 0, 5, 157
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/user-broadcast.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 2, 110
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/cacheBusting.js", "JavaScript", 0, 0, 0, 0, 0, 0, 105, 0, 0, 0, 0, 0, 0, 51, 24, 180
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/helpers.js", "JavaScript", 0, 0, 0, 0, 0, 0, 149, 0, 0, 0, 0, 0, 0, 25, 28, 202
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/logger.js", "JavaScript", 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 2, 3, 43
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/build-nocodb.sh", "Shell Script", 925, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 93, 1078
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods (Centroid Point)_20250807.geojson", "JSON", 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods_20250807.geojson", "JSON", 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton Ward Boundary and Council Composition_Current_20250807.geojson", "JSON", 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 689, 0, 0, 1, 690
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 407, 0, 0, 1, 408
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 1, 14
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_edmonton_optimized.js", "JavaScript", 0, 0, 0, 0, 0, 0, 185, 0, 0, 0, 0, 0, 0, 21, 43, 249
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_ward_boundaries_optimized.js", "JavaScript", 0, 0, 0, 0, 0, 0, 173, 0, 0, 0, 0, 0, 0, 24, 45, 242
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_neighborhoods_nocodb_optimized.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 689, 0, 0, 0, 689
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_nocodb_optimized.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1095, 0, 0, 406, 1501
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_ward_boundaries_optimized.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 13
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/exampledata.csv", "CSV", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/docker-compose.yml", "YAML", 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 34
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/files-explainer.md", "Markdown", 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 293, 585
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/ADMIN_IMPLEMENTATION.md", "Markdown", 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 130
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md", "Markdown", 0, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 190
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md", "Markdown", 0, 124, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 31, 160
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md", "Markdown", 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 86
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/LISTMONK_INTEGRATION_GUIDE.md", "Markdown", 0, 249, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 319
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/SHIFT_PERFORMANCE_FIX.md", "Markdown", 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 93
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_IMPLEMENTATION.md", "Markdown", 0, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 116
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_TEST.md", "Markdown", 0, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 147
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/build-nocodb.md", "Markdown", 0, 374, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67, 441
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package-lock.json", "JSON", 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package.json", "JSON", 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/ansible.md", "Markdown", 0, 373, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 152, 525
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/index.md", "Markdown", 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/vscode-ssh.md", "Markdown", 0, 511, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 174, 685
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/anthropics-claude-code.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/coder-code-server.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/gethomepage-homepage.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/go-gitea-gitea.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/knadh-listmonk.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/n8n-io-n8n.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/nocodb-nocodb.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/ollama-ollama.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/index.md", "Markdown", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/1.md", "Markdown", 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/2.md", "Markdown", 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 14
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/3.md", "Markdown", 0, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 75
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/index.md", "Markdown", 0, 336, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 486
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/map.md", "Markdown", 0, 155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 217
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/server.md", "Markdown", 0, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 149
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/site.md", "Markdown", 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 108
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/cloudflare-config.md", "Markdown", 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 61
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/coder.md", "Markdown", 0, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 216
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/index.md", "Markdown", 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 7
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/map.md", "Markdown", 0, 299, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 390
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/mkdocs.md", "Markdown", 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 144
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/hooks/repo_widget_hook.py", "Python", 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 0, 0, 0, 24, 16, 159
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/how to/canvass.md", "Markdown", 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 5
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/index.md", "Markdown", 0, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 114
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/gitea-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 121, 0, 0, 0, 0, 0, 0, 3, 8, 132
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/github-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 10, 14, 168
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/home.js", "JavaScript", 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 30, 37, 338
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/index.md", "Markdown", 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/map.md", "Markdown", 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 139
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/lander.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1872, 0, 0, 0, 0, 0, 11, 259, 2142
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/main.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 1, 3, 12
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/cost-comparison.md", "Markdown", 0, 153, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 204
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/index.md", "Markdown", 0, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 164
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/code-server.md", "Markdown", 0, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 62
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/gitea.md", "Markdown", 0, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 58
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/homepage.md", "Markdown", 0, 146, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 212
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/index.md", "Markdown", 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 118
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/listmonk.md", "Markdown", 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 69
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/map.md", "Markdown", 0, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 97
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mini-qr.md", "Markdown", 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 38
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mkdocs.md", "Markdown", 0, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 133
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/n8n.md", "Markdown", 0, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 158
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/nocodb.md", "Markdown", 0, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 163
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/postgresql.md", "Markdown", 0, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 91
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/static-server.md", "Markdown", 0, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 101
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/extra.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 498, 0, 0, 0, 0, 18, 82, 598
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/home.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 866, 0, 0, 0, 0, 54, 153, 1073
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/test.md", "Markdown", 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 11
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/mkdocs.yml", "YAML", 0, 0, 0, 167, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 189
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/404.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 251, 0, 0, 0, 0, 0, 1, 437, 689
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/ansible/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1653, 0, 0, 0, 0, 0, 1, 1326, 2980
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 549, 0, 0, 0, 0, 0, 1, 1083, 1633
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/vscode-ssh/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1926, 0, 0, 0, 0, 0, 1, 1358, 3285
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/bundle.50899def.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 1, 2, 17
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 16, 1, 18
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/tinyseg.js", "JavaScript", 0, 0, 0, 0, 0, 0, 176, 0, 0, 0, 0, 0, 0, 21, 9, 206
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/wordcut.js", "JavaScript", 0, 0, 0, 0, 0, 0, 4882, 0, 0, 0, 0, 0, 0, 915, 911, 6708
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js", "JavaScript", 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 3, 2, 43
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/admin-changemaker.lite.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/anthropics-claude-code.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/coder-code-server.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/gethomepage-homepage.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/go-gitea-gitea.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/knadh-listmonk.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/lyqht-mini-qr.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/n8n-io-n8n.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/nocodb-nocodb.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/ollama-ollama.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json", "JSON", 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/main.7e37652d.min.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/palette.06af60db.min.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/03/blog-1/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 374, 0, 0, 0, 0, 0, 1, 582, 957
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/10/2/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 392, 0, 0, 0, 0, 0, 1, 583, 976
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/08/01/3/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 455, 0, 0, 0, 0, 0, 1, 590, 1046
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/archive/2025/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 442, 0, 0, 0, 0, 0, 1, 582, 1025
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 451, 0, 0, 0, 0, 0, 1, 572, 1024
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1312, 0, 0, 0, 0, 0, 1, 1199, 2512
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 983, 0, 0, 0, 0, 0, 1, 1182, 2166
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/server/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1045, 0, 0, 0, 0, 0, 1, 1234, 2280
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/site/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 748, 0, 0, 0, 0, 0, 1, 1138, 1887
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/cloudflare-config/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 740, 0, 0, 0, 0, 0, 1, 1146, 1887
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/coder/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1117, 0, 0, 0, 0, 0, 1, 1240, 2358
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 550, 0, 0, 0, 0, 0, 1, 1083, 1634
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1672, 0, 0, 0, 0, 0, 1, 1346, 3019
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/mkdocs/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 833, 0, 0, 0, 0, 0, 1, 1151, 1985
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/hooks/repo_widget_hook.py", "Python", 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 0, 0, 0, 24, 16, 159
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/how to/canvass/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 275, 0, 0, 0, 0, 0, 1, 484, 760
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1872, 0, 0, 0, 0, 0, 11, 259, 2142
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/gitea-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 121, 0, 0, 0, 0, 0, 0, 3, 8, 132
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/github-widget.js", "JavaScript", 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 10, 14, 168
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/home.js", "JavaScript", 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 30, 37, 338
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 549, 0, 0, 0, 0, 0, 1, 1083, 1633
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 987, 0, 0, 0, 0, 0, 1, 1202, 2190
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/lander.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1872, 0, 0, 0, 0, 0, 11, 259, 2142
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/main.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 1, 3, 12
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/cost-comparison/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1300, 0, 0, 0, 0, 0, 1, 766, 2067
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 717, 0, 0, 0, 0, 0, 1, 661, 1379
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/search/search_index.json", "JSON", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/code-server/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 755, 0, 0, 0, 0, 0, 1, 1155, 1911
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/gitea/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 737, 0, 0, 0, 0, 0, 1, 1151, 1889
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/homepage/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1119, 0, 0, 0, 0, 0, 1, 1235, 2355
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 769, 0, 0, 0, 0, 0, 1, 1113, 1883
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/listmonk/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 775, 0, 0, 0, 0, 0, 1, 1159, 1935
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/map/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 877, 0, 0, 0, 0, 0, 1, 1170, 2048
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mini-qr/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 707, 0, 0, 0, 0, 0, 1, 1147, 1855
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mkdocs/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 905, 0, 0, 0, 0, 0, 1, 1187, 2093
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/n8n/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1057, 0, 0, 0, 0, 0, 1, 1223, 2281
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/nocodb/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 1040, 0, 0, 0, 0, 0, 1, 1219, 2260
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/postgresql/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 880, 0, 0, 0, 0, 0, 1, 1190, 2071
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/static-server/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 877, 0, 0, 0, 0, 0, 1, 1182, 2060
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/sitemap.xml", "XML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 0, 0, 147
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/extra.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 498, 0, 0, 0, 0, 18, 82, 598
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/home.css", "PostCSS", 0, 0, 0, 0, 0, 0, 0, 0, 866, 0, 0, 0, 0, 54, 153, 1073
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/test/index.html", "HTML", 0, 0, 0, 0, 0, 0, 0, 278, 0, 0, 0, 0, 0, 1, 484, 763
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/reset-site.sh", "Shell Script", 258, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 73, 359
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/start-production.sh", "Shell Script", 487, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 98, 662
|
||||||
|
"/mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/test.md", "Markdown", 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
"Total", "-", 2480, 6210, 705, 608, 2634, 33, 27027, 38504, 10405, 238, 20, 2913, 147, 4969, 47542, 144435
|
||||||
|
1
.VSCodeCounter/2025-09-05_12-42-08/results.json
Normal file
1
.VSCodeCounter/2025-09-05_12-42-08/results.json
Normal file
File diff suppressed because one or more lines are too long
148
.VSCodeCounter/2025-09-05_12-42-08/results.md
Normal file
148
.VSCodeCounter/2025-09-05_12-42-08/results.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
Date : 2025-09-05 12:42:08
|
||||||
|
|
||||||
|
Directory /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite
|
||||||
|
|
||||||
|
Total : 335 files, 91924 codes, 4969 comments, 47542 blanks, all 144435 lines
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| HTML | 53 | 38,504 | 164 | 37,745 | 76,413 |
|
||||||
|
| JavaScript | 129 | 27,027 | 3,870 | 4,499 | 35,396 |
|
||||||
|
| PostCSS | 46 | 10,405 | 501 | 1,857 | 12,763 |
|
||||||
|
| Markdown | 50 | 6,210 | 5 | 2,406 | 8,621 |
|
||||||
|
| CSV | 7 | 2,913 | 0 | 409 | 3,322 |
|
||||||
|
| JSON | 30 | 2,634 | 0 | 5 | 2,639 |
|
||||||
|
| Shell Script | 4 | 2,480 | 342 | 488 | 3,310 |
|
||||||
|
| Log | 2 | 705 | 0 | 17 | 722 |
|
||||||
|
| YAML | 9 | 608 | 25 | 65 | 698 |
|
||||||
|
| Python | 2 | 238 | 48 | 32 | 318 |
|
||||||
|
| XML | 1 | 147 | 0 | 0 | 147 |
|
||||||
|
| Properties | 1 | 33 | 7 | 9 | 49 |
|
||||||
|
| Docker | 1 | 20 | 7 | 10 | 37 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 335 | 91,924 | 4,969 | 47,542 | 144,435 |
|
||||||
|
| . (Files) | 7 | 1,862 | 284 | 435 | 2,581 |
|
||||||
|
| configs | 10 | 897 | 19 | 63 | 979 |
|
||||||
|
| configs/homepage | 9 | 864 | 12 | 54 | 930 |
|
||||||
|
| configs/homepage (Files) | 8 | 172 | 12 | 40 | 224 |
|
||||||
|
| configs/homepage/logs | 1 | 692 | 0 | 14 | 706 |
|
||||||
|
| configs/mkdocs-site | 1 | 33 | 7 | 9 | 49 |
|
||||||
|
| map | 167 | 40,025 | 3,126 | 6,580 | 49,731 |
|
||||||
|
| map (Files) | 7 | 2,177 | 60 | 664 | 2,901 |
|
||||||
|
| map/app | 139 | 33,222 | 3,016 | 5,083 | 41,321 |
|
||||||
|
| map/app (Files) | 4 | 2,515 | 50 | 61 | 2,626 |
|
||||||
|
| map/app/config | 1 | 118 | 15 | 18 | 151 |
|
||||||
|
| map/app/controllers | 12 | 3,060 | 276 | 548 | 3,884 |
|
||||||
|
| map/app/middleware | 2 | 255 | 23 | 34 | 312 |
|
||||||
|
| map/app/public | 86 | 24,382 | 2,278 | 3,910 | 30,570 |
|
||||||
|
| map/app/public (Files) | 6 | 2,131 | 93 | 280 | 2,504 |
|
||||||
|
| map/app/public/css | 40 | 7,726 | 357 | 1,399 | 9,482 |
|
||||||
|
| map/app/public/css (Files) | 6 | 1,474 | 62 | 288 | 1,824 |
|
||||||
|
| map/app/public/css/admin | 11 | 2,363 | 114 | 437 | 2,914 |
|
||||||
|
| map/app/public/css/modules | 23 | 3,889 | 181 | 674 | 4,744 |
|
||||||
|
| map/app/public/js | 40 | 14,525 | 1,828 | 2,231 | 18,584 |
|
||||||
|
| map/app/public/js (Files) | 33 | 14,525 | 1,828 | 2,224 | 18,577 |
|
||||||
|
| map/app/public/js/admin | 7 | 0 | 0 | 7 | 7 |
|
||||||
|
| map/app/routes | 17 | 767 | 120 | 188 | 1,075 |
|
||||||
|
| map/app/services | 8 | 1,189 | 176 | 230 | 1,595 |
|
||||||
|
| map/app/templates | 6 | 644 | 0 | 39 | 683 |
|
||||||
|
| map/app/templates/email | 6 | 644 | 0 | 39 | 683 |
|
||||||
|
| map/app/utils | 3 | 292 | 78 | 55 | 425 |
|
||||||
|
| map/data | 12 | 3,284 | 45 | 498 | 3,827 |
|
||||||
|
| map/instruct | 9 | 1,342 | 5 | 335 | 1,682 |
|
||||||
|
| mkdocs | 151 | 49,140 | 1,540 | 40,464 | 91,144 |
|
||||||
|
| mkdocs (Files) | 1 | 167 | 11 | 11 | 189 |
|
||||||
|
| mkdocs/docs | 54 | 7,637 | 151 | 2,040 | 9,828 |
|
||||||
|
| mkdocs/docs (Files) | 2 | 90 | 0 | 35 | 125 |
|
||||||
|
| mkdocs/docs/adv | 3 | 886 | 0 | 327 | 1,213 |
|
||||||
|
| mkdocs/docs/assets | 11 | 176 | 0 | 0 | 176 |
|
||||||
|
| mkdocs/docs/assets/repo-data | 11 | 176 | 0 | 0 | 176 |
|
||||||
|
| mkdocs/docs/blog | 4 | 70 | 0 | 29 | 99 |
|
||||||
|
| mkdocs/docs/blog (Files) | 1 | 0 | 0 | 1 | 1 |
|
||||||
|
| mkdocs/docs/blog/posts | 3 | 70 | 0 | 28 | 98 |
|
||||||
|
| mkdocs/docs/build | 4 | 687 | 0 | 273 | 960 |
|
||||||
|
| mkdocs/docs/config | 5 | 588 | 0 | 230 | 818 |
|
||||||
|
| mkdocs/docs/hooks | 1 | 119 | 24 | 16 | 159 |
|
||||||
|
| mkdocs/docs/how to | 1 | 2 | 0 | 3 | 5 |
|
||||||
|
| mkdocs/docs/javascripts | 3 | 536 | 43 | 59 | 638 |
|
||||||
|
| mkdocs/docs/manual | 2 | 93 | 0 | 49 | 142 |
|
||||||
|
| mkdocs/docs/overrides | 2 | 1,880 | 12 | 262 | 2,154 |
|
||||||
|
| mkdocs/docs/phil | 2 | 248 | 0 | 120 | 368 |
|
||||||
|
| mkdocs/docs/services | 12 | 898 | 0 | 402 | 1,300 |
|
||||||
|
| mkdocs/docs/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 |
|
||||||
|
| mkdocs/site | 96 | 41,336 | 1,378 | 38,413 | 81,127 |
|
||||||
|
| mkdocs/site (Files) | 3 | 2,270 | 12 | 696 | 2,978 |
|
||||||
|
| mkdocs/site/adv | 3 | 4,128 | 3 | 3,767 | 7,898 |
|
||||||
|
| mkdocs/site/adv (Files) | 1 | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| mkdocs/site/adv/ansible | 1 | 1,653 | 1 | 1,326 | 2,980 |
|
||||||
|
| mkdocs/site/adv/vscode-ssh | 1 | 1,926 | 1 | 1,358 | 3,285 |
|
||||||
|
| mkdocs/site/assets | 49 | 5,320 | 1,180 | 939 | 7,439 |
|
||||||
|
| mkdocs/site/assets/javascripts | 36 | 5,142 | 1,180 | 939 | 7,261 |
|
||||||
|
| mkdocs/site/assets/javascripts (Files) | 1 | 14 | 1 | 2 | 17 |
|
||||||
|
| mkdocs/site/assets/javascripts/lunr | 34 | 5,090 | 1,176 | 935 | 7,201 |
|
||||||
|
| mkdocs/site/assets/javascripts/lunr (Files) | 2 | 5,058 | 936 | 920 | 6,914 |
|
||||||
|
| mkdocs/site/assets/javascripts/lunr/min | 32 | 32 | 240 | 15 | 287 |
|
||||||
|
| mkdocs/site/assets/javascripts/workers | 1 | 38 | 3 | 2 | 43 |
|
||||||
|
| mkdocs/site/assets/repo-data | 11 | 176 | 0 | 0 | 176 |
|
||||||
|
| mkdocs/site/assets/stylesheets | 2 | 2 | 0 | 0 | 2 |
|
||||||
|
| mkdocs/site/blog | 5 | 2,114 | 5 | 2,909 | 5,028 |
|
||||||
|
| mkdocs/site/blog (Files) | 1 | 451 | 1 | 572 | 1,024 |
|
||||||
|
| mkdocs/site/blog/2025 | 3 | 1,221 | 3 | 1,755 | 2,979 |
|
||||||
|
| mkdocs/site/blog/2025/07 | 2 | 766 | 2 | 1,165 | 1,933 |
|
||||||
|
| mkdocs/site/blog/2025/07/03 | 1 | 374 | 1 | 582 | 957 |
|
||||||
|
| mkdocs/site/blog/2025/07/03/blog-1 | 1 | 374 | 1 | 582 | 957 |
|
||||||
|
| mkdocs/site/blog/2025/07/10 | 1 | 392 | 1 | 583 | 976 |
|
||||||
|
| mkdocs/site/blog/2025/07/10/2 | 1 | 392 | 1 | 583 | 976 |
|
||||||
|
| mkdocs/site/blog/2025/08 | 1 | 455 | 1 | 590 | 1,046 |
|
||||||
|
| mkdocs/site/blog/2025/08/01 | 1 | 455 | 1 | 590 | 1,046 |
|
||||||
|
| mkdocs/site/blog/2025/08/01/3 | 1 | 455 | 1 | 590 | 1,046 |
|
||||||
|
| mkdocs/site/blog/archive | 1 | 442 | 1 | 582 | 1,025 |
|
||||||
|
| mkdocs/site/blog/archive/2025 | 1 | 442 | 1 | 582 | 1,025 |
|
||||||
|
| mkdocs/site/build | 4 | 4,088 | 4 | 4,753 | 8,845 |
|
||||||
|
| mkdocs/site/build (Files) | 1 | 1,312 | 1 | 1,199 | 2,512 |
|
||||||
|
| mkdocs/site/build/map | 1 | 983 | 1 | 1,182 | 2,166 |
|
||||||
|
| mkdocs/site/build/server | 1 | 1,045 | 1 | 1,234 | 2,280 |
|
||||||
|
| mkdocs/site/build/site | 1 | 748 | 1 | 1,138 | 1,887 |
|
||||||
|
| mkdocs/site/config | 5 | 4,912 | 5 | 5,966 | 10,883 |
|
||||||
|
| mkdocs/site/config (Files) | 1 | 550 | 1 | 1,083 | 1,634 |
|
||||||
|
| mkdocs/site/config/cloudflare-config | 1 | 740 | 1 | 1,146 | 1,887 |
|
||||||
|
| mkdocs/site/config/coder | 1 | 1,117 | 1 | 1,240 | 2,358 |
|
||||||
|
| mkdocs/site/config/map | 1 | 1,672 | 1 | 1,346 | 3,019 |
|
||||||
|
| mkdocs/site/config/mkdocs | 1 | 833 | 1 | 1,151 | 1,985 |
|
||||||
|
| mkdocs/site/hooks | 1 | 119 | 24 | 16 | 159 |
|
||||||
|
| mkdocs/site/how to | 1 | 275 | 1 | 484 | 760 |
|
||||||
|
| mkdocs/site/how to/canvass | 1 | 275 | 1 | 484 | 760 |
|
||||||
|
| mkdocs/site/javascripts | 3 | 536 | 43 | 59 | 638 |
|
||||||
|
| mkdocs/site/manual | 2 | 1,536 | 2 | 2,285 | 3,823 |
|
||||||
|
| mkdocs/site/manual (Files) | 1 | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| mkdocs/site/manual/map | 1 | 987 | 1 | 1,202 | 2,190 |
|
||||||
|
| mkdocs/site/overrides | 2 | 1,880 | 12 | 262 | 2,154 |
|
||||||
|
| mkdocs/site/phil | 2 | 2,017 | 2 | 1,427 | 3,446 |
|
||||||
|
| mkdocs/site/phil (Files) | 1 | 717 | 1 | 661 | 1,379 |
|
||||||
|
| mkdocs/site/phil/cost-comparison | 1 | 1,300 | 1 | 766 | 2,067 |
|
||||||
|
| mkdocs/site/search | 1 | 1 | 0 | 0 | 1 |
|
||||||
|
| mkdocs/site/services | 12 | 10,498 | 12 | 14,131 | 24,641 |
|
||||||
|
| mkdocs/site/services (Files) | 1 | 769 | 1 | 1,113 | 1,883 |
|
||||||
|
| mkdocs/site/services/code-server | 1 | 755 | 1 | 1,155 | 1,911 |
|
||||||
|
| mkdocs/site/services/gitea | 1 | 737 | 1 | 1,151 | 1,889 |
|
||||||
|
| mkdocs/site/services/homepage | 1 | 1,119 | 1 | 1,235 | 2,355 |
|
||||||
|
| mkdocs/site/services/listmonk | 1 | 775 | 1 | 1,159 | 1,935 |
|
||||||
|
| mkdocs/site/services/map | 1 | 877 | 1 | 1,170 | 2,048 |
|
||||||
|
| mkdocs/site/services/mini-qr | 1 | 707 | 1 | 1,147 | 1,855 |
|
||||||
|
| mkdocs/site/services/mkdocs | 1 | 905 | 1 | 1,187 | 2,093 |
|
||||||
|
| mkdocs/site/services/n8n | 1 | 1,057 | 1 | 1,223 | 2,281 |
|
||||||
|
| mkdocs/site/services/nocodb | 1 | 1,040 | 1 | 1,219 | 2,260 |
|
||||||
|
| mkdocs/site/services/postgresql | 1 | 880 | 1 | 1,190 | 2,071 |
|
||||||
|
| mkdocs/site/services/static-server | 1 | 877 | 1 | 1,182 | 2,060 |
|
||||||
|
| mkdocs/site/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 |
|
||||||
|
| mkdocs/site/test | 1 | 278 | 1 | 484 | 763 |
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
486
.VSCodeCounter/2025-09-05_12-42-08/results.txt
Normal file
486
.VSCodeCounter/2025-09-05_12-42-08/results.txt
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
Date : 2025-09-05 12:42:08
|
||||||
|
Directory : /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite
|
||||||
|
Total : 335 files, 91924 codes, 4969 comments, 47542 blanks, all 144435 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+--------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+--------------+------------+------------+------------+------------+------------+
|
||||||
|
| HTML | 53 | 38,504 | 164 | 37,745 | 76,413 |
|
||||||
|
| JavaScript | 129 | 27,027 | 3,870 | 4,499 | 35,396 |
|
||||||
|
| PostCSS | 46 | 10,405 | 501 | 1,857 | 12,763 |
|
||||||
|
| Markdown | 50 | 6,210 | 5 | 2,406 | 8,621 |
|
||||||
|
| CSV | 7 | 2,913 | 0 | 409 | 3,322 |
|
||||||
|
| JSON | 30 | 2,634 | 0 | 5 | 2,639 |
|
||||||
|
| Shell Script | 4 | 2,480 | 342 | 488 | 3,310 |
|
||||||
|
| Log | 2 | 705 | 0 | 17 | 722 |
|
||||||
|
| YAML | 9 | 608 | 25 | 65 | 698 |
|
||||||
|
| Python | 2 | 238 | 48 | 32 | 318 |
|
||||||
|
| XML | 1 | 147 | 0 | 0 | 147 |
|
||||||
|
| Properties | 1 | 33 | 7 | 9 | 49 |
|
||||||
|
| Docker | 1 | 20 | 7 | 10 | 37 |
|
||||||
|
+--------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 335 | 91,924 | 4,969 | 47,542 | 144,435 |
|
||||||
|
| . (Files) | 7 | 1,862 | 284 | 435 | 2,581 |
|
||||||
|
| configs | 10 | 897 | 19 | 63 | 979 |
|
||||||
|
| configs/homepage | 9 | 864 | 12 | 54 | 930 |
|
||||||
|
| configs/homepage (Files) | 8 | 172 | 12 | 40 | 224 |
|
||||||
|
| configs/homepage/logs | 1 | 692 | 0 | 14 | 706 |
|
||||||
|
| configs/mkdocs-site | 1 | 33 | 7 | 9 | 49 |
|
||||||
|
| map | 167 | 40,025 | 3,126 | 6,580 | 49,731 |
|
||||||
|
| map (Files) | 7 | 2,177 | 60 | 664 | 2,901 |
|
||||||
|
| map/app | 139 | 33,222 | 3,016 | 5,083 | 41,321 |
|
||||||
|
| map/app (Files) | 4 | 2,515 | 50 | 61 | 2,626 |
|
||||||
|
| map/app/config | 1 | 118 | 15 | 18 | 151 |
|
||||||
|
| map/app/controllers | 12 | 3,060 | 276 | 548 | 3,884 |
|
||||||
|
| map/app/middleware | 2 | 255 | 23 | 34 | 312 |
|
||||||
|
| map/app/public | 86 | 24,382 | 2,278 | 3,910 | 30,570 |
|
||||||
|
| map/app/public (Files) | 6 | 2,131 | 93 | 280 | 2,504 |
|
||||||
|
| map/app/public/css | 40 | 7,726 | 357 | 1,399 | 9,482 |
|
||||||
|
| map/app/public/css (Files) | 6 | 1,474 | 62 | 288 | 1,824 |
|
||||||
|
| map/app/public/css/admin | 11 | 2,363 | 114 | 437 | 2,914 |
|
||||||
|
| map/app/public/css/modules | 23 | 3,889 | 181 | 674 | 4,744 |
|
||||||
|
| map/app/public/js | 40 | 14,525 | 1,828 | 2,231 | 18,584 |
|
||||||
|
| map/app/public/js (Files) | 33 | 14,525 | 1,828 | 2,224 | 18,577 |
|
||||||
|
| map/app/public/js/admin | 7 | 0 | 0 | 7 | 7 |
|
||||||
|
| map/app/routes | 17 | 767 | 120 | 188 | 1,075 |
|
||||||
|
| map/app/services | 8 | 1,189 | 176 | 230 | 1,595 |
|
||||||
|
| map/app/templates | 6 | 644 | 0 | 39 | 683 |
|
||||||
|
| map/app/templates/email | 6 | 644 | 0 | 39 | 683 |
|
||||||
|
| map/app/utils | 3 | 292 | 78 | 55 | 425 |
|
||||||
|
| map/data | 12 | 3,284 | 45 | 498 | 3,827 |
|
||||||
|
| map/instruct | 9 | 1,342 | 5 | 335 | 1,682 |
|
||||||
|
| mkdocs | 151 | 49,140 | 1,540 | 40,464 | 91,144 |
|
||||||
|
| mkdocs (Files) | 1 | 167 | 11 | 11 | 189 |
|
||||||
|
| mkdocs/docs | 54 | 7,637 | 151 | 2,040 | 9,828 |
|
||||||
|
| mkdocs/docs (Files) | 2 | 90 | 0 | 35 | 125 |
|
||||||
|
| mkdocs/docs/adv | 3 | 886 | 0 | 327 | 1,213 |
|
||||||
|
| mkdocs/docs/assets | 11 | 176 | 0 | 0 | 176 |
|
||||||
|
| mkdocs/docs/assets/repo-data | 11 | 176 | 0 | 0 | 176 |
|
||||||
|
| mkdocs/docs/blog | 4 | 70 | 0 | 29 | 99 |
|
||||||
|
| mkdocs/docs/blog (Files) | 1 | 0 | 0 | 1 | 1 |
|
||||||
|
| mkdocs/docs/blog/posts | 3 | 70 | 0 | 28 | 98 |
|
||||||
|
| mkdocs/docs/build | 4 | 687 | 0 | 273 | 960 |
|
||||||
|
| mkdocs/docs/config | 5 | 588 | 0 | 230 | 818 |
|
||||||
|
| mkdocs/docs/hooks | 1 | 119 | 24 | 16 | 159 |
|
||||||
|
| mkdocs/docs/how to | 1 | 2 | 0 | 3 | 5 |
|
||||||
|
| mkdocs/docs/javascripts | 3 | 536 | 43 | 59 | 638 |
|
||||||
|
| mkdocs/docs/manual | 2 | 93 | 0 | 49 | 142 |
|
||||||
|
| mkdocs/docs/overrides | 2 | 1,880 | 12 | 262 | 2,154 |
|
||||||
|
| mkdocs/docs/phil | 2 | 248 | 0 | 120 | 368 |
|
||||||
|
| mkdocs/docs/services | 12 | 898 | 0 | 402 | 1,300 |
|
||||||
|
| mkdocs/docs/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 |
|
||||||
|
| mkdocs/site | 96 | 41,336 | 1,378 | 38,413 | 81,127 |
|
||||||
|
| mkdocs/site (Files) | 3 | 2,270 | 12 | 696 | 2,978 |
|
||||||
|
| mkdocs/site/adv | 3 | 4,128 | 3 | 3,767 | 7,898 |
|
||||||
|
| mkdocs/site/adv (Files) | 1 | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| mkdocs/site/adv/ansible | 1 | 1,653 | 1 | 1,326 | 2,980 |
|
||||||
|
| mkdocs/site/adv/vscode-ssh | 1 | 1,926 | 1 | 1,358 | 3,285 |
|
||||||
|
| mkdocs/site/assets | 49 | 5,320 | 1,180 | 939 | 7,439 |
|
||||||
|
| mkdocs/site/assets/javascripts | 36 | 5,142 | 1,180 | 939 | 7,261 |
|
||||||
|
| mkdocs/site/assets/javascripts (Files) | 1 | 14 | 1 | 2 | 17 |
|
||||||
|
| mkdocs/site/assets/javascripts/lunr | 34 | 5,090 | 1,176 | 935 | 7,201 |
|
||||||
|
| mkdocs/site/assets/javascripts/lunr (Files) | 2 | 5,058 | 936 | 920 | 6,914 |
|
||||||
|
| mkdocs/site/assets/javascripts/lunr/min | 32 | 32 | 240 | 15 | 287 |
|
||||||
|
| mkdocs/site/assets/javascripts/workers | 1 | 38 | 3 | 2 | 43 |
|
||||||
|
| mkdocs/site/assets/repo-data | 11 | 176 | 0 | 0 | 176 |
|
||||||
|
| mkdocs/site/assets/stylesheets | 2 | 2 | 0 | 0 | 2 |
|
||||||
|
| mkdocs/site/blog | 5 | 2,114 | 5 | 2,909 | 5,028 |
|
||||||
|
| mkdocs/site/blog (Files) | 1 | 451 | 1 | 572 | 1,024 |
|
||||||
|
| mkdocs/site/blog/2025 | 3 | 1,221 | 3 | 1,755 | 2,979 |
|
||||||
|
| mkdocs/site/blog/2025/07 | 2 | 766 | 2 | 1,165 | 1,933 |
|
||||||
|
| mkdocs/site/blog/2025/07/03 | 1 | 374 | 1 | 582 | 957 |
|
||||||
|
| mkdocs/site/blog/2025/07/03/blog-1 | 1 | 374 | 1 | 582 | 957 |
|
||||||
|
| mkdocs/site/blog/2025/07/10 | 1 | 392 | 1 | 583 | 976 |
|
||||||
|
| mkdocs/site/blog/2025/07/10/2 | 1 | 392 | 1 | 583 | 976 |
|
||||||
|
| mkdocs/site/blog/2025/08 | 1 | 455 | 1 | 590 | 1,046 |
|
||||||
|
| mkdocs/site/blog/2025/08/01 | 1 | 455 | 1 | 590 | 1,046 |
|
||||||
|
| mkdocs/site/blog/2025/08/01/3 | 1 | 455 | 1 | 590 | 1,046 |
|
||||||
|
| mkdocs/site/blog/archive | 1 | 442 | 1 | 582 | 1,025 |
|
||||||
|
| mkdocs/site/blog/archive/2025 | 1 | 442 | 1 | 582 | 1,025 |
|
||||||
|
| mkdocs/site/build | 4 | 4,088 | 4 | 4,753 | 8,845 |
|
||||||
|
| mkdocs/site/build (Files) | 1 | 1,312 | 1 | 1,199 | 2,512 |
|
||||||
|
| mkdocs/site/build/map | 1 | 983 | 1 | 1,182 | 2,166 |
|
||||||
|
| mkdocs/site/build/server | 1 | 1,045 | 1 | 1,234 | 2,280 |
|
||||||
|
| mkdocs/site/build/site | 1 | 748 | 1 | 1,138 | 1,887 |
|
||||||
|
| mkdocs/site/config | 5 | 4,912 | 5 | 5,966 | 10,883 |
|
||||||
|
| mkdocs/site/config (Files) | 1 | 550 | 1 | 1,083 | 1,634 |
|
||||||
|
| mkdocs/site/config/cloudflare-config | 1 | 740 | 1 | 1,146 | 1,887 |
|
||||||
|
| mkdocs/site/config/coder | 1 | 1,117 | 1 | 1,240 | 2,358 |
|
||||||
|
| mkdocs/site/config/map | 1 | 1,672 | 1 | 1,346 | 3,019 |
|
||||||
|
| mkdocs/site/config/mkdocs | 1 | 833 | 1 | 1,151 | 1,985 |
|
||||||
|
| mkdocs/site/hooks | 1 | 119 | 24 | 16 | 159 |
|
||||||
|
| mkdocs/site/how to | 1 | 275 | 1 | 484 | 760 |
|
||||||
|
| mkdocs/site/how to/canvass | 1 | 275 | 1 | 484 | 760 |
|
||||||
|
| mkdocs/site/javascripts | 3 | 536 | 43 | 59 | 638 |
|
||||||
|
| mkdocs/site/manual | 2 | 1,536 | 2 | 2,285 | 3,823 |
|
||||||
|
| mkdocs/site/manual (Files) | 1 | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| mkdocs/site/manual/map | 1 | 987 | 1 | 1,202 | 2,190 |
|
||||||
|
| mkdocs/site/overrides | 2 | 1,880 | 12 | 262 | 2,154 |
|
||||||
|
| mkdocs/site/phil | 2 | 2,017 | 2 | 1,427 | 3,446 |
|
||||||
|
| mkdocs/site/phil (Files) | 1 | 717 | 1 | 661 | 1,379 |
|
||||||
|
| mkdocs/site/phil/cost-comparison | 1 | 1,300 | 1 | 766 | 2,067 |
|
||||||
|
| mkdocs/site/search | 1 | 1 | 0 | 0 | 1 |
|
||||||
|
| mkdocs/site/services | 12 | 10,498 | 12 | 14,131 | 24,641 |
|
||||||
|
| mkdocs/site/services (Files) | 1 | 769 | 1 | 1,113 | 1,883 |
|
||||||
|
| mkdocs/site/services/code-server | 1 | 755 | 1 | 1,155 | 1,911 |
|
||||||
|
| mkdocs/site/services/gitea | 1 | 737 | 1 | 1,151 | 1,889 |
|
||||||
|
| mkdocs/site/services/homepage | 1 | 1,119 | 1 | 1,235 | 2,355 |
|
||||||
|
| mkdocs/site/services/listmonk | 1 | 775 | 1 | 1,159 | 1,935 |
|
||||||
|
| mkdocs/site/services/map | 1 | 877 | 1 | 1,170 | 2,048 |
|
||||||
|
| mkdocs/site/services/mini-qr | 1 | 707 | 1 | 1,147 | 1,855 |
|
||||||
|
| mkdocs/site/services/mkdocs | 1 | 905 | 1 | 1,187 | 2,093 |
|
||||||
|
| mkdocs/site/services/n8n | 1 | 1,057 | 1 | 1,223 | 2,281 |
|
||||||
|
| mkdocs/site/services/nocodb | 1 | 1,040 | 1 | 1,219 | 2,260 |
|
||||||
|
| mkdocs/site/services/postgresql | 1 | 880 | 1 | 1,190 | 2,071 |
|
||||||
|
| mkdocs/site/services/static-server | 1 | 877 | 1 | 1,182 | 2,060 |
|
||||||
|
| mkdocs/site/stylesheets | 2 | 1,364 | 72 | 235 | 1,671 |
|
||||||
|
| mkdocs/site/test | 1 | 278 | 1 | 484 | 763 |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/README.md | Markdown | 55 | 0 | 24 | 79 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/combined.log | Log | 13 | 0 | 3 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/config.sh | Shell Script | 810 | 177 | 224 | 1,211 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/bookmarks.yaml | YAML | 47 | 2 | 5 | 54 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.css | PostCSS | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/custom.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/docker.yaml | YAML | 3 | 2 | 2 | 7 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/kubernetes.yaml | YAML | 1 | 1 | 1 | 3 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/logs/homepage.log | Log | 692 | 0 | 14 | 706 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/services.yaml | YAML | 59 | 1 | 15 | 75 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/settings.yaml | YAML | 36 | 4 | 10 | 50 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/homepage/widgets.yaml | YAML | 26 | 2 | 5 | 33 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/configs/mkdocs-site/default.conf | Properties | 33 | 7 | 9 | 49 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/docker-compose.yml | YAML | 238 | 2 | 13 | 253 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/Instuctions.md | Markdown | 56 | 0 | 21 | 77 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/README.md | Markdown | 851 | 0 | 252 | 1,103 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/Dockerfile | Docker | 20 | 7 | 10 | 37 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/config/index.js | JavaScript | 118 | 15 | 18 | 151 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/authController.js | JavaScript | 172 | 13 | 25 | 210 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/cutsController.js | JavaScript | 276 | 41 | 47 | 364 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dashboardController.js | JavaScript | 70 | 9 | 15 | 94 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/dataConvertController.js | JavaScript | 352 | 50 | 76 | 478 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/externalDataController.js | JavaScript | 101 | 11 | 21 | 133 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/listmonkController.js | JavaScript | 212 | 12 | 29 | 253 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/locationsController.js | JavaScript | 333 | 23 | 58 | 414 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/passwordRecoveryController.js | JavaScript | 45 | 4 | 12 | 61 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/publicShiftsController.js | JavaScript | 212 | 21 | 38 | 271 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/settingsController.js | JavaScript | 303 | 17 | 50 | 370 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/shiftsController.js | JavaScript | 643 | 57 | 123 | 823 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/controllers/usersController.js | JavaScript | 341 | 18 | 54 | 413 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/auth.js | JavaScript | 170 | 13 | 25 | 208 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/middleware/rateLimiter.js | JavaScript | 85 | 10 | 9 | 104 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package-lock.json | JSON | 2,203 | 0 | 1 | 2,204 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/package.json | JSON | 43 | 0 | 1 | 44 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/admin.html | HTML | 1,080 | 46 | 135 | 1,261 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/REFACTORING_SUMMARY.md | Markdown | 51 | 0 | 13 | 64 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin.css | PostCSS | 12 | 4 | 3 | 19 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/cuts-shifts.css | PostCSS | 225 | 13 | 44 | 282 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/data-convert.css | PostCSS | 176 | 7 | 33 | 216 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/forms.css | PostCSS | 207 | 9 | 36 | 252 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/layout.css | PostCSS | 202 | 7 | 36 | 245 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/modals.css | PostCSS | 259 | 6 | 46 | 311 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/nocodb-links.css | PostCSS | 109 | 4 | 23 | 136 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/responsive.css | PostCSS | 552 | 28 | 112 | 692 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/status-messages.css | PostCSS | 154 | 9 | 27 | 190 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/user-management.css | PostCSS | 179 | 10 | 31 | 220 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/variables.css | PostCSS | 48 | 9 | 10 | 67 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/admin/walk-sheet.css | PostCSS | 252 | 12 | 39 | 303 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-marker.css | PostCSS | 35 | 2 | 5 | 42 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/apartment-popup.css | PostCSS | 197 | 9 | 30 | 236 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/base.css | PostCSS | 68 | 22 | 12 | 102 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/buttons.css | PostCSS | 70 | 2 | 16 | 88 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cache-busting.css | PostCSS | 99 | 2 | 13 | 114 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/cuts.css | PostCSS | 829 | 22 | 146 | 997 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/dashboard.css | PostCSS | 129 | 3 | 29 | 161 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/doc-search.css | PostCSS | 123 | 2 | 20 | 145 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/forms.css | PostCSS | 105 | 2 | 18 | 125 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/layout.css | PostCSS | 83 | 5 | 11 | 99 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/leaflet-custom.css | PostCSS | 147 | 20 | 32 | 199 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/listmonk.css | PostCSS | 295 | 4 | 55 | 354 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/map-controls.css | PostCSS | 281 | 13 | 45 | 339 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/mobile-ui.css | PostCSS | 205 | 8 | 36 | 249 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/modal.css | PostCSS | 73 | 1 | 10 | 84 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/nocodb-dropdown.css | PostCSS | 134 | 4 | 24 | 162 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/notifications.css | PostCSS | 105 | 5 | 16 | 126 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/print.css | PostCSS | 11 | 1 | 2 | 14 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/qr-code.css | PostCSS | 59 | 3 | 13 | 75 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/responsive.css | PostCSS | 150 | 12 | 29 | 191 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/start-location-marker.css | PostCSS | 65 | 3 | 8 | 76 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/temp-user.css | PostCSS | 46 | 6 | 5 | 57 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/modules/unified-search.css | PostCSS | 580 | 30 | 99 | 709 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/public-shifts.css | PostCSS | 418 | 24 | 83 | 525 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/shifts.css | PostCSS | 608 | 21 | 118 | 747 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/style.css | PostCSS | 21 | 0 | 1 | 22 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/css/user.css | PostCSS | 364 | 13 | 70 | 447 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/index.html | HTML | 384 | 28 | 45 | 457 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-auth.js | JavaScript | 63 | 11 | 14 | 88 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-core.js | JavaScript | 239 | 46 | 40 | 325 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-cuts.js | JavaScript | 1,505 | 184 | 302 | 1,991 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-email.js | JavaScript | 319 | 29 | 47 | 395 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-map.js | JavaScript | 178 | 26 | 46 | 250 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shift-volunteers.js | JavaScript | 416 | 49 | 66 | 531 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-shifts.js | JavaScript | 330 | 35 | 55 | 420 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-users.js | JavaScript | 305 | 16 | 45 | 366 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin-walksheet.js | JavaScript | 387 | 35 | 50 | 472 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin.js | JavaScript | 2,309 | 178 | 288 | 2,775 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/auth.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/navigation.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/shifts.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/startLocation.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/users.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/utils.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/admin/walkSheet.js | JavaScript | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/auth.js | JavaScript | 181 | 28 | 34 | 243 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cache-manager.js | JavaScript | 219 | 71 | 28 | 318 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/config.js | JavaScript | 32 | 3 | 3 | 38 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-controls.js | JavaScript | 982 | 163 | 145 | 1,290 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-drawing.js | JavaScript | 206 | 72 | 59 | 337 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/cut-manager.js | JavaScript | 359 | 96 | 79 | 534 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/dashboard.js | JavaScript | 333 | 23 | 33 | 389 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/data-convert.js | JavaScript | 510 | 64 | 118 | 692 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/database-search.js | JavaScript | 186 | 76 | 51 | 313 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/external-layers.js | JavaScript | 513 | 39 | 45 | 597 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-admin.js | JavaScript | 410 | 76 | 85 | 571 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/listmonk-status.js | JavaScript | 169 | 25 | 32 | 226 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/location-manager.js | JavaScript | 998 | 56 | 91 | 1,145 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/main.js | JavaScript | 178 | 40 | 36 | 254 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-manager.js | JavaScript | 82 | 12 | 19 | 113 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/map-search.js | JavaScript | 120 | 44 | 36 | 200 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/mkdocs-search.js | JavaScript | 263 | 36 | 52 | 351 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/public-shifts.js | JavaScript | 518 | 15 | 25 | 558 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/search-manager.js | JavaScript | 407 | 138 | 100 | 645 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/shifts.js | JavaScript | 1,059 | 35 | 55 | 1,149 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/ui-controls.js | JavaScript | 572 | 74 | 101 | 747 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/user.js | JavaScript | 81 | 12 | 19 | 112 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/js/utils.js | JavaScript | 96 | 21 | 25 | 142 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/login.html | HTML | 459 | 3 | 76 | 538 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/public-shifts.html | HTML | 88 | 4 | 7 | 99 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/shifts.html | HTML | 73 | 5 | 8 | 86 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/public/user.html | HTML | 47 | 7 | 9 | 63 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/admin.js | JavaScript | 80 | 15 | 16 | 111 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/auth.js | JavaScript | 14 | 5 | 6 | 25 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/cuts.js | JavaScript | 18 | 6 | 7 | 31 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dashboard.js | JavaScript | 5 | 0 | 3 | 8 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/dataConvert.js | JavaScript | 21 | 4 | 6 | 31 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/debug.js | JavaScript | 236 | 10 | 36 | 282 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/external.js | JavaScript | 7 | 2 | 4 | 13 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/geocoding.js | JavaScript | 120 | 24 | 31 | 175 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/index.js | JavaScript | 153 | 29 | 36 | 218 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/listmonk.js | JavaScript | 12 | 5 | 9 | 26 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/locations.js | JavaScript | 11 | 5 | 6 | 22 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/public.js | JavaScript | 8 | 1 | 3 | 12 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/publicShifts.js | JavaScript | 8 | 1 | 2 | 11 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/qr.js | JavaScript | 39 | 1 | 8 | 48 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/settings.js | JavaScript | 8 | 2 | 3 | 13 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/shifts.js | JavaScript | 16 | 4 | 5 | 25 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/routes/users.js | JavaScript | 11 | 6 | 7 | 24 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/server.js | JavaScript | 249 | 43 | 49 | 341 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/accountExpiration.js | JavaScript | 59 | 11 | 19 | 89 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/email.js | JavaScript | 109 | 4 | 19 | 132 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/emailTemplates.js | JavaScript | 59 | 10 | 16 | 85 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/geocoding.js | JavaScript | 219 | 51 | 44 | 314 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/listmonk.js | JavaScript | 412 | 36 | 66 | 514 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/nocodb.js | JavaScript | 172 | 22 | 36 | 230 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/qrcode.js | JavaScript | 110 | 33 | 20 | 163 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/services/socrata.js | JavaScript | 49 | 9 | 10 | 68 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/login-details.html | HTML | 113 | 0 | 4 | 117 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/password-recovery.html | HTML | 79 | 0 | 1 | 80 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-existing.html | HTML | 161 | 0 | 22 | 183 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/public-shift-signup-new.html | HTML | 31 | 0 | 5 | 36 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/shift-details.html | HTML | 152 | 0 | 5 | 157 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/templates/email/user-broadcast.html | HTML | 108 | 0 | 2 | 110 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/cacheBusting.js | JavaScript | 105 | 51 | 24 | 180 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/helpers.js | JavaScript | 149 | 25 | 28 | 202 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/app/utils/logger.js | JavaScript | 38 | 2 | 3 | 43 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/build-nocodb.sh | Shell Script | 925 | 60 | 93 | 1,078 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods (Centroid Point)_20250807.geojson | JSON | 3 | 0 | 1 | 4 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton - Neighbourhoods_20250807.geojson | JSON | 5 | 0 | 0 | 5 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City of Edmonton Ward Boundary and Council Composition_Current_20250807.geojson | JSON | 5 | 0 | 0 | 5 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods_20250807.csv | CSV | 689 | 0 | 1 | 690 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_-_Neighbourhoods__Centroid_Point__20250807.csv | CSV | 407 | 0 | 1 | 408 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/City_of_Edmonton_Ward_Boundary_and_Council_Composition_Current_20250807.csv | CSV | 13 | 0 | 1 | 14 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_edmonton_optimized.js | JavaScript | 185 | 21 | 43 | 249 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/convert_ward_boundaries_optimized.js | JavaScript | 173 | 24 | 45 | 242 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_neighborhoods_nocodb_optimized.csv | CSV | 689 | 0 | 0 | 689 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_nocodb_optimized.csv | CSV | 1,095 | 0 | 406 | 1,501 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/edmonton_ward_boundaries_optimized.csv | CSV | 13 | 0 | 0 | 13 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/data/exampledata.csv | CSV | 7 | 0 | 0 | 7 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/docker-compose.yml | YAML | 31 | 0 | 3 | 34 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/files-explainer.md | Markdown | 292 | 0 | 293 | 585 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/ADMIN_IMPLEMENTATION.md | Markdown | 102 | 0 | 28 | 130 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_IMPLEMENTATION_SUMMARY.md | Markdown | 156 | 0 | 34 | 190 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_PUBLIC_IMPLEMENTATION.md | Markdown | 124 | 5 | 31 | 160 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/CUT_SIMPLIFICATION_SUMMARY.md | Markdown | 65 | 0 | 21 | 86 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/LISTMONK_INTEGRATION_GUIDE.md | Markdown | 249 | 0 | 70 | 319 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/SHIFT_PERFORMANCE_FIX.md | Markdown | 71 | 0 | 22 | 93 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_IMPLEMENTATION.md | Markdown | 88 | 0 | 28 | 116 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/TEMP_USER_TEST.md | Markdown | 113 | 0 | 34 | 147 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/instruct/build-nocodb.md | Markdown | 374 | 0 | 67 | 441 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package-lock.json | JSON | 17 | 0 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/map/package.json | JSON | 5 | 0 | 1 | 6 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/ansible.md | Markdown | 373 | 0 | 152 | 525 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/index.md | Markdown | 2 | 0 | 1 | 3 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/adv/vscode-ssh.md | Markdown | 511 | 0 | 174 | 685 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/anthropics-claude-code.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/coder-code-server.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/gethomepage-homepage.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/go-gitea-gitea.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/knadh-listmonk.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/n8n-io-n8n.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/nocodb-nocodb.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/ollama-ollama.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/index.md | Markdown | 0 | 0 | 1 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/1.md | Markdown | 6 | 0 | 3 | 9 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/2.md | Markdown | 10 | 0 | 4 | 14 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/blog/posts/3.md | Markdown | 54 | 0 | 21 | 75 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/index.md | Markdown | 336 | 0 | 150 | 486 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/map.md | Markdown | 155 | 0 | 62 | 217 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/server.md | Markdown | 122 | 0 | 27 | 149 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/build/site.md | Markdown | 74 | 0 | 34 | 108 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/cloudflare-config.md | Markdown | 45 | 0 | 16 | 61 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/coder.md | Markdown | 139 | 0 | 77 | 216 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/index.md | Markdown | 3 | 0 | 4 | 7 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/map.md | Markdown | 299 | 0 | 91 | 390 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/config/mkdocs.md | Markdown | 102 | 0 | 42 | 144 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/hooks/repo_widget_hook.py | Python | 119 | 24 | 16 | 159 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/how to/canvass.md | Markdown | 2 | 0 | 3 | 5 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/index.md | Markdown | 85 | 0 | 29 | 114 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/gitea-widget.js | JavaScript | 121 | 3 | 8 | 132 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/github-widget.js | JavaScript | 144 | 10 | 14 | 168 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/javascripts/home.js | JavaScript | 271 | 30 | 37 | 338 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/index.md | Markdown | 2 | 0 | 1 | 3 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/manual/map.md | Markdown | 91 | 0 | 48 | 139 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/lander.html | HTML | 1,872 | 11 | 259 | 2,142 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/overrides/main.html | HTML | 8 | 1 | 3 | 12 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/cost-comparison.md | Markdown | 153 | 0 | 51 | 204 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/phil/index.md | Markdown | 95 | 0 | 69 | 164 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/code-server.md | Markdown | 41 | 0 | 21 | 62 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/gitea.md | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/homepage.md | Markdown | 146 | 0 | 66 | 212 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/index.md | Markdown | 101 | 0 | 17 | 118 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/listmonk.md | Markdown | 47 | 0 | 22 | 69 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/map.md | Markdown | 70 | 0 | 27 | 97 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mini-qr.md | Markdown | 23 | 0 | 15 | 38 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/mkdocs.md | Markdown | 86 | 0 | 47 | 133 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/n8n.md | Markdown | 109 | 0 | 49 | 158 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/nocodb.md | Markdown | 108 | 0 | 55 | 163 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/postgresql.md | Markdown | 59 | 0 | 32 | 91 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/services/static-server.md | Markdown | 69 | 0 | 32 | 101 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/extra.css | PostCSS | 498 | 18 | 82 | 598 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/stylesheets/home.css | PostCSS | 866 | 54 | 153 | 1,073 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/docs/test.md | Markdown | 5 | 0 | 6 | 11 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/mkdocs.yml | YAML | 167 | 11 | 11 | 189 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/404.html | HTML | 251 | 1 | 437 | 689 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/ansible/index.html | HTML | 1,653 | 1 | 1,326 | 2,980 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/index.html | HTML | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/adv/vscode-ssh/index.html | HTML | 1,926 | 1 | 1,358 | 3,285 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/bundle.50899def.min.js | JavaScript | 14 | 1 | 2 | 17 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js | JavaScript | 1 | 16 | 1 | 18 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js | JavaScript | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/tinyseg.js | JavaScript | 176 | 21 | 9 | 206 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/lunr/wordcut.js | JavaScript | 4,882 | 915 | 911 | 6,708 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/javascripts/workers/search.d50fe291.min.js | JavaScript | 38 | 3 | 2 | 43 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/admin-changemaker.lite.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/anthropics-claude-code.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/coder-code-server.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/gethomepage-homepage.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/go-gitea-gitea.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/knadh-listmonk.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/lyqht-mini-qr.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/n8n-io-n8n.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/nocodb-nocodb.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/ollama-ollama.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json | JSON | 16 | 0 | 0 | 16 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/main.7e37652d.min.css | PostCSS | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/assets/stylesheets/palette.06af60db.min.css | PostCSS | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/03/blog-1/index.html | HTML | 374 | 1 | 582 | 957 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/07/10/2/index.html | HTML | 392 | 1 | 583 | 976 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/2025/08/01/3/index.html | HTML | 455 | 1 | 590 | 1,046 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/archive/2025/index.html | HTML | 442 | 1 | 582 | 1,025 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/blog/index.html | HTML | 451 | 1 | 572 | 1,024 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/index.html | HTML | 1,312 | 1 | 1,199 | 2,512 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/map/index.html | HTML | 983 | 1 | 1,182 | 2,166 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/server/index.html | HTML | 1,045 | 1 | 1,234 | 2,280 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/build/site/index.html | HTML | 748 | 1 | 1,138 | 1,887 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/cloudflare-config/index.html | HTML | 740 | 1 | 1,146 | 1,887 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/coder/index.html | HTML | 1,117 | 1 | 1,240 | 2,358 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/index.html | HTML | 550 | 1 | 1,083 | 1,634 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/map/index.html | HTML | 1,672 | 1 | 1,346 | 3,019 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/config/mkdocs/index.html | HTML | 833 | 1 | 1,151 | 1,985 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/hooks/repo_widget_hook.py | Python | 119 | 24 | 16 | 159 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/how to/canvass/index.html | HTML | 275 | 1 | 484 | 760 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/index.html | HTML | 1,872 | 11 | 259 | 2,142 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/gitea-widget.js | JavaScript | 121 | 3 | 8 | 132 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/github-widget.js | JavaScript | 144 | 10 | 14 | 168 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/javascripts/home.js | JavaScript | 271 | 30 | 37 | 338 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/index.html | HTML | 549 | 1 | 1,083 | 1,633 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/manual/map/index.html | HTML | 987 | 1 | 1,202 | 2,190 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/lander.html | HTML | 1,872 | 11 | 259 | 2,142 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/overrides/main.html | HTML | 8 | 1 | 3 | 12 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/cost-comparison/index.html | HTML | 1,300 | 1 | 766 | 2,067 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/phil/index.html | HTML | 717 | 1 | 661 | 1,379 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/search/search_index.json | JSON | 1 | 0 | 0 | 1 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/code-server/index.html | HTML | 755 | 1 | 1,155 | 1,911 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/gitea/index.html | HTML | 737 | 1 | 1,151 | 1,889 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/homepage/index.html | HTML | 1,119 | 1 | 1,235 | 2,355 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/index.html | HTML | 769 | 1 | 1,113 | 1,883 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/listmonk/index.html | HTML | 775 | 1 | 1,159 | 1,935 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/map/index.html | HTML | 877 | 1 | 1,170 | 2,048 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mini-qr/index.html | HTML | 707 | 1 | 1,147 | 1,855 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/mkdocs/index.html | HTML | 905 | 1 | 1,187 | 2,093 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/n8n/index.html | HTML | 1,057 | 1 | 1,223 | 2,281 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/nocodb/index.html | HTML | 1,040 | 1 | 1,219 | 2,260 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/postgresql/index.html | HTML | 880 | 1 | 1,190 | 2,071 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/services/static-server/index.html | HTML | 877 | 1 | 1,182 | 2,060 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/sitemap.xml | XML | 147 | 0 | 0 | 147 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/extra.css | PostCSS | 498 | 18 | 82 | 598 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/stylesheets/home.css | PostCSS | 866 | 54 | 153 | 1,073 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/mkdocs/site/test/index.html | HTML | 278 | 1 | 484 | 763 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/reset-site.sh | Shell Script | 258 | 28 | 73 | 359 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/start-production.sh | Shell Script | 487 | 77 | 98 | 662 |
|
||||||
|
| /mnt/storagessd1tb/changemaker.lite.dev/changemaker.lite/test.md | Markdown | 1 | 0 | 0 | 1 |
|
||||||
|
| Total | | 91,924 | 4,969 | 47,542 | 144,435 |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------------------------+--------------+------------+------------+------------+------------+
|
||||||
@ -1,180 +0,0 @@
|
|||||||
---
|
|
||||||
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
408
.env.example
@ -1,408 +0,0 @@
|
|||||||
# ==============================================================================
|
|
||||||
# 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
56
.gitignore
vendored
@ -1,16 +1,14 @@
|
|||||||
# Node modules
|
|
||||||
node_modules/
|
|
||||||
*/node_modules/
|
|
||||||
**/node_modules/
|
|
||||||
|
|
||||||
/configs/code-server/.local/*
|
/configs/code-server/.local/*
|
||||||
!/configs/code-server/.local/.gitkeep
|
!/configs/code-server/.local/.gitkeep
|
||||||
|
|
||||||
/configs/code-server/.config/*
|
/configs/code-server/.config/*
|
||||||
!/configs/code-server/.config/.gitkeep
|
!/configs/code-server/.config/.gitkeep
|
||||||
|
|
||||||
# Root assets (generated by containers)
|
# MkDocs cache and built site (created by containers)
|
||||||
/assets/
|
/mkdocs/.cache/*
|
||||||
|
!/mkdocs/.cache/.gitkeep
|
||||||
|
/mkdocs/site/*
|
||||||
|
!/mkdocs/site/.gitkeep
|
||||||
|
|
||||||
# Homepage logs (created by container)
|
# Homepage logs (created by container)
|
||||||
/configs/homepage/logs/*
|
/configs/homepage/logs/*
|
||||||
@ -18,7 +16,6 @@ node_modules/
|
|||||||
|
|
||||||
.env
|
.env
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
|
||||||
|
|
||||||
/configs/cloudflare/*.json
|
/configs/cloudflare/*.json
|
||||||
/configs/cloudflare/*.yaml
|
/configs/cloudflare/*.yaml
|
||||||
@ -28,45 +25,4 @@ node_modules/
|
|||||||
|
|
||||||
/.VSCodeCounter
|
/.VSCodeCounter
|
||||||
|
|
||||||
/influence/app/public/uploadsdata/
|
/influence/app/public/uploads
|
||||||
|
|
||||||
# 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
18
.mcp.json
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 287ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4004/favicon.ico:0
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 915ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3002/favicon.ico:0
|
|
||||||
@ -1,442 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 567ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 605ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3002/api/auth/me:0
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 1538ms] [ERROR] Failed to load resource: the server responded with a status of 400 (Bad Request) @ http://localhost:8091/auth/token/refresh:0
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 32ms] [WARNING] Manifest: property 'start_url' ignored, should be same origin as document. @ data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHRlYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cHM6Ly9naXQuY21saXRlLm9yZy8iLCJpY29ucyI6W3sic3JjIjoiaHR0cHM6Ly9naXQuY21saXRlLm9yZy9hc3NldHMvaW1nL2xvZ28ucG5nIiwidHlwZSI6ImltYWdlL3BuZyIsInNpemVzIjoiNTEyeDUxMiJ9LHsic3JjIjoiaHR0cHM6Ly9naXQuY21saXRlLm9yZy9hc3NldHMvaW1nL2xvZ28uc3ZnIiwidHlwZSI6ImltYWdlL3N2Zyt4bWwiLCJzaXplcyI6IjUxMng1MTIifV19:0
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 871ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:5678/rest/login:0
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 343ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:4003/favicon.ico:0
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 238ms] [WARNING] Simple Analytics: Set hostname on localhost:8090. See https://docs.simpleanalytics.com/overwrite-domain-name @ https://scripts.simpleanalyticscdn.com/latest.js:2
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
[ 1014ms] [WARNING] GPS permission denied — enable location access in your browser settings @ http://localhost:3002/src/components/canvass/GPSTracker.tsx:32
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
[ 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
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
[ 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.
|
Before Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 161 KiB |
747
CLAUDE.md
747
CLAUDE.md
@ -1,747 +0,0 @@
|
|||||||
# 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
288
DEV_WORKFLOW.md
@ -1,288 +0,0 @@
|
|||||||
# 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"}`
|
|
||||||
@ -2,13 +2,16 @@ FROM codercom/code-server:latest
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
# Install Node.js (for npm/claude-code — code-server bundles its own node but doesn't expose it)
|
# Install Node.js 18+ and npm
|
||||||
RUN apt-get update && apt-get install -y nodejs npm --no-install-recommends \
|
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& apt-get install -y nodejs
|
||||||
|
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally as root
|
||||||
RUN npm install -g @anthropic-ai/claude-code
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
|
|
||||||
|
# Install Ollama
|
||||||
|
RUN curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
|
||||||
# Install Python and dependencies
|
# Install Python and dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@ -1,257 +0,0 @@
|
|||||||
# 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
BIN
NARguide.pdf
Binary file not shown.
249
README.md
249
README.md
@ -1,157 +1,140 @@
|
|||||||
<p align="center">
|
# Changemaker Lite
|
||||||
<img src="mkdocs/docs/assets/logo.png" alt="Changemaker Lite" width="120" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">Changemaker Lite</h1>
|
Changemaker Lite is a streamlined documentation and development platform featuring essential self-hosted services for creating, managing, and automating political campaign workflows.
|
||||||
|
|
||||||
<p align="center">
|
## Features
|
||||||
A self-hosted campaign platform for community organizers who want to own their data.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
- **Homepage**: Modern dashboard for accessing all services
|
||||||
<a href="https://cmlite.org/docs/getting-started/">Documentation</a> ·
|
- **Code Server**: VS Code in your browser for remote development
|
||||||
<a href="https://cmlite.org">Website</a> ·
|
- **MkDocs Material**: Beautiful documentation with live preview
|
||||||
<a href="https://opensource.org/license/apache-2-0">Apache 2.0 License</a>
|
- **Static Site Server**: High-performance hosting for built sites
|
||||||
</p>
|
- **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
|
||||||
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.
|
- **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">
|
|
||||||
<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
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
# 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
|
|
||||||
|
|
||||||
cd ~/changemaker.lite
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://gitea.bnkops.com/admin/changemaker.lite
|
||||||
|
cd changemaker.lite
|
||||||
|
|
||||||
|
# Configure environment (creates .env file)
|
||||||
|
./config.sh
|
||||||
|
|
||||||
|
# Start all services
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Or clone and build from source:
|
## 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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo-url> changemaker.lite
|
cd map
|
||||||
cd changemaker.lite && git checkout v2
|
docker compose up -d
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open **http://localhost:3000** and log in with the admin credentials from your `.env`.
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
**Full documentation is available at [cmlite.org/docs/getting-started](https://cmlite.org/docs/getting-started/).**
|
Complete documentation is available in the MkDocs site, including:
|
||||||
|
|
||||||
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.
|
- Service configuration guides
|
||||||
|
- Integration examples
|
||||||
|
- Workflow automation tutorials
|
||||||
|
- Map application setup and usage
|
||||||
|
- Troubleshooting guides
|
||||||
|
|
||||||
## Architecture at a Glance
|
Visit http://localhost:4000 after starting services to access the full documentation.
|
||||||
|
|
||||||
| Layer | Technology |
|
## Licensing
|
||||||
|-------|-----------|
|
|
||||||
| 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) |
|
|
||||||
|
|
||||||
The entire stack runs on Docker Compose. Enable optional modules (media, newsletters, SMS, monitoring) with feature flags in `.env`.
|
This project is licensed under the Apache License 2.0 - https://opensource.org/license/apache-2-0
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[Apache License 2.0](https://opensource.org/license/apache-2-0)
|
|
||||||
|
|
||||||
## AI Disclaimer
|
## AI Disclaimer
|
||||||
|
|
||||||
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.
|
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
|
||||||
|
```
|
||||||
BIN
RNAguide.pdf
BIN
RNAguide.pdf
Binary file not shown.
@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
.git
|
|
||||||
*.log
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
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;"]
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<!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">↻</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>
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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
4055
admin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
<!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
1041
admin/src/App.tsx
File diff suppressed because it is too large
Load Diff
@ -1,89 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,734 +0,0 @@
|
|||||||
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 />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,242 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
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'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
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}</>;
|
|
||||||
}
|
|
||||||
@ -1,579 +0,0 @@
|
|||||||
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;">❤</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;">👑</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;">🛒</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;">📢</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;">🔀</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;
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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}</>;
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,612 +0,0 @@
|
|||||||
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)} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,270 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,252 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,233 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,192 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,479 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,435 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,525 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,236 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
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
Loading…
x
Reference in New Issue
Block a user