diff --git a/.env.example b/.env.example index 909a6678..59aabe42 100644 --- a/.env.example +++ b/.env.example @@ -343,7 +343,7 @@ SMS_DEVICE_MONITOR_INTERVAL_MS=300000 # --- Monitoring (only used with --profile monitoring) --- PROMETHEUS_PORT=9090 GRAFANA_PORT=3005 -GRAFANA_ADMIN_PASSWORD=admin +GRAFANA_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS GRAFANA_ROOT_URL=http://localhost:3005 CADVISOR_PORT=8086 NODE_EXPORTER_PORT=9100 @@ -351,7 +351,7 @@ REDIS_EXPORTER_PORT=9121 ALERTMANAGER_PORT=9093 GOTIFY_PORT=8889 GOTIFY_ADMIN_USER=admin -GOTIFY_ADMIN_PASSWORD=admin +GOTIFY_ADMIN_PASSWORD=REQUIRED_STRONG_PASSWORD_CHANGE_THIS # --- Bunker Ops (Fleet Management) --- INSTANCE_LABEL= # Unique label for this instance (defaults to DOMAIN) diff --git a/CLAUDE.md b/CLAUDE.md index a884cbe4..6036e66e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,10 @@ Changemaker Lite is a self-hosted political campaign platform built with Docker - **JWT-based auth:** access tokens (15min) + refresh tokens (7 days, stored in DB) - **Password policy:** 12+ characters, uppercase, lowercase, digit (enforced at schema level) - **Initial admin:** Configured via `INITIAL_ADMIN_EMAIL` and `INITIAL_ADMIN_PASSWORD` env vars (auto-created during database seeding) -- **Roles:** `SUPER_ADMIN`, `INFLUENCE_ADMIN`, `MAP_ADMIN`, `USER`, `TEMP` -- **RBAC:** `requireRole(...roles)`, `requireNonTemp`, `authenticate` middleware +- **Roles:** `SUPER_ADMIN`, `INFLUENCE_ADMIN`, `MAP_ADMIN`, `BROADCAST_ADMIN`, `CONTENT_ADMIN`, `MEDIA_ADMIN`, `PAYMENTS_ADMIN`, `EVENTS_ADMIN`, `SOCIAL_ADMIN`, `USER`, `TEMP` +- **RBAC:** `requireRole(...roles)`, `requireNonTemp`, `authenticate` middleware; `SUPER_ADMIN` implicitly bypasses all role checks +- **Module-specific role groups** (defined in `api/src/utils/roles.ts`): `INFLUENCE_ROLES`, `MAP_ROLES`, `BROADCAST_ROLES`, `CONTENT_ROLES`, `MEDIA_ROLES`, `PAYMENTS_ROLES`, `EVENTS_ROLES`, `SOCIAL_ROLES`, `SYSTEM_ROLES`, `SCHEDULING_ROLES` +- **User management:** `SUPER_ADMIN` always; other admins need `permissions.canManageUsers: true` for write operations - **Security features:** - Refresh token rotation (atomic transaction) - User enumeration prevention (401 not 404) diff --git a/admin/package-lock.json b/admin/package-lock.json index 7a81311c..d07a00ce 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -11,6 +11,7 @@ "@ant-design/icons": "^5.6.0", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@dagrejs/dagre": "^2.0.4", + "@hocuspocus/provider": "^3.4.4", "@monaco-editor/react": "^4.7.0", "@types/d3-force": "^3.0.10", "@types/dompurify": "^3.2.0", @@ -42,7 +43,9 @@ "react-leaflet-cluster": "^4.0.0", "react-router-dom": "^7.1.1", "recharts": "^3.7.0", + "y-monaco": "^0.1.6", "yaml": "^2.8.2", + "yjs": "^13.6.29", "zustand": "^5.0.3" }, "devDependencies": { @@ -868,6 +871,29 @@ "node": ">=18" } }, + "node_modules/@hocuspocus/common": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-3.4.4.tgz", + "integrity": "sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA==", + "dependencies": { + "lib0": "^0.2.87" + } + }, + "node_modules/@hocuspocus/provider": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@hocuspocus/provider/-/provider-3.4.4.tgz", + "integrity": "sha512-KbsMAfdYcIJD8eMU/5QnpXcSOvIWAcCNI33FSRSaKCIpYBFtAwkYIwWnZJmPZ8a1BMAtqQc+uvy9+UQf7GHnGQ==", + "dependencies": { + "@hocuspocus/common": "^3.4.4", + "@lifeomic/attempt": "^3.0.2", + "lib0": "^0.2.87", + "ws": "^8.17.1" + }, + "peerDependencies": { + "y-protocols": "^1.0.6", + "yjs": "^13.6.8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -913,6 +939,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lifeomic/attempt": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@lifeomic/attempt/-/attempt-3.1.0.tgz", + "integrity": "sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==" + }, "node_modules/@monaco-editor/loader": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", @@ -2629,6 +2660,15 @@ "node": ">=12" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2690,6 +2730,26 @@ "leaflet": "^1.3.1" } }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3870,6 +3930,62 @@ } } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y-monaco": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz", + "integrity": "sha512-sYRywMmcylt+Nupl+11AvizD2am06ST8lkVbUXuaEmrtV6Tf+TD4rsEm6u9YGGowYue+Vfg1IJ97SUP2J+PVXg==", + "dependencies": { + "lib0": "^0.2.43" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "monaco-editor": ">=0.20.0", + "yjs": "^13.3.1" + } + }, + "node_modules/y-protocols": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", + "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", + "peer": true, + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3891,6 +4007,22 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yjs": { + "version": "13.6.29", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz", + "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zustand": { "version": "5.0.11", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", diff --git a/admin/package.json b/admin/package.json index e1050a65..10ce366a 100644 --- a/admin/package.json +++ b/admin/package.json @@ -12,6 +12,7 @@ "@ant-design/icons": "^5.6.0", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@dagrejs/dagre": "^2.0.4", + "@hocuspocus/provider": "^3.4.4", "@monaco-editor/react": "^4.7.0", "@types/d3-force": "^3.0.10", "@types/dompurify": "^3.2.0", @@ -43,7 +44,9 @@ "react-leaflet-cluster": "^4.0.0", "react-router-dom": "^7.1.1", "recharts": "^3.7.0", + "y-monaco": "^0.1.6", "yaml": "^2.8.2", + "yjs": "^13.6.29", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 26ef8ced..7fc6b99b 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -98,7 +98,19 @@ 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 { ADMIN_ROLES } from '@/types/api'; +import { + ADMIN_ROLES, + INFLUENCE_ROLES, + BROADCAST_ROLES, + CONTENT_ROLES, + MAP_ROLES, + SCHEDULING_ROLES, + MEDIA_ROLES, + PAYMENTS_ROLES, + EVENTS_ROLES, + SOCIAL_ROLES, + SYSTEM_ROLES, +} from '@/types/api'; import { isAdmin } from '@/utils/roles'; import QuickJoinPage from '@/pages/public/QuickJoinPage'; import VerifyEmailPage from '@/pages/VerifyEmailPage'; @@ -128,7 +140,6 @@ import SchedulingPollPage from '@/pages/public/SchedulingPollPage'; import PollsListPage from '@/pages/public/PollsListPage'; import JitsiAuthPage from '@/pages/JitsiAuthPage'; import SchedulingCalendarPage from '@/pages/SchedulingCalendarPage'; -import AdminCalendarPage from '@/pages/AdminCalendarPage'; import AdminCalendarViewPage from '@/pages/AdminCalendarViewPage'; import TicketedEventsPage from '@/pages/events/TicketedEventsPage'; import EventDetailPage from '@/pages/events/EventDetailPage'; @@ -381,7 +392,7 @@ export default function App() { + @@ -422,7 +433,7 @@ export default function App() { + @@ -432,7 +443,7 @@ export default function App() { + @@ -442,7 +453,7 @@ export default function App() { + @@ -452,7 +463,7 @@ export default function App() { + @@ -462,7 +473,7 @@ export default function App() { + @@ -472,7 +483,7 @@ export default function App() { + @@ -482,7 +493,7 @@ export default function App() { + } @@ -490,7 +501,7 @@ export default function App() { + } @@ -498,7 +509,7 @@ export default function App() { + } @@ -506,7 +517,7 @@ export default function App() { + } @@ -514,7 +525,7 @@ export default function App() { + } @@ -522,7 +533,7 @@ export default function App() { + } @@ -530,7 +541,7 @@ export default function App() { + } @@ -538,7 +549,7 @@ export default function App() { + } @@ -546,7 +557,7 @@ export default function App() { + } @@ -554,7 +565,7 @@ export default function App() { + } @@ -562,7 +573,7 @@ export default function App() { + } @@ -570,7 +581,7 @@ export default function App() { + } @@ -578,7 +589,7 @@ export default function App() { + } @@ -586,7 +597,7 @@ export default function App() { + } @@ -594,7 +605,7 @@ export default function App() { + } @@ -602,7 +613,7 @@ export default function App() { + } @@ -610,7 +621,7 @@ export default function App() { + } @@ -618,7 +629,7 @@ export default function App() { + } @@ -626,7 +637,7 @@ export default function App() { + } @@ -634,7 +645,7 @@ export default function App() { + } @@ -642,7 +653,7 @@ export default function App() { + } @@ -650,7 +661,7 @@ export default function App() { + } @@ -658,7 +669,7 @@ export default function App() { + } @@ -674,7 +685,7 @@ export default function App() { + } @@ -682,7 +693,7 @@ export default function App() { + } @@ -699,7 +710,7 @@ export default function App() { + } @@ -707,7 +718,7 @@ export default function App() { + } @@ -715,7 +726,7 @@ export default function App() { + } @@ -723,7 +734,7 @@ export default function App() { + } @@ -731,7 +742,7 @@ export default function App() { + } @@ -739,7 +750,7 @@ export default function App() { + } @@ -747,7 +758,7 @@ export default function App() { + } @@ -755,7 +766,7 @@ export default function App() { + } @@ -763,7 +774,7 @@ export default function App() { + } @@ -771,7 +782,7 @@ export default function App() { + } @@ -779,7 +790,7 @@ export default function App() { + } @@ -787,7 +798,7 @@ export default function App() { + } @@ -795,23 +806,15 @@ export default function App() { + } /> - - - - } - /> + } @@ -819,7 +822,7 @@ export default function App() { + @@ -829,7 +832,7 @@ export default function App() { + @@ -839,7 +842,7 @@ export default function App() { + } @@ -847,7 +850,7 @@ export default function App() { + } @@ -855,7 +858,7 @@ export default function App() { + } @@ -863,7 +866,7 @@ export default function App() { + } @@ -871,7 +874,7 @@ export default function App() { + } @@ -879,7 +882,7 @@ export default function App() { + } @@ -887,7 +890,7 @@ export default function App() { + } @@ -895,7 +898,7 @@ export default function App() { + } @@ -903,7 +906,7 @@ export default function App() { + } @@ -911,7 +914,7 @@ export default function App() { + } @@ -919,7 +922,7 @@ export default function App() { + } @@ -927,7 +930,7 @@ export default function App() { + } @@ -935,7 +938,7 @@ export default function App() { + } @@ -943,7 +946,7 @@ export default function App() { + } @@ -951,7 +954,7 @@ export default function App() { + } @@ -959,7 +962,7 @@ export default function App() { + } @@ -967,7 +970,7 @@ export default function App() { + } @@ -975,7 +978,7 @@ export default function App() { + } diff --git a/admin/src/components/AppLayout.tsx b/admin/src/components/AppLayout.tsx index a42bbc7a..06a58bad 100644 --- a/admin/src/components/AppLayout.tsx +++ b/admin/src/components/AppLayout.tsx @@ -60,7 +60,17 @@ import { api } from '@/lib/api'; import { useAuthStore } from '@/stores/auth.store'; import { useSettingsStore } from '@/stores/settings.store'; import { hasAnyRole } from '@/utils/roles'; -import type { PageHeaderConfig, AppOutletContext } from '@/types/api'; +import type { PageHeaderConfig, AppOutletContext, User } from '@/types/api'; +import { + INFLUENCE_ROLES, + BROADCAST_ROLES, + CONTENT_ROLES, + MAP_ROLES, + SCHEDULING_ROLES, + MEDIA_ROLES, + PAYMENTS_ROLES, + SOCIAL_ROLES, +} from '@/types/api'; import { buildHomeUrl, resolveNavUrl } from '@/lib/service-url'; import type { NavItem } from '@/types/api'; import { @@ -122,7 +132,10 @@ const ADMIN_ICON_OVERRIDES: Record = { PlayCircleOutlined: , }; -function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isSuperAdmin: boolean, badges?: { pendingResponses?: number; pendingEmails?: number; pendingCampaignReview?: number; pendingComments?: number }): MenuProps['items'] { +function buildMenuItems(settings: import('@/types/api').SiteSettings | null, user: User | null, badges?: { pendingResponses?: number; pendingEmails?: number; pendingCampaignReview?: number; pendingComments?: number }): MenuProps['items'] { + const isSuperAdmin = hasAnyRole(user, ['SUPER_ADMIN']); + const can = (roles: import('@/types/api').UserRole[]) => hasAnyRole(user, roles); + const items: MenuProps['items'] = [ { key: '/app', @@ -131,14 +144,14 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS }, ]; - // People & Access submenu — Users always visible, People gated by feature flag + // People & Access submenu — Users visible to any admin, social sub-items gated by SOCIAL_ROLES { const communityChildren: MenuProps['items'] = []; if (settings?.enablePeople) { communityChildren.push({ key: '/app/people', icon: , label: 'People' }); } communityChildren.push({ key: '/app/users', icon: , label: 'Users' }); - if (settings?.enableSocial) { + if (settings?.enableSocial && can(SOCIAL_ROLES)) { communityChildren.push( { key: '/app/social', icon: , label: 'Social Dashboard' }, { key: '/app/social/graph', icon: , label: 'Social Graph' }, @@ -156,7 +169,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS }); } - if (settings?.enableInfluence !== false) { + if (settings?.enableInfluence !== false && can(INFLUENCE_ROLES)) { items.push({ key: 'influence-submenu', icon: , @@ -173,7 +186,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS }); } - if (settings?.enableNewsletter !== false) { + if (settings?.enableNewsletter !== false && can(BROADCAST_ROLES)) { const broadcastChildren: MenuProps['items'] = [ { key: '/app/listmonk', icon: , label: 'Newsletter' }, { key: '/app/email-templates', icon: , label: 'Email Templates' }, @@ -204,24 +217,26 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS } // Web submenu — conditionally include Landing Pages - const webChildren: MenuProps['items'] = []; - if (settings?.enableLandingPages !== false) { - webChildren.push({ key: '/app/pages', icon: , label: 'Landing Pages' }); + if (can(CONTENT_ROLES)) { + const webChildren: MenuProps['items'] = []; + if (settings?.enableLandingPages !== false) { + webChildren.push({ key: '/app/pages', icon: , label: 'Landing Pages' }); + } + webChildren.push({ key: '/app/navigation', icon: , label: 'Navigation' }); + webChildren.push({ key: '/app/docs', icon: , label: 'Documentation' }); + webChildren.push({ key: '/app/docs/analytics', icon: , label: 'Analytics' }); + webChildren.push({ key: '/app/docs/comments', icon: , label: badges?.pendingComments ? Comments : 'Comments' }); + webChildren.push({ key: '/app/docs/settings', icon: , label: 'Docs Settings' }); + webChildren.push({ key: '/app/code', icon: , label: 'Code Editor' }); + items.push({ + key: 'web-submenu', + icon: , + label: 'Web', + children: webChildren, + }); } - webChildren.push({ key: '/app/navigation', icon: , label: 'Navigation' }); - webChildren.push({ key: '/app/docs', icon: , label: 'Documentation' }); - webChildren.push({ key: '/app/docs/analytics', icon: , label: 'Analytics' }); - webChildren.push({ key: '/app/docs/comments', icon: , label: badges?.pendingComments ? Comments : 'Comments' }); - webChildren.push({ key: '/app/docs/settings', icon: , label: 'Docs Settings' }); - webChildren.push({ key: '/app/code', icon: , label: 'Code Editor' }); - items.push({ - key: 'web-submenu', - icon: , - label: 'Web', - children: webChildren, - }); - if (settings?.enableMap !== false) { + if (settings?.enableMap !== false && can(MAP_ROLES)) { items.push({ key: 'map-submenu', icon: , @@ -236,8 +251,8 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS }); } - // Scheduling submenu — visible if Shifts, Meeting Planner, or Ticketed Events is enabled - if (settings?.enableMap !== false || settings?.enableMeetingPlanner || settings?.enableTicketedEvents || settings?.enableMeet || settings?.enableEvents) { + // Scheduling submenu — visible if relevant features are enabled AND user has SCHEDULING_ROLES + if (can(SCHEDULING_ROLES) && (settings?.enableMap !== false || settings?.enableMeetingPlanner || settings?.enableTicketedEvents || settings?.enableMeet || settings?.enableEvents)) { const schedulingChildren: any[] = []; if (settings?.enableMap !== false) { schedulingChildren.push({ key: '/app/map/shifts', icon: , label: 'Shifts' }); @@ -254,7 +269,6 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS if (settings?.enableEvents) { schedulingChildren.push({ key: '/app/services/gancio', icon: , label: 'Gancio' }); } - schedulingChildren.push({ key: '/app/scheduling/calendar-views', icon: , label: 'Calendar Views' }); // Always add Calendar as the last item in scheduling schedulingChildren.push({ key: '/app/scheduling/calendar', icon: , label: 'Calendar' }); if (schedulingChildren.length > 0) { @@ -267,7 +281,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS } } - if (settings?.enableMediaFeatures !== false) { + if (settings?.enableMediaFeatures !== false && can(MEDIA_ROLES)) { items.push({ key: 'media-submenu', icon: , @@ -282,7 +296,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS }); } - if (settings?.enablePayments) { + if (settings?.enablePayments && can(PAYMENTS_ROLES)) { items.push({ key: 'payments-submenu', icon: , @@ -323,13 +337,15 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS }); } - items.push( - { - key: '/app/settings', - icon: , - label: 'Settings', - }, - ); + if (isSuperAdmin) { + items.push( + { + key: '/app/settings', + icon: , + label: 'Settings', + }, + ); + } return items; } @@ -345,7 +361,6 @@ export default function AppLayout() { const { token } = theme.useToken(); const screens = useBreakpoint(); const isMobile = !screens.md; - const isSuperAdmin = hasAnyRole(user, ['SUPER_ADMIN']); const [badgeCounts, setBadgeCounts] = useState<{ pendingResponses: number; pendingEmails: number; pendingCampaignReview: number; pendingComments: number }>({ pendingResponses: 0, pendingEmails: 0, pendingCampaignReview: 0, pendingComments: 0 }); const fetchBadges = useCallback(() => { @@ -365,7 +380,7 @@ export default function AppLayout() { return () => clearInterval(interval); }, [fetchBadges]); - const baseMenuItems = buildMenuItems(settings, isSuperAdmin, badgeCounts); + const baseMenuItems = buildMenuItems(settings, user, badgeCounts); const { favorites } = useFavoritesStore(); // Build final menu: resolve favorites, add stars, prepend favorites section diff --git a/admin/src/components/PublicNavBar.tsx b/admin/src/components/PublicNavBar.tsx index 1d029392..9f410bcd 100644 --- a/admin/src/components/PublicNavBar.tsx +++ b/admin/src/components/PublicNavBar.tsx @@ -32,6 +32,7 @@ import { isItemActive, } from '@/lib/nav-defaults'; import type { NavItem } from '@/types/api'; +import { isAdmin as checkIsAdmin } from '@/utils/roles'; const navItemStyle: React.CSSProperties = { color: 'rgba(255, 255, 255, 0.85)', @@ -65,7 +66,7 @@ interface PublicNavBarProps { export default function PublicNavBar({ activePath, showAuth = true, onSignInClick }: PublicNavBarProps) { const { settings } = useSettingsStore(); const { isAuthenticated, logout, user } = useAuthStore(); - const isAdmin = user?.role === 'SUPER_ADMIN' || user?.role === 'INFLUENCE_ADMIN' || user?.role === 'MAP_ADMIN'; + const isAdmin = isAuthenticated && user ? checkIsAdmin(user) : false; const location = useLocation(); const navigate = useNavigate(); const [drawerOpen, setDrawerOpen] = useState(false); diff --git a/admin/src/components/VolunteerLayout.tsx b/admin/src/components/VolunteerLayout.tsx index f2e5673e..dffc808c 100644 --- a/admin/src/components/VolunteerLayout.tsx +++ b/admin/src/components/VolunteerLayout.tsx @@ -21,6 +21,7 @@ import VolunteerFooterNav from '@/components/VolunteerFooterNav'; import PublicNavBar from '@/components/PublicNavBar'; import { useSSE } from '@/hooks/useSSE'; import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { isAdmin as checkIsAdmin } from '@/utils/roles'; const { Content, Footer } = Layout; @@ -35,7 +36,7 @@ export default function VolunteerLayout() { // Initialize SSE connection for real-time notifications + online presence useSSE(); - const isAdmin = user?.role === 'SUPER_ADMIN' || user?.role === 'INFLUENCE_ADMIN' || user?.role === 'MAP_ADMIN'; + const isAdmin = user ? checkIsAdmin(user) : false; const colorBgBase = settings?.publicColorBgBase ?? '#0d1b2a'; const colorBgContainer = settings?.publicColorBgContainer ?? '#1b2838'; diff --git a/admin/src/components/calendar/CalendarItemDetail.tsx b/admin/src/components/calendar/CalendarItemDetail.tsx new file mode 100644 index 00000000..9d70adeb --- /dev/null +++ b/admin/src/components/calendar/CalendarItemDetail.tsx @@ -0,0 +1,192 @@ +import { Drawer, Button, Space, Tag, Typography, Divider, theme } from 'antd'; +import { + ClockCircleOutlined, + EnvironmentOutlined, + CalendarOutlined, + EditOutlined, + DeleteOutlined, + BellOutlined, + LockOutlined, +} from '@ant-design/icons'; +import dayjs from 'dayjs'; +import type { PersonalCalendarItem } from '@/types/api'; +import { hexToRgba, formatTimeShort } from './calendarUtils'; + +const { Text, Title } = Typography; + +interface CalendarItemDetailProps { + item: PersonalCalendarItem | null; + open: boolean; + onClose: () => void; + onEdit: (item: PersonalCalendarItem) => void; + onDelete: (item: PersonalCalendarItem) => void; +} + +export default function CalendarItemDetail({ + item, + open, + onClose, + onEdit, + onDelete, +}: CalendarItemDetailProps) { + const { token } = theme.useToken(); + + if (!item) return null; + + const isPersonal = item.type === 'personal'; + const isTimeBlock = item.itemType === 'TIME_BLOCK'; + const isReminder = item.itemType === 'REMINDER'; + const color = item.color || token.colorPrimary; + + return ( + + {/* Color bar + title */} +
+
+
+ + {isReminder && <BellOutlined style={{ marginRight: 6, fontSize: 14 }} />} + {isTimeBlock && item.showDetailsTo === 'NOBODY' ? ( + <> + <LockOutlined style={{ marginRight: 6, fontSize: 14 }} /> + Busy + </> + ) : ( + item.title + )} + + {item.type !== 'personal' && ( + + {item.type} + + )} + {isTimeBlock && ( + Time Block + )} +
+
+ + + + {/* Date & time */} + }> + {dayjs(item.date).format('dddd, MMMM D, YYYY')} + + + }> + {item.isAllDay + ? 'All day' + : `${formatTimeShort(item.startTime)} - ${formatTimeShort(item.endTime)}`} + + + {/* Location */} + {item.location && ( + }> + {item.location} + + )} + + {/* Busy status */} + {item.busyStatus && item.busyStatus !== 'BUSY' && ( + + + {item.busyStatus.toLowerCase().replace('_', ' ')} + + + )} + + {/* Recurrence */} + {item.isRecurring && ( + + Recurring + + )} + + {/* Layer color indicator */} +
+
+
+ + {item.type === 'personal' ? 'Personal' : item.type} + +
+
+ + {/* Bottom actions: Close on left, Edit/Delete on right */} +
+ + {isPersonal && ( + + + + + )} +
+ + ); +} + +function DetailRow({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { + return ( +
+ {icon && ( + + {icon} + + )} + {children} +
+ ); +} diff --git a/admin/src/components/calendar/CalendarTimeGrid.tsx b/admin/src/components/calendar/CalendarTimeGrid.tsx new file mode 100644 index 00000000..330e44b8 --- /dev/null +++ b/admin/src/components/calendar/CalendarTimeGrid.tsx @@ -0,0 +1,525 @@ +import { useMemo, useRef, useEffect, useCallback } from 'react'; +import { Typography, theme } from 'antd'; +import { + BellOutlined, + EnvironmentOutlined, + LockOutlined, +} from '@ant-design/icons'; +import dayjs from 'dayjs'; +import type { PersonalCalendarItem } from '@/types/api'; +import { + hexToRgba, + assignLanes, + formatHourLabel, + getViewDates, + formatTimeShort, + START_HOUR, + END_HOUR, + SLOT_HEIGHT, +} from './calendarUtils'; + +const { Text } = Typography; + +interface CalendarTimeGridProps { + items: PersonalCalendarItem[]; + viewMode: 'day' | '3day' | 'week'; + currentDate: dayjs.Dayjs; + onDateSelect: (date: string) => void; + onItemClick: (item: PersonalCalendarItem) => void; + onNavigate: (direction: 'prev' | 'next') => void; +} + +const GUTTER_WIDTH = 44; +const totalHeight = (END_HOUR - START_HOUR) * SLOT_HEIGHT; +const SWIPE_THRESHOLD = 50; + +export default function CalendarTimeGrid({ + items, + viewMode, + currentDate, + onDateSelect, + onItemClick, + onNavigate, +}: CalendarTimeGridProps) { + const { token } = theme.useToken(); + const scrollRef = useRef(null); + const touchStartRef = useRef<{ x: number; y: number } | null>(null); + + const dates = useMemo(() => getViewDates(viewMode, currentDate), [viewMode, currentDate]); + const todayKey = dayjs().format('YYYY-MM-DD'); + + // Scroll to current time on mount + useEffect(() => { + if (scrollRef.current) { + const now = dayjs(); + const minutes = now.hour() * 60 + now.minute(); + const scrollTo = ((minutes - START_HOUR * 60) / 60) * SLOT_HEIGHT - 100; + scrollRef.current.scrollTop = Math.max(0, scrollTo); + } + }, []); + + // Group items by date + const itemsByDate = useMemo(() => { + const map: Record = {}; + for (const item of items) { + if (!map[item.date]) map[item.date] = []; + map[item.date]!.push(item); + } + return map; + }, [items]); + + // Touch swipe handling + const handleTouchStart = useCallback((e: React.TouchEvent) => { + const touch = e.touches[0]; + if (touch) { + touchStartRef.current = { x: touch.clientX, y: touch.clientY }; + } + }, []); + + const handleTouchEnd = useCallback((e: React.TouchEvent) => { + if (!touchStartRef.current) return; + const touch = e.changedTouches[0]; + if (!touch) return; + const dx = touch.clientX - touchStartRef.current.x; + const dy = touch.clientY - touchStartRef.current.y; + touchStartRef.current = null; + + // Only trigger swipe if horizontal movement is dominant + if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.5) { + onNavigate(dx < 0 ? 'next' : 'prev'); + } + }, [onNavigate]); + + // Current time indicator + const nowIndicatorTop = useMemo(() => { + const now = dayjs(); + const minutes = now.hour() * 60 + now.minute(); + const top = ((minutes - START_HOUR * 60) / 60) * SLOT_HEIGHT; + if (top < 0 || top > totalHeight) return null; + return top; + }, []); + + const columnCount = dates.length; + const isNarrow = viewMode === 'week'; + + return ( +
+ {/* Column headers */} +
+ {/* Gutter spacer */} +
+ + {dates.map((date) => { + const dateKey = date.format('YYYY-MM-DD'); + const isToday = dateKey === todayKey; + + return ( +
onDateSelect(dateKey)} + style={{ + flex: 1, + textAlign: 'center', + padding: '8px 2px', + cursor: 'pointer', + borderLeft: '1px solid rgba(255,255,255,0.06)', + }} + > + + {isNarrow ? date.format('dd') : date.format('ddd')} + +
+ {date.format('D')} +
+
+ ); + })} +
+ + {/* All-day section */} + + + {/* Scrollable time grid */} +
+
+ {/* Hour lines */} + {Array.from({ length: END_HOUR - START_HOUR + 1 }, (_, i) => { + const hour = START_HOUR + i; + const top = i * SLOT_HEIGHT; + return ( +
+ + {formatHourLabel(hour)} + +
+
+ ); + })} + + {/* Column dividers */} + {dates.map((date, i) => { + if (i === 0) return null; + return ( +
+ ); + })} + + {/* Column click zones (for creating events on empty space) */} + {dates.map((date, i) => { + const dateKey = date.format('YYYY-MM-DD'); + return ( +
onDateSelect(dateKey)} + style={{ + position: 'absolute', + top: 0, + bottom: 0, + left: `calc(${GUTTER_WIDTH}px + ${(i / columnCount)} * (100% - ${GUTTER_WIDTH}px))`, + width: `calc((100% - ${GUTTER_WIDTH}px) / ${columnCount})`, + cursor: 'pointer', + }} + /> + ); + })} + + {/* Current time indicator */} + {nowIndicatorTop !== null && dates.some((d) => d.format('YYYY-MM-DD') === todayKey) && ( +
+
+
+ )} + + {/* Events per column */} + {dates.map((date, colIndex) => { + const dateKey = date.format('YYYY-MM-DD'); + const dayItems = (itemsByDate[dateKey] ?? []).filter((it) => !it.isAllDay); + const lanedItems = assignLanes(dayItems); + + return lanedItems.map(({ item, top, height, lane, totalLanes }) => { + const colWidth = `calc((100% - ${GUTTER_WIDTH}px) / ${columnCount})`; + const colLeft = `calc(${GUTTER_WIDTH}px + ${colIndex} * (100% - ${GUTTER_WIDTH}px) / ${columnCount})`; + const laneWidth = totalLanes > 1 ? `calc(${100 / totalLanes}% - 2px)` : 'calc(100% - 6px)'; + const laneOffset = totalLanes > 1 ? `calc(${(lane / totalLanes) * 100}% + 1px)` : '3px'; + + return ( + onItemClick(item)} + /> + ); + }); + })} +
+
+
+ ); +} + +/** All-day events section spanning across columns */ +function AllDaySection({ + dates, + itemsByDate, + onItemClick, + isNarrow, +}: { + dates: dayjs.Dayjs[]; + itemsByDate: Record; + onItemClick: (item: PersonalCalendarItem) => void; + isNarrow: boolean; +}) { + const hasAllDay = dates.some((d) => { + const items = itemsByDate[d.format('YYYY-MM-DD')]; + return items?.some((it) => it.isAllDay); + }); + + if (!hasAllDay) return null; + + return ( +
+
+ all-day +
+ {dates.map((date) => { + const dateKey = date.format('YYYY-MM-DD'); + const allDay = (itemsByDate[dateKey] ?? []).filter((it) => it.isAllDay); + return ( +
+ {allDay.map((item) => ( +
onItemClick(item)} + style={{ + background: hexToRgba(item.color, 0.2), + borderLeft: `3px solid ${item.color}`, + borderRadius: 4, + padding: '2px 4px', + fontSize: isNarrow ? 10 : 11, + color: 'rgba(255,255,255,0.85)', + cursor: 'pointer', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {item.title} +
+ ))} +
+ ); + })} +
+ ); +} + +/** Individual event block positioned within a column */ +function EventBlock({ + item, + top, + height, + colLeft, + colWidth, + laneWidth, + laneOffset, + isNarrow, + onClick, +}: { + item: PersonalCalendarItem; + top: number; + height: number; + colLeft: string; + colWidth: string; + laneWidth: string; + laneOffset: string; + isNarrow: boolean; + onClick: () => void; +}) { + const { token } = theme.useToken(); + const isTimeBlock = item.itemType === 'TIME_BLOCK'; + const isReminder = item.itemType === 'REMINDER'; + const color = item.color || token.colorPrimary; + const isBusyHidden = isTimeBlock && item.showDetailsTo === 'NOBODY'; + + return ( +
{ + e.stopPropagation(); + onClick(); + }} + style={{ + position: 'absolute', + top: top + 1, + left: `calc(${colLeft} + ${laneOffset})`, + width: `calc(min(${colWidth}, ${laneWidth}))`, + height: height - 2, + background: isTimeBlock ? hexToRgba(color, 0.1) : hexToRgba(color, 0.2), + border: isTimeBlock + ? `1px dashed ${hexToRgba(color, 0.35)}` + : `1px solid ${hexToRgba(color, 0.4)}`, + borderLeft: `3px solid ${color}`, + borderRadius: 6, + padding: isNarrow ? '2px 3px' : '4px 8px', + cursor: 'pointer', + overflow: 'hidden', + zIndex: 2, + transition: 'background 0.15s', + }} + > + {isBusyHidden ? ( + + + {!isNarrow && 'Busy'} + + ) : ( + <> + + {isReminder && } + {item.title} + + {!isNarrow && ( + + {formatTimeShort(item.startTime)} - {formatTimeShort(item.endTime)} + + )} + {isNarrow && height > 30 && ( + + {formatTimeShort(item.startTime)} + + )} + {item.location && height > 45 && !isNarrow && ( + + + {item.location} + + )} + + )} +
+ ); +} diff --git a/admin/src/components/calendar/MobileDayView.tsx b/admin/src/components/calendar/MobileDayView.tsx deleted file mode 100644 index d25b6570..00000000 --- a/admin/src/components/calendar/MobileDayView.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import { useMemo } from 'react'; -import { Button, Typography, Empty, theme } from 'antd'; -import { - LeftOutlined, - RightOutlined, - PlusOutlined, - BellOutlined, - EnvironmentOutlined, - LockOutlined, -} from '@ant-design/icons'; -import dayjs from 'dayjs'; -import type { Dayjs } from 'dayjs'; -import type { PersonalCalendarItem, CalendarLayer } from '@/types/api'; - -const { Text } = Typography; -const { useToken } = theme; - -interface MobileDayViewProps { - items: PersonalCalendarItem[]; - layers?: CalendarLayer[]; - currentMonth: Dayjs; - selectedDate: string | null; - onDateSelect: (date: string) => void; - onMonthChange: (month: Dayjs) => void; - onItemClick: (item: PersonalCalendarItem) => void; - onAddItem: (date: string) => void; -} - -// Time slots from 6am to 10pm -const START_HOUR = 6; -const END_HOUR = 22; -const SLOT_HEIGHT = 60; // px per hour - -export default function MobileDayView({ - items, - selectedDate, - onDateSelect, - onItemClick, - onAddItem, -}: MobileDayViewProps) { - const { token } = useToken(); - - const currentDate = selectedDate ? dayjs(selectedDate) : dayjs(); - const dateKey = currentDate.format('YYYY-MM-DD'); - const isToday = dateKey === dayjs().format('YYYY-MM-DD'); - - // Filter items for the selected date - const dayItems = useMemo(() => { - return items - .filter((item) => item.date === dateKey) - .sort((a, b) => a.startTime.localeCompare(b.startTime)); - }, [items, dateKey]); - - const allDayItems = dayItems.filter((item) => item.isAllDay); - const timedItems = dayItems.filter((item) => !item.isAllDay); - - // Calculate position and height of timed items - const positionedItems = useMemo(() => { - return timedItems.map((item) => { - const parts = item.startTime.split(':'); - const sh = parseInt(parts[0] ?? '0', 10); - const sm = parseInt(parts[1] ?? '0', 10); - const endParts = item.endTime.split(':'); - const eh = parseInt(endParts[0] ?? '0', 10); - const em = parseInt(endParts[1] ?? '0', 10); - const startMinutes = sh * 60 + sm; - const endMinutes = eh * 60 + em; - const topMinutes = startMinutes - START_HOUR * 60; - const durationMinutes = Math.max(endMinutes - startMinutes, 15); // min 15min display - - return { - item, - top: Math.max(0, (topMinutes / 60) * SLOT_HEIGHT), - height: Math.max(20, (durationMinutes / 60) * SLOT_HEIGHT), - }; - }); - }, [timedItems]); - - const totalHeight = (END_HOUR - START_HOUR) * SLOT_HEIGHT; - - // Current time indicator position - const nowIndicatorTop = useMemo(() => { - if (!isToday) return null; - const now = dayjs(); - const minutes = now.hour() * 60 + now.minute(); - const top = ((minutes - START_HOUR * 60) / 60) * SLOT_HEIGHT; - if (top < 0 || top > totalHeight) return null; - return top; - }, [isToday, totalHeight]); - - const handlePrev = () => { - onDateSelect(currentDate.subtract(1, 'day').format('YYYY-MM-DD')); - }; - - const handleNext = () => { - onDateSelect(currentDate.add(1, 'day').format('YYYY-MM-DD')); - }; - - return ( -
- {/* Date header with navigation */} -
-
- - {/* Today button */} - {!isToday && ( -
- -
- )} - - {/* All-day items section */} - {allDayItems.length > 0 && ( -
- - All Day - - {allDayItems.map((item) => ( - onItemClick(item)} - /> - ))} -
- )} - - {/* Scrollable time grid */} -
- {dayItems.length === 0 && ( -
- -
- )} - - {(timedItems.length > 0 || allDayItems.length > 0) && ( -
- {/* Hour lines */} - {Array.from({ length: END_HOUR - START_HOUR + 1 }, (_, i) => { - const hour = START_HOUR + i; - const top = i * SLOT_HEIGHT; - return ( -
- - {hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`} - -
-
- ); - })} - - {/* Current time indicator */} - {nowIndicatorTop !== null && ( -
-
-
- )} - - {/* Positioned items */} - {positionedItems.map(({ item, top, height }) => { - const isTimeBlock = item.itemType === 'TIME_BLOCK'; - const isReminder = item.itemType === 'REMINDER'; - const color = item.color || token.colorPrimary; - const isBusyHidden = isTimeBlock && item.showDetailsTo === 'NOBODY'; - - return ( -
onItemClick(item)} - style={{ - position: 'absolute', - top: top + 1, - left: 50, - right: 4, - height: height - 2, - background: isTimeBlock - ? hexToRgba(color, 0.1) - : hexToRgba(color, 0.2), - border: isTimeBlock - ? `1px dashed ${hexToRgba(color, 0.35)}` - : `1px solid ${hexToRgba(color, 0.4)}`, - borderLeft: `3px solid ${color}`, - borderRadius: 6, - padding: '4px 8px', - cursor: 'pointer', - overflow: 'hidden', - zIndex: 2, - transition: 'background 0.15s', - }} - > - {isBusyHidden ? ( - - - Busy - - ) : ( - <> - - {isReminder && } - {item.title} - - - {item.startTime} - {item.endTime} - - {item.location && height > 45 && ( - - - {item.location} - - )} - - )} -
- ); - })} -
- )} -
- - {/* Floating add button */} -
- ); -} - -/** Compact item pill for all-day items */ -function ItemPill({ - item, - onClick, -}: { - item: PersonalCalendarItem; - onClick: () => void; -}) { - const isTimeBlock = item.itemType === 'TIME_BLOCK'; - const isReminder = item.itemType === 'REMINDER'; - const color = item.color; - - return ( -
- {isReminder && } - {item.title} -
- ); -} - -/** Convert a hex color to rgba string */ -function hexToRgba(hex: string, alpha: number): string { - const cleaned = hex.replace('#', ''); - if (cleaned.length !== 6) return `rgba(24, 144, 255, ${alpha})`; - const r = parseInt(cleaned.slice(0, 2), 16); - const g = parseInt(cleaned.slice(2, 4), 16); - const b = parseInt(cleaned.slice(4, 6), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} diff --git a/admin/src/components/calendar/PersonalCalendarView.tsx b/admin/src/components/calendar/PersonalCalendarView.tsx index 77e27bc5..c061ec91 100644 --- a/admin/src/components/calendar/PersonalCalendarView.tsx +++ b/admin/src/components/calendar/PersonalCalendarView.tsx @@ -1,8 +1,9 @@ import { useMemo } from 'react'; import { Calendar, Spin, Empty, theme } from 'antd'; -import { BellOutlined } from '@ant-design/icons'; +import { BellOutlined, EnvironmentOutlined } from '@ant-design/icons'; import type { Dayjs } from 'dayjs'; import type { PersonalCalendarItem, CalendarLayer } from '@/types/api'; +import { hexToRgba, formatTimeShort } from './calendarUtils'; const { useToken } = theme; @@ -22,6 +23,7 @@ const MAX_CELL_ITEMS = 3; export default function PersonalCalendarView({ items, loading, + currentMonth, onDateSelect, onItemClick, onMonthChange, @@ -39,7 +41,6 @@ export default function PersonalCalendarView({ map[item.date] = [item]; } } - // Sort items within each date by startTime for (const key of Object.keys(map)) { map[key]!.sort((a, b) => a.startTime.localeCompare(b.startTime)); } @@ -77,6 +78,7 @@ export default function PersonalCalendarView({ const color = item.color || '#1890ff'; const bgAlpha = isTimeBlock ? 0.1 : 0.2; const borderAlpha = isTimeBlock ? 0.3 : 0.5; + const isSystemType = item.type !== 'personal'; return (
{!item.isAllDay && ( - {item.startTime} + {formatTimeShort(item.startTime)}-{formatTimeShort(item.endTime)} )} {isReminder && ( )} + {isSystemType && ( + + [{item.type}] + + )} {item.title} + {item.location && ( + + + {item.location} + + )}
); })} @@ -141,9 +161,11 @@ export default function PersonalCalendarView({ )} cellRender(date)} onSelect={handleSelect} onPanelChange={handlePanelChange} + headerRender={() => null} /> {items.length === 0 && (
); } - -/** Convert a hex color to rgba string */ -function hexToRgba(hex: string, alpha: number): string { - const cleaned = hex.replace('#', ''); - if (cleaned.length !== 6) return `rgba(24, 144, 255, ${alpha})`; - const r = parseInt(cleaned.slice(0, 2), 16); - const g = parseInt(cleaned.slice(2, 4), 16); - const b = parseInt(cleaned.slice(4, 6), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} diff --git a/admin/src/components/calendar/calendarUtils.ts b/admin/src/components/calendar/calendarUtils.ts new file mode 100644 index 00000000..d77f53fe --- /dev/null +++ b/admin/src/components/calendar/calendarUtils.ts @@ -0,0 +1,172 @@ +import type { Dayjs } from 'dayjs'; +import type { PersonalCalendarItem } from '@/types/api'; + +export type CalendarViewMode = 'day' | '3day' | 'week' | 'month'; + +export const START_HOUR = 6; +export const END_HOUR = 22; +export const SLOT_HEIGHT = 60; // px per hour + +/** Convert a hex color to rgba string */ +export function hexToRgba(hex: string, alpha: number): string { + const cleaned = hex.replace('#', ''); + if (cleaned.length !== 6) return `rgba(24, 144, 255, ${alpha})`; + const r = parseInt(cleaned.slice(0, 2), 16); + const g = parseInt(cleaned.slice(2, 4), 16); + const b = parseInt(cleaned.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +/** Calculate pixel position and height for a timed calendar item */ +export function positionTimedItem(item: PersonalCalendarItem): { top: number; height: number } { + const parts = item.startTime.split(':'); + const sh = parseInt(parts[0] ?? '0', 10); + const sm = parseInt(parts[1] ?? '0', 10); + const endParts = item.endTime.split(':'); + const eh = parseInt(endParts[0] ?? '0', 10); + const em = parseInt(endParts[1] ?? '0', 10); + const startMinutes = sh * 60 + sm; + const endMinutes = eh * 60 + em; + const topMinutes = startMinutes - START_HOUR * 60; + const durationMinutes = Math.max(endMinutes - startMinutes, 15); + + return { + top: Math.max(0, (topMinutes / 60) * SLOT_HEIGHT), + height: Math.max(20, (durationMinutes / 60) * SLOT_HEIGHT), + }; +} + +export interface LanedItem { + item: PersonalCalendarItem; + top: number; + height: number; + lane: number; + totalLanes: number; +} + +/** Assign lanes for overlapping timed items so they render side-by-side */ +export function assignLanes(items: PersonalCalendarItem[]): LanedItem[] { + if (items.length === 0) return []; + + const positioned = items + .map((item) => ({ item, ...positionTimedItem(item) })) + .sort((a, b) => a.top - b.top || a.height - b.height); + + // Greedy lane assignment: for each item, find the first lane where it doesn't overlap + const lanes: { end: number }[] = []; + const result: LanedItem[] = []; + + for (const p of positioned) { + const itemEnd = p.top + p.height; + let assignedLane = -1; + + for (let i = 0; i < lanes.length; i++) { + if (lanes[i]!.end <= p.top) { + assignedLane = i; + lanes[i]!.end = itemEnd; + break; + } + } + + if (assignedLane === -1) { + assignedLane = lanes.length; + lanes.push({ end: itemEnd }); + } + + result.push({ ...p, lane: assignedLane, totalLanes: 0 }); + } + + // Second pass: determine totalLanes for each overlap group + // Group overlapping items and set totalLanes for the group + const groups: number[][] = []; + for (let i = 0; i < result.length; i++) { + const r = result[i]!; + let added = false; + for (const group of groups) { + const overlaps = group.some((gi) => { + const g = result[gi]!; + return r.top < g.top + g.height && r.top + r.height > g.top; + }); + if (overlaps) { + group.push(i); + added = true; + break; + } + } + if (!added) { + groups.push([i]); + } + } + + for (const group of groups) { + const maxLane = Math.max(...group.map((i) => result[i]!.lane)) + 1; + for (const i of group) { + result[i]!.totalLanes = maxLane; + } + } + + return result; +} + +/** Get the date range needed for API fetching based on view mode */ +export function getDateRangeForView( + viewMode: CalendarViewMode, + anchorDate: Dayjs, +): { startDate: string; endDate: string } { + switch (viewMode) { + case 'day': + return { + startDate: anchorDate.format('YYYY-MM-DD'), + endDate: anchorDate.format('YYYY-MM-DD'), + }; + case '3day': + return { + startDate: anchorDate.format('YYYY-MM-DD'), + endDate: anchorDate.add(2, 'day').format('YYYY-MM-DD'), + }; + case 'week': + return { + startDate: anchorDate.startOf('week').format('YYYY-MM-DD'), + endDate: anchorDate.endOf('week').format('YYYY-MM-DD'), + }; + case 'month': + return { + startDate: anchorDate.startOf('month').subtract(7, 'day').format('YYYY-MM-DD'), + endDate: anchorDate.endOf('month').add(7, 'day').format('YYYY-MM-DD'), + }; + } +} + +/** Format an hour number to a display label like "6 AM", "12 PM" */ +export function formatHourLabel(hour: number): string { + if (hour === 0) return '12 AM'; + if (hour < 12) return `${hour} AM`; + if (hour === 12) return '12 PM'; + return `${hour - 12} PM`; +} + +/** Get the number of columns and dates for a view mode */ +export function getViewDates(viewMode: CalendarViewMode, anchorDate: Dayjs): Dayjs[] { + switch (viewMode) { + case 'day': + return [anchorDate]; + case '3day': + return [anchorDate, anchorDate.add(1, 'day'), anchorDate.add(2, 'day')]; + case 'week': { + const start = anchorDate.startOf('week'); + return Array.from({ length: 7 }, (_, i) => start.add(i, 'day')); + } + default: + return [anchorDate]; + } +} + +/** Format a time string like "09:30" to "9:30 AM" */ +export function formatTimeShort(time: string): string { + const [h, m] = time.split(':'); + const hour = parseInt(h ?? '0', 10); + const min = m ?? '00'; + const suffix = hour < 12 ? 'AM' : 'PM'; + const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + return min === '00' ? `${h12} ${suffix}` : `${h12}:${min} ${suffix}`; +} diff --git a/admin/src/components/command-palette/registry.ts b/admin/src/components/command-palette/registry.ts index 1b7e92cb..f73a3869 100644 --- a/admin/src/components/command-palette/registry.ts +++ b/admin/src/components/command-palette/registry.ts @@ -64,7 +64,7 @@ export const commandRegistry: CommandItem[] = [ keywords: ['social', 'community', 'friends', 'connections', 'engagement'], category: 'navigation', featureFlag: 'enableSocial', - requiredRoles: ['SUPER_ADMIN', 'INFLUENCE_ADMIN', 'MAP_ADMIN'], + requiredRoles: ['SUPER_ADMIN', 'SOCIAL_ADMIN'], }, { id: 'nav-social-graph', @@ -76,7 +76,7 @@ export const commandRegistry: CommandItem[] = [ keywords: ['network', 'connections', 'graph', 'relationships', 'visualization'], category: 'navigation', featureFlag: 'enableSocial', - requiredRoles: ['SUPER_ADMIN', 'INFLUENCE_ADMIN', 'MAP_ADMIN'], + requiredRoles: ['SUPER_ADMIN', 'SOCIAL_ADMIN'], }, { id: 'nav-social-moderation', @@ -88,7 +88,7 @@ export const commandRegistry: CommandItem[] = [ keywords: ['moderation', 'reports', 'flagged', 'content review', 'social moderation'], category: 'navigation', featureFlag: 'enableSocial', - requiredRoles: ['SUPER_ADMIN', 'INFLUENCE_ADMIN', 'MAP_ADMIN'], + requiredRoles: ['SUPER_ADMIN', 'SOCIAL_ADMIN'], }, // ── Navigation: Advocacy ────────────────────────────── @@ -170,7 +170,7 @@ export const commandRegistry: CommandItem[] = [ keywords: ['listmonk', 'mailing list', 'subscribers', 'broadcast', 'email marketing'], category: 'navigation', featureFlag: 'enableNewsletter', - requiredRoles: ['SUPER_ADMIN'], + requiredRoles: ['SUPER_ADMIN', 'BROADCAST_ADMIN'], }, { id: 'nav-email-templates', diff --git a/admin/src/components/command-palette/useEntitySearch.ts b/admin/src/components/command-palette/useEntitySearch.ts index a40c6181..691ff735 100644 --- a/admin/src/components/command-palette/useEntitySearch.ts +++ b/admin/src/components/command-palette/useEntitySearch.ts @@ -81,7 +81,7 @@ const entityConfigs: EntitySearchConfig[] = [ subtitleField: 'slug', pathPrefix: '/app/payments/products', featureFlag: 'enablePayments', - requiredRoles: ['SUPER_ADMIN'], + requiredRoles: ['SUPER_ADMIN', 'PAYMENTS_ADMIN'], extractItems: (data: unknown) => (data as { products?: unknown[] })?.products ?? [], }, { @@ -91,7 +91,7 @@ const entityConfigs: EntitySearchConfig[] = [ subtitleField: 'slug', pathPrefix: '/app/payments/donation-pages', featureFlag: 'enablePayments', - requiredRoles: ['SUPER_ADMIN'], + requiredRoles: ['SUPER_ADMIN', 'PAYMENTS_ADMIN'], extractItems: (data: unknown) => (data as { donationPages?: unknown[] })?.donationPages ?? [], }, { diff --git a/admin/src/components/docs/CollaboratorAvatars.tsx b/admin/src/components/docs/CollaboratorAvatars.tsx new file mode 100644 index 00000000..0d6defea --- /dev/null +++ b/admin/src/components/docs/CollaboratorAvatars.tsx @@ -0,0 +1,66 @@ +import { Tooltip, Badge, theme } from 'antd'; +import { WifiOutlined, DisconnectOutlined } from '@ant-design/icons'; +import type { Collaborator } from '@/hooks/useDocsCollaboration'; + +interface CollaboratorAvatarsProps { + collaborators: Collaborator[]; + connected: boolean; +} + +function getInitials(name: string): string { + const parts = name.trim().split(/\s+/); + if (parts.length >= 2) { + return ((parts[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +} + +export function CollaboratorAvatars({ collaborators, connected }: CollaboratorAvatarsProps) { + const { token } = theme.useToken(); + + return ( +
+ {/* Connection status indicator */} + + {connected ? ( + + ) : ( + + )} + + + {/* Collaborator avatars */} + {collaborators.map((c) => ( + +
+ {getInitials(c.name)} +
+
+ ))} + + {/* Count badge when there are collaborators */} + {collaborators.length > 0 && ( + + )} +
+ ); +} diff --git a/admin/src/components/docs/MobileDocsEditor.tsx b/admin/src/components/docs/MobileDocsEditor.tsx index f4c63559..de3ba8c8 100644 --- a/admin/src/components/docs/MobileDocsEditor.tsx +++ b/admin/src/components/docs/MobileDocsEditor.tsx @@ -51,11 +51,15 @@ import type { ProductInsertResult } from '@/components/payments/ProductInsertMod import { AdPickerModal } from '@/components/media/AdPickerModal'; import type { AdInsertResult } from '@/components/media/AdPickerModal'; import { PollInsertModal } from '@/components/scheduling/PollInsertModal'; +import { useDocsCollaboration } from '@/hooks/useDocsCollaboration'; +import { CollaboratorAvatars } from '@/components/docs/CollaboratorAvatars'; +import { YTextareaBinding } from '@/lib/y-textarea'; type MobileTab = 'files' | 'editor' | 'preview'; interface MobileDocsEditorProps { editor: UseDocsEditorReturn; + collabEnabled?: boolean; } // Flatten file tree into a searchable list of file paths @@ -105,7 +109,49 @@ function LineNumberedEditor({ token: ReturnType['token']; }) { const gutterRef = useRef(null); - const lineCount = useMemo(() => value.split('\n').length, [value]); + const mirrorRef = useRef(null); + const wrapperRef = useRef(null); + const [lineHeights, setLineHeights] = useState([]); + const lines = useMemo(() => value.split('\n'), [value]); + + // Measure actual rendered height of each line using a hidden mirror div + const measureLines = useCallback(() => { + const mirror = mirrorRef.current; + const wrapper = wrapperRef.current; + if (!mirror || !wrapper) return; + + // Mirror width must match textarea content area (wrapper - gutter - textarea padding) + const contentWidth = wrapper.clientWidth - 36 - 12; + if (contentWidth <= 0) return; + mirror.style.width = `${contentWidth}px`; + + // Render all lines as child divs, measure in a single reflow + const fragment = document.createDocumentFragment(); + for (const line of lines) { + const div = document.createElement('div'); + div.textContent = line || '\u00a0'; + fragment.appendChild(div); + } + mirror.textContent = ''; + mirror.appendChild(fragment); + + const heights: number[] = []; + const children = mirror.children; + for (let i = 0; i < children.length; i++) { + heights.push((children[i] as HTMLElement).offsetHeight); + } + setLineHeights(heights); + }, [lines]); + + // Remeasure on content change and container resize + useEffect(() => { + measureLines(); + const wrapper = wrapperRef.current; + if (!wrapper) return; + const observer = new ResizeObserver(measureLines); + observer.observe(wrapper); + return () => observer.disconnect(); + }, [measureLines]); // Sync gutter scroll with textarea scroll const handleScroll = useCallback(() => { @@ -123,7 +169,22 @@ function LineNumberedEditor({ }, [textareaRef, handleScroll]); return ( -
+
+ {/* Hidden mirror div — same font/wrapping as textarea, used to measure line heights */} + diff --git a/admin/src/components/people/UserAccountStatusPanel.tsx b/admin/src/components/people/UserAccountStatusPanel.tsx index 9ea72e1d..7de427f4 100644 --- a/admin/src/components/people/UserAccountStatusPanel.tsx +++ b/admin/src/components/people/UserAccountStatusPanel.tsx @@ -10,6 +10,12 @@ const roleColors: Record = { SUPER_ADMIN: 'red', INFLUENCE_ADMIN: 'volcano', MAP_ADMIN: 'orange', + BROADCAST_ADMIN: 'gold', + CONTENT_ADMIN: 'lime', + MEDIA_ADMIN: 'purple', + PAYMENTS_ADMIN: 'green', + EVENTS_ADMIN: 'cyan', + SOCIAL_ADMIN: 'magenta', USER: 'blue', TEMP: 'default', }; diff --git a/admin/src/components/scheduling/SchedulingPollWidget.tsx b/admin/src/components/scheduling/SchedulingPollWidget.tsx index 1db293a1..d760b209 100644 --- a/admin/src/components/scheduling/SchedulingPollWidget.tsx +++ b/admin/src/components/scheduling/SchedulingPollWidget.tsx @@ -86,6 +86,8 @@ interface PollData { finalizedOptionId: string | null; finalizedOption: PollOption | null; allowAnonymous: boolean; + isPrivate?: boolean; + requiresAuth?: boolean; createdBy?: { name: string | null; email: string }; options: PollOption[]; voters: PollVoter[]; @@ -248,6 +250,43 @@ export function SchedulingPollWidget({ pollSlug, showComments = true, title }: S ); } + if (poll.requiresAuth) { + return ( +
+ {title && ( +

+ {title} +

+ )} +
+
🔒
+

{poll.title}

+

+ This poll is private. Please sign in to view the details and participate. +

+ + Sign In to View + +
+
+ ); + } + const isOpen = poll.status === 'OPEN'; const isFinalized = poll.status === 'FINALIZED'; const bestScore = poll.options.length ? Math.max(...poll.options.map((o) => o.score ?? 0)) : 0; diff --git a/admin/src/hooks/useDocsCollaboration.ts b/admin/src/hooks/useDocsCollaboration.ts new file mode 100644 index 00000000..f97c616d --- /dev/null +++ b/admin/src/hooks/useDocsCollaboration.ts @@ -0,0 +1,156 @@ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import * as Y from 'yjs'; +import { HocuspocusProvider } from '@hocuspocus/provider'; +import { useAuthStore } from '@/stores/auth.store'; + +// Deterministic color from user ID (must match server palette) +const COLLAB_COLORS = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', + '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', + '#F0B27A', '#82E0AA', '#F1948A', '#AED6F1', '#D7BDE2', +]; + +function getUserColor(userId: string): string { + let hash = 0; + for (let i = 0; i < userId.length; i++) { + hash = ((hash << 5) - hash + userId.charCodeAt(i)) | 0; + } + return COLLAB_COLORS[Math.abs(hash) % COLLAB_COLORS.length] ?? '#85C1E9'; +} + +export interface Collaborator { + id: string; + name: string; + color: string; + clientId: number; +} + +export interface UseDocsCollaborationReturn { + yDoc: Y.Doc | null; + yText: Y.Text | null; + provider: HocuspocusProvider | null; + connected: boolean; + synced: boolean; + collaborators: Collaborator[]; + /** Whether collaboration is active and synced (ready for binding) */ + active: boolean; +} + +/** + * Manages Y.Doc lifecycle, WebSocket connection, and awareness for collaborative editing. + * Creates a new Y.Doc + HocuspocusProvider when filePath changes. + * `active` is only true after the initial Y.js sync completes (content is available). + */ +export function useDocsCollaboration( + filePath: string | null, + enabled: boolean, +): UseDocsCollaborationReturn { + const [connected, setConnected] = useState(false); + const [synced, setSynced] = useState(false); + const [collaborators, setCollaborators] = useState([]); + + // Use state (not refs) so changes trigger re-renders for dependent effects + const [collabState, setCollabState] = useState<{ + yDoc: Y.Doc; + yText: Y.Text; + provider: HocuspocusProvider; + } | null>(null); + + const user = useAuthStore((s) => s.user); + const accessToken = useAuthStore((s) => s.accessToken); + + // Build WebSocket URL + const wsUrl = useMemo(() => { + if (!filePath || !enabled || !accessToken) return null; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/api/docs/collaborate`; + }, [filePath, enabled, accessToken]); + + // Cleanup function + const cleanup = useCallback(() => { + setCollabState((prev) => { + if (prev) { + prev.provider.disconnect(); + prev.provider.destroy(); + prev.yDoc.destroy(); + } + return null; + }); + setConnected(false); + setSynced(false); + setCollaborators([]); + }, []); + + useEffect(() => { + if (!wsUrl || !filePath || !enabled || !accessToken || !user) { + cleanup(); + return; + } + + // Create new Y.Doc and provider + const doc = new Y.Doc(); + const yText = doc.getText('content'); + + const provider = new HocuspocusProvider({ + url: wsUrl, + name: filePath, + document: doc, + token: accessToken, + onConnect: () => setConnected(true), + onDisconnect: () => { + setConnected(false); + setSynced(false); + }, + onSynced: () => setSynced(true), + }); + + // Set local awareness state + provider.setAwarenessField('user', { + id: user.id, + name: user.name || user.email.split('@')[0], + color: getUserColor(user.id), + }); + + // Track collaborators via awareness changes + const handleAwarenessChange = () => { + const states = provider.awareness?.getStates(); + if (!states) return; + const collab: Collaborator[] = []; + states.forEach((state, clientId) => { + const u = state.user as { id: string; name: string; color: string } | undefined; + if (u && u.id !== user.id) { + collab.push({ id: u.id, name: u.name, color: u.color, clientId }); + } + }); + setCollaborators(collab); + }; + provider.awareness?.on('change', handleAwarenessChange); + + // Set state LAST so the component re-renders with everything ready + setCollabState({ yDoc: doc, yText, provider }); + + return () => { + provider.awareness?.off('change', handleAwarenessChange); + provider.disconnect(); + provider.destroy(); + doc.destroy(); + setCollabState(null); + setConnected(false); + setSynced(false); + setCollaborators([]); + }; + // We intentionally only recreate when these core values change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wsUrl, filePath, enabled, accessToken, user?.id]); + + return { + yDoc: collabState?.yDoc ?? null, + yText: collabState?.yText ?? null, + provider: collabState?.provider ?? null, + connected, + synced, + collaborators, + // Only active after provider is connected AND initial sync is complete + active: enabled && !!collabState && synced && !!filePath, + }; +} diff --git a/admin/src/lib/nav-defaults.ts b/admin/src/lib/nav-defaults.ts index c5b8ae45..1a6daf40 100644 --- a/admin/src/lib/nav-defaults.ts +++ b/admin/src/lib/nav-defaults.ts @@ -68,8 +68,6 @@ export const DEFAULT_NAV_ITEMS: NavItem[] = [ { id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'ScheduleOutlined', enabled: true, order: 0, type: 'builtin', featureFlag: 'enableMap' }, { id: 'events', label: 'Calendar', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableEvents' }, { id: 'polls', label: 'Polls', path: '/polls', icon: 'BarChartOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMeetingPlanner' }, - { id: 'tickets', label: 'Tickets', path: '/events/tickets', icon: 'TagOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableTicketedEvents' }, - { id: 'meet', label: 'Meet', path: '/meet', icon: 'VideoCameraOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableMeet' }, ], }, { id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableMediaFeatures' }, diff --git a/admin/src/lib/y-textarea.ts b/admin/src/lib/y-textarea.ts new file mode 100644 index 00000000..6e44dccb --- /dev/null +++ b/admin/src/lib/y-textarea.ts @@ -0,0 +1,90 @@ +import * as Y from 'yjs'; + +/** + * Lightweight Y.Text ↔ textarea binding. + * Observes Y.Text changes → updates textarea value. + * Listens to textarea input events → applies delta to Y.Text. + * Preserves cursor position across remote updates. + */ +export class YTextareaBinding { + private yText: Y.Text; + private textarea: HTMLTextAreaElement; + private observer: (event: Y.YTextEvent) => void; + private inputHandler: (e: Event) => void; + private destroyed = false; + + constructor(yText: Y.Text, textarea: HTMLTextAreaElement) { + this.yText = yText; + this.textarea = textarea; + + // Initialize textarea with current Y.Text content + textarea.value = yText.toString(); + + // Observe Y.Text changes (remote updates) → update textarea + this.observer = () => { + if (this.destroyed) return; + const newVal = yText.toString(); + if (textarea.value === newVal) return; + + // Save cursor position and adjust for changes + const cursorBefore = textarea.selectionStart; + const lengthBefore = textarea.value.length; + textarea.value = newVal; + const lengthAfter = newVal.length; + + // Adjust cursor: shift by the length difference if cursor was after the change + const delta = lengthAfter - lengthBefore; + const newCursor = Math.max(0, Math.min(cursorBefore + delta, lengthAfter)); + textarea.selectionStart = newCursor; + textarea.selectionEnd = newCursor; + }; + yText.observe(this.observer); + + // Listen to textarea input events → apply delta to Y.Text + this.inputHandler = () => { + if (this.destroyed) return; + const newVal = textarea.value; + const oldVal = yText.toString(); + if (newVal === oldVal) return; + + // Find the changed region + + // Find common prefix length + let prefixLen = 0; + const minLen = Math.min(oldVal.length, newVal.length); + while (prefixLen < minLen && oldVal[prefixLen] === newVal[prefixLen]) { + prefixLen++; + } + + // Find common suffix length (don't overlap with prefix) + let suffixLen = 0; + while ( + suffixLen < (oldVal.length - prefixLen) && + suffixLen < (newVal.length - prefixLen) && + oldVal[oldVal.length - 1 - suffixLen] === newVal[newVal.length - 1 - suffixLen] + ) { + suffixLen++; + } + + const deleteCount = oldVal.length - prefixLen - suffixLen; + const insertText = newVal.slice(prefixLen, newVal.length - suffixLen); + + // Apply changes to Y.Text in a transaction + yText.doc?.transact(() => { + if (deleteCount > 0) { + yText.delete(prefixLen, deleteCount); + } + if (insertText.length > 0) { + yText.insert(prefixLen, insertText); + } + }); + }; + textarea.addEventListener('input', this.inputHandler); + } + + destroy(): void { + this.destroyed = true; + this.yText.unobserve(this.observer); + this.textarea.removeEventListener('input', this.inputHandler); + } +} diff --git a/admin/src/pages/AdminCalendarViewPage.tsx b/admin/src/pages/AdminCalendarViewPage.tsx index df7f9ad4..5d98caaf 100644 --- a/admin/src/pages/AdminCalendarViewPage.tsx +++ b/admin/src/pages/AdminCalendarViewPage.tsx @@ -77,7 +77,7 @@ export default function AdminCalendarViewPage() { setTotalUsers(itemsRes.data.totalUsers); setTruncated(itemsRes.data.truncated); } catch { - navigate('/app/scheduling/calendar-views'); + navigate('/app/scheduling/calendar'); } finally { setLoading(false); } @@ -221,7 +221,7 @@ export default function AdminCalendarViewPage() {
@@ -50,6 +236,72 @@ export default function SchedulingCalendarPage() { onShiftSignup={handleShiftClick} onAddEvent={addEventRef} /> + + {/* Shared Views list drawer — shifts left when form drawer opens */} + } onClick={openCreate}> + Create View + + } + > + ({ + onClick: () => navigate(`/app/scheduling/calendar-views/${record.id}`), + style: { cursor: 'pointer' }, + })} + /> + + + {/* Create/Edit form drawer */} + setFormOpen(false)} + mask={false} + width={FORM_PANEL_WIDTH} + rootStyle={DRAWER_ROOT} + destroyOnHidden + extra={ + + } + > +
+ + + + + + + + + + +
); } diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx index cefa8e2e..5be80ab2 100644 --- a/admin/src/pages/SettingsPage.tsx +++ b/admin/src/pages/SettingsPage.tsx @@ -25,6 +25,7 @@ import { Modal, Checkbox, Timeline, + Select, message, Spin, } from 'antd'; @@ -53,11 +54,12 @@ import { ExclamationCircleOutlined, LoadingOutlined, ReloadOutlined, + ClockCircleOutlined, } from '@ant-design/icons'; import { useSettingsStore } from '@/stores/settings.store'; import { api } from '@/lib/api'; import type { AppOutletContext } from '@/components/AppLayout'; -import type { SmtpTestResult, SmtpSendTestResult, UpgradeStatusResponse, UpgradeStatus, UpgradeProgress, UpgradeResult } from '@/types/api'; +import type { SmtpTestResult, SmtpSendTestResult, UpgradeStatusResponse, UpgradeStatus, UpgradeProgress, UpgradeResult, UpgradeHistoryResponse } from '@/types/api'; const { Text, Paragraph } = Typography; @@ -489,7 +491,10 @@ export default function SettingsPage() { - + + + + @@ -697,6 +702,7 @@ const UPGRADE_PHASES = [ ]; function SystemUpgradeTab() { + const { settings, updateSettings } = useSettingsStore(); const [status, setStatus] = useState(null); const [progress, setProgress] = useState(null); const [result, setResult] = useState(null); @@ -707,6 +713,7 @@ function SystemUpgradeTab() { const [confirmOpen, setConfirmOpen] = useState(false); const [skipBackup, setSkipBackup] = useState(false); const [pullServices, setPullServices] = useState(false); + const [history, setHistory] = useState([]); const pollRef = useRef | null>(null); const checkStartRef = useRef(null); @@ -725,6 +732,15 @@ function SystemUpgradeTab() { } }, []); + const fetchHistory = useCallback(async () => { + try { + const { data } = await api.get('/upgrade/history'); + setHistory(data.history || []); + } catch { + // Ignore — history is non-critical + } + }, []); + // Initial fetch on mount useEffect(() => { fetchStatus().then((data) => { @@ -734,6 +750,7 @@ function SystemUpgradeTab() { startUpgradePoll(); } }); + fetchHistory(); return () => stopPoll(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -758,12 +775,10 @@ function SystemUpgradeTab() { message.success('Update check complete'); } }, 2000); - // Auto-stop after 30s + // Auto-stop after 30s (idempotent — harmless if check already completed) setTimeout(() => { - if (checking) { - setChecking(false); - stopPoll(); - } + setChecking(false); + stopPoll(); }, 30000); }; @@ -777,6 +792,7 @@ function SystemUpgradeTab() { if (data.result && !data.running) { setUpgrading(false); stopPoll(); + fetchHistory(); // Refresh history after upgrade if (data.result.success) { message.success('Upgrade completed successfully'); } else { @@ -819,6 +835,14 @@ function SystemUpgradeTab() { } }; + const handleAutoUpgradeToggle = async (field: string, value: unknown) => { + try { + await updateSettings({ [field]: value }); + } catch { + message.error('Failed to save auto-upgrade settings'); + } + }; + const formatDate = (dateStr: string) => { try { return new Date(dateStr).toLocaleString(); @@ -845,6 +869,7 @@ function SystemUpgradeTab() { }; const isUpgrading = upgrading || running; + const autoUpgradeEnabled = settings?.enableAutoUpgrade ?? false; return (
@@ -1050,6 +1075,135 @@ function SystemUpgradeTab() { )} + {/* Auto-Upgrade Configuration */} + Auto-Upgrade} + style={{ marginBottom: 16 }} + > + +
+ handleAutoUpgradeToggle('enableAutoUpgrade', v)} + /> + Enable automatic upgrades +
+ {autoUpgradeEnabled && ( + <> +
+ Schedule +
`, + ``, + ``, + ``, + ``, + ``, + `
Previous:${result.previousCommit}
Current:${result.newCommit}
Commits:${result.commitCount}
Duration:${result.durationSeconds}s
Completed:${result.completedAt}
`, + warningsHtml, + `

You can configure auto-upgrade settings in Settings → System.

`, + ].join('\n'); + + for (const email of emails) { + await emailService.sendEmail({ + to: email, + subject, + html, + text: `Auto-upgrade ${statusWord}: ${result.message}\nPrevious: ${result.previousCommit}\nCurrent: ${result.newCommit}\nCommits: ${result.commitCount}\nDuration: ${result.durationSeconds}s`, + }); + } + logger.info(`Auto-upgrade: notification sent to ${emails.length} admin(s)`); + } + } + } catch (err) { + logger.warn('Auto-upgrade: failed to send notification:', err); + } + } + + // Clean up result and marker files + upgradeService.clearResult(); + upgradeService.clearTriggeredBy(); + } + + /** Calculate ms until the next scheduled run. */ + private calculateInitialDelay( + schedule: SchedulePreset, + config: { intervalMs: number; targetHour?: number; targetDay?: number }, + ): number { + if (config.targetHour === undefined) { + // Interval presets (12h, 24h) — start after grace period + return STARTUP_GRACE_MS; + } + + const now = new Date(); + const target = new Date(now); + target.setHours(config.targetHour, 0, 0, 0); + + if (config.targetDay !== undefined) { + // Weekly preset — find the next matching day + const daysUntil = (config.targetDay - now.getDay() + 7) % 7; + if (daysUntil === 0 && now >= target) { + // Same day but already past — next week + target.setDate(target.getDate() + 7); + } else { + target.setDate(target.getDate() + daysUntil); + } + } else { + // Daily preset — if already past today's target, use tomorrow + if (now >= target) { + target.setDate(target.getDate() + 1); + } + } + + return target.getTime() - now.getTime(); + } + + /** Poll status.json until checkedAt changes (indicating check completed). */ + private pollForCheckComplete(previousCheckedAt: string | null): Promise { + return new Promise((resolve) => { + const start = Date.now(); + const interval = setInterval(() => { + if (Date.now() - start > CHECK_POLL_TIMEOUT_MS) { + clearInterval(interval); + resolve(false); + return; + } + const status = upgradeService.getStatus(); + if (status && status.checkedAt !== previousCheckedAt) { + clearInterval(interval); + resolve(true); + } + }, CHECK_POLL_INTERVAL_MS); + }); + } +} + +export const autoUpgradeService = new AutoUpgradeService(); diff --git a/api/src/utils/roles.ts b/api/src/utils/roles.ts index 08f87f64..3cbb6dc4 100644 --- a/api/src/utils/roles.ts +++ b/api/src/utils/roles.ts @@ -3,12 +3,41 @@ import { UserRole } from '@prisma/client'; const ROLE_PRIORITY: Record = { SUPER_ADMIN: 5, INFLUENCE_ADMIN: 4, - MAP_ADMIN: 3, + MAP_ADMIN: 4, + BROADCAST_ADMIN: 4, + CONTENT_ADMIN: 4, + MEDIA_ADMIN: 4, + PAYMENTS_ADMIN: 4, + EVENTS_ADMIN: 4, + SOCIAL_ADMIN: 4, USER: 2, TEMP: 1, }; -export const ADMIN_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.INFLUENCE_ADMIN, UserRole.MAP_ADMIN]; +/** All admin roles (any user with one of these can access /app) */ +export const ADMIN_ROLES: UserRole[] = [ + UserRole.SUPER_ADMIN, + UserRole.INFLUENCE_ADMIN, + UserRole.MAP_ADMIN, + UserRole.BROADCAST_ADMIN, + UserRole.CONTENT_ADMIN, + UserRole.MEDIA_ADMIN, + UserRole.PAYMENTS_ADMIN, + UserRole.EVENTS_ADMIN, + UserRole.SOCIAL_ADMIN, +]; + +// Module-specific role groups +export const INFLUENCE_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.INFLUENCE_ADMIN]; +export const MAP_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN]; +export const BROADCAST_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.BROADCAST_ADMIN]; +export const CONTENT_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.CONTENT_ADMIN]; +export const MEDIA_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MEDIA_ADMIN]; +export const PAYMENTS_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.PAYMENTS_ADMIN]; +export const EVENTS_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.EVENTS_ADMIN]; +export const SOCIAL_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.SOCIAL_ADMIN]; +export const SYSTEM_ROLES: UserRole[] = [UserRole.SUPER_ADMIN]; +export const SCHEDULING_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN, UserRole.EVENTS_ADMIN]; /** Check if the user has any of the specified roles */ export function hasAnyRole(user: { roles?: unknown; role?: UserRole }, roles: UserRole[]): boolean { diff --git a/docker-compose.yml b/docker-compose.yml index 173935c2..f6198c78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: environment: - NODE_ENV=${NODE_ENV:-development} - PORT=4000 - - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:-changemaker}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} + - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} - REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379 - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} @@ -34,7 +34,7 @@ services: - JWT_REFRESH_EXPIRY=${JWT_REFRESH_EXPIRY:-7d} - ENCRYPTION_KEY=${ENCRYPTION_KEY} - INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-admin@cmlite.org} - - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:-REQUIRED_STRONG_PASSWORD_CHANGE_THIS} + - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:?INITIAL_ADMIN_PASSWORD must be set in .env} - SMTP_HOST=${SMTP_HOST:-mailhog-changemaker} - SMTP_PORT=${SMTP_PORT:-1025} - SMTP_USER=${SMTP_USER:-} @@ -139,7 +139,7 @@ services: environment: - NODE_ENV=${NODE_ENV:-development} - MEDIA_API_PORT=${MEDIA_API_PORT:-4100} - - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:-changemaker}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} + - DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2} - REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379 - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} @@ -210,7 +210,7 @@ services: - "127.0.0.1:${V2_POSTGRES_PORT:-5433}:5432" environment: POSTGRES_USER: ${V2_POSTGRES_USER:-changemaker} - POSTGRES_PASSWORD: ${V2_POSTGRES_PASSWORD:-changemaker} + POSTGRES_PASSWORD: ${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} POSTGRES_DB: ${V2_POSTGRES_DB:-changemaker_v2} volumes: - v2-postgres-data:/var/lib/postgresql/data @@ -233,19 +233,19 @@ services: ports: - "${NGINX_HTTP_PORT:-80}:80" - "${NGINX_HTTPS_PORT:-443}:443" - - "8881:8881" # NocoDB embed proxy (strips X-Frame-Options) - - "8882:8882" # n8n embed proxy - - "8883:8883" # Gitea embed proxy - - "8884:8884" # MailHog embed proxy - - "8885:8885" # Mini QR embed proxy - - "8886:8886" # Excalidraw embed proxy - - "8887:8887" # Homepage embed proxy - - "8890:8890" # Vaultwarden embed proxy - - "8891:8891" # Rocket.Chat embed proxy - - "8892:8892" # Gancio embed proxy - - "8893:8893" # Jitsi Meet embed proxy - - "8894:8894" # Grafana embed proxy - - "8895:8895" # Alertmanager embed proxy + - "127.0.0.1:8881:8881" # NocoDB embed proxy (strips X-Frame-Options) + - "127.0.0.1:8882:8882" # n8n embed proxy + - "127.0.0.1:8883:8883" # Gitea embed proxy + - "127.0.0.1:8884:8884" # MailHog embed proxy + - "127.0.0.1:8885:8885" # Mini QR embed proxy + - "127.0.0.1:8886:8886" # Excalidraw embed proxy + - "127.0.0.1:8887:8887" # Homepage embed proxy + - "127.0.0.1:8890:8890" # Vaultwarden embed proxy + - "127.0.0.1:8891:8891" # Rocket.Chat embed proxy + - "127.0.0.1:8892:8892" # Gancio embed proxy + - "127.0.0.1:8893:8893" # Jitsi Meet embed proxy + - "127.0.0.1:8894:8894" # Grafana embed proxy + - "127.0.0.1:8895:8895" # Alertmanager embed proxy healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"] interval: 30s @@ -278,7 +278,7 @@ services: retries: 3 start_period: 30s environment: - NC_DB: "pg://changemaker-v2-postgres:5432?u=${V2_POSTGRES_USER:-changemaker}&p=${V2_POSTGRES_PASSWORD:-changemaker}&d=nocodb_meta" + NC_DB: "pg://changemaker-v2-postgres:5432?u=${V2_POSTGRES_USER:-changemaker}&p=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}&d=nocodb_meta" NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env} NC_PUBLIC_URL: ${NC_PUBLIC_URL:-http://localhost:8091} @@ -307,7 +307,7 @@ services: DB_HOST: changemaker-v2-postgres DB_PORT: "5432" DB_USER: ${V2_POSTGRES_USER:-changemaker} - DB_PASSWORD: ${V2_POSTGRES_PASSWORD:-changemaker} + DB_PASSWORD: ${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} DB_NAME: ${V2_POSTGRES_DB:-changemaker_v2} volumes: - ./scripts/nocodb-init.sh:/init.sh:ro @@ -872,7 +872,7 @@ services: - GANCIO_DB_PORT=5432 - GANCIO_DB_DATABASE=gancio - GANCIO_DB_USERNAME=${V2_POSTGRES_USER:-changemaker} - - GANCIO_DB_PASSWORD=${V2_POSTGRES_PASSWORD:-changemaker} + - GANCIO_DB_PASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} - server__baseurl=${GANCIO_BASE_URL:-https://events.cmlite.org} volumes: - gancio-data:/home/node/data @@ -890,7 +890,7 @@ services: environment: - PGHOST=changemaker-v2-postgres - PGUSER=${V2_POSTGRES_USER:-changemaker} - - PGPASSWORD=${V2_POSTGRES_PASSWORD:-changemaker} + - PGPASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env} - PGDATABASE=gancio - GANCIO_ADMIN_USER=${GANCIO_ADMIN_USER:-admin} - GANCIO_ADMIN_PASSWORD=${GANCIO_ADMIN_PASSWORD} diff --git a/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json b/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json index ba58fd86..787e9886 100644 --- a/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json +++ b/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json @@ -7,10 +7,10 @@ "stars_count": 0, "forks_count": 0, "open_issues_count": 23, - "updated_at": "2026-03-05T12:20:58-07:00", + "updated_at": "2026-03-07T13:10:15-07:00", "created_at": "2025-05-28T14:54:59-06:00", "clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git", "ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git", "default_branch": "main", - "last_build_update": "2026-03-05T12:20:58-07:00" + "last_build_update": "2026-03-07T13:10:15-07:00" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/anthropics-claude-code.json b/mkdocs/docs/assets/repo-data/anthropics-claude-code.json index 035746f5..3ad69445 100644 --- a/mkdocs/docs/assets/repo-data/anthropics-claude-code.json +++ b/mkdocs/docs/assets/repo-data/anthropics-claude-code.json @@ -4,10 +4,10 @@ "description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.", "html_url": "https://github.com/anthropics/claude-code", "language": "Shell", - "stars_count": 74943, - "forks_count": 6013, - "open_issues_count": 5785, - "updated_at": "2026-03-07T19:48:10Z", + "stars_count": 74978, + "forks_count": 6017, + "open_issues_count": 5743, + "updated_at": "2026-03-07T23:09:30Z", "created_at": "2025-02-22T17:41:21Z", "clone_url": "https://github.com/anthropics/claude-code.git", "ssh_url": "git@github.com:anthropics/claude-code.git", diff --git a/mkdocs/docs/assets/repo-data/coder-code-server.json b/mkdocs/docs/assets/repo-data/coder-code-server.json index 5f53681f..b89f89a6 100644 --- a/mkdocs/docs/assets/repo-data/coder-code-server.json +++ b/mkdocs/docs/assets/repo-data/coder-code-server.json @@ -4,10 +4,10 @@ "description": "VS Code in the browser", "html_url": "https://github.com/coder/code-server", "language": "TypeScript", - "stars_count": 76519, - "forks_count": 6539, + "stars_count": 76525, + "forks_count": 6538, "open_issues_count": 169, - "updated_at": "2026-03-07T18:20:51Z", + "updated_at": "2026-03-07T21:35:32Z", "created_at": "2019-02-27T16:50:41Z", "clone_url": "https://github.com/coder/code-server.git", "ssh_url": "git@github.com:coder/code-server.git", diff --git a/mkdocs/docs/assets/repo-data/gethomepage-homepage.json b/mkdocs/docs/assets/repo-data/gethomepage-homepage.json index 30345a2b..a1fae1d5 100644 --- a/mkdocs/docs/assets/repo-data/gethomepage-homepage.json +++ b/mkdocs/docs/assets/repo-data/gethomepage-homepage.json @@ -4,10 +4,10 @@ "description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.", "html_url": "https://github.com/gethomepage/homepage", "language": "JavaScript", - "stars_count": 28761, + "stars_count": 28765, "forks_count": 1808, "open_issues_count": 1, - "updated_at": "2026-03-07T19:52:28Z", + "updated_at": "2026-03-07T22:19:42Z", "created_at": "2022-08-24T07:29:42Z", "clone_url": "https://github.com/gethomepage/homepage.git", "ssh_url": "git@github.com:gethomepage/homepage.git", diff --git a/mkdocs/docs/assets/repo-data/go-gitea-gitea.json b/mkdocs/docs/assets/repo-data/go-gitea-gitea.json index 35540980..8ddc88bc 100644 --- a/mkdocs/docs/assets/repo-data/go-gitea-gitea.json +++ b/mkdocs/docs/assets/repo-data/go-gitea-gitea.json @@ -4,13 +4,13 @@ "description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD", "html_url": "https://github.com/go-gitea/gitea", "language": "Go", - "stars_count": 54164, - "forks_count": 6434, - "open_issues_count": 2847, - "updated_at": "2026-03-07T18:54:25Z", + "stars_count": 54165, + "forks_count": 6433, + "open_issues_count": 2845, + "updated_at": "2026-03-07T20:46:55Z", "created_at": "2016-11-01T02:13:26Z", "clone_url": "https://github.com/go-gitea/gitea.git", "ssh_url": "git@github.com:go-gitea/gitea.git", "default_branch": "main", - "last_build_update": "2026-03-07T05:30:59Z" + "last_build_update": "2026-03-07T20:41:14Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/knadh-listmonk.json b/mkdocs/docs/assets/repo-data/knadh-listmonk.json index 5151e8a6..ce5478fa 100644 --- a/mkdocs/docs/assets/repo-data/knadh-listmonk.json +++ b/mkdocs/docs/assets/repo-data/knadh-listmonk.json @@ -4,10 +4,10 @@ "description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.", "html_url": "https://github.com/knadh/listmonk", "language": "Go", - "stars_count": 19208, + "stars_count": 19210, "forks_count": 1946, "open_issues_count": 99, - "updated_at": "2026-03-07T18:41:21Z", + "updated_at": "2026-03-07T22:13:24Z", "created_at": "2019-06-26T05:08:39Z", "clone_url": "https://github.com/knadh/listmonk.git", "ssh_url": "git@github.com:knadh/listmonk.git", diff --git a/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json b/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json index 4c874edf..d9a0fe0a 100644 --- a/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json +++ b/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json @@ -4,10 +4,10 @@ "description": "Create & scan cute qr codes easily \ud83d\udc7e", "html_url": "https://github.com/lyqht/mini-qr", "language": "Vue", - "stars_count": 1896, + "stars_count": 1897, "forks_count": 238, "open_issues_count": 21, - "updated_at": "2026-03-07T17:03:03Z", + "updated_at": "2026-03-07T20:18:46Z", "created_at": "2023-04-21T14:20:14Z", "clone_url": "https://github.com/lyqht/mini-qr.git", "ssh_url": "git@github.com:lyqht/mini-qr.git", diff --git a/mkdocs/docs/assets/repo-data/n8n-io-n8n.json b/mkdocs/docs/assets/repo-data/n8n-io-n8n.json index d5ceaee8..97603d91 100644 --- a/mkdocs/docs/assets/repo-data/n8n-io-n8n.json +++ b/mkdocs/docs/assets/repo-data/n8n-io-n8n.json @@ -4,13 +4,13 @@ "description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.", "html_url": "https://github.com/n8n-io/n8n", "language": "TypeScript", - "stars_count": 178014, - "forks_count": 55521, - "open_issues_count": 1413, - "updated_at": "2026-03-07T19:43:47Z", + "stars_count": 178023, + "forks_count": 55523, + "open_issues_count": 1415, + "updated_at": "2026-03-07T22:25:31Z", "created_at": "2019-06-22T09:24:21Z", "clone_url": "https://github.com/n8n-io/n8n.git", "ssh_url": "git@github.com:n8n-io/n8n.git", "default_branch": "master", - "last_build_update": "2026-03-07T18:51:26Z" + "last_build_update": "2026-03-07T20:45:37Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/nocodb-nocodb.json b/mkdocs/docs/assets/repo-data/nocodb-nocodb.json index 417a6f5b..2f14b81d 100644 --- a/mkdocs/docs/assets/repo-data/nocodb-nocodb.json +++ b/mkdocs/docs/assets/repo-data/nocodb-nocodb.json @@ -7,7 +7,7 @@ "stars_count": 62371, "forks_count": 4655, "open_issues_count": 627, - "updated_at": "2026-03-07T19:49:07Z", + "updated_at": "2026-03-07T22:40:39Z", "created_at": "2017-10-29T18:51:48Z", "clone_url": "https://github.com/nocodb/nocodb.git", "ssh_url": "git@github.com:nocodb/nocodb.git", diff --git a/mkdocs/docs/assets/repo-data/ollama-ollama.json b/mkdocs/docs/assets/repo-data/ollama-ollama.json index a52d1e2b..f3be3063 100644 --- a/mkdocs/docs/assets/repo-data/ollama-ollama.json +++ b/mkdocs/docs/assets/repo-data/ollama-ollama.json @@ -4,10 +4,10 @@ "description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.", "html_url": "https://github.com/ollama/ollama", "language": "Go", - "stars_count": 164358, + "stars_count": 164368, "forks_count": 14823, "open_issues_count": 2590, - "updated_at": "2026-03-07T19:18:40Z", + "updated_at": "2026-03-07T23:01:55Z", "created_at": "2023-06-26T19:39:32Z", "clone_url": "https://github.com/ollama/ollama.git", "ssh_url": "git@github.com:ollama/ollama.git", diff --git a/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json b/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json index 18a7e100..b16cb38b 100644 --- a/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json +++ b/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json @@ -4,10 +4,10 @@ "description": "Documentation that simply works", "html_url": "https://github.com/squidfunk/mkdocs-material", "language": "Python", - "stars_count": 26199, + "stars_count": 26201, "forks_count": 4047, "open_issues_count": 2, - "updated_at": "2026-03-07T18:01:45Z", + "updated_at": "2026-03-07T22:55:28Z", "created_at": "2016-01-28T22:09:23Z", "clone_url": "https://github.com/squidfunk/mkdocs-material.git", "ssh_url": "git@github.com:squidfunk/mkdocs-material.git", diff --git a/mkdocs/docs/test.md b/mkdocs/docs/test.md index 8e8a9503..6d7fcb0a 100644 --- a/mkdocs/docs/test.md +++ b/mkdocs/docs/test.md @@ -6,6 +6,236 @@ Testing page.
Loading...
+
+

Choose Your Plan

+

Get access to exclusive content and features.

+ View Plans +
+ + + + + +
+
🛒
+
DIGITAL
+

Test Product 1

+

A test product

+

$90.00

+
+ + + + +

+ Secure payment via Stripe. Browse all products +

+
+ +
+ + + + +
+

❤️

+

Support Our Work

+

Every contribution makes a difference. Choose an amount below.

+ +
+ + + + + +
+ + + +
+ + + +
+

❤️

+

Support Our Work

+

Every contribution makes a difference. Choose an amount below.

+
+ $10 + $25 + $50 + $100 +
+ Custom Amount +
+ + +
+

🛒

+

Browse Our Products

+

Reports, toolkits, event tickets, and more.

+ Shop Now +
+ + +
+

Choose Your Plan

+

Get access to exclusive content and features.

+ View Plans +
+ + +
+

❤️

+

Support Our Cause

+

Your contribution helps us create lasting change in our community.

+ Donate Now +
+ + + + +# Test + +Testing page. + + +
Loading...
+ +
play_circleGallery @@ -246,8 +244,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet
play_circleGallery @@ -362,7 +358,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/assets/repo-data/admin-changemaker.lite.json b/mkdocs/site/assets/repo-data/admin-changemaker.lite.json index ba58fd86..787e9886 100644 --- a/mkdocs/site/assets/repo-data/admin-changemaker.lite.json +++ b/mkdocs/site/assets/repo-data/admin-changemaker.lite.json @@ -7,10 +7,10 @@ "stars_count": 0, "forks_count": 0, "open_issues_count": 23, - "updated_at": "2026-03-05T12:20:58-07:00", + "updated_at": "2026-03-07T13:10:15-07:00", "created_at": "2025-05-28T14:54:59-06:00", "clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git", "ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git", "default_branch": "main", - "last_build_update": "2026-03-05T12:20:58-07:00" + "last_build_update": "2026-03-07T13:10:15-07:00" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/anthropics-claude-code.json b/mkdocs/site/assets/repo-data/anthropics-claude-code.json index 035746f5..3ad69445 100644 --- a/mkdocs/site/assets/repo-data/anthropics-claude-code.json +++ b/mkdocs/site/assets/repo-data/anthropics-claude-code.json @@ -4,10 +4,10 @@ "description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.", "html_url": "https://github.com/anthropics/claude-code", "language": "Shell", - "stars_count": 74943, - "forks_count": 6013, - "open_issues_count": 5785, - "updated_at": "2026-03-07T19:48:10Z", + "stars_count": 74978, + "forks_count": 6017, + "open_issues_count": 5743, + "updated_at": "2026-03-07T23:09:30Z", "created_at": "2025-02-22T17:41:21Z", "clone_url": "https://github.com/anthropics/claude-code.git", "ssh_url": "git@github.com:anthropics/claude-code.git", diff --git a/mkdocs/site/assets/repo-data/coder-code-server.json b/mkdocs/site/assets/repo-data/coder-code-server.json index 5f53681f..b89f89a6 100644 --- a/mkdocs/site/assets/repo-data/coder-code-server.json +++ b/mkdocs/site/assets/repo-data/coder-code-server.json @@ -4,10 +4,10 @@ "description": "VS Code in the browser", "html_url": "https://github.com/coder/code-server", "language": "TypeScript", - "stars_count": 76519, - "forks_count": 6539, + "stars_count": 76525, + "forks_count": 6538, "open_issues_count": 169, - "updated_at": "2026-03-07T18:20:51Z", + "updated_at": "2026-03-07T21:35:32Z", "created_at": "2019-02-27T16:50:41Z", "clone_url": "https://github.com/coder/code-server.git", "ssh_url": "git@github.com:coder/code-server.git", diff --git a/mkdocs/site/assets/repo-data/gethomepage-homepage.json b/mkdocs/site/assets/repo-data/gethomepage-homepage.json index 30345a2b..a1fae1d5 100644 --- a/mkdocs/site/assets/repo-data/gethomepage-homepage.json +++ b/mkdocs/site/assets/repo-data/gethomepage-homepage.json @@ -4,10 +4,10 @@ "description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.", "html_url": "https://github.com/gethomepage/homepage", "language": "JavaScript", - "stars_count": 28761, + "stars_count": 28765, "forks_count": 1808, "open_issues_count": 1, - "updated_at": "2026-03-07T19:52:28Z", + "updated_at": "2026-03-07T22:19:42Z", "created_at": "2022-08-24T07:29:42Z", "clone_url": "https://github.com/gethomepage/homepage.git", "ssh_url": "git@github.com:gethomepage/homepage.git", diff --git a/mkdocs/site/assets/repo-data/go-gitea-gitea.json b/mkdocs/site/assets/repo-data/go-gitea-gitea.json index 35540980..8ddc88bc 100644 --- a/mkdocs/site/assets/repo-data/go-gitea-gitea.json +++ b/mkdocs/site/assets/repo-data/go-gitea-gitea.json @@ -4,13 +4,13 @@ "description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD", "html_url": "https://github.com/go-gitea/gitea", "language": "Go", - "stars_count": 54164, - "forks_count": 6434, - "open_issues_count": 2847, - "updated_at": "2026-03-07T18:54:25Z", + "stars_count": 54165, + "forks_count": 6433, + "open_issues_count": 2845, + "updated_at": "2026-03-07T20:46:55Z", "created_at": "2016-11-01T02:13:26Z", "clone_url": "https://github.com/go-gitea/gitea.git", "ssh_url": "git@github.com:go-gitea/gitea.git", "default_branch": "main", - "last_build_update": "2026-03-07T05:30:59Z" + "last_build_update": "2026-03-07T20:41:14Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/knadh-listmonk.json b/mkdocs/site/assets/repo-data/knadh-listmonk.json index 5151e8a6..ce5478fa 100644 --- a/mkdocs/site/assets/repo-data/knadh-listmonk.json +++ b/mkdocs/site/assets/repo-data/knadh-listmonk.json @@ -4,10 +4,10 @@ "description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.", "html_url": "https://github.com/knadh/listmonk", "language": "Go", - "stars_count": 19208, + "stars_count": 19210, "forks_count": 1946, "open_issues_count": 99, - "updated_at": "2026-03-07T18:41:21Z", + "updated_at": "2026-03-07T22:13:24Z", "created_at": "2019-06-26T05:08:39Z", "clone_url": "https://github.com/knadh/listmonk.git", "ssh_url": "git@github.com:knadh/listmonk.git", diff --git a/mkdocs/site/assets/repo-data/lyqht-mini-qr.json b/mkdocs/site/assets/repo-data/lyqht-mini-qr.json index 4c874edf..d9a0fe0a 100644 --- a/mkdocs/site/assets/repo-data/lyqht-mini-qr.json +++ b/mkdocs/site/assets/repo-data/lyqht-mini-qr.json @@ -4,10 +4,10 @@ "description": "Create & scan cute qr codes easily \ud83d\udc7e", "html_url": "https://github.com/lyqht/mini-qr", "language": "Vue", - "stars_count": 1896, + "stars_count": 1897, "forks_count": 238, "open_issues_count": 21, - "updated_at": "2026-03-07T17:03:03Z", + "updated_at": "2026-03-07T20:18:46Z", "created_at": "2023-04-21T14:20:14Z", "clone_url": "https://github.com/lyqht/mini-qr.git", "ssh_url": "git@github.com:lyqht/mini-qr.git", diff --git a/mkdocs/site/assets/repo-data/n8n-io-n8n.json b/mkdocs/site/assets/repo-data/n8n-io-n8n.json index d5ceaee8..97603d91 100644 --- a/mkdocs/site/assets/repo-data/n8n-io-n8n.json +++ b/mkdocs/site/assets/repo-data/n8n-io-n8n.json @@ -4,13 +4,13 @@ "description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.", "html_url": "https://github.com/n8n-io/n8n", "language": "TypeScript", - "stars_count": 178014, - "forks_count": 55521, - "open_issues_count": 1413, - "updated_at": "2026-03-07T19:43:47Z", + "stars_count": 178023, + "forks_count": 55523, + "open_issues_count": 1415, + "updated_at": "2026-03-07T22:25:31Z", "created_at": "2019-06-22T09:24:21Z", "clone_url": "https://github.com/n8n-io/n8n.git", "ssh_url": "git@github.com:n8n-io/n8n.git", "default_branch": "master", - "last_build_update": "2026-03-07T18:51:26Z" + "last_build_update": "2026-03-07T20:45:37Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/nocodb-nocodb.json b/mkdocs/site/assets/repo-data/nocodb-nocodb.json index 417a6f5b..2f14b81d 100644 --- a/mkdocs/site/assets/repo-data/nocodb-nocodb.json +++ b/mkdocs/site/assets/repo-data/nocodb-nocodb.json @@ -7,7 +7,7 @@ "stars_count": 62371, "forks_count": 4655, "open_issues_count": 627, - "updated_at": "2026-03-07T19:49:07Z", + "updated_at": "2026-03-07T22:40:39Z", "created_at": "2017-10-29T18:51:48Z", "clone_url": "https://github.com/nocodb/nocodb.git", "ssh_url": "git@github.com:nocodb/nocodb.git", diff --git a/mkdocs/site/assets/repo-data/ollama-ollama.json b/mkdocs/site/assets/repo-data/ollama-ollama.json index a52d1e2b..f3be3063 100644 --- a/mkdocs/site/assets/repo-data/ollama-ollama.json +++ b/mkdocs/site/assets/repo-data/ollama-ollama.json @@ -4,10 +4,10 @@ "description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.", "html_url": "https://github.com/ollama/ollama", "language": "Go", - "stars_count": 164358, + "stars_count": 164368, "forks_count": 14823, "open_issues_count": 2590, - "updated_at": "2026-03-07T19:18:40Z", + "updated_at": "2026-03-07T23:01:55Z", "created_at": "2023-06-26T19:39:32Z", "clone_url": "https://github.com/ollama/ollama.git", "ssh_url": "git@github.com:ollama/ollama.git", diff --git a/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json b/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json index 18a7e100..b16cb38b 100644 --- a/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json +++ b/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json @@ -4,10 +4,10 @@ "description": "Documentation that simply works", "html_url": "https://github.com/squidfunk/mkdocs-material", "language": "Python", - "stars_count": 26199, + "stars_count": 26201, "forks_count": 4047, "open_issues_count": 2, - "updated_at": "2026-03-07T18:01:45Z", + "updated_at": "2026-03-07T22:55:28Z", "created_at": "2016-01-28T22:09:23Z", "clone_url": "https://github.com/squidfunk/mkdocs-material.git", "ssh_url": "git@github.com:squidfunk/mkdocs-material.git", diff --git a/mkdocs/site/blog/index.html b/mkdocs/site/blog/index.html index bba86a5a..fb730f11 100644 --- a/mkdocs/site/blog/index.html +++ b/mkdocs/site/blog/index.html @@ -203,8 +203,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet
play_circleGallery @@ -268,8 +266,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet
play_circleGallery @@ -384,7 +380,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/comments/callback/index.html b/mkdocs/site/comments/callback/index.html index d0035d81..b641219a 100644 --- a/mkdocs/site/comments/callback/index.html +++ b/mkdocs/site/comments/callback/index.html @@ -196,8 +196,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet
play_circleGallery @@ -261,8 +259,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet
play_circleGallery @@ -377,7 +373,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/advocacy/campaigns/index.html b/mkdocs/site/docs/admin/advocacy/campaigns/index.html index 0a44be0f..46fd38b0 100644 --- a/mkdocs/site/docs/admin/advocacy/campaigns/index.html +++ b/mkdocs/site/docs/admin/advocacy/campaigns/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/advocacy/email-queue/index.html b/mkdocs/site/docs/admin/advocacy/email-queue/index.html index 4085faa2..626a01b7 100644 --- a/mkdocs/site/docs/admin/advocacy/email-queue/index.html +++ b/mkdocs/site/docs/admin/advocacy/email-queue/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/advocacy/index.html b/mkdocs/site/docs/admin/advocacy/index.html index 234ceb1e..2dc28858 100644 --- a/mkdocs/site/docs/admin/advocacy/index.html +++ b/mkdocs/site/docs/admin/advocacy/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/advocacy/representatives/index.html b/mkdocs/site/docs/admin/advocacy/representatives/index.html index a802f3c3..1859b632 100644 --- a/mkdocs/site/docs/admin/advocacy/representatives/index.html +++ b/mkdocs/site/docs/admin/advocacy/representatives/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/advocacy/responses/index.html b/mkdocs/site/docs/admin/advocacy/responses/index.html index 783c44fd..cde012cc 100644 --- a/mkdocs/site/docs/admin/advocacy/responses/index.html +++ b/mkdocs/site/docs/admin/advocacy/responses/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/broadcast/email-templates/index.html b/mkdocs/site/docs/admin/broadcast/email-templates/index.html index f23fe11e..133ef196 100644 --- a/mkdocs/site/docs/admin/broadcast/email-templates/index.html +++ b/mkdocs/site/docs/admin/broadcast/email-templates/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/broadcast/index.html b/mkdocs/site/docs/admin/broadcast/index.html index e455ee60..ae24ccc9 100644 --- a/mkdocs/site/docs/admin/broadcast/index.html +++ b/mkdocs/site/docs/admin/broadcast/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/broadcast/newsletter/index.html b/mkdocs/site/docs/admin/broadcast/newsletter/index.html index 5c883b8e..5aeb8876 100644 --- a/mkdocs/site/docs/admin/broadcast/newsletter/index.html +++ b/mkdocs/site/docs/admin/broadcast/newsletter/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/broadcast/sms/index.html b/mkdocs/site/docs/admin/broadcast/sms/index.html index 371e0743..8510815e 100644 --- a/mkdocs/site/docs/admin/broadcast/sms/index.html +++ b/mkdocs/site/docs/admin/broadcast/sms/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/dashboard/index.html b/mkdocs/site/docs/admin/dashboard/index.html index faf901fc..8e69f419 100644 --- a/mkdocs/site/docs/admin/dashboard/index.html +++ b/mkdocs/site/docs/admin/dashboard/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/index.html b/mkdocs/site/docs/admin/index.html index 182e708c..66f0bef7 100644 --- a/mkdocs/site/docs/admin/index.html +++ b/mkdocs/site/docs/admin/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/areas/index.html b/mkdocs/site/docs/admin/map/areas/index.html index bb505fd5..388beffb 100644 --- a/mkdocs/site/docs/admin/map/areas/index.html +++ b/mkdocs/site/docs/admin/map/areas/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/canvassing/index.html b/mkdocs/site/docs/admin/map/canvassing/index.html index aacbf1e5..1503be4b 100644 --- a/mkdocs/site/docs/admin/map/canvassing/index.html +++ b/mkdocs/site/docs/admin/map/canvassing/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/data-quality/index.html b/mkdocs/site/docs/admin/map/data-quality/index.html index 354f0ae5..d66671af 100644 --- a/mkdocs/site/docs/admin/map/data-quality/index.html +++ b/mkdocs/site/docs/admin/map/data-quality/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/index.html b/mkdocs/site/docs/admin/map/index.html index 2c2470f9..73b6ec80 100644 --- a/mkdocs/site/docs/admin/map/index.html +++ b/mkdocs/site/docs/admin/map/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/locations/index.html b/mkdocs/site/docs/admin/map/locations/index.html index e3dbe9c5..7d365bb5 100644 --- a/mkdocs/site/docs/admin/map/locations/index.html +++ b/mkdocs/site/docs/admin/map/locations/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/settings/index.html b/mkdocs/site/docs/admin/map/settings/index.html index c756105b..b3a40dd8 100644 --- a/mkdocs/site/docs/admin/map/settings/index.html +++ b/mkdocs/site/docs/admin/map/settings/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/map/shifts/index.html b/mkdocs/site/docs/admin/map/shifts/index.html index 9dd41b0d..015dd7ad 100644 --- a/mkdocs/site/docs/admin/map/shifts/index.html +++ b/mkdocs/site/docs/admin/map/shifts/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/media/ads/index.html b/mkdocs/site/docs/admin/media/ads/index.html index 2ebb50db..85f67266 100644 --- a/mkdocs/site/docs/admin/media/ads/index.html +++ b/mkdocs/site/docs/admin/media/ads/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/media/analytics/index.html b/mkdocs/site/docs/admin/media/analytics/index.html index 7e0bfbc9..7af0d61c 100644 --- a/mkdocs/site/docs/admin/media/analytics/index.html +++ b/mkdocs/site/docs/admin/media/analytics/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/media/curated/index.html b/mkdocs/site/docs/admin/media/curated/index.html index 29f8f249..1ad30366 100644 --- a/mkdocs/site/docs/admin/media/curated/index.html +++ b/mkdocs/site/docs/admin/media/curated/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/media/index.html b/mkdocs/site/docs/admin/media/index.html index 766fcb25..a6380dd5 100644 --- a/mkdocs/site/docs/admin/media/index.html +++ b/mkdocs/site/docs/admin/media/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/media/library/index.html b/mkdocs/site/docs/admin/media/library/index.html index 330bd70d..6dcfba79 100644 --- a/mkdocs/site/docs/admin/media/library/index.html +++ b/mkdocs/site/docs/admin/media/library/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/media/moderation/index.html b/mkdocs/site/docs/admin/media/moderation/index.html index 973bb91a..21d3042b 100644 --- a/mkdocs/site/docs/admin/media/moderation/index.html +++ b/mkdocs/site/docs/admin/media/moderation/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/payments/donations/index.html b/mkdocs/site/docs/admin/payments/donations/index.html index 28f68970..be25bd6c 100644 --- a/mkdocs/site/docs/admin/payments/donations/index.html +++ b/mkdocs/site/docs/admin/payments/donations/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/payments/index.html b/mkdocs/site/docs/admin/payments/index.html index e9fce633..b18e7a11 100644 --- a/mkdocs/site/docs/admin/payments/index.html +++ b/mkdocs/site/docs/admin/payments/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/payments/plans/index.html b/mkdocs/site/docs/admin/payments/plans/index.html index a2f23b5a..afbce5e1 100644 --- a/mkdocs/site/docs/admin/payments/plans/index.html +++ b/mkdocs/site/docs/admin/payments/plans/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/payments/products/index.html b/mkdocs/site/docs/admin/payments/products/index.html index ea42a59e..1121a77e 100644 --- a/mkdocs/site/docs/admin/payments/products/index.html +++ b/mkdocs/site/docs/admin/payments/products/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/payments/settings/index.html b/mkdocs/site/docs/admin/payments/settings/index.html index 2a84592e..346a7dd5 100644 --- a/mkdocs/site/docs/admin/payments/settings/index.html +++ b/mkdocs/site/docs/admin/payments/settings/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/people-access/index.html b/mkdocs/site/docs/admin/people-access/index.html index 13dd70ab..30873504 100644 --- a/mkdocs/site/docs/admin/people-access/index.html +++ b/mkdocs/site/docs/admin/people-access/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/services/crowdsec/index.html b/mkdocs/site/docs/admin/services/crowdsec/index.html index 9095572b..aa621078 100644 --- a/mkdocs/site/docs/admin/services/crowdsec/index.html +++ b/mkdocs/site/docs/admin/services/crowdsec/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/services/index.html b/mkdocs/site/docs/admin/services/index.html index 9669ee34..46c6c7d4 100644 --- a/mkdocs/site/docs/admin/services/index.html +++ b/mkdocs/site/docs/admin/services/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/services/integrations/index.html b/mkdocs/site/docs/admin/services/integrations/index.html index 3f2d3a78..443b8110 100644 --- a/mkdocs/site/docs/admin/services/integrations/index.html +++ b/mkdocs/site/docs/admin/services/integrations/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/services/monitoring/index.html b/mkdocs/site/docs/admin/services/monitoring/index.html index a805b77a..e3302124 100644 --- a/mkdocs/site/docs/admin/services/monitoring/index.html +++ b/mkdocs/site/docs/admin/services/monitoring/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/services/tunnel/index.html b/mkdocs/site/docs/admin/services/tunnel/index.html index b05ddc09..b6448ed0 100644 --- a/mkdocs/site/docs/admin/services/tunnel/index.html +++ b/mkdocs/site/docs/admin/services/tunnel/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/services/user-provisioning/index.html b/mkdocs/site/docs/admin/services/user-provisioning/index.html index b67a9b82..2a282590 100644 --- a/mkdocs/site/docs/admin/services/user-provisioning/index.html +++ b/mkdocs/site/docs/admin/services/user-provisioning/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/settings/index.html b/mkdocs/site/docs/admin/settings/index.html index 7fe40852..2d2be7ba 100644 --- a/mkdocs/site/docs/admin/settings/index.html +++ b/mkdocs/site/docs/admin/settings/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/web/documentation/index.html b/mkdocs/site/docs/admin/web/documentation/index.html index 2fe88dde..26b9f678 100644 --- a/mkdocs/site/docs/admin/web/documentation/index.html +++ b/mkdocs/site/docs/admin/web/documentation/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/web/homepage/index.html b/mkdocs/site/docs/admin/web/homepage/index.html index 7a3dc356..aab9f78a 100644 --- a/mkdocs/site/docs/admin/web/homepage/index.html +++ b/mkdocs/site/docs/admin/web/homepage/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/web/index.html b/mkdocs/site/docs/admin/web/index.html index 5c0328f7..1426de37 100644 --- a/mkdocs/site/docs/admin/web/index.html +++ b/mkdocs/site/docs/admin/web/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/web/landing-pages/index.html b/mkdocs/site/docs/admin/web/landing-pages/index.html index 3ec4322b..10bba88c 100644 --- a/mkdocs/site/docs/admin/web/landing-pages/index.html +++ b/mkdocs/site/docs/admin/web/landing-pages/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/admin/web/navigation/index.html b/mkdocs/site/docs/admin/web/navigation/index.html index 00c00924..88af389d 100644 --- a/mkdocs/site/docs/admin/web/navigation/index.html +++ b/mkdocs/site/docs/admin/web/navigation/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/api/index.html b/mkdocs/site/docs/api/index.html index cb82a697..eec06b46 100644 --- a/mkdocs/site/docs/api/index.html +++ b/mkdocs/site/docs/api/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/architecture/index.html b/mkdocs/site/docs/architecture/index.html index acfd670b..08efe595 100644 --- a/mkdocs/site/docs/architecture/index.html +++ b/mkdocs/site/docs/architecture/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/deployment/index.html b/mkdocs/site/docs/deployment/index.html index 368251bb..5e162879 100644 --- a/mkdocs/site/docs/deployment/index.html +++ b/mkdocs/site/docs/deployment/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/control-panel/index.html b/mkdocs/site/docs/getting-started/control-panel/index.html index a852cf69..b0f2f5d3 100644 --- a/mkdocs/site/docs/getting-started/control-panel/index.html +++ b/mkdocs/site/docs/getting-started/control-panel/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/environment-variables/index.html b/mkdocs/site/docs/getting-started/environment-variables/index.html index 1eb71c0e..c2370be6 100644 --- a/mkdocs/site/docs/getting-started/environment-variables/index.html +++ b/mkdocs/site/docs/getting-started/environment-variables/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/features/index.html b/mkdocs/site/docs/getting-started/features/index.html index 452405b2..7c73a1cf 100644 --- a/mkdocs/site/docs/getting-started/features/index.html +++ b/mkdocs/site/docs/getting-started/features/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/first-steps/index.html b/mkdocs/site/docs/getting-started/first-steps/index.html index 9bc3c44a..033c3f36 100644 --- a/mkdocs/site/docs/getting-started/first-steps/index.html +++ b/mkdocs/site/docs/getting-started/first-steps/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/index.html b/mkdocs/site/docs/getting-started/index.html index 95b72b34..2e271a2a 100644 --- a/mkdocs/site/docs/getting-started/index.html +++ b/mkdocs/site/docs/getting-started/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/installation/index.html b/mkdocs/site/docs/getting-started/installation/index.html index 1c4ee0ca..5c01b2d9 100644 --- a/mkdocs/site/docs/getting-started/installation/index.html +++ b/mkdocs/site/docs/getting-started/installation/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/services/index.html b/mkdocs/site/docs/getting-started/services/index.html index 5477388f..62d43c62 100644 --- a/mkdocs/site/docs/getting-started/services/index.html +++ b/mkdocs/site/docs/getting-started/services/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/getting-started/upgrades/index.html b/mkdocs/site/docs/getting-started/upgrades/index.html index 3b1162ec..e266308a 100644 --- a/mkdocs/site/docs/getting-started/upgrades/index.html +++ b/mkdocs/site/docs/getting-started/upgrades/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/index.html b/mkdocs/site/docs/index.html index 48dbe5d9..fc15f870 100644 --- a/mkdocs/site/docs/index.html +++ b/mkdocs/site/docs/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/phil/index.html b/mkdocs/site/docs/phil/index.html index 3401fa5e..81a3b555 100644 --- a/mkdocs/site/docs/phil/index.html +++ b/mkdocs/site/docs/phil/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/services/index.html b/mkdocs/site/docs/services/index.html index 535f342f..bd810a21 100644 --- a/mkdocs/site/docs/services/index.html +++ b/mkdocs/site/docs/services/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/troubleshooting/index.html b/mkdocs/site/docs/troubleshooting/index.html index 8af1c3e1..85f4f0e2 100644 --- a/mkdocs/site/docs/troubleshooting/index.html +++ b/mkdocs/site/docs/troubleshooting/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/campaigns/index.html b/mkdocs/site/docs/user-guide/campaigns/index.html index 594bf5bf..63093d4a 100644 --- a/mkdocs/site/docs/user-guide/campaigns/index.html +++ b/mkdocs/site/docs/user-guide/campaigns/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/donations/index.html b/mkdocs/site/docs/user-guide/donations/index.html index fdc5770d..d5a75b1a 100644 --- a/mkdocs/site/docs/user-guide/donations/index.html +++ b/mkdocs/site/docs/user-guide/donations/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/events/index.html b/mkdocs/site/docs/user-guide/events/index.html index 57c6e8f2..d868257a 100644 --- a/mkdocs/site/docs/user-guide/events/index.html +++ b/mkdocs/site/docs/user-guide/events/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/gallery/index.html b/mkdocs/site/docs/user-guide/gallery/index.html index c874d321..9203cd44 100644 --- a/mkdocs/site/docs/user-guide/gallery/index.html +++ b/mkdocs/site/docs/user-guide/gallery/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/index.html b/mkdocs/site/docs/user-guide/index.html index e25a6b35..9affd44d 100644 --- a/mkdocs/site/docs/user-guide/index.html +++ b/mkdocs/site/docs/user-guide/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/map/index.html b/mkdocs/site/docs/user-guide/map/index.html index 86ad6982..5306d7d6 100644 --- a/mkdocs/site/docs/user-guide/map/index.html +++ b/mkdocs/site/docs/user-guide/map/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/profile/index.html b/mkdocs/site/docs/user-guide/profile/index.html index fb45ea90..ce9057d8 100644 --- a/mkdocs/site/docs/user-guide/profile/index.html +++ b/mkdocs/site/docs/user-guide/profile/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/shifts/index.html b/mkdocs/site/docs/user-guide/shifts/index.html index cc407e32..bbe7d4a0 100644 --- a/mkdocs/site/docs/user-guide/shifts/index.html +++ b/mkdocs/site/docs/user-guide/shifts/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/user-guide/shop/index.html b/mkdocs/site/docs/user-guide/shop/index.html index 13ae3066..9d884e7b 100644 --- a/mkdocs/site/docs/user-guide/shop/index.html +++ b/mkdocs/site/docs/user-guide/shop/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/volunteer/achievements/index.html b/mkdocs/site/docs/volunteer/achievements/index.html index e432fbe6..90cc827f 100644 --- a/mkdocs/site/docs/volunteer/achievements/index.html +++ b/mkdocs/site/docs/volunteer/achievements/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/volunteer/canvassing/index.html b/mkdocs/site/docs/volunteer/canvassing/index.html index 67b8cdfa..1ed4bf59 100644 --- a/mkdocs/site/docs/volunteer/canvassing/index.html +++ b/mkdocs/site/docs/volunteer/canvassing/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/volunteer/index.html b/mkdocs/site/docs/volunteer/index.html index 18297afa..85c7ee13 100644 --- a/mkdocs/site/docs/volunteer/index.html +++ b/mkdocs/site/docs/volunteer/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/volunteer/shifts/index.html b/mkdocs/site/docs/volunteer/shifts/index.html index aa517fc9..f6e6f0ee 100644 --- a/mkdocs/site/docs/volunteer/shifts/index.html +++ b/mkdocs/site/docs/volunteer/shifts/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/docs/volunteer/social/index.html b/mkdocs/site/docs/volunteer/social/index.html index 02877c75..96e4d28a 100644 --- a/mkdocs/site/docs/volunteer/social/index.html +++ b/mkdocs/site/docs/volunteer/social/index.html @@ -205,8 +205,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -270,8 +268,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -386,7 +382,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/main/index.html b/mkdocs/site/main/index.html index 0b2981f0..d456dc57 100644 --- a/mkdocs/site/main/index.html +++ b/mkdocs/site/main/index.html @@ -196,8 +196,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -261,8 +259,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -377,7 +373,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/overrides/main.html b/mkdocs/site/overrides/main.html index a1a01e25..14471db0 100644 --- a/mkdocs/site/overrides/main.html +++ b/mkdocs/site/overrides/main.html @@ -24,8 +24,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -89,8 +87,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -205,7 +201,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/test-page/index.html b/mkdocs/site/test-page/index.html index 4264a3c0..9716cfd4 100644 --- a/mkdocs/site/test-page/index.html +++ b/mkdocs/site/test-page/index.html @@ -196,8 +196,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -261,8 +259,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -377,7 +373,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/site/test/index.html b/mkdocs/site/test/index.html index e4fe5358..8775d557 100644 --- a/mkdocs/site/test/index.html +++ b/mkdocs/site/test/index.html @@ -201,8 +201,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -266,8 +264,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -382,7 +378,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/scripts/upgrade-watcher.sh b/scripts/upgrade-watcher.sh index 87d6fa75..898ed9c5 100755 --- a/scripts/upgrade-watcher.sh +++ b/scripts/upgrade-watcher.sh @@ -36,6 +36,10 @@ fi log "Received trigger: action=${ACTION}" +# Extract triggeredBy and write marker file for the API to read post-restart +TRIGGERED_BY="$(echo "$TRIGGER_CONTENT" | grep -o '"triggeredBy"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"triggeredBy"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//' || true)" +echo "${TRIGGERED_BY:-manual}" > "${UPGRADE_DIR}/triggered-by.txt" + # Remove trigger immediately to prevent re-execution rm -f "$TRIGGER_FILE"