Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5331cdcc67 | |||
| 8af11af720 | |||
| bf997e84c1 | |||
| 35175a7136 | |||
| abb4034e4b | |||
| 97444645cb | |||
| f34382ebdd | |||
| 4a3d9d7c41 | |||
| 731e70ee42 | |||
| a7d3dd772b | |||
| 9613c3ec81 | |||
| e88ac79ae8 | |||
| 1b80e8294c | |||
| a531f9b9ce | |||
| a82e95946b | |||
| 3f6102cf6d | |||
| 1f240ad518 | |||
| 21208b58c7 | |||
| 2ae7d8b968 | |||
| aba935c8ac | |||
| 4ccc433eb9 | |||
| 94451f9aa0 | |||
| 6d562da4b2 | |||
| 3f8c064649 | |||
| 5082fe7b76 | |||
| 3a528d9a49 | |||
| 8a2b82a4e8 | |||
| 5968df5b42 | |||
| 824f3cce99 | |||
| 450b5ad4ba | |||
| d2da13929a | |||
| 6504598752 | |||
| ce8c5aaf1f | |||
| c2f12aa2bf | |||
| 6e01d580b2 | |||
| dbbff8adc9 | |||
| f9d566bd84 | |||
| 13513aeca5 | |||
| ac901c9e53 | |||
| 47704667b1 | |||
| 12708e5824 | |||
| 23df6a8b52 | |||
| 5115c65691 | |||
| e55bc07eb6 | |||
| 26ec925d9b | |||
| 29d1f3998a | |||
| 054902b9f9 | |||
| 82db26fcef | |||
| df65b1b72e | |||
| 80321f04e7 | |||
| 96ff2a85d6 | |||
| 76fd3c7065 | |||
| c00b4432d7 | |||
| ae5a90d8d4 | |||
| ed011a762b | |||
| 3fc67cd81a | |||
| 5f0ae6bc5a | |||
| aa69048024 | |||
| c180bb5ace | |||
| c5209887cc | |||
| ca446136a1 | |||
| 0510420772 | |||
| 36b709b911 | |||
| 0a8e1fe46b | |||
| f8c8a939d7 | |||
| bca4cb8227 | |||
| f0d994074d | |||
| 849dea7ce2 | |||
| 72dbd0189c | |||
| 0b0c33cfee | |||
| c6f8a49925 | |||
| 215da79284 | |||
| 145ba4268f | |||
| d010993994 | |||
| 513b8cfea5 | |||
| 38ccaa8a5b | |||
| d17e197a1b | |||
| cbfa4f9e28 | |||
| 530551f568 | |||
| 74e5fa6475 | |||
| 72622671a2 | |||
| 08bd1f92b0 | |||
| 0a20444a74 | |||
| 610f547dbf | |||
| 6db44eadc6 | |||
| 5a0c4641a1 | |||
| d7ab8f0d99 | |||
| c306e061ab | |||
| f378db89b5 |
71
.env.example
71
.env.example
@ -46,20 +46,27 @@ 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
|
||||
# Reduced from 7d → 24h on 2026-04-12 (P2-3 hardening). Combined with
|
||||
# device-fingerprint binding in the JWT payload, this tightens the
|
||||
# exploitation window for stolen refresh tokens.
|
||||
JWT_REFRESH_EXPIRY=24h
|
||||
|
||||
# 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
|
||||
|
||||
# Gitea SSO cookie signing secret (separate from JWT — falls back to JWT_ACCESS_SECRET if empty)
|
||||
# Generate with: openssl rand -hex 32
|
||||
GITEA_SSO_SECRET=
|
||||
# Salt for deriving deterministic service passwords (Gitea, Rocket.Chat)
|
||||
# Falls back to JWT_ACCESS_SECRET if empty — set a dedicated value to isolate secret rotation
|
||||
# Generate with: openssl rand -hex 32
|
||||
SERVICE_PASSWORD_SALT=
|
||||
# BREAKING CHANGE (2026-04-12): both GITEA_SSO_SECRET and SERVICE_PASSWORD_SALT
|
||||
# are now REQUIRED (min 32 chars). The previous fallback to JWT_ACCESS_SECRET
|
||||
# has been removed — a JWT leak must not compromise SSO cookies or service
|
||||
# account passwords. Both values must be distinct from each other and from
|
||||
# all JWT_* secrets. Generate with: openssl rand -hex 32
|
||||
|
||||
# Gitea SSO cookie signing secret (required, ≥32 chars, distinct from JWT secrets)
|
||||
GITEA_SSO_SECRET=GENERATE_WITH_openssl_rand_hex_32
|
||||
# Salt for deriving deterministic service passwords (Gitea, Rocket.Chat).
|
||||
# Required, ≥32 chars, distinct from all other secrets.
|
||||
SERVICE_PASSWORD_SALT=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
|
||||
@ -181,6 +188,13 @@ 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
|
||||
# HLS adaptive bitrate transcoding. When 'true', uploaded videos are queued
|
||||
# for FFmpeg transcoding into 360p/720p/1080p HLS variants and the player
|
||||
# prefers HLS over the MP4 range-request stream. When 'false' (default),
|
||||
# uploads are tagged SKIPPED and the player falls back to MP4 — no DB or
|
||||
# disk impact, fully reversible. The worker is always registered so existing
|
||||
# PENDING jobs from a prior run still process if you flip the flag back on.
|
||||
ENABLE_HLS_TRANSCODE=false
|
||||
MEDIA_ROOT=/media/library
|
||||
MEDIA_UPLOADS=/media/uploads
|
||||
MAX_UPLOAD_SIZE_GB=10
|
||||
@ -212,12 +226,21 @@ COMPOSE_PROFILES=
|
||||
# For docker push/pull, run: docker login gitea.bnkops.com
|
||||
GITEA_REGISTRY_USER=admin
|
||||
GITEA_REGISTRY_PASS=
|
||||
# API token for the REMOTE registry (gitea.bnkops.com) — used by build-release.sh --upload
|
||||
# Create at: https://gitea.bnkops.com/user/settings/applications
|
||||
# This is NOT the same as GITEA_API_TOKEN (which is for the local platform Gitea below)
|
||||
GITEA_REGISTRY_API_TOKEN=
|
||||
|
||||
# --- Gitea ---
|
||||
# --- Gitea (Local Platform Instance) ---
|
||||
GITEA_URL=http://gitea-changemaker:3000
|
||||
GITEA_PORT=3030
|
||||
GITEA_WEB_PORT=3030
|
||||
GITEA_SSH_PORT=2222
|
||||
# Admin user (auto-created on first boot by gitea-init.sh)
|
||||
GITEA_ADMIN_USER=admin
|
||||
# Leave blank to reuse INITIAL_ADMIN_PASSWORD (compose resolves the fallback).
|
||||
# Set only if you want a distinct password for the Gitea admin account.
|
||||
GITEA_ADMIN_PASSWORD=
|
||||
GITEA_DB_TYPE=mysql
|
||||
GITEA_DB_HOST=gitea-db:3306
|
||||
GITEA_DB_NAME=gitea
|
||||
@ -230,7 +253,9 @@ 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)
|
||||
# Personal access token for the LOCAL Gitea instance (docs comments, user provisioning, SSO)
|
||||
# Create at: http://localhost:3030/user/settings/applications (or https://git.DOMAIN/...)
|
||||
# This is NOT the same as GITEA_REGISTRY_API_TOKEN (which is for the remote registry above)
|
||||
GITEA_API_TOKEN=
|
||||
# Repository owner (Gitea username that will own the docs-comments repo)
|
||||
GITEA_COMMENTS_REPO_OWNER=
|
||||
@ -263,6 +288,7 @@ MKDOCS_DOCS_PATH=/mkdocs/docs
|
||||
# --- Code Server ---
|
||||
CODE_SERVER_PORT=8888
|
||||
CODE_SERVER_URL=http://code-server-changemaker:8443
|
||||
USER_NAME=coder
|
||||
|
||||
# --- Homepage ---
|
||||
HOMEPAGE_PORT=3010
|
||||
@ -397,6 +423,26 @@ SMS_MAX_RETRIES=3
|
||||
SMS_RESPONSE_SYNC_INTERVAL_MS=120000
|
||||
SMS_DEVICE_MONITOR_INTERVAL_MS=300000
|
||||
|
||||
# --- Social, People & Analytics ---
|
||||
# ENABLE_SOCIAL is the initial default; once saved in admin Settings, the DB value is authoritative
|
||||
ENABLE_SOCIAL=false
|
||||
# ENABLE_PEOPLE is the initial default; once saved in admin Settings, the DB value is authoritative
|
||||
ENABLE_PEOPLE=false
|
||||
# ENABLE_ANALYTICS is the initial default; once saved in admin Settings, the DB value is authoritative
|
||||
ENABLE_ANALYTICS=false
|
||||
|
||||
# --- Control Panel Agent ---
|
||||
# Set to true to enable the CCP remote management agent
|
||||
ENABLE_CCP_AGENT=false
|
||||
# URL of the Changemaker Control Panel
|
||||
CCP_URL=
|
||||
# One-time invite code for registration
|
||||
CCP_INVITE_CODE=
|
||||
# How the CCP can reach this agent (must be externally accessible)
|
||||
CCP_AGENT_URL=
|
||||
# Agent port (default 7443)
|
||||
CCP_AGENT_PORT=7443
|
||||
|
||||
# --- Monitoring (only used with --profile monitoring) ---
|
||||
PROMETHEUS_PORT=9090
|
||||
GRAFANA_PORT=3005
|
||||
@ -414,3 +460,8 @@ GOTIFY_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS
|
||||
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)
|
||||
|
||||
# --- GeoIP (MaxMind GeoLite2) ---
|
||||
# Free account: https://www.maxmind.com/en/geolite2/signup
|
||||
MAXMIND_ACCOUNT_ID= # MaxMind account ID
|
||||
MAXMIND_LICENSE_KEY= # MaxMind license key (auto-downloads GeoLite2-City DB at startup)
|
||||
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@ -9,6 +9,9 @@ node_modules/
|
||||
/configs/code-server/.config/*
|
||||
!/configs/code-server/.config/.gitkeep
|
||||
|
||||
/configs/code-server/data/*
|
||||
!/configs/code-server/data/.gitkeep
|
||||
|
||||
# Root assets (generated by containers)
|
||||
/assets/
|
||||
|
||||
@ -33,7 +36,8 @@ node_modules/
|
||||
# NAR data directory (large voter registry files)
|
||||
/data/*
|
||||
!/data/upgrade/
|
||||
/data/upgrade/*.json
|
||||
/data/upgrade/*
|
||||
!/data/upgrade/.gitkeep
|
||||
|
||||
# Media files (managed by Docker volumes, not git)
|
||||
/media/
|
||||
@ -60,13 +64,35 @@ core.*
|
||||
/backups/
|
||||
.upgrade.lock
|
||||
|
||||
# Pre-upgrade mkdocs snapshots (created by scripts/lib/mkdocs-snapshot.sh).
|
||||
# These are the tenant-content rescue archives written before every upgrade;
|
||||
# discoverable in the install root via `ls`. Retention: last 5 (see helper).
|
||||
/mkdocs-backup-*.tar.gz
|
||||
|
||||
# Release tarballs (generated by build-release.sh)
|
||||
/releases/
|
||||
|
||||
# API compiled output (generated by tsc, baked into Docker images)
|
||||
/api/dist/
|
||||
|
||||
# TypeScript incremental build cache (machine-specific)
|
||||
*.tsbuildinfo
|
||||
|
||||
# Control Panel runtime data (managed deployments + backups)
|
||||
/changemaker-control-panel/instances/
|
||||
/changemaker-control-panel/backups/
|
||||
logs/
|
||||
|
||||
# Playwright MCP browser automation logs
|
||||
.playwright-mcp/
|
||||
|
||||
/docs
|
||||
|
||||
# MkDocs build cache (regenerated each build)
|
||||
/mkdocs/.cache/
|
||||
|
||||
# Claude scheduler lock file
|
||||
.claude/scheduled_tasks.lock
|
||||
|
||||
# Old release zip archive (no longer tracked, see chore: gitignore hygiene)
|
||||
/archive/
|
||||
@ -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 |
280
CLAUDE.md
280
CLAUDE.md
@ -10,15 +10,19 @@ Changemaker Lite is a self-hosted political campaign platform built with Docker
|
||||
|
||||
**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)
|
||||
- ✅ Drizzle to Prisma Migration Complete (single-ORM, Feb 2026)
|
||||
- ✅ Automated Pangolin Setup (one-command tunnel deployment)
|
||||
- ✅ 3 Security Audits Complete (Feb 2025 + Mar 22/27/30 2026)
|
||||
- ✅ Social Connections + Calendar (friendship, shared views, availability finder)
|
||||
- ✅ Payments + Ticketed Events (Stripe integration, check-in scanner)
|
||||
- ✅ Meeting Planner + Straw Polls (scheduling, voting)
|
||||
- ✅ SMS Campaign Connector (Termux Android bridge)
|
||||
- ✅ Docs CMS (blog authoring, access policies, collaboration, version history)
|
||||
- ✅ User Provisioning Framework (Gitea, Vaultwarden, Listmonk)
|
||||
- ✅ Granular Admin Roles (9 admin roles + module-specific RBAC)
|
||||
- ✅ Collaborative Docs Editing (Y.js CRDT + Hocuspocus)
|
||||
- ✅ Engagement Scoring + EventBus + Gitea SSO
|
||||
- ✅ MCP Server (Claude Code integration, 27 core + 6 on-demand packs (~65 tools))
|
||||
- 🚧 Phase 15 (Testing + Polish) - Next
|
||||
|
||||
---
|
||||
@ -59,10 +63,9 @@ Changemaker Lite is a self-hosted political campaign platform built with Docker
|
||||
changemaker.lite/
|
||||
├── api/ # Dual API servers (Express + Fastify)
|
||||
│ ├── prisma/
|
||||
│ │ ├── schema.prisma # 30+ models: User, Campaign, Location, Shift, etc.
|
||||
│ │ ├── migrations/ # Prisma migration history
|
||||
│ │ ├── schema.prisma # 192 models: User, Campaign, Location, Shift, Payment, Social, etc.
|
||||
│ │ ├── migrations/ # 50 Prisma migrations (full schema 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)
|
||||
@ -70,10 +73,10 @@ changemaker.lite/
|
||||
│ ├── config/
|
||||
│ │ └── env.ts # Zod-validated environment config (100+ vars)
|
||||
│ ├── middleware/ # auth, rbac, rate-limit, validate, error-handler
|
||||
│ ├── modules/
|
||||
│ ├── modules/ # 44 modules total
|
||||
│ │ ├── auth/ # JWT login, register, refresh, logout
|
||||
│ │ ├── users/ # User CRUD + pagination + search
|
||||
│ │ ├── settings/ # Site settings singleton
|
||||
│ │ ├── settings/ # Site settings singleton (20+ feature flags)
|
||||
│ │ ├── services/ # Service health checks
|
||||
│ │ ├── influence/
|
||||
│ │ │ ├── campaigns/ # Campaign CRUD + public routes
|
||||
@ -90,16 +93,39 @@ changemaker.lite/
|
||||
│ │ │ ├── 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
|
||||
│ │ ├── pages/ # Landing page CRUD + block library + public renderer
|
||||
│ │ ├── email-templates/ # Email template CRUD + rendering
|
||||
│ │ ├── media/ # Fastify media API (videos, reactions, jobs)
|
||||
│ │ ├── media/ # Fastify media API (videos, reactions, jobs, analytics)
|
||||
│ │ ├── social/ # Friendships, challenges, spotlights, referrals
|
||||
│ │ ├── calendar/ # Calendar layers, items, shared views, availability
|
||||
│ │ ├── payments/ # Stripe products, donations, subscriptions
|
||||
│ │ ├── ticketed-events/ # Event ticketing, tiers, check-in
|
||||
│ │ ├── sms/ # SMS campaigns via Termux Android bridge
|
||||
│ │ ├── meeting-planner/ # Meeting scheduling with polls
|
||||
│ │ ├── meetings/ # Meeting agendas, minutes, action items
|
||||
│ │ ├── polls/ # Straw polls with comments + voting
|
||||
│ │ ├── docs/ # MkDocs health checks + export routes
|
||||
│ │ ├── docs-analytics/ # Docs page view tracking
|
||||
│ │ ├── docs-comments/ # Gitea-backed comments on docs
|
||||
│ │ ├── people/ # CRM people module
|
||||
│ │ ├── events/ # Gancio event integration
|
||||
│ │ ├── newsletter/ # Newsletter management
|
||||
│ │ ├── listmonk/ # Newsletter sync admin routes
|
||||
│ │ ├── pangolin/ # Tunnel management (Newt integration)
|
||||
│ │ ├── docs/ # MkDocs + Code Server health checks
|
||||
│ │ ├── rocketchat/ # Rocket.Chat integration
|
||||
│ │ ├── jitsi/ # Jitsi video conferencing auth
|
||||
│ │ ├── registry/ # Docker image registry management
|
||||
│ │ ├── upgrade/ # Auto-upgrade checks + deployment
|
||||
│ │ ├── gitea-setup/ # Gitea SSO + API token management
|
||||
│ │ ├── volunteer-invite/ # Invite codes + setup workflows
|
||||
│ │ ├── gallery-ads/ # Media gallery ads
|
||||
│ │ ├── homepage/ # Homepage stats + dashboard
|
||||
│ │ ├── search/ # Cross-module search
|
||||
│ │ ├── reports/ # Analytics + reporting
|
||||
│ │ ├── og/ # Open Graph metadata
|
||||
│ │ ├── qr/ # QR code PNG generation (public)
|
||||
│ │ ├── dashboard/ # Admin dashboard data
|
||||
│ │ ├── activity/ # Activity feed
|
||||
│ │ └── observability/ # Prometheus/Grafana/Alertmanager integration
|
||||
│ ├── services/ # email, email-queue, geocode-queue, listmonk, pangolin, docker
|
||||
│ ├── types/ # express.d.ts (Request augmentation)
|
||||
@ -119,34 +145,50 @@ changemaker.lite/
|
||||
│ │ ├── 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)
|
||||
│ ├── pages/ # 52 root pages + 8 subdirectories
|
||||
│ │ ├── influence/ # Campaign moderation, effectiveness, impact stories, straw polls
|
||||
│ │ ├── map/ # LocationsPage, CutsPage, ShiftsPage, MapSettingsPage, DataQualityDashboard
|
||||
│ │ ├── media/ # Library, Playlists, Analytics, Gallery Ads, Comment Moderation
|
||||
│ │ ├── payments/ # Dashboard, Products, Plans, Donations, Subscribers, Settings
|
||||
│ │ ├── social/ # Dashboard, Graph, Moderation, Referrals, Spotlights, Challenges
|
||||
│ │ ├── sms/ # Dashboard, Contacts, Campaigns, Conversations, Templates, Setup
|
||||
│ │ ├── events/ # Ticketed Events, Event Detail, Check-in Scanner
|
||||
│ │ ├── volunteer/ # Map, Shifts, Routes, Calendar, Friends, Profile, Groups, Achievements
|
||||
│ │ ├── public/ # Homepage, Campaigns, Map, Events, Media Gallery, Pricing, Donations, Meet
|
||||
│ │ └── (root) # Dashboard, Users, Settings, Docs*, MeetingPlanner, Observability, etc.
|
||||
│ ├── stores/ # 9 Zustand stores (auth, canvass, chat-widget, command-palette, favorites, settings, social, tour, tracking)
|
||||
│ ├── lib/ # api.ts, media-api.ts, media-public-api.ts, nav-defaults.ts, service-url.ts, y-textarea.ts
|
||||
│ ├── hooks/ # useDebounce, useLocalStorage
|
||||
│ └── types/ # api.ts, canvass.ts, media.ts (TypeScript interfaces)
|
||||
│
|
||||
├── media-manager/ # Legacy media manager (reference)
|
||||
├── mcp-server/ # Claude Code MCP server (27 core + 6 on-demand packs (~65 tools))
|
||||
├── nginx/ # Reverse proxy config (subdomain routing + CSP)
|
||||
├── configs/ # Prometheus, Grafana, Alertmanager configs
|
||||
├── configs/ # Prometheus, Grafana, Alertmanager, Pangolin configs
|
||||
├── scripts/ # Deployment, backup, upgrade, registry scripts
|
||||
│ ├── install.sh # Curl-friendly installer (downloads tarball + runs config.sh)
|
||||
│ ├── uninstall.sh # Remove containers, volumes, and install dir
|
||||
│ ├── 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)
|
||||
│ ├── update-env.sh # Merge new variables from .env.example into existing .env
|
||||
│ ├── backup.sh / restore.sh # PostgreSQL + Listmonk + uploads backup/restore
|
||||
│ ├── validate-env.sh # Required env variable validation
|
||||
│ ├── validate-compose-parity.sh # Check docker-compose.yml ↔ docker-compose.prod.yml parity
|
||||
│ ├── test-deployment.sh # Post-deploy smoke tests (auth, services, health)
|
||||
│ ├── register-with-ccp.sh # Register instance with a Control Panel via invite code
|
||||
│ ├── ccp-deregister.sh # Deregister instance from its CCP
|
||||
│ ├── pangolin-teardown.sh # Delete Pangolin resources/sites (dry-run by default)
|
||||
│ ├── gitea-init.sh # Bootstrap Gitea admin user + SSO app
|
||||
│ ├── nocodb-init.sh # Bootstrap NocoDB project + base connection
|
||||
│ ├── mkdocs-entrypoint.sh # MkDocs container entrypoint (live + built modes)
|
||||
│ ├── mkdocs-build-trigger.py # Trigger MkDocs rebuild from API hooks
|
||||
│ ├── legacy/ # Archived Cloudflare tunnel configs (pre-Pangolin)
|
||||
│ └── systemd/ # Systemd unit files (backup timer, upgrade watcher)
|
||||
├── docker-compose.yml # V2 orchestration (40+ services)
|
||||
├── docker-compose.prod.yml # Production (image-only, no source mounts)
|
||||
├── .env.example # All required environment variables
|
||||
└── V2_PLAN.md # Full 14-phase roadmap
|
||||
```
|
||||
@ -238,27 +280,35 @@ cd api && npm run dev:media
|
||||
|---------|-----|---------------------|
|
||||
| Admin GUI | http://localhost:3000 | See INITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORD in .env |
|
||||
| API | http://localhost:4000 | - |
|
||||
| Media API | http://localhost:4100 | - |
|
||||
| NocoDB | http://localhost:8091 | See `NC_ADMIN_EMAIL`/`NC_ADMIN_PASSWORD` in .env |
|
||||
| Gitea | http://localhost:3030 | See `GITEA_ADMIN_USER`/`GITEA_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 |
|
||||
| Rocket.Chat | http://localhost:3100 | See RC env vars in .env |
|
||||
| Excalidraw | http://localhost:8090 | - |
|
||||
| Vaultwarden | http://localhost:8093 | See `VAULTWARDEN_ADMIN_TOKEN` in .env |
|
||||
|
||||
### Feature Flags
|
||||
|
||||
Enable optional features in `.env`:
|
||||
Most features are toggled via **SiteSettings** in the database (admin Settings page). Some also have `.env` overrides:
|
||||
|
||||
```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
|
||||
# .env feature flags (env-level)
|
||||
ENABLE_MEDIA_FEATURES=true # Media manager
|
||||
ENABLE_HLS_TRANSCODE=true # HLS adaptive bitrate transcoding (off by default)
|
||||
ENABLE_PAYMENTS=true # Stripe integration
|
||||
ENABLE_SMS=true # SMS campaigns
|
||||
ENABLE_CHAT=true # Rocket.Chat
|
||||
ENABLE_MEET=true # Jitsi meetings
|
||||
LISTMONK_SYNC_ENABLED=true # Newsletter sync
|
||||
EMAIL_TEST_MODE=true # MailHog vs SMTP
|
||||
```
|
||||
|
||||
**Database feature flags (SiteSettings):** `enableInfluence`, `enableMap`, `enableNewsletter`, `enableLandingPages`, `enableMediaFeatures`, `enablePayments`, `enableGalleryAds`, `enableChat`, `enableEvents`, `enableDocsComments`, `enableSms`, `enablePeople`, `enableSocial`, `enableMeet`, `enableMeetingPlanner`, `enableTicketedEvents`, `enableSocialCalendar`, `enablePolls`, `enableDocsCollaboration`, `enableUserProvisioning`
|
||||
|
||||
---
|
||||
|
||||
## Development Commands
|
||||
@ -272,7 +322,6 @@ 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
|
||||
@ -295,7 +344,6 @@ 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
|
||||
@ -442,9 +490,13 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
|
||||
**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/modules/media/services/` — FFprobe, thumbnail, **HLS transcode** services
|
||||
- `api/src/modules/media/routes/` — Video CRUD, actions, schedule, analytics, tracking, upload, **HLS streaming**
|
||||
- `api/src/services/video-schedule-queue.service.ts` — BullMQ queue for scheduled publishing
|
||||
- `api/src/services/hls-transcode-queue.service.ts` — BullMQ queue for HLS adaptive bitrate transcoding (concurrency 1, in-process worker)
|
||||
- `api/src/modules/media/routes/hls.routes.ts` — Master/variant playlist + segment serving with signed URLs
|
||||
- `api/scripts/backfill-hls.ts` — Backfill HLS for pre-existing videos (`npm run backfill:hls`)
|
||||
- `admin/src/lib/use-hls.ts` — React hook attaching hls.js (Chrome/FF/Edge) or native (Safari/iOS)
|
||||
- `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
|
||||
@ -452,7 +504,15 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
- `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
|
||||
**Features:** Video CRUD with FFprobe metadata, quick actions, scheduled publishing (BullMQ + timezones), analytics (GDPR-compliant), public tracking endpoints, keyboard shortcuts, **HLS adaptive bitrate streaming (360p/720p/1080p, MP4 fallback)**.
|
||||
|
||||
**HLS adaptive bitrate streaming:**
|
||||
- On upload, a BullMQ `hls-transcode` job runs FFmpeg to produce a master playlist + 3 keyframe-aligned variants under `/media/local/hls/{videoId}/`. Concurrency is 1; the worker runs in-process with the media-api Fastify server.
|
||||
- Player prefers HLS over MP4 when `Video.hlsStatus === 'READY'`. MP4 streaming routes stay as the always-on fallback for un-transcoded videos and for hover-preview cards (where 200ms hls.js init defeats the UX — `PublicVideoCard` stays MP4).
|
||||
- `useHls()` hook lazy-imports hls.js (~75 KB gzipped, never enters main bundle), uses native HLS on Safari/iOS, gives up after 2 NETWORK_ERROR retries so the MP4 fallback can kick in.
|
||||
- Manifest URLs are HMAC-signed (`?sig=&exp=&uid=`) per existing `signMediaPath()` pattern. Variant playlists rewrite their segment URIs server-side at fetch time so each segment carries a fresh signature.
|
||||
- Feature flag: `ENABLE_HLS_TRANSCODE` (default `false`). When off, uploads are tagged `SKIPPED` and the player falls back to MP4 — fully reversible. The worker stays registered so existing `PENDING` jobs still process if the flag flips back on.
|
||||
- Backfill: `docker compose exec api npm run backfill:hls` enqueues all `hlsStatus IS NULL` videos. Bypasses the flag (operator opt-in). At ~2 min per 1080p video, throughput is ~30/hour.
|
||||
|
||||
**Routes:**
|
||||
- Admin: `/app/media/library`, `/app/media/analytics`, `/app/media/shared`, `/app/media/jobs`
|
||||
@ -471,10 +531,12 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
- `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
|
||||
- `scripts/register-with-ccp.sh` — Register this instance with a Control Panel (CCP) using an invite code
|
||||
- `scripts/pangolin-teardown.sh` — Delete all Pangolin resources/sites for an org (dry-run by default, idempotent)
|
||||
- `scripts/ccp-deregister.sh` — Deregister instance from its CCP
|
||||
- `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)
|
||||
- **Automated setup:** One-command deployment via CCP registration (creates site, updates .env, restarts Newt)
|
||||
- **Continuous sync:** Hourly resource sync via nginx cron job
|
||||
|
||||
**MkDocs + Code Server:**
|
||||
@ -513,20 +575,25 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
| **Core Services** | | |
|
||||
| 3000 | Admin GUI | Vite dev / React production |
|
||||
| 4000 | Express API | Main V2 API (Prisma) |
|
||||
| 4100 | Fastify Media API | Video library (Drizzle) |
|
||||
| 4100 | Fastify Media API | Video library (Prisma) |
|
||||
| 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 |
|
||||
| 3030 | Gitea | Git hosting + SSO |
|
||||
| 3100 | Rocket.Chat | Team chat (embed proxy) |
|
||||
| 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 |
|
||||
| 8090 | Excalidraw | Collaborative whiteboard |
|
||||
| 8091 | NocoDB | Data browser |
|
||||
| 8092 | Gancio | Event management |
|
||||
| 8093 | Vaultwarden | Password manager |
|
||||
| 8443 | Jitsi Web | Video conferencing |
|
||||
| 8885 | Mini QR Proxy | Iframe-friendly |
|
||||
| 8888 | Code Server | Web IDE |
|
||||
| 9001 | Listmonk | Newsletter platform |
|
||||
@ -551,11 +618,17 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
| `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 |
|
||||
| `git.cmlite.org` | Gitea (3030) | Git hosting + SSO |
|
||||
| `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 |
|
||||
| `chat.cmlite.org` | Rocket.Chat (3100) | Team chat |
|
||||
| `meet.cmlite.org` | Jitsi (8443) | Video conferencing |
|
||||
| `events.cmlite.org` | Gancio (8092) | Event management |
|
||||
| `draw.cmlite.org` | Excalidraw (8090) | Collaborative whiteboard |
|
||||
| `vault.cmlite.org` | Vaultwarden (8093) | Password manager |
|
||||
| `mail.cmlite.org` | MailHog (8025) | Email capture (dev) |
|
||||
| `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.
|
||||
@ -564,7 +637,7 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Note:** See `MEMORY.md` for comprehensive development patterns, gotchas, and lessons learned. Below are V2-specific patterns only.
|
||||
**Note:** Below are the key development patterns for this project.
|
||||
|
||||
### API Router Structure
|
||||
- Service layer (`*.service.ts`) — business logic, database queries
|
||||
@ -579,47 +652,57 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
|
||||
- Login redirects: ADMIN_ROLES → `/app`, USER/TEMP → `/volunteer`
|
||||
|
||||
### Frontend Architecture
|
||||
- Admin pages: `admin/src/pages/` (AppLayout)
|
||||
- Admin pages: `admin/src/pages/` + subdirs (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`
|
||||
- Zustand stores (9): auth, canvass, chat-widget, command-palette, favorites, settings, social, tour, tracking
|
||||
- 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
|
||||
### Database ORM
|
||||
- **Prisma** (both APIs): 192 models in single `schema.prisma`. Use `UncheckedCreateInput`/`UncheckedUpdateInput` for foreign keys, `Prisma.InputJsonValue` for JSON arrays
|
||||
|
||||
### 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
|
||||
- **Migration history:** 50 migrations in `api/prisma/migrations/` fully cover the schema
|
||||
- **Production deploys:** Use `prisma migrate deploy` (not `migrate dev`)
|
||||
|
||||
### V2-Specific Gotchas
|
||||
### Key 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.)
|
||||
- **`!` in passwords** triggers bash history expansion — use Write tool to write JSON to file, then `curl -d @file`
|
||||
- **Port mappings:** API container 4000 → host 4002, Admin container 3000 → host 3002
|
||||
- **BullMQ** needs its own Redis connections (pass URL string, not shared ioredis instance)
|
||||
- **Public pages** use `axios` directly (no auth interceptor), admin pages use `{ api }` from lib
|
||||
- **Prisma JSON fields:** typed arrays need `as unknown as Prisma.InputJsonValue` cast
|
||||
- **nginx conf.d files** have `.template` counterparts used by envsubst at startup
|
||||
|
||||
---
|
||||
|
||||
## Security & Configuration
|
||||
|
||||
### Security Audit
|
||||
Comprehensive security audit completed 2025-02-11, addressing 13 findings. See `SECURITY_AUDIT_2025-02-11.md` for full report.
|
||||
### Security Audits
|
||||
Four security audits completed. See audit reports for full details:
|
||||
- **Feb 2025:** 13 findings (password policy, rate limits, token rotation, XSS prevention). `SECURITY_AUDIT_2025-02-11.md`
|
||||
- **Mar 22 2026:** JWT algorithm lockdown, invite secret separation, webhook hardening, CSV injection, QR DoS
|
||||
- **Mar 27 2026:** 33 findings (30 fixed) — IDOR, XSS, path traversal, MongoDB auth, SSTI, open redirect
|
||||
- **Mar 30 2026:** 19 findings — IDOR action items/ticketed events, nginx rate limit, JWT secret reuse
|
||||
|
||||
**Key improvements:**
|
||||
**Key security features:**
|
||||
- Password policy: 12+ chars, uppercase, lowercase, digit (schema-enforced)
|
||||
- Rate limits on auth endpoints (10/min per IP)
|
||||
- Refresh token rotation (atomic transaction)
|
||||
- Rate limits on auth endpoints (10/min per IP) + nginx rate limiting
|
||||
- Refresh token rotation (atomic Prisma transaction)
|
||||
- JWT algorithm locked to HS256, separate invite secret
|
||||
- User enumeration prevention (401 not 404)
|
||||
- Redis authentication required
|
||||
- XSS/injection prevention (HTML escaping)
|
||||
- Path traversal protection
|
||||
- XSS/injection prevention (HTML escaping, DOMPurify, SSTI protection)
|
||||
- Path traversal protection (resolve + startsWith checks)
|
||||
- Encryption key for DB secrets (`ENCRYPTION_KEY` required in all environments)
|
||||
- Nginx security headers (HSTS, Permissions-Policy, CSP)
|
||||
- Nginx security headers (HSTS, Permissions-Policy, CSP, X-Forwarded-For)
|
||||
- MongoDB keyfile authentication
|
||||
- httpOnly cookies for refresh tokens
|
||||
|
||||
### Required Environment Variables
|
||||
See `.env.example` for all 100+ variables. Critical ones:
|
||||
@ -642,8 +725,8 @@ See `.env.example` for all 100+ variables. Critical ones:
|
||||
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
|
||||
# Example for cmlite.org
|
||||
CORS_ORIGINS=http://app.cmlite.org,https://app.cmlite.org,http://localhost:3000,http://localhost
|
||||
|
||||
# Also set production mode
|
||||
NODE_ENV=production
|
||||
@ -672,18 +755,16 @@ docker compose restart api
|
||||
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
|
||||
- `api.${DOMAIN}` - Main API (all endpoints fail without this)
|
||||
- `app.${DOMAIN}` - Admin GUI + public pages
|
||||
- `media.${DOMAIN}` - Media API
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
# Should return JSON, NOT a 302 redirect
|
||||
curl https://api.betteredmonton.org/api/health
|
||||
curl https://api.cmlite.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.
|
||||
@ -702,29 +783,46 @@ Check in order:
|
||||
### 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`.
|
||||
|
||||
### Video Stuck in HLS PROCESSING / FAILED with EACCES
|
||||
**Symptom:** A video shows `hlsStatus = 'PROCESSING'` for many minutes; or `'FAILED'` with `hls_transcode_error LIKE '%EACCES%'`. Player keeps falling back to MP4.
|
||||
|
||||
Check in order:
|
||||
1. **First-run perms.** If `hls_transcode_error` contains `EACCES: permission denied, mkdir '/media/local/hls/<id>'`, the bind-mount got created as `root:root` but the Node process runs as `node` (UID 1000). One-time fix:
|
||||
```
|
||||
docker compose exec -u 0 media-api chown -R 1000:1000 /media/local/hls
|
||||
```
|
||||
Then reset and re-enqueue:
|
||||
```
|
||||
docker compose exec -T v2-postgres psql -U changemaker -d changemaker_v2 -c "UPDATE videos SET hls_status = NULL, hls_transcode_error = NULL WHERE hls_status = 'FAILED';"
|
||||
docker compose exec api npm run backfill:hls
|
||||
```
|
||||
2. **Worker running:** `docker compose logs media-api --tail 100 | grep -i hls` — expect `[hls]` lines for the queue worker startup and per-job progress.
|
||||
3. **FFmpeg in container:** `docker compose exec media-api ffmpeg -version` — should print FFmpeg version. (Already in `Dockerfile.media`.)
|
||||
4. **Queue depth:** `docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD LLEN bull:hls-transcode:wait` — non-zero means jobs are queued behind a slow one.
|
||||
5. **Disk space at output:** `docker compose exec media-api df -h /media/local/hls` — transcoding can consume several GB per video.
|
||||
6. **Failure record:** `docker compose exec api npx prisma studio` → Video table → check `hlsTranscodeError`.
|
||||
|
||||
To force a re-transcode of a failed video, set `hlsStatus = NULL` in the DB and run `npm run backfill:hls`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
V1 code has been removed from the repo. History preserved as `v1-archive` git tag. `docker-compose.v1.yml` remains as reference only.
|
||||
|
||||
---
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
### Infrastructure
|
||||
- `docker-compose.yml` — Development orchestration (build blocks + source mounts, 20+ services)
|
||||
- `docker-compose.yml` — Development orchestration (build blocks + source mounts, 40+ 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/schema.prisma` — Main schema (192 Prisma models)
|
||||
- `api/prisma/migrations/` — 50 migration files (full schema history)
|
||||
- `api/prisma/seed.ts` — Database seeding
|
||||
|
||||
### Nginx
|
||||
@ -742,5 +840,5 @@ V1 code archived in `influence/`, `map/`, and `docker-compose.v1.yml`. Two indep
|
||||
### 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
|
||||
- `SECURITY_AUDIT_2025-02-11.md` — Initial security audit report
|
||||
- `.mcp.json` — MCP server configuration for Claude Code
|
||||
|
||||
@ -33,8 +33,9 @@ All three methods share the same Gitea container registry at `gitea.bnkops.com/a
|
||||
│ 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 │
|
||||
│ Builds 5 production images, pushes to Gitea registry │
|
||||
│ (api, admin, media-api, nginx, ccp-agent) │
|
||||
│ tagged :SHA + :latest │
|
||||
│ │
|
||||
│ Step 2: ./scripts/mirror-images.sh (run once/rarely) │
|
||||
│ Mirrors 36 third-party images to Gitea registry │
|
||||
@ -43,7 +44,7 @@ All three methods share the same Gitea container registry at `gitea.bnkops.com/a
|
||||
│ Step 3: ./scripts/build-release.sh --tag vX.Y.Z --upload │
|
||||
│ Packages runtime files into ~9MB tarball, uploads to │
|
||||
│ Gitea Releases │
|
||||
└──────────────────┬───────────────────────────────────────────────┘
|
||||
└──────────────────┬─────────────────100.90.78.47──────────────────────────────┘
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
▼ ▼
|
||||
@ -98,7 +99,7 @@ After code changes are tested locally:
|
||||
./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}`:
|
||||
This builds **5 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 |
|
||||
|---------|-----------|-----------------|
|
||||
@ -106,6 +107,7 @@ This builds **4 services** with multi-stage Dockerfiles (production target, no d
|
||||
| `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 |
|
||||
| `ccp-agent` | `../changemaker-control-panel/agent/Dockerfile` | Remote management agent (sibling repo) |
|
||||
|
||||
```bash
|
||||
# Build specific services only
|
||||
@ -157,8 +159,17 @@ Packages only runtime files (~9 MB) — no source code, no node_modules:
|
||||
|
||||
# Preview contents without creating tarball
|
||||
./scripts/build-release.sh --dry-run
|
||||
|
||||
# --upload refuses to overwrite an existing tag. To deliberately replace
|
||||
# a release (destructive — users on that tag see no upgrade signal):
|
||||
./scripts/build-release.sh --tag v2.2.0 --upload --replace
|
||||
```
|
||||
|
||||
**Version hygiene:** bump the tag when changing release contents. Overwriting
|
||||
an existing release silently breaks upgrade checks for users already on that
|
||||
version — they see "no update available" even though the tarball they'd
|
||||
download differs.
|
||||
|
||||
The tarball contains:
|
||||
- `docker-compose.yml` (copy of `docker-compose.prod.yml` — image-only, no build blocks)
|
||||
- `.env.example`, `config.sh` (configuration wizard)
|
||||
@ -260,9 +271,10 @@ 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/build-and-push.sh # Build + push 5 images
|
||||
./scripts/mirror-images.sh # Mirror 36 third-party images
|
||||
./scripts/build-release.sh --tag v2.2.0 --upload # Package + upload release
|
||||
git tag --sort=-v:refname | head -3 # Check latest version tags
|
||||
./scripts/build-release.sh --tag vX.Y.Z --upload # Package + upload release
|
||||
|
||||
# ── Deploy ──
|
||||
curl -fsSL .../install.sh | bash # New install (release)
|
||||
@ -276,13 +288,46 @@ docker compose ps # Container status
|
||||
|
||||
---
|
||||
|
||||
## Gitea API Tokens
|
||||
|
||||
There are **two separate Gitea tokens** with different purposes. Using the wrong one is a common mistake:
|
||||
|
||||
| Variable | Target | Used by | Create at |
|
||||
|----------|--------|---------|-----------|
|
||||
| `GITEA_REGISTRY_API_TOKEN` | Remote registry (`gitea.bnkops.com`) | `build-release.sh --upload`, release API calls | `https://gitea.bnkops.com/user/settings/applications` |
|
||||
| `GITEA_API_TOKEN` | Local Gitea instance | Docs comments, user provisioning, SSO | `http://localhost:3030/user/settings/applications` |
|
||||
|
||||
**Key:** Release uploads and the Gitea Releases API require `GITEA_REGISTRY_API_TOKEN`. If you get `"user does not exist"` from the API, you're using the wrong token.
|
||||
|
||||
---
|
||||
|
||||
## Checklist: Cutting a New Release
|
||||
|
||||
1. [ ] All code changes committed and pushed to `v2` branch
|
||||
1. [ ] All code changes committed and pushed to `main` 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"}`
|
||||
3. [ ] **Determine version tag:**
|
||||
```bash
|
||||
# Check the latest existing tag to pick the next version
|
||||
git tag --sort=-v:refname | head -5
|
||||
# Check commits since the last tag
|
||||
git log $(git tag --sort=-v:refname | head -1)..HEAD --oneline
|
||||
```
|
||||
4. [ ] `./scripts/build-and-push.sh` — builds and pushes 5 production images
|
||||
5. [ ] `./scripts/mirror-images.sh` — only if third-party versions changed
|
||||
6. [ ] `./scripts/build-release.sh --tag vX.Y.Z --upload` — packages and uploads tarball
|
||||
7. [ ] **Add release notes** (via Gitea web UI or API):
|
||||
```bash
|
||||
# Update release body via API (use GITEA_REGISTRY_API_TOKEN, not GITEA_API_TOKEN)
|
||||
GITEA_TOKEN=$(grep -oP 'GITEA_REGISTRY_API_TOKEN=\K.*' .env)
|
||||
# Find release ID
|
||||
curl -s "https://gitea.bnkops.com/api/v1/repos/admin/changemaker.lite/releases?limit=1" \
|
||||
-H "Authorization: token $GITEA_TOKEN" | python3 -c "import sys,json; r=json.load(sys.stdin)[0]; print(f'ID: {r[\"id\"]}, Tag: {r[\"tag_name\"]}')"
|
||||
# Update with release notes (write JSON body to /tmp/release-notes.json first)
|
||||
curl -s -X PATCH "https://gitea.bnkops.com/api/v1/repos/admin/changemaker.lite/releases/RELEASE_ID" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/release-notes.json
|
||||
```
|
||||
8. [ ] Test clean install: `tar xzf ... && cd changemaker-lite && bash config.sh && docker compose up -d`
|
||||
9. [ ] Test upgrade: `./scripts/upgrade.sh` on an existing installation
|
||||
10. [ ] Verify: `curl http://localhost:4000/api/health` returns `{"status":"ok"}`
|
||||
|
||||
@ -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
|
||||
24
README.md
24
README.md
@ -103,15 +103,22 @@ Send SMS campaigns via an Android bridge, sync subscribers to Listmonk for newsl
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Production (pre-built images)
|
||||
|
||||
```bash
|
||||
# One-command install (downloads pre-built images, runs config wizard)
|
||||
# 1. One-command install: checks host ports, downloads tarball, runs config wizard
|
||||
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
|
||||
|
||||
cd ~/changemaker.lite
|
||||
docker compose up -d
|
||||
# 2. Start services (first pull ~3 min + ~90s stabilization)
|
||||
cd ~/changemaker.lite && docker compose up -d
|
||||
|
||||
# 3. Verify the install
|
||||
bash scripts/test-deployment.sh --wait 60
|
||||
```
|
||||
|
||||
Or clone and build from source:
|
||||
The installer checks your host's port availability before extracting — no more half-started stacks from cockpit on `:9090` or other surprises. The generated admin password is printed to stdout **and** saved to `data/admin-credentials.txt` (mode 0600). See [Prerequisites](https://cmlite.org/docs/getting-started/prerequisites/) for what you need lined up first.
|
||||
|
||||
### Development (source)
|
||||
|
||||
```bash
|
||||
git clone <repo-url> changemaker.lite
|
||||
@ -127,6 +134,15 @@ docker compose exec api npx prisma db seed
|
||||
|
||||
Then open **http://localhost:3000** and log in with the admin credentials from your `.env`.
|
||||
|
||||
### Useful tools
|
||||
|
||||
```bash
|
||||
bash scripts/validate-env.sh # re-check .env + host ports
|
||||
bash scripts/test-deployment.sh # full deployment health sweep
|
||||
bash scripts/pangolin-teardown.sh # wipe tunnel org before reinstall (dry-run by default)
|
||||
bash scripts/ccp-deregister.sh # deregister from Changemaker Control Panel (dry-run by default)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
**Full documentation is available at [cmlite.org/docs/getting-started](https://cmlite.org/docs/getting-started/).**
|
||||
|
||||
@ -1,161 +0,0 @@
|
||||
# Service Integrations — EventBus Architecture
|
||||
|
||||
Tracking document for the platform-wide EventBus and service integration work.
|
||||
|
||||
**Started:** 2026-03-30
|
||||
**Branch:** v2
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Changemaker Lite has 30+ services but most operate as isolated tools. The EventBus provides a centralized, typed, in-process pub/sub system that decouples event producers from consumers.
|
||||
|
||||
```
|
||||
Service Handler (shift created, donation completed, etc.)
|
||||
|
|
||||
v
|
||||
eventBus.publish('shift.created', payload)
|
||||
|
|
||||
+-- ListmonkListener (newsletter sync)
|
||||
+-- RocketChatListener (team notifications)
|
||||
+-- CrmActivityListener (contact timeline)
|
||||
+-- CalendarSyncListener (unified calendar)
|
||||
+-- N8nWebhookListener (external automation)
|
||||
+-- GancioSyncListener (public event calendar)
|
||||
```
|
||||
|
||||
### Why In-Process EventEmitter (not Redis PubSub)
|
||||
|
||||
- Single Express process — no distributed coordination needed
|
||||
- Zero serialization overhead (pass JS objects directly)
|
||||
- Data already persisted in DB — events are ephemeral notifications
|
||||
- Matches the existing fire-and-forget pattern used by Listmonk/RC services
|
||||
- Can be swapped to Redis PubSub later if we go multi-process
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `api/src/types/events.ts` | Typed event catalog (all event names + payloads) |
|
||||
| `api/src/services/event-bus.service.ts` | Core EventBus (publish/subscribe/stats) |
|
||||
| `api/src/services/event-listeners/listmonk.listener.ts` | Listmonk newsletter sync |
|
||||
| `api/src/services/event-listeners/rocketchat.listener.ts` | Rocket.Chat notifications |
|
||||
| `api/src/services/event-listeners/crm-activity.listener.ts` | CRM ContactActivity writer |
|
||||
| `api/src/services/event-listeners/calendar-sync.listener.ts` | Calendar unification |
|
||||
| `api/src/services/event-listeners/n8n-webhook.listener.ts` | n8n automation bridge |
|
||||
| `api/src/services/event-listeners/gancio.listener.ts` | Gancio event sync (shifts + ticketed events) |
|
||||
| `api/src/services/event-listeners/engagement-scoring.listener.ts` | Contact engagement scores (Redis ZSET) |
|
||||
| `api/src/services/event-listeners/homepage-stats.listener.ts` | Homepage real-time counters + cache invalidation |
|
||||
|
||||
---
|
||||
|
||||
## Progress Tracker
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
- [x] Explore existing event patterns (Listmonk, RC, Gancio, provisioning)
|
||||
- [x] Design EventBus architecture
|
||||
- [x] Implement EventBus service (`api/src/services/event-bus.service.ts`)
|
||||
- [x] Define typed event catalog (`api/src/types/events.ts` — 46 events across 14 modules)
|
||||
- [x] Register EventBus in server.ts startup
|
||||
- [x] Add EventBus stats endpoint (`GET /api/observability/event-bus`)
|
||||
|
||||
### Phase 2: Migrate Existing Integrations
|
||||
- [x] Listmonk event sync → EventBus listener (9 event subscriptions)
|
||||
- [x] Rocket.Chat webhook service → EventBus listener (4 event subscriptions)
|
||||
- [x] Gancio shift/event sync → EventBus listener (3 event subscriptions)
|
||||
|
||||
### Phase 3: New Listeners
|
||||
- [x] CRM Activity auto-generation listener (11 event subscriptions)
|
||||
- [x] Calendar sync listener (8 event subscriptions)
|
||||
- [x] n8n webhook emitter listener (wildcard subscription, forwards all events)
|
||||
- [x] Listmonk webhook receiver (inbound: open, click, bounce, unsubscribe → EventBus)
|
||||
|
||||
### Phase 4: Wire Up Publishers (migrated from inline calls)
|
||||
- [x] Shift CRUD + signup (shift.created/updated/deleted, shift.signup.created/cancelled)
|
||||
- [x] Canvass session complete + visits (canvass.session.completed, contact.address.updated)
|
||||
- [x] Response submit (response.submitted)
|
||||
- [x] Campaign email sent (campaign.email.sent)
|
||||
- [x] Payment/donation/subscription events (3 event types)
|
||||
- [x] Contact tag changes (contact.tags.changed — 3 call sites)
|
||||
- [x] Reengagement sent (reengagement.sent)
|
||||
- [x] Campaign CRUD + publish + moderation (campaign.created/updated/deleted/published/status.changed)
|
||||
- [x] User create/update/delete/approve (user.created/updated/deleted/approved)
|
||||
- [x] SMS campaign start/complete + message send/receive (4 event types)
|
||||
- [x] Media video publish/unpublish/view (3 event types)
|
||||
- [x] Ticketed event publish/cancel (EventBus publishes alongside existing Gancio calls)
|
||||
- [x] Impact story publish (social.impact-story.published)
|
||||
- [x] Meeting create/delete (jitsi.routes.ts — meeting.created, meeting.deleted)
|
||||
|
||||
### Phase 4b: Extended Listeners (2026-03-31)
|
||||
- [x] RC listener: +7 subscriptions (campaign.published, donations, subscriptions, SMS escalation, user.approved, video.published, ticketed-event.published)
|
||||
- [x] CRM listener: +2 subscriptions (subscription activated, email bounced)
|
||||
- [x] RC webhook service: +7 new formatter methods
|
||||
- [x] Prisma migration: SHIFT, MEETING, TICKETED_EVENT added to CalendarItemSource enum
|
||||
- [x] Calendar sync listener: uses proper source types (SHIFT, MEETING, TICKETED_EVENT)
|
||||
|
||||
### Phase 4c: New Data Listeners (2026-03-31)
|
||||
- [x] Engagement scoring listener (11 subscriptions, Redis ZSET leaderboard)
|
||||
- [x] Homepage stats listener (12 subscriptions, Redis counters + recent activity)
|
||||
- [x] GET /api/homepage/live-stats endpoint (public, real-time counters + recent)
|
||||
- [x] GET /api/observability/engagement-leaderboard endpoint (admin, top contacts)
|
||||
|
||||
### Phase 5: Future
|
||||
- [ ] Migrate meeting-planner Gancio calls to EventBus (blocked: synchronous return value needed)
|
||||
- [ ] Homepage service: swap COUNT queries for Redis counters in getStats()
|
||||
- [ ] Engagement score materialization: periodic job to denormalize scores to Contact model
|
||||
|
||||
---
|
||||
|
||||
## Event Catalog
|
||||
|
||||
### Currently Wired (11 event points, 3 consumers)
|
||||
|
||||
| Event | Listmonk | Rocket.Chat | Gancio |
|
||||
|-------|----------|-------------|--------|
|
||||
| shift.signup | yes | yes | - |
|
||||
| shift.signup.cancelled | - | yes | - |
|
||||
| shift.created | - | - | yes |
|
||||
| shift.updated | - | - | yes |
|
||||
| shift.deleted | - | - | yes |
|
||||
| canvass.session.completed | yes | yes | - |
|
||||
| canvass.address.updated | yes | - | - |
|
||||
| campaign.email.sent | yes | - | - |
|
||||
| response.submitted | - | yes | - |
|
||||
| subscription.activated | yes | - | - |
|
||||
| donation.completed | yes | - | - |
|
||||
| product.purchased | yes | - | - |
|
||||
| contact.tags.changed | yes | - | - |
|
||||
| reengagement.sent | yes | - | - |
|
||||
|
||||
### New Events (49+ handlers need publishers)
|
||||
|
||||
| Event | CRM Activity | Calendar | RC | n8n |
|
||||
|-------|-------------|----------|-----|-----|
|
||||
| campaign.created | - | - | - | yes |
|
||||
| campaign.published | - | - | yes | yes |
|
||||
| campaign.status.changed | - | - | yes | yes |
|
||||
| user.approved | - | - | yes | yes |
|
||||
| user.created | - | - | - | yes |
|
||||
| video.published | - | - | yes | yes |
|
||||
| video.viewed | yes | - | - | - |
|
||||
| sms.message.received | yes | - | yes* | yes |
|
||||
| sms.campaign.completed | - | - | yes | yes |
|
||||
| ticketed-event.published | - | yes | - | yes |
|
||||
| meeting.created | - | yes | - | - |
|
||||
| impact-story.published | - | - | yes | yes |
|
||||
| shift.created | - | yes | - | yes |
|
||||
| donation.completed | yes | - | yes | yes |
|
||||
| subscription.activated | yes | - | - | yes |
|
||||
|
||||
*SMS escalations (QUESTION/NEGATIVE sentiment) to relevant RC channel
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Listeners self-guard**: Each listener checks its own feature flag (ENABLE_CHAT, LISTMONK_SYNC_ENABLED, etc.) — the EventBus doesn't filter
|
||||
2. **Error isolation**: Each listener wraps its handler in try-catch; one listener failing doesn't affect others
|
||||
3. **No persistence**: Events are ephemeral — if the server restarts mid-event, it's lost (data is already in DB)
|
||||
4. **Stats tracking**: EventBus tracks per-event emission counts + per-listener execution counts for observability
|
||||
5. **Wildcard subscriptions**: Listeners can subscribe to `shift.*` to catch all shift events
|
||||
8
admin/.vite/deps/_metadata.json
Normal file
8
admin/.vite/deps/_metadata.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "46070c3d",
|
||||
"configHash": "70922fab",
|
||||
"lockfileHash": "ee36a2d0",
|
||||
"browserHash": "5aa32ba6",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
admin/.vite/deps/package.json
Normal file
3
admin/.vite/deps/package.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
13
admin/package-lock.json
generated
13
admin/package-lock.json
generated
@ -33,9 +33,11 @@
|
||||
"grapesjs-tabs": "^1.0.6",
|
||||
"grapesjs-touch": "^0.1.1",
|
||||
"grapesjs-typed": "^2.0.1",
|
||||
"hls.js": "^1.6.16",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"minisearch": "^7.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@ -2633,6 +2635,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.16",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
|
||||
"integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz",
|
||||
@ -2722,6 +2730,11 @@
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/leaflet.heat": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet.heat/-/leaflet.heat-0.2.0.tgz",
|
||||
"integrity": "sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ=="
|
||||
},
|
||||
"node_modules/leaflet.markercluster": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
||||
|
||||
@ -34,9 +34,11 @@
|
||||
"grapesjs-tabs": "^1.0.6",
|
||||
"grapesjs-touch": "^0.1.1",
|
||||
"grapesjs-typed": "^2.0.1",
|
||||
"hls.js": "^1.6.16",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"minisearch": "^7.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@ -43,8 +43,13 @@ import JitsiMeetPage from '@/pages/JitsiMeetPage';
|
||||
import SettingsPage from '@/pages/SettingsPage';
|
||||
import NavigationSettingsPage from '@/pages/NavigationSettingsPage';
|
||||
import PangolinPage from '@/pages/PangolinPage';
|
||||
import ControlPanelPage from '@/pages/ControlPanelPage';
|
||||
import ObservabilityPage from '@/pages/ObservabilityPage';
|
||||
import DocsAnalyticsPage from '@/pages/DocsAnalyticsPage';
|
||||
import AnalyticsOverviewPage from '@/pages/analytics/AnalyticsOverviewPage';
|
||||
import GeoAnalyticsPage from '@/pages/analytics/GeoAnalyticsPage';
|
||||
import ContentAnalyticsPage from '@/pages/analytics/ContentAnalyticsPage';
|
||||
import UserAnalyticsPage from '@/pages/analytics/UserAnalyticsPage';
|
||||
import DocsCommentsPage from '@/pages/DocsCommentsPage';
|
||||
import DocsMetadataPage from '@/pages/DocsMetadataPage';
|
||||
import PaymentsDashboardPage from '@/pages/payments/PaymentsDashboardPage';
|
||||
@ -62,6 +67,11 @@ import GalleryAdsPage from '@/pages/media/GalleryAdsPage';
|
||||
import AdAnalyticsDashboardPage from '@/pages/media/AdAnalyticsDashboardPage';
|
||||
import CampaignModerationPage from '@/pages/influence/CampaignModerationPage';
|
||||
import CampaignEffectivenessPage from '@/pages/influence/CampaignEffectivenessPage';
|
||||
import PetitionsPage from '@/pages/influence/PetitionsPage';
|
||||
import PetitionSignaturesPage from '@/pages/influence/PetitionSignaturesPage';
|
||||
import PetitionModerationPage from '@/pages/influence/PetitionModerationPage';
|
||||
import PetitionsListPage from '@/pages/public/PetitionsListPage';
|
||||
import PetitionPage from '@/pages/public/PetitionPage';
|
||||
import PublicLandingPage from '@/pages/public/LandingPage';
|
||||
import PagesIndexPage from '@/pages/public/PagesIndexPage';
|
||||
import EventsPage from '@/pages/public/EventsPage';
|
||||
@ -100,6 +110,7 @@ import SocialFeedPage from '@/pages/volunteer/SocialFeedPage';
|
||||
import DiscoverPage from '@/pages/volunteer/DiscoverPage';
|
||||
import GroupDetailPage from '@/pages/volunteer/GroupDetailPage';
|
||||
import AchievementsPage from '@/pages/volunteer/AchievementsPage';
|
||||
import MyAnalyticsPage from '@/pages/volunteer/MyAnalyticsPage';
|
||||
import {
|
||||
ADMIN_ROLES,
|
||||
INFLUENCE_ROLES,
|
||||
@ -113,6 +124,7 @@ import {
|
||||
SOCIAL_ROLES,
|
||||
SYSTEM_ROLES,
|
||||
POLLS_ROLES,
|
||||
ANALYTICS_ROLES,
|
||||
} from '@/types/api';
|
||||
import { isAdmin } from '@/utils/roles';
|
||||
import QuickJoinPage from '@/pages/public/QuickJoinPage';
|
||||
@ -134,6 +146,9 @@ import SpotlightAdminPage from '@/pages/social/SpotlightAdminPage';
|
||||
import ChallengesAdminPage from '@/pages/social/ChallengesAdminPage';
|
||||
import ImpactStoriesPage from '@/pages/influence/ImpactStoriesPage';
|
||||
import StrawPollsPage from '@/pages/influence/StrawPollsPage';
|
||||
import ActionCampaignsPage from '@/pages/influence/ActionCampaignsPage';
|
||||
import ActionCampaignEditorPage from '@/pages/influence/ActionCampaignEditorPage';
|
||||
import VolunteerDashboardPage from '@/pages/volunteer/VolunteerDashboardPage';
|
||||
import ReferralsPage from '@/pages/volunteer/ReferralsPage';
|
||||
import ChallengesPage from '@/pages/volunteer/ChallengesPage';
|
||||
import ChallengeDetailPage from '@/pages/volunteer/ChallengeDetailPage';
|
||||
@ -172,7 +187,7 @@ function RoleAwareRedirect() {
|
||||
|
||||
function NavigateToCutMap() {
|
||||
const { cutId } = useParams<{ cutId: string }>();
|
||||
return <Navigate to={`/volunteer?cutId=${cutId}`} replace />;
|
||||
return <Navigate to={`/volunteer/map?cutId=${cutId}`} replace />;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
@ -240,6 +255,12 @@ export default function App() {
|
||||
<Route path="/campaigns" element={<FeatureGate feature="enableInfluence"><PublicLayout /></FeatureGate>}>
|
||||
<Route index element={<CampaignsListPage />} />
|
||||
</Route>
|
||||
<Route path="/petitions" element={<FeatureGate feature="enablePetitions"><PublicLayout /></FeatureGate>}>
|
||||
<Route index element={<PetitionsListPage />} />
|
||||
</Route>
|
||||
<Route path="/petition/:slug" element={<FeatureGate feature="enablePetitions"><PublicLayout /></FeatureGate>}>
|
||||
<Route index element={<PetitionPage />} />
|
||||
</Route>
|
||||
<Route path="/campaigns/create" element={
|
||||
<FeatureGate feature="enableInfluence">
|
||||
<ProtectedRoute>
|
||||
@ -352,9 +373,9 @@ export default function App() {
|
||||
{/* Email link alias for video viewer */}
|
||||
<Route path="/media/:id" element={<MediaViewerPage />} />
|
||||
|
||||
{/* Volunteer map — full-screen, default landing page */}
|
||||
{/* Volunteer map — full-screen (moved from /volunteer to /volunteer/map) */}
|
||||
<Route
|
||||
path="/volunteer"
|
||||
path="/volunteer/map"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<VolunteerMapPage />
|
||||
@ -380,6 +401,7 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route path="/volunteer" element={<VolunteerDashboardPage />} />
|
||||
<Route path="/volunteer/activity" element={<MyActivityPage />} />
|
||||
<Route path="/volunteer/shifts" element={<VolunteerShiftsPage />} />
|
||||
<Route path="/volunteer/routes" element={<MyRoutesPage />} />
|
||||
@ -399,6 +421,7 @@ export default function App() {
|
||||
<Route path="/volunteer/calendar/shared" element={<FeatureGate feature="enableSocialCalendar"><SharedCalendarsPage /></FeatureGate>} />
|
||||
<Route path="/volunteer/calendar/friend/:userId" element={<FeatureGate feature="enableSocialCalendar"><FriendCalendarPage /></FeatureGate>} />
|
||||
<Route path="/volunteer/calendar" element={<FeatureGate feature="enableSocialCalendar"><MyCalendarPage /></FeatureGate>} />
|
||||
<Route path="/volunteer/my-analytics" element={<FeatureGate feature="enableAnalytics"><MyAnalyticsPage /></FeatureGate>} />
|
||||
<Route path="/volunteer/*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
@ -574,6 +597,30 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/petitions"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={INFLUENCE_ROLES}>
|
||||
<PetitionsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/petitions/:id/signatures"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={INFLUENCE_ROLES}>
|
||||
<PetitionSignaturesPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/petitions/moderation"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={INFLUENCE_ROLES}>
|
||||
<PetitionModerationPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/straw-polls"
|
||||
element={
|
||||
@ -582,6 +629,30 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/action-campaigns"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={INFLUENCE_ROLES}>
|
||||
<ActionCampaignsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/action-campaigns/new"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={INFLUENCE_ROLES}>
|
||||
<ActionCampaignEditorPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="influence/action-campaigns/:id"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={INFLUENCE_ROLES}>
|
||||
<ActionCampaignEditorPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="listmonk"
|
||||
element={
|
||||
@ -807,6 +878,14 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="control-panel"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={SYSTEM_ROLES}>
|
||||
<ControlPanelPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="observability"
|
||||
element={
|
||||
@ -815,6 +894,46 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="analytics"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={ANALYTICS_ROLES}>
|
||||
<AnalyticsOverviewPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="analytics/geo"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={ANALYTICS_ROLES}>
|
||||
<GeoAnalyticsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="analytics/content"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={ANALYTICS_ROLES}>
|
||||
<ContentAnalyticsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="analytics/users"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={ANALYTICS_ROLES}>
|
||||
<UserAnalyticsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="analytics/users/:userId"
|
||||
element={
|
||||
<ProtectedRoute requiredRoles={ANALYTICS_ROLES}>
|
||||
<UserAnalyticsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="map"
|
||||
element={
|
||||
|
||||
@ -72,6 +72,7 @@ import {
|
||||
PAYMENTS_ROLES,
|
||||
SOCIAL_ROLES,
|
||||
POLLS_ROLES,
|
||||
ANALYTICS_ROLES,
|
||||
} from '@/types/api';
|
||||
import { buildHomeUrl, resolveNavUrl } from '@/lib/service-url';
|
||||
import type { NavItem } from '@/types/api';
|
||||
@ -186,8 +187,13 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, use
|
||||
{ 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/action-campaigns', icon: <TrophyOutlined />, label: 'Action Campaigns' },
|
||||
{ key: '/app/influence/effectiveness', icon: <LineChartOutlined />, label: 'Effectiveness' },
|
||||
{ key: '/app/influence/stories', icon: <TrophyOutlined />, label: 'Impact Stories' },
|
||||
...(settings?.enablePetitions !== false ? [
|
||||
{ key: '/app/influence/petitions', icon: <FileTextOutlined />, label: 'Petitions' },
|
||||
{ key: '/app/influence/petitions/moderation', icon: <FileTextOutlined />, label: 'Petition Review' },
|
||||
] : []),
|
||||
...(settings?.enablePolls !== false && can(POLLS_ROLES) ? [{ key: '/app/influence/straw-polls', icon: <BarChartOutlined />, label: 'Straw Polls' }] : []),
|
||||
],
|
||||
});
|
||||
@ -326,6 +332,20 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, use
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
if (settings?.enableAnalytics !== false && can(ANALYTICS_ROLES)) {
|
||||
items.push({
|
||||
key: 'analytics-submenu',
|
||||
icon: <BarChartOutlined />,
|
||||
label: 'Analytics',
|
||||
children: [
|
||||
{ key: '/app/analytics', icon: <DashboardOutlined />, label: 'Overview' },
|
||||
{ key: '/app/analytics/geo', icon: <GlobalOutlined />, label: 'Geography' },
|
||||
{ key: '/app/analytics/content', icon: <FileTextOutlined />, label: 'Content' },
|
||||
{ key: '/app/analytics/users', icon: <TeamOutlined />, label: 'Users' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'services-submenu',
|
||||
icon: <CloudServerOutlined />,
|
||||
@ -333,6 +353,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, use
|
||||
children: [
|
||||
{ type: 'group', label: 'Infrastructure', children: [
|
||||
{ key: '/app/tunnel', icon: <CloudServerOutlined />, label: 'Tunnel' },
|
||||
{ key: '/app/control-panel', icon: <ApiOutlined />, label: 'Control Panel' },
|
||||
{ key: '/app/observability', icon: <LineChartOutlined />, label: 'Monitoring' },
|
||||
{ key: '/app/services/nocodb', icon: <DatabaseOutlined />, label: 'Database' },
|
||||
{ key: '/app/services/vaultwarden', icon: <LockOutlined />, label: 'Vault' },
|
||||
@ -638,7 +659,7 @@ export default function AppLayout() {
|
||||
/>
|
||||
</Tooltip>
|
||||
{pageHeader?.actions}
|
||||
{(() => {
|
||||
{!isMobile && (() => {
|
||||
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
|
||||
const withOverrides = applyAdminOverrides(merged);
|
||||
const flags = buildFeatureFlags(settings);
|
||||
@ -647,11 +668,14 @@ export default function AppLayout() {
|
||||
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');
|
||||
window.open(resolveNavUrl(item.path), '_blank', 'noopener,noreferrer');
|
||||
} else if (item.external && item.id === 'home') {
|
||||
window.open(buildHomeUrl(), '_blank');
|
||||
window.open(buildHomeUrl(), '_blank', 'noopener,noreferrer');
|
||||
} else if (item.external) {
|
||||
window.open(item.path, '_blank');
|
||||
// Only open http/https URLs to prevent javascript: URI injection
|
||||
if (item.path.startsWith('http://') || item.path.startsWith('https://')) {
|
||||
window.open(item.path, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} else {
|
||||
navigate(item.path);
|
||||
}
|
||||
@ -673,7 +697,7 @@ export default function AppLayout() {
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button type="text" size="small" icon={getIcon(item.icon)}>
|
||||
{!isMobile && !collapsed && item.label}
|
||||
{!collapsed && item.label}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
@ -686,23 +710,25 @@ export default function AppLayout() {
|
||||
icon={getIcon(item.icon)}
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
{!isMobile && !collapsed && item.label}
|
||||
{!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>
|
||||
{!isMobile && (
|
||||
<Tooltip title="Switch to Volunteer Portal">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<TeamOutlined />}
|
||||
onClick={() => navigate('/volunteer')}
|
||||
>
|
||||
{!collapsed && 'Volunteer'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<Button type="text" icon={<UserOutlined />} data-tour="user-menu">
|
||||
{!isMobile && !collapsed && (
|
||||
|
||||
@ -22,11 +22,13 @@ const FEATURE_LABELS: Record<string, string> = {
|
||||
enableMeetingPlanner: 'Meeting Planner',
|
||||
enableTicketedEvents: 'Ticketed Events',
|
||||
enableSocialCalendar: 'Social Calendar',
|
||||
enablePetitions: 'Petitions',
|
||||
enablePolls: 'Straw Polls',
|
||||
enableAnalytics: 'Analytics Dashboard',
|
||||
};
|
||||
|
||||
interface FeatureGateProps {
|
||||
feature: keyof Pick<SiteSettings, 'enableInfluence' | 'enableMap' | 'enableLandingPages' | 'enableNewsletter' | 'enableMediaFeatures' | 'enablePayments' | 'enableGalleryAds' | 'enablePeople' | 'enableEvents' | 'enableSocial' | 'enableMeet' | 'enableMeetingPlanner' | 'enableTicketedEvents' | 'enableSocialCalendar' | 'enablePolls'>;
|
||||
feature: keyof Pick<SiteSettings, 'enableInfluence' | 'enableMap' | 'enableLandingPages' | 'enableNewsletter' | 'enableMediaFeatures' | 'enablePayments' | 'enableGalleryAds' | 'enablePeople' | 'enableEvents' | 'enableSocial' | 'enableMeet' | 'enableMeetingPlanner' | 'enableTicketedEvents' | 'enableSocialCalendar' | 'enablePetitions' | 'enablePolls' | 'enableAnalytics'>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
47
admin/src/components/MobilePageHeader.tsx
Normal file
47
admin/src/components/MobilePageHeader.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Row, Col, Typography, Space } from 'antd';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMobile } from '@/hooks/useMobile';
|
||||
|
||||
interface MobilePageHeaderProps {
|
||||
title: string;
|
||||
/** Optional element next to the title (badge, count, etc.) */
|
||||
extra?: ReactNode;
|
||||
/** Action buttons — will wrap on mobile, stay inline on desktop */
|
||||
actions?: ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsive page header that stacks title and actions on mobile.
|
||||
* On desktop: title left, actions right (single row).
|
||||
* On mobile: title full-width, actions below with wrapping.
|
||||
*/
|
||||
export function MobilePageHeader({ title, extra, actions, style }: MobilePageHeaderProps) {
|
||||
const { isMobile } = useMobile();
|
||||
|
||||
return (
|
||||
<Row
|
||||
justify="space-between"
|
||||
align={isMobile ? 'top' : 'middle'}
|
||||
style={{ marginBottom: 16, ...style }}
|
||||
gutter={[0, isMobile ? 12 : 0]}
|
||||
wrap
|
||||
>
|
||||
<Col xs={24} md="auto">
|
||||
<Space>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
{extra}
|
||||
</Space>
|
||||
</Col>
|
||||
{actions && (
|
||||
<Col xs={24} md="auto">
|
||||
<Space wrap size={isMobile ? 'small' : 'middle'}>
|
||||
{actions}
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { theme } from 'antd';
|
||||
import {
|
||||
HomeOutlined,
|
||||
EnvironmentOutlined,
|
||||
ScheduleOutlined,
|
||||
HistoryOutlined,
|
||||
@ -15,7 +16,8 @@ import {
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
const BASE_NAV_ITEMS = [
|
||||
{ key: '/volunteer', icon: EnvironmentOutlined, label: 'Map' },
|
||||
{ key: '/volunteer', icon: HomeOutlined, label: 'Home' },
|
||||
{ key: '/volunteer/map', 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' },
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
UserOutlined,
|
||||
GlobalOutlined,
|
||||
AppstoreOutlined,
|
||||
HomeOutlined,
|
||||
EnvironmentOutlined,
|
||||
ScheduleOutlined,
|
||||
HistoryOutlined,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
TagOutlined,
|
||||
TeamOutlined,
|
||||
MessageOutlined,
|
||||
BarChartOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuthStore } from '@/stores/auth.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
@ -48,7 +50,8 @@ export default function VolunteerLayout() {
|
||||
// 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', icon: <HomeOutlined />, label: 'Home' },
|
||||
{ key: '/volunteer/map', 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' },
|
||||
@ -65,6 +68,9 @@ export default function VolunteerLayout() {
|
||||
if (settings?.enableChat) {
|
||||
items.push({ key: '/volunteer/chat', icon: <MessageOutlined />, label: 'Chat' });
|
||||
}
|
||||
if (settings?.enableAnalytics) {
|
||||
items.push({ key: '/volunteer/my-analytics', icon: <BarChartOutlined />, label: 'My Stats' });
|
||||
}
|
||||
return items;
|
||||
}, [settings?.enableSocialCalendar, settings?.enableTicketedEvents, settings?.enableSocial, settings?.enableChat]);
|
||||
|
||||
@ -97,7 +103,7 @@ export default function VolunteerLayout() {
|
||||
|
||||
<Content
|
||||
style={{
|
||||
maxWidth: 800,
|
||||
maxWidth: location.pathname === '/volunteer' ? 1280 : 800,
|
||||
width: '100%',
|
||||
margin: '0 auto',
|
||||
padding: '16px 12px max(60px, calc(44px + 16px + env(safe-area-inset-bottom))) 12px',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
DatePicker,
|
||||
@ -169,13 +169,20 @@ export default function CalendarItemModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
onClose={onCancel}
|
||||
title={isEditing ? 'Edit Calendar Item' : 'New Calendar Item'}
|
||||
footer={null}
|
||||
width={520}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Button type="primary" onClick={() => form.submit()} loading={loading}>
|
||||
{isEditing ? 'Save Changes' : 'Create'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
@ -454,26 +461,18 @@ export default function CalendarItemModal({
|
||||
)}
|
||||
|
||||
{/* 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'}
|
||||
{isEditing && onDelete && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Modal, Form, Select, Checkbox, Slider, DatePicker, Switch,
|
||||
Button, Statistic, Row, Col, Descriptions, Alert, Spin, App, Grid,
|
||||
Drawer, Form, Select, Checkbox, Slider, DatePicker, Switch,
|
||||
Button, Statistic, Row, Col, Descriptions, Alert, Spin, App, Grid, Space,
|
||||
} from 'antd';
|
||||
import { ExportOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { api } from '@/lib/api';
|
||||
@ -152,32 +152,34 @@ export default function ExportContactsModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Export Canvass Contacts to Campaign"
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onClose={onClose}
|
||||
width={isMobile ? '95vw' : 640}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>Cancel</Button>,
|
||||
<Button
|
||||
key="preview"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handlePreview}
|
||||
loading={previewing}
|
||||
>
|
||||
Preview
|
||||
</Button>,
|
||||
<Button
|
||||
key="export"
|
||||
type="primary"
|
||||
icon={<ExportOutlined />}
|
||||
onClick={handleExport}
|
||||
loading={exporting}
|
||||
disabled={!preview || preview.contactsWithEmail === 0}
|
||||
>
|
||||
Export
|
||||
</Button>,
|
||||
]}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handlePreview}
|
||||
loading={previewing}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ExportOutlined />}
|
||||
onClick={handleExport}
|
||||
loading={exporting}
|
||||
disabled={!preview || preview.contactsWithEmail === 0}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical" size="small">
|
||||
<Form.Item
|
||||
@ -294,6 +296,6 @@ export default function ExportContactsModal({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Drawer,
|
||||
Button,
|
||||
Input,
|
||||
Space,
|
||||
@ -150,7 +150,7 @@ export function AuthorsManagementModal({
|
||||
const authorEntries = Object.entries(localAuthors);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={
|
||||
<span>
|
||||
<UserOutlined style={{ marginRight: 8 }} />
|
||||
@ -158,23 +158,23 @@ export function AuthorsManagementModal({
|
||||
</span>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveAll}
|
||||
loading={saving}
|
||||
disabled={!dirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
destroyOnHidden
|
||||
onClose={onClose}
|
||||
width={560}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveAll}
|
||||
loading={saving}
|
||||
disabled={!dirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{contextHolder}
|
||||
|
||||
@ -236,7 +236,7 @@ export function AuthorsManagementModal({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
129
admin/src/components/docs/DocsEditorToolbar.tsx
Normal file
129
admin/src/components/docs/DocsEditorToolbar.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Button, Dropdown, Tooltip } from 'antd';
|
||||
import {
|
||||
BoldOutlined,
|
||||
ItalicOutlined,
|
||||
StrikethroughOutlined,
|
||||
HighlightOutlined,
|
||||
CodeOutlined,
|
||||
FontSizeOutlined,
|
||||
AlertOutlined,
|
||||
PlusOutlined,
|
||||
DownOutlined,
|
||||
LinkOutlined,
|
||||
FileMarkdownOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { editor as monacoEditor } from 'monaco-editor';
|
||||
import { SNIPPETS, PLATFORM_INSERT_IDS, applySnippet } from './mkdocs-snippets';
|
||||
|
||||
interface DocsEditorToolbarProps {
|
||||
editorRef: React.RefObject<monacoEditor.IStandaloneCodeEditor | null>;
|
||||
monacoRef: React.RefObject<typeof import('monaco-editor') | null>;
|
||||
/** If true, show platform-specific inserts (video card, donate, etc.) */
|
||||
showPlatformInserts?: boolean;
|
||||
/** Custom handler for snippet IDs that need special treatment (modals, etc.) */
|
||||
onCustomSnippet?: (snippetId: string) => boolean;
|
||||
/** Background color — defaults to transparent */
|
||||
background?: string;
|
||||
/** Border color — defaults to rgba(255,255,255,0.08) */
|
||||
borderColor?: string;
|
||||
}
|
||||
|
||||
export default function DocsEditorToolbar({
|
||||
editorRef,
|
||||
monacoRef,
|
||||
showPlatformInserts = false,
|
||||
onCustomSnippet,
|
||||
background = 'transparent',
|
||||
borderColor = 'rgba(255,255,255,0.08)',
|
||||
}: DocsEditorToolbarProps) {
|
||||
const handleSnippet = useCallback((snippetId: string) => {
|
||||
if (onCustomSnippet?.(snippetId)) return;
|
||||
|
||||
const snippet = SNIPPETS.find(s => s.id === snippetId);
|
||||
if (!snippet || !editorRef.current || !monacoRef.current) return;
|
||||
applySnippet(editorRef.current, snippet, monacoRef.current);
|
||||
}, [editorRef, monacoRef, onCustomSnippet]);
|
||||
|
||||
const insertSnippets = SNIPPETS.filter(s =>
|
||||
s.group === 'insert' && (showPlatformInserts || !PLATFORM_INSERT_IDS.has(s.id))
|
||||
);
|
||||
|
||||
const getInsertIcon = (id: string) => {
|
||||
if (id === 'link') return <LinkOutlined />;
|
||||
if (id === 'image') return <FileMarkdownOutlined />;
|
||||
if (id === 'table') return <TableOutlined />;
|
||||
return <PlusOutlined />;
|
||||
};
|
||||
|
||||
const btnStyle = { width: 26, height: 24 };
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
background,
|
||||
borderBottom: `1px solid ${borderColor}`,
|
||||
gap: 2,
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Bold (Ctrl+B)" mouseEnterDelay={0.4}>
|
||||
<Button type="text" size="small" icon={<BoldOutlined />} onClick={() => handleSnippet('bold')} style={btnStyle} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Italic (Ctrl+I)" mouseEnterDelay={0.4}>
|
||||
<Button type="text" size="small" icon={<ItalicOutlined />} onClick={() => handleSnippet('italic')} style={btnStyle} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Strikethrough" mouseEnterDelay={0.4}>
|
||||
<Button type="text" size="small" icon={<StrikethroughOutlined />} onClick={() => handleSnippet('strikethrough')} style={btnStyle} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Highlight" mouseEnterDelay={0.4}>
|
||||
<Button type="text" size="small" icon={<HighlightOutlined />} onClick={() => handleSnippet('highlight')} style={btnStyle} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Inline Code" mouseEnterDelay={0.4}>
|
||||
<Button type="text" size="small" icon={<CodeOutlined />} onClick={() => handleSnippet('inline-code')} style={btnStyle} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Keyboard Key" mouseEnterDelay={0.4}>
|
||||
<Button type="text" size="small" style={{ ...btnStyle, fontSize: 11, fontWeight: 700 }} onClick={() => handleSnippet('kbd')}>K</Button>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 16, background: borderColor, margin: '0 4px' }} />
|
||||
|
||||
<Dropdown menu={{ items: SNIPPETS.filter(s => s.group === 'heading').map(s => ({ key: s.id, label: s.label, icon: <FontSizeOutlined />, onClick: () => handleSnippet(s.id) })) }} trigger={['click']}>
|
||||
<Button type="text" size="small" style={{ height: 24, fontSize: 12 }}>
|
||||
<FontSizeOutlined /> H <DownOutlined style={{ fontSize: 8 }} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<div style={{ width: 1, height: 16, background: borderColor, margin: '0 4px' }} />
|
||||
|
||||
<Dropdown menu={{ items: SNIPPETS.filter(s => s.group === 'admonition').map(s => ({ key: s.id, label: s.label, icon: <AlertOutlined />, onClick: () => handleSnippet(s.id) })) }} trigger={['click']}>
|
||||
<Button type="text" size="small" style={{ height: 24, fontSize: 12 }}>
|
||||
<AlertOutlined /> Admonitions <DownOutlined style={{ fontSize: 8 }} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown menu={{ items: SNIPPETS.filter(s => s.group === 'code').map(s => ({ key: s.id, label: s.label, icon: <CodeOutlined />, onClick: () => handleSnippet(s.id) })) }} trigger={['click']}>
|
||||
<Button type="text" size="small" style={{ height: 24, fontSize: 12 }}>
|
||||
<CodeOutlined /> Code <DownOutlined style={{ fontSize: 8 }} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown menu={{ items: insertSnippets.map(s => ({
|
||||
key: s.id,
|
||||
label: s.label,
|
||||
icon: getInsertIcon(s.id),
|
||||
onClick: () => handleSnippet(s.id),
|
||||
})) }} trigger={['click']}>
|
||||
<Button type="text" size="small" style={{ height: 24, fontSize: 12 }}>
|
||||
<PlusOutlined /> Insert <DownOutlined style={{ fontSize: 8 }} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -30,6 +30,7 @@ import {
|
||||
PlusOutlined,
|
||||
CloseOutlined,
|
||||
FileOutlined,
|
||||
FolderOpenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { UseDocsEditorReturn } from '@/hooks/useDocsEditor';
|
||||
import { isImageFile } from '@/hooks/useDocsEditor';
|
||||
@ -52,6 +53,7 @@ import { AdPickerModal } from '@/components/media/AdPickerModal';
|
||||
import type { AdInsertResult } from '@/components/media/AdPickerModal';
|
||||
import { PollInsertModal } from '@/components/scheduling/PollInsertModal';
|
||||
import { WikiLinkPickerModal } from '@/components/docs/WikiLinkPickerModal';
|
||||
import { MoveToModal } from '@/components/docs/MoveToModal';
|
||||
import { useDocsCollaboration } from '@/hooks/useDocsCollaboration';
|
||||
import { CollaboratorAvatars } from '@/components/docs/CollaboratorAvatars';
|
||||
import { YTextareaBinding } from '@/lib/y-textarea';
|
||||
@ -259,6 +261,8 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
||||
const [adPickerOpen, setAdPickerOpen] = useState(false);
|
||||
const [pollInsertOpen, setPollInsertOpen] = useState(false);
|
||||
const [wikiLinkPickerOpen, setWikiLinkPickerOpen] = useState(false);
|
||||
const [moveToModalOpen, setMoveToModalOpen] = useState(false);
|
||||
const [moveSourcePath, setMoveSourcePath] = useState('');
|
||||
|
||||
const {
|
||||
fileTree,
|
||||
@ -287,6 +291,7 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
||||
onContentChange,
|
||||
handleDelete,
|
||||
handleModalOk,
|
||||
handleMoveFile,
|
||||
handleNewFileRoot,
|
||||
handleNewFolderRoot,
|
||||
refreshTree,
|
||||
@ -430,6 +435,7 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
||||
}
|
||||
items.push(
|
||||
{ key: 'rename', icon: <EditOutlined />, label: 'Rename', onClick: () => { setContextPath(nodePath); setModalInput(nodePath.split('/').pop() || ''); setModalType('rename'); } },
|
||||
{ key: 'moveTo', icon: <FolderOpenOutlined />, label: 'Move to...', onClick: () => { setMoveSourcePath(nodePath); setMoveToModalOpen(true); } },
|
||||
{ key: 'delete', icon: <DeleteOutlined />, label: 'Delete', danger: true, onClick: () => handleDelete(nodePath) },
|
||||
);
|
||||
return items;
|
||||
@ -910,6 +916,14 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
||||
setWikiLinkPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<MoveToModal
|
||||
open={moveToModalOpen}
|
||||
fileTree={fileTree}
|
||||
sourcePath={moveSourcePath}
|
||||
onMove={(targetDir) => { setMoveToModalOpen(false); handleMoveFile(moveSourcePath, targetDir); }}
|
||||
onClose={() => setMoveToModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
156
admin/src/components/docs/MoveToModal.tsx
Normal file
156
admin/src/components/docs/MoveToModal.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Drawer, Input, List, theme, Typography } from 'antd';
|
||||
import { FolderOutlined, HomeOutlined } from '@ant-design/icons';
|
||||
import type { FileNode } from '@/types/api';
|
||||
|
||||
interface DirEntry {
|
||||
path: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
function collectDirs(nodes: FileNode[], exclude?: string, depth = 0): DirEntry[] {
|
||||
const dirs: DirEntry[] = [];
|
||||
for (const node of nodes) {
|
||||
if (!node.isDirectory) continue;
|
||||
if (exclude && (node.path === exclude || node.path.startsWith(exclude + '/'))) continue;
|
||||
dirs.push({ path: node.path, depth });
|
||||
if (node.children) dirs.push(...collectDirs(node.children, exclude, depth + 1));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
interface MoveToModalProps {
|
||||
open: boolean;
|
||||
fileTree: FileNode[];
|
||||
sourcePath: string;
|
||||
onMove: (targetDir: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MoveToModal({ open, fileTree, sourcePath, onMove, onClose }: MoveToModalProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const currentParent = useMemo(() => {
|
||||
const lastSlash = sourcePath.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? sourcePath.substring(0, lastSlash) : '';
|
||||
}, [sourcePath]);
|
||||
|
||||
const isSourceDir = useMemo(() => {
|
||||
function find(nodes: FileNode[]): boolean {
|
||||
for (const n of nodes) {
|
||||
if (n.path === sourcePath) return n.isDirectory;
|
||||
if (n.isDirectory && n.children && find(n.children)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return find(fileTree);
|
||||
}, [fileTree, sourcePath]);
|
||||
|
||||
const allDirs = useMemo(
|
||||
() => collectDirs(fileTree, isSourceDir ? sourcePath : undefined),
|
||||
[fileTree, sourcePath, isSourceDir],
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search.trim()) return allDirs;
|
||||
const q = search.toLowerCase();
|
||||
return allDirs.filter(d => d.path.toLowerCase().includes(q));
|
||||
}, [allDirs, search]);
|
||||
|
||||
const handleSelect = (dir: string) => {
|
||||
onMove(dir);
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const fileName = sourcePath.split('/').pop() || sourcePath;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={`Move "${fileName}"`}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
width={420}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Input.Search
|
||||
placeholder="Search directories..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
allowClear
|
||||
autoFocus
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
<div style={{ maxHeight: 360, overflow: 'auto' }}>
|
||||
{/* Root directory option */}
|
||||
{(!search.trim() || '/ (root)'.includes(search.toLowerCase())) && (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: currentParent === '' ? 'not-allowed' : 'pointer',
|
||||
opacity: currentParent === '' ? 0.5 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderRadius: 4,
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
onClick={() => currentParent !== '' && handleSelect('')}
|
||||
onMouseEnter={e => { if (currentParent !== '') (e.currentTarget.style.background = token.colorBgTextHover); }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
|
||||
>
|
||||
<HomeOutlined style={{ color: token.colorTextSecondary }} />
|
||||
<span style={{ flex: 1 }}>/ (root)</span>
|
||||
{currentParent === '' && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>current</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<List
|
||||
size="small"
|
||||
dataSource={filtered}
|
||||
locale={{ emptyText: 'No matching directories' }}
|
||||
renderItem={item => {
|
||||
const isCurrent = item.path === currentParent;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
paddingLeft: 12 + item.depth * 16,
|
||||
cursor: isCurrent ? 'not-allowed' : 'pointer',
|
||||
opacity: isCurrent ? 0.5 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderRadius: 4,
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
onClick={() => !isCurrent && handleSelect(item.path)}
|
||||
onMouseEnter={e => { if (!isCurrent) (e.currentTarget.style.background = token.colorBgTextHover); }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
|
||||
>
|
||||
<FolderOutlined style={{ color: token.colorTextSecondary, flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{item.path}
|
||||
</div>
|
||||
{isCurrent && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11, flexShrink: 0 }}>current</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Modal, Form, Input, DatePicker, Select, Switch, message, theme } from 'antd';
|
||||
import { Drawer, Form, Input, DatePicker, Select, Switch, Button, message, theme } from 'antd';
|
||||
import { FileMarkdownOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { api } from '@/lib/api';
|
||||
@ -84,7 +84,7 @@ export function NewBlogPostModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={
|
||||
<span>
|
||||
<FileMarkdownOutlined style={{ marginRight: 8 }} />
|
||||
@ -92,12 +92,17 @@ export function NewBlogPostModal({
|
||||
</span>
|
||||
}
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
okText="Create"
|
||||
confirmLoading={submitting}
|
||||
destroyOnHidden
|
||||
onClose={handleClose}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||
Create
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{contextHolder}
|
||||
<Form
|
||||
@ -160,6 +165,6 @@ export function NewBlogPostModal({
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Modal, Input, List, theme, Typography, Tag } from 'antd';
|
||||
import { Drawer, Input, List, theme, Typography, Tag } from 'antd';
|
||||
import { FileOutlined, PictureOutlined } from '@ant-design/icons';
|
||||
import type { FileNode } from '@/types/api';
|
||||
|
||||
@ -62,13 +62,15 @@ export function WikiLinkPickerModal({ open, fileTree, onSelect, onClose }: WikiL
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Insert Wiki Link"
|
||||
open={open}
|
||||
onCancel={() => { onClose(); setSearch(''); }}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
onClose={() => { onClose(); setSearch(''); }}
|
||||
width={420}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Input.Search
|
||||
placeholder="Search files..."
|
||||
@ -148,6 +150,6 @@ export function WikiLinkPickerModal({ open, fileTree, onSelect, onClose }: WikiL
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
117
admin/src/components/docs/mkdocs-snippets.ts
Normal file
117
admin/src/components/docs/mkdocs-snippets.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import type { editor as monacoEditor } from 'monaco-editor';
|
||||
|
||||
export interface MkDocsSnippet {
|
||||
id: string;
|
||||
label: string;
|
||||
group: 'formatting' | 'heading' | 'admonition' | 'code' | 'insert';
|
||||
type: 'wrap' | 'block' | 'insert';
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
template?: string;
|
||||
keybinding?: 'ctrl+b' | 'ctrl+i';
|
||||
}
|
||||
|
||||
export const SNIPPETS: MkDocsSnippet[] = [
|
||||
// Formatting
|
||||
{ id: 'bold', label: 'Bold', group: 'formatting', type: 'wrap', prefix: '**', suffix: '**', keybinding: 'ctrl+b' },
|
||||
{ id: 'italic', label: 'Italic', group: 'formatting', type: 'wrap', prefix: '*', suffix: '*', keybinding: 'ctrl+i' },
|
||||
{ id: 'strikethrough', label: 'Strikethrough', group: 'formatting', type: 'wrap', prefix: '~~', suffix: '~~' },
|
||||
{ id: 'highlight', label: 'Highlight', group: 'formatting', type: 'wrap', prefix: '==', suffix: '==' },
|
||||
{ id: 'inline-code', label: 'Inline Code', group: 'formatting', type: 'wrap', prefix: '`', suffix: '`' },
|
||||
{ id: 'kbd', label: 'Keyboard Key', group: 'formatting', type: 'wrap', prefix: '++', suffix: '++' },
|
||||
// Headings
|
||||
{ id: 'h1', label: 'Heading 1', group: 'heading', type: 'block', template: '# $CURSOR' },
|
||||
{ id: 'h2', label: 'Heading 2', group: 'heading', type: 'block', template: '## $CURSOR' },
|
||||
{ id: 'h3', label: 'Heading 3', group: 'heading', type: 'block', template: '### $CURSOR' },
|
||||
{ id: 'h4', label: 'Heading 4', group: 'heading', type: 'block', template: '#### $CURSOR' },
|
||||
// Admonitions
|
||||
...(['note', 'warning', 'tip', 'danger', 'info', 'success', 'question', 'abstract', 'example', 'bug', 'quote'] as const).map((t) => ({
|
||||
id: `admonition-${t}`,
|
||||
label: `${t.charAt(0).toUpperCase() + t.slice(1)}`,
|
||||
group: 'admonition' as const,
|
||||
type: 'block' as const,
|
||||
template: `!!! ${t} "Title"\n Content here`,
|
||||
})),
|
||||
{ id: 'admonition-collapsible-open', label: 'Collapsible (open)', group: 'admonition', type: 'block', template: '???+ note "Title"\n Content here' },
|
||||
{ id: 'admonition-collapsible-closed', label: 'Collapsible (closed)', group: 'admonition', type: 'block', template: '??? note "Title"\n Content here' },
|
||||
// Code
|
||||
{ id: 'code-block', label: 'Code Block', group: 'code', type: 'block', template: '```python\n$CURSOR\n```' },
|
||||
{ id: 'code-annotated', label: 'Annotated Code', group: 'code', type: 'block', template: '```python\ncode # (1)!\n```\n\n1. Annotation' },
|
||||
{ id: 'mermaid', label: 'Mermaid Diagram', group: 'code', type: 'block', template: '```mermaid\ngraph LR\n A --> B\n```' },
|
||||
// Inserts (standard markdown — no auth required)
|
||||
{ id: 'link', label: 'Link', group: 'insert', type: 'wrap', prefix: '[', suffix: '](url)' },
|
||||
{ id: 'image', label: 'Image', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'button', label: 'Button', group: 'insert', type: 'insert', template: '[Text](url){ .md-button }' },
|
||||
{ id: 'button-primary', label: 'Primary Button', group: 'insert', type: 'insert', template: '[Text](url){ .md-button .md-button--primary }' },
|
||||
{ id: 'icon', label: 'Material Icon', group: 'insert', type: 'insert', template: ':material-icon-name:' },
|
||||
{ id: 'table', label: 'Table', group: 'insert', type: 'insert', template: '| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |' },
|
||||
{ id: 'tasklist', label: 'Task List', group: 'insert', type: 'insert', template: '- [ ] Task 1\n- [ ] Task 2\n- [x] Done' },
|
||||
{ id: 'tabs', label: 'Tabs', group: 'insert', type: 'insert', template: '=== "Tab 1"\n\n Content\n\n=== "Tab 2"\n\n Content' },
|
||||
{ id: 'math-block', label: 'Math Block', group: 'insert', type: 'block', template: '$$\n$CURSOR\n$$' },
|
||||
{ id: 'footnote', label: 'Footnote', group: 'insert', type: 'insert', template: '[^1]\n\n[^1]: Text' },
|
||||
{ id: 'def-list', label: 'Definition List', group: 'insert', type: 'insert', template: 'Term\n: Definition' },
|
||||
{ id: 'hr', label: 'Horizontal Rule', group: 'insert', type: 'insert', template: '---' },
|
||||
// Platform-specific inserts (require auth — handled by DocsPage modals)
|
||||
{ id: 'video-card', label: 'Video Card', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'photo-insert', label: 'Photo', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'donate-button', label: 'Donate Button', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'pricing-table', label: 'Pricing Table', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'product-card', label: 'Product Card', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'ad-insert', label: 'Ad', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'scheduling-poll', label: 'Scheduling Poll', group: 'insert', type: 'insert', template: '' },
|
||||
{ id: 'wiki-link', label: 'Wiki Link [[]]', group: 'insert', type: 'insert', template: '' },
|
||||
];
|
||||
|
||||
/** IDs of insert snippets that require authenticated API access (modal-based) */
|
||||
export const PLATFORM_INSERT_IDS = new Set([
|
||||
'video-card', 'photo-insert', 'donate-button', 'pricing-table',
|
||||
'product-card', 'ad-insert', 'scheduling-poll', 'wiki-link',
|
||||
]);
|
||||
|
||||
export function applySnippet(
|
||||
ed: monacoEditor.IStandaloneCodeEditor,
|
||||
snippet: MkDocsSnippet,
|
||||
monaco: typeof import('monaco-editor'),
|
||||
) {
|
||||
const sel = ed.getSelection();
|
||||
const model = ed.getModel();
|
||||
if (!sel || !model) return;
|
||||
|
||||
const selectedText = model.getValueInRange(sel);
|
||||
|
||||
if (snippet.type === 'wrap' && snippet.prefix != null && snippet.suffix != null) {
|
||||
if (selectedText) {
|
||||
ed.executeEdits('mkdocs-snippet', [{
|
||||
range: sel,
|
||||
text: snippet.prefix + selectedText + snippet.suffix,
|
||||
}]);
|
||||
} else {
|
||||
const placeholder = 'text';
|
||||
ed.executeEdits('mkdocs-snippet', [{
|
||||
range: sel,
|
||||
text: snippet.prefix + placeholder + snippet.suffix,
|
||||
}]);
|
||||
const pos = sel.getStartPosition();
|
||||
const startCol = pos.column + snippet.prefix.length;
|
||||
ed.setSelection(new monaco.Selection(pos.lineNumber, startCol, pos.lineNumber, startCol + placeholder.length));
|
||||
}
|
||||
} else if (snippet.type === 'block' && snippet.template) {
|
||||
const pos = sel.getStartPosition();
|
||||
let text = snippet.template.replace('$CURSOR', selectedText);
|
||||
const lineContent = model.getLineContent(pos.lineNumber);
|
||||
if (pos.column > 1 && lineContent.substring(0, pos.column - 1).trim().length > 0) {
|
||||
text = '\n' + text;
|
||||
}
|
||||
ed.executeEdits('mkdocs-snippet', [{
|
||||
range: sel,
|
||||
text,
|
||||
}]);
|
||||
} else if (snippet.type === 'insert' && snippet.template) {
|
||||
ed.executeEdits('mkdocs-snippet', [{
|
||||
range: sel,
|
||||
text: snippet.template,
|
||||
}]);
|
||||
}
|
||||
|
||||
ed.focus();
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Button, Tabs, Space, message, Table, Tag, Typography, Grid } from 'antd';
|
||||
import { Drawer, Form, Input, Button, Tabs, Space, message, Table, Tag, Typography, Grid } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
@ -118,19 +118,19 @@ export default function TestEmailModal({ open, template, onClose, onSuccess }: T
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={`Send Test Email: ${template.name}`}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onClose={onClose}
|
||||
width={isMobile ? '95vw' : 900}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="send" type="primary" loading={sending} onClick={handleSend}>
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" loading={sending} onClick={handleSend}>
|
||||
Send Test Email
|
||||
</Button>,
|
||||
]}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<Form form={form} layout="vertical">
|
||||
@ -244,6 +244,6 @@ export default function TestEmailModal({ open, template, onClose, onSuccess }: T
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Checkbox, Button, Input, Space, Typography, Spin, Divider, message, theme } from 'antd';
|
||||
import { Drawer, Checkbox, Button, Input, Space, Typography, Spin, Divider, message, theme } from 'antd';
|
||||
import { PlusOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import { mediaPublicApi } from '@/lib/media-public-api';
|
||||
@ -152,13 +152,19 @@ export default function AddToPlaylistModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Add to Playlist"
|
||||
open={open}
|
||||
onOk={handleSave}
|
||||
onCancel={onClose}
|
||||
confirmLoading={saving}
|
||||
okText="Save"
|
||||
onClose={onClose}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleSave} loading={saving}>
|
||||
Save
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 32 }}>
|
||||
@ -238,6 +244,6 @@ export default function AddToPlaylistModal({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,17 +1,8 @@
|
||||
import { Card, Tag, Badge } from 'antd';
|
||||
import { FolderOpenOutlined, GlobalOutlined, PictureOutlined } from '@ant-design/icons';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
import { useSignedMediaUrl } from '@/lib/media-url';
|
||||
import type { PhotoAlbum } from '@/types/media';
|
||||
|
||||
/** Append JWT access token as query param for <img> src URLs */
|
||||
function getAuthenticatedUrl(url: string): string {
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}token=${accessToken}`;
|
||||
}
|
||||
|
||||
interface AlbumCardProps {
|
||||
album: PhotoAlbum;
|
||||
onClick?: (album: PhotoAlbum) => void;
|
||||
@ -19,6 +10,7 @@ interface AlbumCardProps {
|
||||
|
||||
export default function AlbumCard({ album, onClick }: AlbumCardProps) {
|
||||
const coverUrl = album.coverThumbnailUrl;
|
||||
const signedCoverUrl = useSignedMediaUrl(coverUrl);
|
||||
|
||||
return (
|
||||
<Card
|
||||
@ -35,9 +27,9 @@ export default function AlbumCard({ album, onClick }: AlbumCardProps) {
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{coverUrl ? (
|
||||
{coverUrl && signedCoverUrl ? (
|
||||
<img
|
||||
src={getAuthenticatedUrl(coverUrl)}
|
||||
src={signedCoverUrl}
|
||||
alt={album.title}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@ -7,16 +7,26 @@ import {
|
||||
GlobalOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
import { useSignedMediaUrl } from '@/lib/media-url';
|
||||
import type { PhotoAlbum, PhotoAlbumItem } from '@/types/media';
|
||||
|
||||
/** Append JWT access token as query param for <img> src URLs */
|
||||
function getAuthenticatedUrl(url: string): string {
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}token=${accessToken}`;
|
||||
function PhotoThumbnail({ url, alt }: { url: string; alt: string }) {
|
||||
const signed = useSignedMediaUrl(url);
|
||||
if (!signed) {
|
||||
return (
|
||||
<div style={{ width: 60, height: 45, background: '#1a1a1a', borderRadius: 4 }} aria-label={alt} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Image
|
||||
src={signed}
|
||||
width={60}
|
||||
height={45}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
preview={false}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AlbumDetailDrawerProps {
|
||||
@ -200,13 +210,7 @@ export default function AlbumDetailDrawer({ albumId, open, onClose, onRefresh }:
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
photo.thumbnailUrl ? (
|
||||
<Image
|
||||
src={getAuthenticatedUrl(photo.thumbnailUrl)}
|
||||
width={60}
|
||||
height={45}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
preview={false}
|
||||
/>
|
||||
<PhotoThumbnail url={photo.thumbnailUrl} alt={photo.title || photo.originalFilename || ''} />
|
||||
) : (
|
||||
<div style={{ width: 60, height: 45, background: '#1a1a1a', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<PictureOutlined style={{ color: '#555' }} />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Modal, Select, message } from 'antd';
|
||||
import { Drawer, Select, Button, message } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import { getErrorMessage } from '@/utils/getErrorMessage';
|
||||
@ -37,13 +37,19 @@ export default function BulkAccessLevelModal({ open, videoIds, onClose, onSucces
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={`Set Access Level (${videoIds.length} video${videoIds.length !== 1 ? 's' : ''})`}
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={onClose}
|
||||
confirmLoading={loading}
|
||||
okText="Apply"
|
||||
onClose={onClose}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleOk} loading={loading}>
|
||||
Apply
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Select
|
||||
@ -54,6 +60,6 @@ export default function BulkAccessLevelModal({ open, videoIds, onClose, onSucces
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Select, Button, Input, Space, Typography, Spin, Divider, message } from 'antd';
|
||||
import { Drawer, Select, Button, Input, Space, Typography, Spin, Divider, message } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import type { PlaylistSummary } from '@/types/media';
|
||||
@ -113,14 +113,19 @@ export default function BulkAddToPlaylistModal({
|
||||
const selectedPlaylist = playlists.find((p) => p.id === selectedPlaylistId);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={`Add ${videoIds.length} video${videoIds.length > 1 ? 's' : ''} to playlist`}
|
||||
open={open}
|
||||
onOk={handleAdd}
|
||||
onCancel={onClose}
|
||||
confirmLoading={saving}
|
||||
okText="Add"
|
||||
okButtonProps={{ disabled: !selectedPlaylistId }}
|
||||
onClose={onClose}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleAdd} loading={saving} disabled={!selectedPlaylistId}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 32 }}>
|
||||
@ -184,6 +189,6 @@ export default function BulkAddToPlaylistModal({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Modal, Form, Input, message } from 'antd';
|
||||
import { Drawer, Form, Input, Button, message } from 'antd';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import axios from 'axios';
|
||||
|
||||
@ -41,13 +41,19 @@ export default function CreateAlbumModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Create Album"
|
||||
open={open}
|
||||
onOk={handleCreate}
|
||||
onCancel={onClose}
|
||||
confirmLoading={loading}
|
||||
okText="Create"
|
||||
onClose={onClose}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleCreate} loading={loading}>
|
||||
Create
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="title" label="Title" rules={[{ required: true, message: 'Title is required' }]}>
|
||||
@ -62,6 +68,6 @@ export default function CreateAlbumModal({
|
||||
{selectedPhotoIds.length} photo{selectedPhotoIds.length > 1 ? 's' : ''} will be added to this album
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Drawer, Form, Input, Switch, Button, Space, message } from 'antd';
|
||||
import { Drawer, Form, Input, Switch, Button, message } from 'antd';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import axios from 'axios';
|
||||
|
||||
@ -53,17 +53,12 @@ export default function CreatePlaylistModal({
|
||||
}}
|
||||
placement="right"
|
||||
width={420}
|
||||
style={{ top: 64 }}
|
||||
styles={{ body: { paddingTop: 24 } }}
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => { form.resetFields(); onClose(); }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={loading}>
|
||||
Create
|
||||
</Button>
|
||||
</Space>
|
||||
<Button type="primary" onClick={handleSubmit} loading={loading}>
|
||||
Create
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
|
||||
@ -130,7 +130,8 @@ export default function EditPlaylistModal({
|
||||
}}
|
||||
placement="right"
|
||||
width={isMobile ? '100%' : 520}
|
||||
style={{ top: 64 }}
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
loading={loading}
|
||||
>
|
||||
<Tabs
|
||||
|
||||
@ -148,15 +148,9 @@ export default function FetchVideosDrawer({ open, onClose, onSuccess }: FetchVid
|
||||
|
||||
const connectSSE = async () => {
|
||||
try {
|
||||
// Get auth token from localStorage
|
||||
const stored = localStorage.getItem('auth-storage');
|
||||
let token = '';
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
token = parsed?.state?.accessToken || '';
|
||||
} catch {}
|
||||
}
|
||||
// Get auth token from in-memory store (not localStorage)
|
||||
const { useAuthStore } = await import('@/stores/auth.store');
|
||||
const token = useAuthStore.getState().accessToken || '';
|
||||
|
||||
const response = await fetch(baseUrl, {
|
||||
headers: {
|
||||
|
||||
@ -8,18 +8,9 @@ import {
|
||||
FolderOutlined,
|
||||
PictureOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
import { useSignedMediaUrl } from '@/lib/media-url';
|
||||
import type { Photo } from '@/types/media';
|
||||
|
||||
/** Append JWT access token as query param for <img> src URLs */
|
||||
function getAuthenticatedUrl(url: string): string {
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}token=${accessToken}`;
|
||||
}
|
||||
|
||||
interface PhotoCardProps {
|
||||
photo: Photo;
|
||||
selected?: boolean;
|
||||
@ -50,6 +41,7 @@ export default function PhotoCard({
|
||||
onTogglePublish,
|
||||
}: PhotoCardProps) {
|
||||
const thumbnailUrl = photo.thumbnailUrl;
|
||||
const signedThumbnailUrl = useSignedMediaUrl(thumbnailUrl);
|
||||
|
||||
const hoverActions = (
|
||||
<div
|
||||
@ -112,9 +104,9 @@ export default function PhotoCard({
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{thumbnailUrl ? (
|
||||
{thumbnailUrl && signedThumbnailUrl ? (
|
||||
<img
|
||||
src={getAuthenticatedUrl(thumbnailUrl)}
|
||||
src={signedThumbnailUrl}
|
||||
alt={photo.title || photo.filename}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@ -1,17 +1,8 @@
|
||||
import { Modal, Descriptions, Tag, Grid } from 'antd';
|
||||
import { CameraOutlined } from '@ant-design/icons';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
import { useSignedMediaUrl } from '@/lib/media-url';
|
||||
import type { Photo } from '@/types/media';
|
||||
|
||||
/** Append JWT access token as query param for <img> src URLs */
|
||||
function getAuthenticatedUrl(url: string): string {
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}token=${accessToken}`;
|
||||
}
|
||||
|
||||
interface PhotoViewerModalProps {
|
||||
photo: Photo | null;
|
||||
open: boolean;
|
||||
@ -22,9 +13,10 @@ export default function PhotoViewerModal({ photo, open, onClose }: PhotoViewerMo
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.md;
|
||||
|
||||
if (!photo) return null;
|
||||
const adminImageUrl = photo ? `/media/photos/${photo.id}/image?size=large` : null;
|
||||
const signedImageUrl = useSignedMediaUrl(adminImageUrl);
|
||||
|
||||
const adminImageUrl = `/media/photos/${photo.id}/image?size=large`;
|
||||
if (!photo) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -48,7 +40,7 @@ export default function PhotoViewerModal({ photo, open, onClose }: PhotoViewerMo
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getAuthenticatedUrl(adminImageUrl)}
|
||||
src={signedImageUrl}
|
||||
alt={photo.title || photo.filename}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Modal, DatePicker, Select, Space, Alert, Switch, message, Grid } from 'antd';
|
||||
import { Drawer, DatePicker, Select, Space, Alert, Switch, Button, message, Grid } from 'antd';
|
||||
import { ClockCircleOutlined } from '@ant-design/icons';
|
||||
import { useState, useEffect } from 'react';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
@ -152,7 +152,7 @@ export default function SchedulePublishModal({
|
||||
const serverTime = publishAt?.utc().format('YYYY-MM-DD HH:mm:ss UTC');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
@ -160,15 +160,16 @@ export default function SchedulePublishModal({
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onOk={handleSchedule}
|
||||
okText={publishNow ? 'Publish Now' : 'Schedule'}
|
||||
confirmLoading={loading}
|
||||
onClose={onClose}
|
||||
width={isMobile ? '95vw' : 600}
|
||||
style={{ top: 20 }}
|
||||
styles={{
|
||||
body: { maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }
|
||||
}}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleSchedule} loading={loading}>
|
||||
{publishNow ? 'Publish Now' : 'Schedule'}
|
||||
</Button>
|
||||
}
|
||||
aria-label="Schedule video publishing"
|
||||
>
|
||||
{video && (
|
||||
@ -302,6 +303,6 @@ export default function SchedulePublishModal({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,19 +2,10 @@ import { Card, Checkbox, Tag, Spin } from 'antd';
|
||||
import { ClockCircleOutlined, CheckCircleOutlined, ThunderboltFilled, LockOutlined, CrownOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
import type { Video } from '@/types/media';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
import { useSignedMediaUrl } from '@/lib/media-url';
|
||||
import VideoActions from './VideoActions';
|
||||
import ScheduleBadge from './ScheduleBadge';
|
||||
|
||||
/** Append JWT access token as query param for <img>/<video> src URLs */
|
||||
function getAuthenticatedUrl(url: string): string {
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}token=${accessToken}`;
|
||||
}
|
||||
|
||||
interface VideoCardProps {
|
||||
video: Video;
|
||||
selected: boolean;
|
||||
@ -48,6 +39,7 @@ export default function VideoCard({
|
||||
}: VideoCardProps) {
|
||||
const [thumbnailLoading, setThumbnailLoading] = useState(true);
|
||||
const [thumbnailError, setThumbnailError] = useState(false);
|
||||
const signedThumbnailUrl = useSignedMediaUrl(video.thumbnailUrl);
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
@ -76,10 +68,10 @@ export default function VideoCard({
|
||||
}}
|
||||
>
|
||||
{/* Thumbnail image or fallback */}
|
||||
{video.thumbnailUrl && !thumbnailError ? (
|
||||
{video.thumbnailUrl && !thumbnailError && signedThumbnailUrl ? (
|
||||
<>
|
||||
<img
|
||||
src={getAuthenticatedUrl(video.thumbnailUrl)}
|
||||
src={signedThumbnailUrl}
|
||||
alt={video.title}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@ -2,6 +2,8 @@ import { useState, useEffect, useRef, useImperativeHandle, forwardRef } from 're
|
||||
import { Alert, Spin } from 'antd';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
import { signedMediaUrl } from '@/lib/media-url';
|
||||
import { useHls } from '@/lib/use-hls';
|
||||
|
||||
export interface VideoMetadata {
|
||||
id: number;
|
||||
@ -14,6 +16,8 @@ export interface VideoMetadata {
|
||||
quality: string | null;
|
||||
streamUrl: string;
|
||||
thumbnailUrl: string | null;
|
||||
hlsStatus?: 'PENDING' | 'PROCESSING' | 'READY' | 'FAILED' | 'SKIPPED' | null;
|
||||
hlsManifestUrl?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@ -67,6 +71,13 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Attach HLS when manifest is ready. Must be called unconditionally on
|
||||
// every render (rules of hooks) — even before the loading/error early
|
||||
// returns. The hook is a no-op when manifestUrl is null.
|
||||
const hlsManifestUrl = metadata?.hlsStatus === 'READY' ? metadata.hlsManifestUrl ?? null : null;
|
||||
const { error: hlsError } = useHls(videoRef, hlsManifestUrl);
|
||||
const useMp4Src = !hlsManifestUrl || !!hlsError;
|
||||
|
||||
// Expose control methods via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
play: () => {
|
||||
@ -122,15 +133,6 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||
fetchMetadata();
|
||||
}, [videoId]);
|
||||
|
||||
const appendToken = (url: string): string => {
|
||||
if (!isAdmin) return url;
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
return `${url}${sep}token=${accessToken}`;
|
||||
};
|
||||
|
||||
const fetchMetadata = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -157,10 +159,13 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// For admin, append token to stream/thumbnail URLs so <video>/<img> can access them
|
||||
// For admin previews of unpublished media, sign stream/thumbnail URLs
|
||||
// (the legacy ?token=<JWT> path was removed 2026-04-12). The HLS
|
||||
// manifest URL is already signed server-side by the metadata route, so
|
||||
// we leave it untouched.
|
||||
if (isAdmin) {
|
||||
if (data.streamUrl) data.streamUrl = appendToken(data.streamUrl);
|
||||
if (data.thumbnailUrl) data.thumbnailUrl = appendToken(data.thumbnailUrl);
|
||||
if (data.streamUrl) data.streamUrl = await signedMediaUrl(data.streamUrl);
|
||||
if (data.thumbnailUrl) data.thumbnailUrl = await signedMediaUrl(data.thumbnailUrl);
|
||||
}
|
||||
|
||||
setMetadata(data);
|
||||
@ -219,6 +224,10 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||
? (metadata.height / metadata.width) * 100
|
||||
: 56.25; // Default to 16:9
|
||||
|
||||
// (HLS attachment + MP4 fallback flag are computed at the top of the
|
||||
// component, before the loading/error early returns, to satisfy the rules
|
||||
// of hooks. See useMp4Src above.)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -231,7 +240,7 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={metadata.streamUrl}
|
||||
src={useMp4Src ? metadata.streamUrl : undefined}
|
||||
poster={poster || metadata.thumbnailUrl || undefined}
|
||||
autoPlay={autoplay}
|
||||
controls={controls}
|
||||
|
||||
@ -2,16 +2,8 @@ import { Modal } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { Video } from '@/types/media';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
import { getAuthCallbacks } from '@/lib/api';
|
||||
|
||||
/** Append JWT access token as query param for <video> src URLs */
|
||||
function getAuthenticatedUrl(url: string): string {
|
||||
const { getAccessToken } = getAuthCallbacks();
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) return url;
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}token=${accessToken}`;
|
||||
}
|
||||
import { useSignedMediaUrl } from '@/lib/media-url';
|
||||
import { useHls } from '@/lib/use-hls';
|
||||
|
||||
interface VideoViewerModalProps {
|
||||
video: Video | null;
|
||||
@ -24,6 +16,17 @@ export default function VideoViewerModal({ video, open, onClose }: VideoViewerMo
|
||||
const [viewId, setViewId] = useState<number | null>(null);
|
||||
const heartbeatInterval = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const lastWatchTime = useRef<number>(0);
|
||||
const streamUrl = useSignedMediaUrl(video ? `/media/videos/${video.id}/stream` : null);
|
||||
// Sign the HLS manifest URL too so admin previews of unpublished videos
|
||||
// can play HLS. The hook is a no-op for nulls.
|
||||
const hlsManifestUrl = useSignedMediaUrl(
|
||||
video && video.hlsStatus === 'READY'
|
||||
? `/media/videos/${video.id}/hls/master.m3u8`
|
||||
: null,
|
||||
);
|
||||
const { error: hlsError } = useHls(videoRef, hlsManifestUrl ?? null);
|
||||
// Fall back to MP4 src when HLS isn't ready or hls.js fatal-errored.
|
||||
const useMp4Src = !hlsManifestUrl || !!hlsError;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && video) {
|
||||
@ -175,7 +178,7 @@ export default function VideoViewerModal({ video, open, onClose }: VideoViewerMo
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={getAuthenticatedUrl(`/media/videos/${video.id}/stream`)}
|
||||
src={useMp4Src ? streamUrl : undefined}
|
||||
controls
|
||||
autoPlay
|
||||
style={{
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Radio, InputNumber, Spin, Typography, Space, theme, Grid } from 'antd';
|
||||
import { Drawer, Radio, InputNumber, Spin, Typography, Space, Button, theme, Grid } from 'antd';
|
||||
import { HeartOutlined, DollarOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
|
||||
@ -80,14 +80,23 @@ export function DonateInsertModal({ open, onClose, onInsert }: DonateInsertModal
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Insert Donate Block"
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onOk={handleOk}
|
||||
okText="Insert"
|
||||
okButtonProps={{ disabled: variant === 'set-amount' && (!amount || amount <= 0) }}
|
||||
onClose={onClose}
|
||||
width={isMobile ? '95vw' : 520}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleOk}
|
||||
disabled={variant === 'set-amount' && (!amount || amount <= 0)}
|
||||
>
|
||||
Insert
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 16 }}>
|
||||
Choose a donation block style to insert into your document.
|
||||
@ -176,6 +185,6 @@ export function DonateInsertModal({ open, onClose, onInsert }: DonateInsertModal
|
||||
</div>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Card, Row, Col, Typography, Tag, Spin, Empty, Input, Grid } from 'antd';
|
||||
import { Drawer, Card, Row, Col, Typography, Tag, Spin, Empty, Input, Button, Grid } from 'antd';
|
||||
import { ShoppingCartOutlined, SearchOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import type { Product, ProductType } from '@/types/api';
|
||||
@ -35,8 +35,8 @@ export function ProductInsertModal({ open, onClose, onInsert }: ProductInsertMod
|
||||
if (open && products.length === 0) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
axios.get('/api/payments/products')
|
||||
.then(({ data }) => setProducts(data))
|
||||
axios.get('/api/payments/products', { params: { limit: 50 } })
|
||||
.then(({ data }) => setProducts(data.products))
|
||||
.catch(() => setError('Failed to load products'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
@ -60,14 +60,19 @@ export function ProductInsertModal({ open, onClose, onInsert }: ProductInsertMod
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Insert Product Card"
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onOk={handleOk}
|
||||
okText="Insert"
|
||||
okButtonProps={{ disabled: !selectedId }}
|
||||
onClose={onClose}
|
||||
width={isMobile ? '95vw' : 640}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleOk} disabled={!selectedId}>
|
||||
Insert
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
Select a product to embed as an inline purchase card.
|
||||
@ -148,6 +153,6 @@ export function ProductInsertModal({ open, onClose, onInsert }: ProductInsertMod
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,9 +21,9 @@ export function ProductWidget({ productSlug, buttonText = 'Buy Now' }: ProductWi
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/payments/products')
|
||||
axios.get('/api/payments/products', { params: { limit: 50 } })
|
||||
.then(({ data }) => {
|
||||
const found = (data as Product[]).find(p => p.slug === productSlug);
|
||||
const found = (data.products as Product[]).find(p => p.slug === productSlug);
|
||||
if (found) {
|
||||
setProduct(found);
|
||||
} else {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Modal, Form, Input, Select, Switch, Button, Typography, message, Space } from 'antd';
|
||||
import { Drawer, Form, Input, Select, Switch, Button, Typography, message, Space } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { api } from '@/lib/api';
|
||||
@ -76,13 +76,20 @@ export default function CreateUserFromContactModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Create User Account"
|
||||
open={open}
|
||||
onCancel={() => { form.resetFields(); onClose(); }}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
onClose={() => { form.resetFields(); onClose(); }}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Button type="primary" onClick={() => form.submit()} loading={submitting}>
|
||||
Create Account
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>Contact</Typography.Text>
|
||||
@ -123,17 +130,12 @@ export default function CreateUserFromContactModal({
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
Create Account
|
||||
</Button>
|
||||
<Button onClick={() => { form.resetFields(); onClose(); }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Space>
|
||||
<Form.Item style={{ marginBottom: 0, display: 'none' }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Create Account
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Drawer,
|
||||
Select,
|
||||
Typography,
|
||||
Radio,
|
||||
@ -155,7 +155,7 @@ export default function MergeContactModal({ open, targetContact, onClose, onMerg
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title={
|
||||
<Space>
|
||||
<SwapOutlined />
|
||||
@ -163,14 +163,13 @@ export default function MergeContactModal({ open, targetContact, onClose, onMerg
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
onClose={handleClose}
|
||||
width={700}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button
|
||||
key="merge"
|
||||
type="primary"
|
||||
danger
|
||||
onClick={handleMerge}
|
||||
@ -178,8 +177,8 @@ export default function MergeContactModal({ open, targetContact, onClose, onMerg
|
||||
disabled={!sourcePerson}
|
||||
>
|
||||
Confirm Merge
|
||||
</Button>,
|
||||
]}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* Search for source person */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
@ -301,6 +300,6 @@ export default function MergeContactModal({ open, targetContact, onClose, onMerg
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ const roleColors: Record<UserRole, string> = {
|
||||
EVENTS_ADMIN: 'cyan',
|
||||
SOCIAL_ADMIN: 'magenta',
|
||||
POLLS_ADMIN: 'geekblue',
|
||||
ANALYTICS_ADMIN: 'processing',
|
||||
USER: 'blue',
|
||||
TEMP: 'default',
|
||||
};
|
||||
|
||||
@ -57,7 +57,7 @@ export default function VideoCallModal({ open, onClose, personName }: VideoCallM
|
||||
const { data } = await api.post<{ token: string; jitsiRoom: string; domain: string }>(
|
||||
`/jitsi/meetings/${meeting.slug}/token`,
|
||||
);
|
||||
window.open(`https://${data.domain}/${data.jitsiRoom}?jwt=${data.token}`, '_blank');
|
||||
window.open(`https://${data.domain}/${data.jitsiRoom}?jwt=${data.token}`, '_blank', 'noopener,noreferrer');
|
||||
} catch (err: unknown) {
|
||||
message.error(getErrorMessage(err, 'Failed to get moderator token'));
|
||||
} finally {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Tabs, Table, Button, Input, Tag, Form, Select, DatePicker, TimePicker, Space, Spin, Typography, message } from 'antd';
|
||||
import { Drawer, Tabs, Table, Button, Input, Tag, Form, Select, DatePicker, TimePicker, Space, Spin, Typography, message } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { api } from '@/lib/api';
|
||||
import type { SchedulingPoll, PollsListResponse, SchedulingPollStatus } from '@/types/api';
|
||||
@ -130,12 +130,14 @@ export function PollInsertModal({ open, onCancel, onInsert }: PollInsertModalPro
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
onClose={onCancel}
|
||||
title="Insert Scheduling Poll"
|
||||
footer={null}
|
||||
width={700}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Tabs
|
||||
@ -232,6 +234,6 @@ export function PollInsertModal({ open, onCancel, onInsert }: PollInsertModalPro
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Input, Select, Typography, message, Space } from 'antd';
|
||||
import { Drawer, Input, Select, Typography, message, Space, Button } from 'antd';
|
||||
import { VideoCameraOutlined } from '@ant-design/icons';
|
||||
import { api } from '@/lib/api';
|
||||
import { mediaApi } from '@/lib/media-api';
|
||||
@ -100,14 +100,19 @@ export default function RecommendVideoModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Drawer
|
||||
title="Recommend a Video"
|
||||
open={open}
|
||||
onOk={handleSend}
|
||||
onCancel={onClose}
|
||||
okText="Send"
|
||||
confirmLoading={sending}
|
||||
okButtonProps={{ disabled: !selectedFriendId || !selectedVideoId }}
|
||||
onClose={onClose}
|
||||
width={480}
|
||||
placement="right"
|
||||
mask={false}
|
||||
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleSend} loading={sending} disabled={!selectedFriendId || !selectedVideoId}>
|
||||
Send
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<div>
|
||||
@ -165,6 +170,6 @@ export default function RecommendVideoModal({
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
import { Card, Progress, Typography, Tag, Alert } from 'antd';
|
||||
import { TrophyOutlined } from '@ant-design/icons';
|
||||
import type { DashboardActionCampaign } from './types';
|
||||
|
||||
interface ActionCampaignCardProps {
|
||||
campaign: DashboardActionCampaign;
|
||||
}
|
||||
|
||||
export default function ActionCampaignCard({ campaign }: ActionCampaignCardProps) {
|
||||
const percent = campaign.totalSteps > 0
|
||||
? Math.round((campaign.completedSteps / campaign.totalSteps) * 100)
|
||||
: 0;
|
||||
|
||||
const nextStep = [...campaign.steps]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.find((s) => !s.completed);
|
||||
|
||||
return (
|
||||
<Card
|
||||
styles={{
|
||||
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
|
||||
body: { padding: '16px 20px' },
|
||||
}}
|
||||
title={
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
<TrophyOutlined style={{ marginRight: 8, color: '#faad14' }} />
|
||||
Your Goal
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Typography.Title level={5} style={{ margin: '0 0 4px' }}>
|
||||
{campaign.title}
|
||||
</Typography.Title>
|
||||
{campaign.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13, display: 'block', marginBottom: 12 }}>
|
||||
{campaign.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{campaign.completedSteps} of {campaign.totalSteps} complete
|
||||
</Typography.Text>
|
||||
{campaign.rewardEarned && <Tag color="gold" style={{ margin: 0 }}>Reward Earned!</Tag>}
|
||||
</div>
|
||||
<Progress
|
||||
percent={percent}
|
||||
status={campaign.rewardEarned ? 'success' : 'active'}
|
||||
showInfo={false}
|
||||
strokeWidth={10}
|
||||
style={{ marginBottom: 0 }}
|
||||
/>
|
||||
|
||||
{campaign.rewardEarned && campaign.rewardText && (
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
message="You've earned your reward!"
|
||||
description={campaign.rewardText}
|
||||
style={{ marginTop: 12 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!campaign.rewardEarned && campaign.rewardText && (
|
||||
<Typography.Text style={{ fontSize: 12, display: 'block', marginTop: 10, color: 'rgba(255,255,255,0.5)' }}>
|
||||
<TrophyOutlined style={{ marginRight: 4 }} />
|
||||
{campaign.rewardText}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{nextStep && !campaign.rewardEarned && (
|
||||
<div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<Typography.Text style={{ fontSize: 13 }}>
|
||||
<strong>Next:</strong>{' '}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>{nextStep.label}</Typography.Text>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
304
admin/src/components/volunteer/dashboard/ActionStepsList.tsx
Normal file
304
admin/src/components/volunteer/dashboard/ActionStepsList.tsx
Normal file
@ -0,0 +1,304 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, Button, Typography, Tag, Space, App } from 'antd';
|
||||
import {
|
||||
VideoCameraOutlined,
|
||||
MailOutlined,
|
||||
FileTextOutlined,
|
||||
CalendarOutlined,
|
||||
EnvironmentOutlined,
|
||||
TeamOutlined,
|
||||
LinkOutlined,
|
||||
CheckSquareOutlined,
|
||||
CheckCircleFilled,
|
||||
RightOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '@/lib/api';
|
||||
import type { DashboardActionCampaign, DashboardActionStep, ActionStepKind } from './types';
|
||||
|
||||
interface ActionStepsListProps {
|
||||
campaign: DashboardActionCampaign;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const KIND_ICONS: Record<ActionStepKind, React.ReactNode> = {
|
||||
WATCH_VIDEO: <VideoCameraOutlined />,
|
||||
SUBMIT_INFLUENCE: <MailOutlined />,
|
||||
SIGN_PETITION: <FileTextOutlined />,
|
||||
RSVP_EVENT: <CalendarOutlined />,
|
||||
SIGNUP_SHIFT: <EnvironmentOutlined />,
|
||||
JOIN_CHALLENGE: <TeamOutlined />,
|
||||
VISIT_LINK: <LinkOutlined />,
|
||||
CUSTOM: <CheckSquareOutlined />,
|
||||
};
|
||||
|
||||
const KIND_LABELS: Record<ActionStepKind, string> = {
|
||||
WATCH_VIDEO: 'Watch',
|
||||
SUBMIT_INFLUENCE: 'Email',
|
||||
SIGN_PETITION: 'Sign',
|
||||
RSVP_EVENT: 'RSVP',
|
||||
SIGNUP_SHIFT: 'Shift',
|
||||
JOIN_CHALLENGE: 'Join',
|
||||
VISIT_LINK: 'Visit',
|
||||
CUSTOM: 'Action',
|
||||
};
|
||||
|
||||
function resolveStepLink(step: DashboardActionStep): { to: string; external: boolean } | null {
|
||||
if (step.targetUrl) {
|
||||
const external = /^https?:\/\//i.test(step.targetUrl);
|
||||
return { to: step.targetUrl, external };
|
||||
}
|
||||
if (!step.targetId) return null;
|
||||
switch (step.kind) {
|
||||
case 'WATCH_VIDEO':
|
||||
return { to: `/gallery/watch/${step.targetId}`, external: false };
|
||||
case 'SUBMIT_INFLUENCE':
|
||||
return { to: `/campaign/${step.targetId}`, external: false };
|
||||
case 'SIGN_PETITION':
|
||||
return { to: `/petition/${step.targetId}`, external: false };
|
||||
case 'RSVP_EVENT':
|
||||
return { to: `/event/${step.targetId}`, external: false };
|
||||
case 'SIGNUP_SHIFT':
|
||||
return { to: `/volunteer/shifts?shiftId=${step.targetId}`, external: false };
|
||||
case 'JOIN_CHALLENGE':
|
||||
return { to: `/volunteer/challenges/${step.targetId}`, external: false };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function HighlightedStep({
|
||||
step,
|
||||
onNavigate,
|
||||
onSelfReport,
|
||||
loading,
|
||||
}: {
|
||||
step: DashboardActionStep;
|
||||
onNavigate: (step: DashboardActionStep) => void;
|
||||
onSelfReport: (step: DashboardActionStep) => void;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
|
||||
const canNavigate = resolveStepLink(step) !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(52,152,219,0.25) 0%, rgba(41,128,185,0.15) 100%)',
|
||||
border: '1px solid rgba(52,152,219,0.3)',
|
||||
borderRadius: 8,
|
||||
padding: '16px 20px',
|
||||
margin: '0 0 2px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
|
||||
<ThunderboltOutlined style={{ fontSize: 12, color: '#3498db' }} />
|
||||
<Typography.Text strong style={{ fontSize: 12, color: '#3498db', textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
Next Up
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(52,152,219,0.25)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 16,
|
||||
color: '#3498db',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{KIND_ICONS[step.kind]}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography.Text strong style={{ fontSize: 15, display: 'block' }}>
|
||||
{step.label}
|
||||
</Typography.Text>
|
||||
{step.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 2 }}>
|
||||
{step.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
|
||||
{isSelfReport ? (
|
||||
<>
|
||||
{canNavigate && (
|
||||
<Button size="middle" onClick={() => onNavigate(step)} icon={<RightOutlined />}>
|
||||
Open
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
loading={loading}
|
||||
onClick={() => onSelfReport(step)}
|
||||
>
|
||||
Mark as done
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
icon={<RightOutlined />}
|
||||
onClick={() => onNavigate(step)}
|
||||
disabled={!canNavigate}
|
||||
>
|
||||
Take Action
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ActionStepsList({ campaign, onRefresh }: ActionStepsListProps) {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [completingStepId, setCompletingStepId] = useState<string | null>(null);
|
||||
|
||||
const handleSelfReport = async (step: DashboardActionStep) => {
|
||||
setCompletingStepId(step.id);
|
||||
try {
|
||||
await api.post(`/action-campaigns/${campaign.slug}/steps/${step.id}/complete`);
|
||||
message.success('Step marked as done');
|
||||
onRefresh();
|
||||
} catch {
|
||||
message.error('Failed to mark step as done');
|
||||
} finally {
|
||||
setCompletingStepId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = (step: DashboardActionStep) => {
|
||||
const link = resolveStepLink(step);
|
||||
if (!link) return;
|
||||
if (link.external) {
|
||||
window.open(link.to, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
navigate(link.to);
|
||||
}
|
||||
};
|
||||
|
||||
const sortedSteps = [...campaign.steps].sort((a, b) => a.order - b.order);
|
||||
const highlightedStep = sortedSteps.find((s) => !s.completed);
|
||||
const remainingSteps = sortedSteps.filter((s) => s.id !== highlightedStep?.id);
|
||||
|
||||
return (
|
||||
<Card
|
||||
styles={{
|
||||
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
title={
|
||||
<Typography.Text style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
{campaign.completedSteps} of {campaign.totalSteps} Actions
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
{highlightedStep && (
|
||||
<div style={{ padding: '12px 12px 0' }}>
|
||||
<HighlightedStep
|
||||
step={highlightedStep}
|
||||
onNavigate={handleNavigate}
|
||||
onSelfReport={handleSelfReport}
|
||||
loading={completingStepId === highlightedStep.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{remainingSteps.map((step, i) => {
|
||||
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
|
||||
const canNavigate = resolveStepLink(step) !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 20px',
|
||||
borderTop: (highlightedStep || i > 0) ? '1px solid rgba(255,255,255,0.04)' : undefined,
|
||||
opacity: step.completed ? 0.55 : 1,
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
background: step.completed ? '#52c41a' : 'rgba(255,255,255,0.06)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 12,
|
||||
flexShrink: 0,
|
||||
color: step.completed ? '#fff' : 'rgba(255,255,255,0.5)',
|
||||
}}
|
||||
>
|
||||
{step.completed ? <CheckCircleFilled /> : KIND_ICONS[step.kind]}
|
||||
</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<Typography.Text strong style={{ fontSize: 11, color: 'rgba(255,255,255,0.35)', display: 'block', lineHeight: 1 }}>
|
||||
{KIND_LABELS[step.kind]}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textDecoration: step.completed ? 'line-through' : 'none',
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
{step.completed ? (
|
||||
<Tag color="success" style={{ margin: 0, fontSize: 11 }}>Done</Tag>
|
||||
) : isSelfReport ? (
|
||||
<Space size={4}>
|
||||
{canNavigate && (
|
||||
<Button size="small" type="text" onClick={() => handleNavigate(step)}>Open</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={completingStepId === step.id}
|
||||
onClick={() => handleSelfReport(step)}
|
||||
>
|
||||
Mark done
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleNavigate(step)}
|
||||
disabled={!canNavigate}
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
Take Action
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
44
admin/src/components/volunteer/dashboard/ActivityCard.tsx
Normal file
44
admin/src/components/volunteer/dashboard/ActivityCard.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import { TrophyOutlined, StarFilled } from '@ant-design/icons';
|
||||
import type { DashboardPoints } from './types';
|
||||
|
||||
interface ActivityCardProps {
|
||||
points: DashboardPoints;
|
||||
}
|
||||
|
||||
export default function ActivityCard({ points }: ActivityCardProps) {
|
||||
return (
|
||||
<Card
|
||||
styles={{
|
||||
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
|
||||
body: { padding: '20px', textAlign: 'center' },
|
||||
}}
|
||||
title={
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>Activity</span>
|
||||
}
|
||||
extra={
|
||||
<Typography.Text type="warning" style={{ fontSize: 13, fontWeight: 600 }}>
|
||||
{points.total} pts
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'baseline', gap: 4, marginBottom: 4 }}>
|
||||
<TrophyOutlined style={{ fontSize: 24, color: '#faad14' }} />
|
||||
<Typography.Title level={2} style={{ margin: 0, color: '#faad14', fontWeight: 700 }}>
|
||||
{points.total}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
points earned
|
||||
</Typography.Text>
|
||||
{points.achievementCount > 0 && (
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<StarFilled style={{ color: '#f5222d', marginRight: 4 }} />
|
||||
<Typography.Text style={{ fontSize: 13 }}>
|
||||
{points.achievementCount} achievement{points.achievementCount !== 1 ? 's' : ''}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
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