Compare commits
1 Commits
ae_app_3x_
...
ae_app_sve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c212eeb45e |
15
.ae_brief
15
.ae_brief
@@ -1,15 +0,0 @@
|
||||
# Aether Project Brief: aether_app_sveltekit
|
||||
**Last Updated:** 2026-05-21 22:25:05
|
||||
**Current Agent:** mcp_agent
|
||||
|
||||
## 🛠️ What I Just Did
|
||||
Implemented "Force Sync Location" feature. Optimized file download order with a 4-tier chronological sort (Global > Session > Presentation > Creation Date). Added UI button for onsite operators. Updated project documentation. Verified with npm run check.
|
||||
|
||||
## 🚧 Current Blockers
|
||||
None.
|
||||
|
||||
## ➡️ Exact Next Steps
|
||||
User to review changes. Ready for onsite testing/deployment.
|
||||
|
||||
---
|
||||
*Generated by ae_brief*
|
||||
@@ -1,5 +1,5 @@
|
||||
[Dolphin]
|
||||
Timestamp=2024,12,2,17,34,46.735
|
||||
Timestamp=2024,8,7,10,25,9.632
|
||||
Version=4
|
||||
ViewMode=1
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# Build artifacts and local state
|
||||
.svelte-kit/
|
||||
.vite/
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
.cache/
|
||||
|
||||
# VCS and IDE
|
||||
.git/
|
||||
.gitignore
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
.directory
|
||||
|
||||
# Logs and temp files
|
||||
*.log
|
||||
*.bak
|
||||
*.tgz
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Test output and dev-only dirs
|
||||
tests/
|
||||
test-results/
|
||||
test_results/
|
||||
coverage/
|
||||
documentation/
|
||||
backups/
|
||||
.claude/
|
||||
|
||||
# Deployment artifacts
|
||||
npm_deploy/
|
||||
package-lock.json.bak
|
||||
|
||||
# Env files: exclude all live secrets, allow only the per-environment files needed for Docker builds.
|
||||
# .env.local is workstation-only and must never enter a container image.
|
||||
.env
|
||||
.env.*
|
||||
!.env.dev
|
||||
!.env.test
|
||||
!.env.prod
|
||||
@@ -1,19 +0,0 @@
|
||||
# One Sky IT's Aether Framework and System — DEV template (dev-*.oneskyit.com)
|
||||
# Copy to .env.dev and fill in real values.
|
||||
|
||||
|
||||
# Aether API access
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=dev-api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=test-api.oneskyit.com
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
|
||||
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
# One Sky IT's Aether Framework and System — PROD template (api.oneskyit.com)
|
||||
# Copy to .env.prod and fill in real values.
|
||||
# One Sky IT's Aether Framework and System
|
||||
TESTING=This is a test env variable
|
||||
PUBLIC_TESTING=This is a public test env variable
|
||||
|
||||
# Aether API access
|
||||
CONTAINER_AE_APP_NODE=ae_app_node_prod
|
||||
CONTAINER_AE_APP_NODE_RED=ae_app_node_prod_red
|
||||
CONTAINER_AE_APP_NODE_GREEN=ae_app_node_prod_green
|
||||
CONTAINER_AE_APP_NODE_BLUE=ae_app_node_prod_blue
|
||||
OSIT_WEB_HTTP_PORT=8082
|
||||
OSIT_WEB_HTTPS_PORT=4435
|
||||
|
||||
# DOCKER_AE_SERVER_EXTRA_HOST=srv-nyx.oneskyit.com:104.237.143.4
|
||||
# DOCKER_AE_API_SERVER_EXTRA_HOST=api.oneskyit.com:104.237.143.4
|
||||
# DOCKER_AE_API_BAK_SERVER_EXTRA_HOST=bak-api.oneskyit.com:104.237.143.4
|
||||
|
||||
# Aether general shared config options
|
||||
# For general shared config options like API access and use, database access and use, Redis, and SMTP
|
||||
# home development, live testing, live production, onsite development, onsite testing, onsite production???
|
||||
AE_CFG_ID=7
|
||||
|
||||
## Aether API access and use
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=bak-api.oneskyit.com
|
||||
PUBLIC_AE_API_SERVER_INTERNAL=aether_api_gunicorn
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
|
||||
|
||||
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
|
||||
|
||||
# Aether app specific config (SvelteKit)
|
||||
AE_APP_CFG_ID=99
|
||||
AE_APP_ENV=development
|
||||
AE_APP_NODE_PORT=3001
|
||||
AE_APP_NODE_PORT_RED=3002
|
||||
AE_APP_NODE_PORT_GREEN=3003
|
||||
AE_APP_NODE_PORT_BLUE=3004
|
||||
|
||||
PUBLIC_AE_ACCOUNT_ID=XXXX # OSIT = _XY7DXtc9MY; CHOW = 3Iid1aIRY5j
|
||||
PUBLIC_AE_EVENT_ID=XXXX # OSIT = pjrcghqwert; CHOW = Mw6-Nv-Zf-5A
|
||||
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX # OSIT = t8jdjONCs0k; CHOW = ygjEuQQCzvk
|
||||
|
||||
45
.env.staging.default
Normal file
45
.env.staging.default
Normal file
@@ -0,0 +1,45 @@
|
||||
# One Sky IT's Aether Framework and System
|
||||
TESTING=This is a test env variable
|
||||
PUBLIC_TESTING=This is a public test env variable
|
||||
|
||||
CONTAINER_AE_APP_NODE=ae_app_node_prod
|
||||
CONTAINER_AE_APP_NODE_RED=ae_app_node_prod_red
|
||||
CONTAINER_AE_APP_NODE_GREEN=ae_app_node_prod_green
|
||||
CONTAINER_AE_APP_NODE_BLUE=ae_app_node_prod_blue
|
||||
OSIT_WEB_HTTP_PORT=8082
|
||||
OSIT_WEB_HTTPS_PORT=4435
|
||||
|
||||
# DOCKER_AE_SERVER_EXTRA_HOST=srv-nyx.oneskyit.com:104.237.143.4
|
||||
# DOCKER_AE_API_SERVER_EXTRA_HOST=dev-api.oneskyit.com:192.168.32.20 # Odd because this env is the development server
|
||||
# DOCKER_AE_API_SERVER_EXTRA_HOST=dev-api.oneskyit.com:192.168.32.99 # Odd because this env is the development server
|
||||
# DOCKER_AE_API_BAK_SERVER_EXTRA_HOST=test-api.oneskyit.com:104.237.143.4 # Odd because this env is the development server
|
||||
# DOCKER_AE_API_TEST_SERVER_EXTRA_HOST=test-api.oneskyit.com:104.237.143.4
|
||||
|
||||
# Aether general shared config options
|
||||
# For general shared config options like API access and use, database access and use, Redis, and SMTP
|
||||
# home development, live testing, live production, onsite development, onsite testing, onsite production???
|
||||
AE_CFG_ID=5
|
||||
|
||||
## Aether API access and use
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=dev-api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=test-api.oneskyit.com
|
||||
PUBLIC_AE_API_SERVER_INTERNAL=aether_api_gunicorn
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=dFP6J9DVj9hUgIMn-fNIqg
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=zp5PtX4zUsI
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
|
||||
|
||||
# Aether app specific config (SvelteKit)
|
||||
AE_APP_CFG_ID=99
|
||||
AE_APP_ENV=development
|
||||
AE_APP_NODE_PORT=3001
|
||||
AE_APP_NODE_PORT_RED=3002
|
||||
AE_APP_NODE_PORT_GREEN=3003
|
||||
AE_APP_NODE_PORT_BLUE=3004
|
||||
|
||||
PUBLIC_AE_ACCOUNT_ID=XXXX # OSIT = _XY7DXtc9MY; CHOW = 3Iid1aIRY5j
|
||||
PUBLIC_AE_EVENT_ID=XXXX # OSIT = pjrcghqwert; CHOW = Mw6-Nv-Zf-5A
|
||||
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX # OSIT = t8jdjONCs0k; CHOW = ygjEuQQCzvk
|
||||
@@ -1,18 +0,0 @@
|
||||
# One Sky IT's Aether Framework and System — TEST template (test-api.oneskyit.com)
|
||||
# Copy to .env.test and fill in real values.
|
||||
|
||||
# Aether API access
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=test-api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=api.oneskyit.com
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
|
||||
|
||||
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
@@ -1,31 +1,31 @@
|
||||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
.DS_Store
|
||||
.directory
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
@@ -8,8 +7,7 @@ node_modules
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.prod.default
|
||||
!.env.test.default
|
||||
!.env.dev.default
|
||||
!.env.staging.default
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
@@ -31,6 +29,4 @@ backups/
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.kate-swp
|
||||
test_results
|
||||
test-results
|
||||
*.kate-swp
|
||||
21
.prettierrc
21
.prettierrc
@@ -1,17 +1,8 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"bracketSameLine": true,
|
||||
|
||||
"svelteSortOrder": "options-scripts-markup-styles",
|
||||
"svelteIndentScriptAndStyle": false,
|
||||
"svelteAllowShorthand": true,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
247
.vscode/settings.json
vendored
247
.vscode/settings.json
vendored
@@ -1,129 +1,120 @@
|
||||
{
|
||||
"prettier.documentSelectors": ["**/*.svelte"],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"animIndeterminate",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"badge",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonAction",
|
||||
"buttonBack",
|
||||
"buttonClasses",
|
||||
"buttonComplete",
|
||||
"buttonDismiss",
|
||||
"buttonNeutral",
|
||||
"buttonNext",
|
||||
"buttonPositive",
|
||||
"buttonTextCancel",
|
||||
"buttonTextConfirm",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"buttonTextSubmit",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"chips",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"flexDirection",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"position",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionChipList",
|
||||
"regionChipWrapper",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionFooter",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInput",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"transition",
|
||||
"width",
|
||||
"zIndex"
|
||||
],
|
||||
"explorer.fileNesting.enabled": false,
|
||||
"cSpell.words": [
|
||||
"lpignore",
|
||||
"prejoin"
|
||||
],
|
||||
"markdownlint.config": {
|
||||
"MD004": false,
|
||||
"MD007": false,
|
||||
"MD030": false,
|
||||
"MD033": false
|
||||
}
|
||||
}
|
||||
"prettier.documentSelectors": [
|
||||
"**/*.svelte"
|
||||
],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"animIndeterminate",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"badge",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonAction",
|
||||
"buttonBack",
|
||||
"buttonClasses",
|
||||
"buttonComplete",
|
||||
"buttonDismiss",
|
||||
"buttonNeutral",
|
||||
"buttonNext",
|
||||
"buttonPositive",
|
||||
"buttonTextCancel",
|
||||
"buttonTextConfirm",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"buttonTextSubmit",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"chips",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"flexDirection",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"position",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionChipList",
|
||||
"regionChipWrapper",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionFooter",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInput",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"transition",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
}
|
||||
119
CLAUDE.md
119
CLAUDE.md
@@ -1,119 +0,0 @@
|
||||
# Claude Code — Project: Aether App SvelteKit
|
||||
|
||||
**Part of:** One Sky IT / Aether Platform
|
||||
**Backend:** `~/OSIT_dev/aether_api_fastapi/` via Docker
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL: Privacy & Business Rules
|
||||
|
||||
### IDAA — International Doctors in Alcoholics Anonymous
|
||||
- **ALL IDAA content is PRIVATE. Authentication required. Never public.**
|
||||
- BB (Bulletin Board / Posts) — always private
|
||||
- Archives — always private
|
||||
- Recovery Meetings — always private
|
||||
- A previous AI agent accidentally exposed IDAA BB publicly. This is a severe security failure.
|
||||
- IDAA users authenticate via Novi API (Novi UUID per member) at `Authenticated` permission level.
|
||||
- Default permission required: `trusted_access` or higher for IDAA module.
|
||||
|
||||
### Journals
|
||||
- Private personal data. Always authenticated. Passcode/encryption features exist.
|
||||
|
||||
### General
|
||||
- **Never expose private content publicly.** When in doubt — it's private.
|
||||
- Code comments must explain the WHY for any non-obvious business logic.
|
||||
Key features have been silently broken by AI agents that didn't understand intent.
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Workflow
|
||||
|
||||
1. **Before starting:** Read `documentation/TODO__Agents.md` for active tasks
|
||||
2. **Before committing:** Run `npx svelte-check` — no exceptions
|
||||
3. **Commits:** Atomic — one component or fix per commit
|
||||
4. **Never delete files with `rm`** — move to `~/tmp/agents_trash`
|
||||
5. **Backend coordination:** Use `ae_send_message` or flag changes clearly
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Svelte 5 (runes mode) + SvelteKit v2
|
||||
- **Styling:** Tailwind CSS v4, Flowbite; Skeleton UI being phased out
|
||||
- **State:** `$state`, `$derived` runes + Dexie.js IndexedDB (`liveQuery`)
|
||||
- **Icons:** Lucide
|
||||
- **Editors:** CodeMirror 6 (primary), Edra/TipTap (secondary)
|
||||
- **Markdown:** `marked` library
|
||||
- **UI primitives:** ShadCN (`src/lib/components/ui/`)
|
||||
- **Native/Electron bridge:** `src/lib/electron/electron_relay.ts` via `window.aetherNative`
|
||||
|
||||
---
|
||||
|
||||
## API (V3)
|
||||
|
||||
- Base: `/v3/crud/{obj_type}/` for all CRUD
|
||||
- Lookups: `/v3/lookup/`
|
||||
- Search: `POST /v3/crud/{obj_type}/search`
|
||||
- Auth headers: `x-aether-api-key` + `x-account-id` (NOT Bearer tokens)
|
||||
- Permissive PATCH: add `x-ae-ignore-extra-fields: true`
|
||||
- Guide: `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **Canonical reference:** Journals module (`src/lib/ae_journals/`) — use as the pattern template
|
||||
- **Object files:** `ae_<module>__<object>.ts` + `<object>.editable_fields.ts`
|
||||
- **DB:** `db_<module>.ts` per module (Dexie instance)
|
||||
- **LiveQuery:** `lq__xyz` (read-only), `lqw__xyz` (writable form snapshot)
|
||||
- **Load pattern:** SWR — return Dexie cache immediately, refresh from API in background
|
||||
- **Component naming:** `ae_comp__*` (route-level), `ae_<module>_comp__*` (module), `element_*` (reusable)
|
||||
- **Standard fields:** `id`, `id_random`, `code`, `name`, `enable`, `hide`, `priority`, `sort`, `group`, `notes`, `created_on`, `updated_on`, `cfg_json`, `data_json`
|
||||
|
||||
---
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
src/lib/
|
||||
ae_api/ — API helpers (prefer V3)
|
||||
ae_core/ — Account, User, Person, Site, Address, Contact
|
||||
ae_events/ — Events, sessions, presenters, badges, locations, devices
|
||||
ae_journals/ — Journals (canonical/frontier model)
|
||||
ae_archives/ — Archives
|
||||
ae_posts/ — Posts + Post Comments (IDAA BB)
|
||||
ae_idaa/ — IDAA custom module
|
||||
ae_reports/ — Reporting
|
||||
elements/ — Reusable UI: V3 editor, CRUD, data store, CodeMirror
|
||||
electron/ — Native app bridge
|
||||
|
||||
src/routes/
|
||||
/core/ — Admin (accounts, people, sites, users, contacts)
|
||||
/events/[id]/ — Events: pres_mgmt, launcher, badges, leads, settings
|
||||
/journals/ — Journals
|
||||
/idaa/ — IDAA: archives, bb, recovery_meetings, video_conferences
|
||||
/hosted_files/ — File management
|
||||
/testing/ — Dev testing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Active Issues (check TODO__Agents.md for current state)
|
||||
|
||||
- Sev-1: `PUBLIC_AE_API_SECRET_KEY` audit — see TODO__Agents.md (assessed acceptable, 2026-03-11)
|
||||
- V3 CRUD migration — remaining legacy API wrappers in events/sponsorships/core (see `PROJECT__Use_AE_API_V3_CRUD_upgrade.md`)
|
||||
- Style Review Phase 3 — IDAA + Pres Mgmt card polish deferred post-April 2026 conference
|
||||
|
||||
---
|
||||
|
||||
## Key Docs
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `documentation/TODO__Agents.md` | Active task list — read first |
|
||||
| `documentation/GUIDE__Development.md` | Dev SOP |
|
||||
| `documentation/GUIDE__AE_API_V3_for_Frontend.md` | V3 API reference (authoritative) |
|
||||
| `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` | Dexie + liveQuery patterns |
|
||||
| `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns |
|
||||
| `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` | Electron/Launcher |
|
||||
| `documentation/PROJECT__AE_Events_Badges_Review_Print.md` | Badges — kiosk editing (Task 4.0 open) |
|
||||
62
Dockerfile
62
Dockerfile
@@ -1,62 +0,0 @@
|
||||
# Stage 1: Build the application
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies first for better Docker layer caching.
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the source code.
|
||||
COPY . .
|
||||
|
||||
# Build Argument to determine build environment (dev, test, prod).
|
||||
ARG BUILD_MODE=dev
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Sync the SvelteKit project to generate ./.svelte-kit/tsconfig.json
|
||||
RUN npx svelte-kit sync
|
||||
|
||||
# Perform the build based on the BUILD_MODE argument.
|
||||
# Each script uses vite --mode <name>, which reads .env.<name> directly — no cp hack needed.
|
||||
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
|
||||
npm run build:prod; \
|
||||
elif [ "$BUILD_MODE" = "test" ]; then \
|
||||
npm run build:test; \
|
||||
else \
|
||||
npm run build:dev; \
|
||||
fi
|
||||
|
||||
# Copy the source env file to .env.runtime for the deploy stage.
|
||||
# PUBLIC_* vars are baked into the JS bundle by vite; non-PUBLIC vars (AE_CFG_ID,
|
||||
# AE_APP_NODE_PORT) are read by the Node server at runtime and need this file.
|
||||
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
|
||||
cp .env.prod .env.runtime; \
|
||||
elif [ "$BUILD_MODE" = "test" ]; then \
|
||||
cp .env.test .env.runtime; \
|
||||
else \
|
||||
cp .env.dev .env.runtime; \
|
||||
fi
|
||||
|
||||
# Stage 2: Final runtime image
|
||||
FROM node:24-alpine AS deploy-node
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built files and package info.
|
||||
COPY --from=builder /app/build .
|
||||
COPY --from=builder /app/package.json .
|
||||
COPY --from=builder /app/package-lock.json .
|
||||
|
||||
# Install only production dependencies.
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Copy the runtime env file (non-PUBLIC vars for the Node server).
|
||||
COPY --from=builder /app/.env.runtime .env
|
||||
|
||||
# SvelteKit (via adapter-node) defaults to port 3000.
|
||||
EXPOSE 3000
|
||||
|
||||
# Healthcheck to verify the app is running
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD node -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
42
GEMINI.md
42
GEMINI.md
@@ -1,42 +0,0 @@
|
||||
# Aether Frontend Agent Context: Gemini CLI Standard
|
||||
> **Role:** Aether App Orchestrator (SvelteKit Frontend)
|
||||
> **Location:** GEMINI.md (Project Root)
|
||||
|
||||
## 🚨 MANDATORY PROTOCOL
|
||||
You must follow the safety, testing, and coordination standards defined in:
|
||||
`documentation/GUIDE__DEVELOPMENT.md`
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Technical Domain: Aether Frontend
|
||||
### Stack & UI Standards
|
||||
- **Framework:** SvelteKit v2 + Svelte 5 (Runes)
|
||||
- **Styling:** Tailwind 4 + Skeleton UI + Skeleton V3 (SkeletonNext)
|
||||
- **Local Cache:** Dexie.js (IndexedDB)
|
||||
- **Icons:** Standardizing on **Lucide-Svelte**. Avoid legacy Font-Awesome where possible.
|
||||
|
||||
### Reactivity Patterns (The "Aether Way")
|
||||
- **Svelte 5 Runes:** Use `$state`, `$derived`, and `$effect`.
|
||||
- **Navigation Shield:** Use SvelteKit's `page.url` searchParams as the single source of truth for navigation-driven selection. Sync to global stores only via `untrack()` within effects.
|
||||
- **Side-Effect Purge:** Keep `liveQuery` observables pure. Do NOT update global stores ($events_slct) inside derived observables to prevent infinite reactivity loops.
|
||||
- **Dexie LiveQuery:** Use the `$` prefix (e.g., `$lq__obj`) consistently. Results from `liveQuery` are observables.
|
||||
- **Initialization:** Always initialize reactive state (`$state`) outside of `$props()` destructuring.
|
||||
|
||||
### Performance & Data
|
||||
- **SWR (Stale-While-Revalidate):** Never block SvelteKit `load` functions with API calls for cached data. Let the UI render instantly from IndexedDB and update reactively.
|
||||
- **Triple-ID Pattern:** Map `id`, `[obj_type]_id`, and `[obj_type]_id_random` consistently.
|
||||
- **ID Vision:** The backend uses String IDs. Primary keys should use `_id` or `id` (String) mapping to backend `id_random`.
|
||||
|
||||
## 🧠 Recent Strategy & Patterns
|
||||
- **Safe Handover (Native):** Rename `.tmp` to `.file` ONLY after SHA-256 verification in Electron.
|
||||
- **Envelopes:** API helpers automatically handle the `{data: ...}` envelope returned by the backend.
|
||||
- **Bootstrap Paradox:** Use unauthenticated bypass (`x-no-account-id: "Nothing to See Here"`) for initial site/domain lookups.
|
||||
- **Sev-1 Incident Recovery (2026-02-13):** Purged redundant/misplaced headers (`x-aether-api-token`, `Access-Control-Allow-Origin`). Unified all CRUD helpers to standard `/v3/crud/...` paths.
|
||||
- **Account ID Scavenging:** Core fetch helpers now proactively read `account_id` from `localStorage` (`ae_loc`) if missing from config. This is the mandatory fix for Svelte 5 hydration race conditions where `onMount` triggers API calls before global stores are synced.
|
||||
- **V3 Event File Mapping (2026-02-19):** Mapped prefixed backend fields (`hosted_file_hash_sha256`, `hosted_file_size`) to flat properties (`hash_sha256`, `file_size`) in the data layer. Required `inc_hosted_file=true` query param for full metadata retrieval.
|
||||
- **Launcher Location Discovery:** Resolved missing locations in room select by ensuring `load_ae_obj_li__event_location` requests `hidden: 'all'` during background sync and initial load.
|
||||
|
||||
## 🤝 Coordination & Continuity
|
||||
- **Handshake:** Use the `message` tool to notify the Backend Agent of UI/Data requirements.
|
||||
- **Active Tasks:** Track your progress in `documentation/AGENT_TODO.md`.
|
||||
- **Reference:** See `README.md` for build/deploy steps and `TODO.md` for project milestones.
|
||||
326
README.md
326
README.md
@@ -1,286 +1,130 @@
|
||||
# One Sky IT's Aether App - SvelteKit v2 with Svelte v5
|
||||
# One Sky IT's Aether App - SvelteKit v2
|
||||
This uses SvelteKit version 2, TailwindCSS, and Skelton.
|
||||
|
||||
This uses SvelteKit version 2.x with Svelte version 5.x, DexieJS 4.x, TailwindCSS 4.1, and Skeleton.
|
||||
|
||||
# Modules
|
||||
# Current Modules
|
||||
## AE Events - Speakers (/events_speakers)
|
||||
### Components
|
||||
* +page.svelte - The main page for the Events - Speakers module
|
||||
* 10_edit_modal__event_presenter_obj.svelte - The modal for editing a presenter
|
||||
* 10_list__event_presenter_obj.svelte - The list of presenters/speakers
|
||||
* 10_view_modal__event_presenter_obj.svelte - The modal for viewing a presenter
|
||||
#### [slug]
|
||||
* +page.svelte - The main page for the presenter ID [slug]
|
||||
|
||||
## Core (`/core/`)
|
||||
## AE Sponsorships (/sponsorships)
|
||||
* +page.svelte - The main page for the Sponsorships module
|
||||
* 10_edit_modal__sponsorship_obj.svelte - The modal for editing a sponsorship
|
||||
* 10_list__sponsorship_obj.svelte - The list of sponsorships
|
||||
* 10_view_modal__sponsorship_obj.svelte - The modal for viewing a sponsorship
|
||||
#### [slug]
|
||||
* +page.svelte - The main page for the sponsorship ID [slug]
|
||||
|
||||
Admin-only views for foundational Aether objects. Minimal UI — primarily used for data management.
|
||||
|
||||
- **Accounts** (`/core/accounts/`, `/core/accounts/[account_id]/`)
|
||||
- **Activity Logs** (`/core/activity_logs/`)
|
||||
- **Addresses** (`/core/addresses/`, `/core/addresses/[address_id]/`)
|
||||
- **Contacts** (`/core/contacts/`, `/core/contacts/[contact_id]/`)
|
||||
- **Lookups** (`/core/lookups/`) — Countries, subdivisions, time zones
|
||||
- **People** (`/core/people/`, `/core/people/[person_id]/`)
|
||||
- **Sites** (`/core/sites/`, `/core/sites/[site_id]/`)
|
||||
- **Users** (`/core/users/`, `/core/users/[user_id]/`)
|
||||
# Future Modules
|
||||
## AE Events - Badges (/events_badges)
|
||||
* +page.svelte - The main page for the Events - Badges module
|
||||
* 10_list__event_badge_obj.svelte - The list of badges
|
||||
* 10_view_modal__event_badge_obj.svelte - The modal for viewing a badge
|
||||
|
||||
## Events (`/events/`)
|
||||
## AE Events - Exhibit Leads (/events_exhibit_leads)
|
||||
## AE Events - Presentation Management (/events_pres_mgmt)
|
||||
|
||||
The primary client-facing module for conference and event management.
|
||||
|
||||
### Event List (`/events/`)
|
||||
|
||||
### Event Detail (`/events/[event_id]/`)
|
||||
|
||||
Each event has four sub-modules, each in its own SvelteKit route group:
|
||||
|
||||
#### Presentation Management (`/(pres_mgmt)/`)
|
||||
|
||||
Manages the full conference program.
|
||||
|
||||
- `/events/[event_id]/pres_mgmt/` — Dashboard
|
||||
- `/events/[event_id]/locations/` — Location list
|
||||
- `/events/[event_id]/location/[event_location_id]/` — Location detail
|
||||
- `/events/[event_id]/presenter/[presenter_id]/` — Presenter detail
|
||||
- `/events/[event_id]/session/[session_id]/` — Session detail
|
||||
- `/events/[event_id]/reports/` — Presenter, session, and file reports
|
||||
|
||||
#### Launcher (`/(launcher)/`)
|
||||
|
||||
Kiosk display system; runs on-site to show session schedules and presenter info.
|
||||
|
||||
- `/events/[event_id]/launcher/` — Launcher home
|
||||
- `/events/[event_id]/launcher/[event_location_id]/` — Location-specific display
|
||||
|
||||
#### Badges (`/(badges)/`)
|
||||
|
||||
Badge printing and management for event attendees.
|
||||
|
||||
- `/events/[event_id]/badges/` — Badge list
|
||||
- `/events/[event_id]/badges/[badge_id]/` — Badge detail
|
||||
- `/events/[event_id]/badges/[badge_id]/print` — Print a single badge
|
||||
- `/events/[event_id]/badges/[badge_id]/review` — Review before printing
|
||||
- `/events/[event_id]/badges/print_list/` — Bulk print queue
|
||||
- `/events/[event_id]/badges/stats/` — Badge statistics
|
||||
- `/events/[event_id]/templates/` — Badge template management
|
||||
|
||||
#### Leads (`/(leads)/`)
|
||||
|
||||
Exhibitor lead capture via QR scan or manual entry.
|
||||
|
||||
- `/events/[event_id]/leads/` — Exhibit list
|
||||
- `/events/[event_id]/leads/exhibit/[exhibit_id]/` — Exhibit detail and lead capture
|
||||
- `/events/[event_id]/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/` — Lead detail
|
||||
|
||||
#### Event Settings (`/settings/`)
|
||||
|
||||
- `/events/[event_id]/settings/` — Event configuration (basic info, pres mgmt, badges, abstracts)
|
||||
|
||||
## Journals (`/journals/`)
|
||||
|
||||
The "frontier" module — most fully-featured and used as the canonical implementation reference.
|
||||
|
||||
- `/journals/` — Journal list
|
||||
- `/journals/[journal_id]/` — Journal detail and entry list
|
||||
- `/journals/[journal_id]/entry/[journal_entry_id]/` — Journal entry detail and editor
|
||||
|
||||
## IDAA (`/idaa/`)
|
||||
|
||||
Custom module for the IDAA client. Built on core Aether objects (Events, Posts, Archives).
|
||||
|
||||
- `/idaa/` — IDAA home / dashboard
|
||||
|
||||
### Archives (`/idaa/archives/`)
|
||||
|
||||
- `/idaa/archives/` — Archive list with media player
|
||||
- `/idaa/archives/[archive_id]/` — Archive detail and content list
|
||||
|
||||
### Bulletin Board (`/idaa/bb/`)
|
||||
|
||||
Built on the Posts and Post Comments objects.
|
||||
|
||||
- `/idaa/bb/` — Post list
|
||||
- `/idaa/bb/[post_id]/` — Post detail and comments
|
||||
|
||||
### Recovery Meetings (`/idaa/recovery_meetings/`)
|
||||
|
||||
Built on the Events object.
|
||||
|
||||
- `/idaa/recovery_meetings/` — Meeting list with search/filter
|
||||
- `/idaa/recovery_meetings/[event_id]/` — Meeting detail
|
||||
|
||||
### Video Conferences (`/idaa/video_conferences/`)
|
||||
|
||||
- `/idaa/video_conferences/` — Video conference list (Jitsi integration)
|
||||
- `/idaa/jitsi_reports/` — Jitsi usage reports
|
||||
|
||||
## Hosted Files (`/hosted_files/`)
|
||||
|
||||
- `/hosted_files/` — File list and upload management
|
||||
- `/hosted_files/video_util/` — Video processing utility
|
||||
|
||||
## Testing (`/testing/`)
|
||||
|
||||
Developer sandbox pages — not for production use.
|
||||
|
||||
- `/testing/ae_obj_field_editor/` — V3 field editor playground
|
||||
- `/testing/data_store/` — Data store V3 playground
|
||||
- `/testing/editor_test/` — CodeMirror / TipTap editor tests
|
||||
- `/testing/hosted_files/` — File upload tests
|
||||
|
||||
# How to build and deploy SvelteKit:
|
||||
|
||||
The deployment is fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built inside a clean Docker container using `vite build --mode <env>`, which reads the corresponding `.env.<env>` file for `PUBLIC_` variables.
|
||||
|
||||
## Environments
|
||||
|
||||
| Environment | Env file | Vite mode | API server |
|
||||
| ----------- | ----------- | --------- | ------------------------- |
|
||||
| dev | `.env.dev` | `dev` | `dev-api.oneskyit.com` |
|
||||
| test | `.env.test` | `test` | `test-api.oneskyit.com` |
|
||||
| prod | `.env.prod` | `prod` | `api.oneskyit.com` |
|
||||
|
||||
## Commands (from `aether_app_sveltekit/`)
|
||||
|
||||
Copy the contents of the "build" directory to ./npm_deploy/build/
|
||||
```bash
|
||||
# Active development — Vite HMR, no Docker
|
||||
npm run dev
|
||||
|
||||
# Build Vite output only (no Docker)
|
||||
npm run build:dev
|
||||
npm run build:test
|
||||
npm run build:prod
|
||||
|
||||
# Build Docker image and restart container locally
|
||||
npm run build:docker:dev
|
||||
npm run build:docker:test
|
||||
npm run build:docker:prod
|
||||
|
||||
# Deploy to remote server (SSH → linode.oneskyit.com → deploy.sh)
|
||||
npm run deploy:remote:test
|
||||
npm run deploy:remote:prod
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Unified Orchestration**: All services (API, UI, Redis) are managed via `~/OSIT_dev/aether_container_env/docker-compose.yml`.
|
||||
- **Dockerfile**: Multi-stage build. Stage 1 (builder) runs `vite build --mode $BUILD_MODE` using `.env.$BUILD_MODE`. Stage 2 (runtime) creates the final lightweight Node image.
|
||||
- **Environment Handling**:
|
||||
- `PUBLIC_` variables are baked into the image at build time via the `.env.<mode>` file.
|
||||
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
|
||||
- **Remote deploy**: `aether_container_env/deploy.sh` handles git pull + Docker build + restart on the server. Triggered via `npm run deploy:remote:*`.
|
||||
|
||||
### Client-Side Cache & IDB Version Management
|
||||
|
||||
The app uses Dexie (IndexedDB) as a local cache for API data (SWR pattern). To prevent
|
||||
stale cached records from persisting across deploys, two version-tracking systems exist
|
||||
in `src/lib/stores/store_versions.ts`:
|
||||
|
||||
**localStorage store versions (`AE_LOC_VERSION`, etc.)**
|
||||
Track the schema of persisted Svelte stores (`ae_loc`, `ae_events_loc`, etc.).
|
||||
Bump when a store's shape changes in a breaking way (field type change, required rename).
|
||||
The check runs synchronously at module import time, before any store hydrates.
|
||||
|
||||
**IDB content versions (`IDB_CONTENT_VERSIONS`)**
|
||||
Track the content shape of Dexie table rows — specifically what `properties_to_save`
|
||||
writes to each table. Bump when `properties_to_save` in an object file changes in a way
|
||||
that makes existing cached rows stale (fields added/removed/renamed, computed field behavior
|
||||
changed). The `check_and_clear_idb_table()` helper reads a localStorage key per table and
|
||||
clears the Dexie table on mismatch. Call it from the module's layout on mount.
|
||||
|
||||
**When to bump `IDB_CONTENT_VERSIONS`:**
|
||||
If you change `properties_to_save` in `ae_events__event.ts` (or any other object file),
|
||||
bump the matching entry here. Failure to do so has historically caused silent "no data"
|
||||
states that are extremely difficult to diagnose — stale rows pass silently, filter to zero,
|
||||
and the error looks identical to a genuinely empty result.
|
||||
|
||||
Currently wired: `events.event` (via `src/routes/idaa/(idaa)/+layout.svelte`).
|
||||
All other tables are defined but not yet wired — see the comment block in `store_versions.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Developing (Local HMR)
|
||||
|
||||
For the best developer experience with Hot Module Replacement (HMR), start a local development server on your host machine:
|
||||
|
||||
If this is just a quick build update then only the build directory needs to be copied (rsync).
|
||||
```bash
|
||||
npm run dev
|
||||
rsync -vhrz --exclude 'node_modules' ~/OSIT_dev/ae_app_svelte_tailwind_skeleton/build/ ~/OSIT_dev/ae_env_node_app/npm_deploy/build/ --delete
|
||||
|
||||
rsync -vhrz ~/OSIT_dev/ae_env_node_app/npm_deploy/build/ scott@linode.oneskyit.com:/srv/env/prod_aether_sveltekit/npm_deploy/build/ --delete
|
||||
```
|
||||
|
||||
The local dev server will communicate with the **FastAPI backend running in Docker** (typically via `dev-api.oneskyit.com`). This gives you the speed of local Svelte development with the power of the full Aether stack.
|
||||
If this includes package updates we need to copy the new package.json. Manually copy the new package.json file to ./npm_deploy/. This also needs to be copied to the server.
|
||||
```bash
|
||||
# copy/paste, rsync, or cp
|
||||
```
|
||||
|
||||
Run the --omit dev to clear out the node_modules directory. Copy the root node_modules directory to ./npm_deploy/build/node_modules/ after running te omit dev command.
|
||||
```bash
|
||||
npm ci --omit dev
|
||||
```
|
||||
|
||||
Everything should be ready to run on the development server and production server.
|
||||
|
||||
|
||||
# Rebuild the node_modules directory and manually install extra Svelte packages
|
||||
|
||||
Run the npm update to fix the node_modules directory and package.json
|
||||
|
||||
```bash
|
||||
npm list
|
||||
npm outdated
|
||||
npm update
|
||||
npm outdated
|
||||
npm list
|
||||
```
|
||||
|
||||
Other installs?:
|
||||
Are both still needed? I know at least one of these is. 2024-07-23
|
||||
|
||||
```bash
|
||||
npm install --save-dev svelte-highlight
|
||||
npm install --save-dev typescript-svelte-plugin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Set up and run
|
||||
|
||||
## Packages and dependencies
|
||||
|
||||
```bash
|
||||
npm install --save-dev svelte-highlight typescript-svelte-plugin
|
||||
npm install flowbite flowbite-svelte tailwind-merge @popperjs/core
|
||||
```
|
||||
|
||||
I am slowly switching from Font-Awesome to Lucide
|
||||
## Build
|
||||
|
||||
## Tiptap Editor
|
||||
## Environment file
|
||||
### ".env"
|
||||
This is the default used if others are not found when when "npm run dev" or "npm run build" is run.
|
||||
|
||||
- Eventually use Edra? https://edra.tsuzat.com/
|
||||
- Best Rich Text Editor, made for Svelte Developers with Tiptap
|
||||
- ShadEditor is "evolving" to be Edra.
|
||||
- ShadCN is still stuck on Tailwind 3. Waiting to upgrade to Tailwind 4.x. Tailwind 4.x was released in late January 2025. ShadCN is still being worked on as of late March 2025.
|
||||
- [https://github.com/huntabyte/shadcn-svelte/issues/1643](https://github.com/huntabyte/shadcn-svelte/issues/1643)
|
||||
### ".env.local"
|
||||
This is used when "npm run dev" is run. This is not used in the production build.
|
||||
|
||||
Need to install ShadCN and Lucide for the Tiptap editor.
|
||||
### ".env.production"
|
||||
This is used when "npm run build" is run. This is not used in the development build.
|
||||
|
||||
### ".env:prod"
|
||||
This is modified to allow for a staging environment and production environment built.
|
||||
|
||||
### ".env:staging"
|
||||
This is modified to allow for a staging environment and production environment built.
|
||||
|
||||
### Example Important Values when running in dev:
|
||||
Note our home IP address that changes.
|
||||
```bash
|
||||
npm install shadcn-svelte
|
||||
npm install lucide-svelte
|
||||
npm install mode-watcher
|
||||
DOCKER_AE_API_SERVER_EXTRA_HOST=dev-api.oneskyit.com:108.28.68.107
|
||||
|
||||
PUBLIC_AE_API_SERVER=api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=bak-api.oneskyit.com
|
||||
```
|
||||
|
||||
Now we initialize the ShadCN and ShadEditor packages. Follow the command line instructions.
|
||||
|
||||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
npx shadcn-svelte@next init
|
||||
npx shadcn-svelte@next add dropdown-menu button tooltip input popover separator
|
||||
npx shadeditor init
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
More packages related to the Tiptap editor???
|
||||
|
||||
```bash
|
||||
npm install @tiptap/extension-link @tiptap/extension-bullet-list @tiptap/extension-history @tiptap/extension-typography @tiptap/extension-underline
|
||||
```
|
||||
|
||||
### Environment file
|
||||
|
||||
The application uses standard SvelteKit `.env` files for build-time configuration (specifically for `PUBLIC_` prefixed variables).
|
||||
|
||||
- **`.env.dev`**: Used by `npm run build:docker:dev` and `npm run build:dev`.
|
||||
- **`.env.test`**: Used by `npm run build:docker:test` and `npm run build:test`.
|
||||
- **`.env.prod`**: Used by `npm run build:docker:prod` and `npm run build:prod`.
|
||||
- **`.env.local`**: Used during local development (`npm run dev`).
|
||||
|
||||
**Note:** Runtime variables (like private API keys or DB credentials) are managed in the deployment directory's `.env` file and passed to the containers via Docker Compose.
|
||||
|
||||
---
|
||||
|
||||
## Developing
|
||||
|
||||
Start a local development server:
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -289,16 +133,14 @@ npm run dev
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Deployment
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
# Build Docker image locally and restart container
|
||||
npm run build:docker:dev
|
||||
npm run build:docker:prod
|
||||
|
||||
# Deploy to remote server (linode.oneskyit.com)
|
||||
npm run deploy:remote:test
|
||||
npm run deploy:remote:prod
|
||||
npm run build
|
||||
```
|
||||
|
||||
These commands use the multi-stage **Dockerfile** to build the app in a clean environment and automatically restart the corresponding Docker containers.
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
||||
14
ae_app_svelte_tailwind_skeleton.code-workspace
Normal file
14
ae_app_svelte_tailwind_skeleton.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"cSpell.words": [
|
||||
"filelist"
|
||||
],
|
||||
"git.autofetch": true,
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"cSpell.words": [
|
||||
"autofetch",
|
||||
"Axonius",
|
||||
"displayplacer",
|
||||
"elif",
|
||||
"filelist",
|
||||
"gsettings",
|
||||
"onsave"
|
||||
],
|
||||
"git.autofetch": true,
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"npx svelte-check": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
## BuildKit-friendly multi-stage Dockerfile example for Aether frontend
|
||||
|
||||
# Stage 1: dependencies
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --prefer-offline
|
||||
|
||||
# Stage 2: build
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
# optionally reuse deps from previous stage
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
# If you want to use BuildKit cache mounts during local development, uncomment the next line
|
||||
# RUN --mount=type=cache,target=/root/.npm npm ci
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: runtime (static site served by nginx)
|
||||
FROM nginx:stable-alpine AS runtime
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
# Notes:
|
||||
# - Keep dependency installation separate from copying source to maximize cache hits when only application code changes.
|
||||
# - For backend images, follow the same pattern: install deps early, copy source later, and keep a small final runtime image.
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Example CI script to build and push an image with buildx using registry cache.
|
||||
# This script is provider-agnostic and intended to be run inside CI where
|
||||
# Docker and buildx are available and authenticated against the registry.
|
||||
|
||||
REGISTRY=${REGISTRY:-ghcr.io/ORG/REPO}
|
||||
IMAGE_TAG=${IMAGE_TAG:-staging}
|
||||
CACHE_REF=${CACHE_REF:-${REGISTRY}:cache}
|
||||
|
||||
echo "Building ${REGISTRY}:${IMAGE_TAG} using registry cache ${CACHE_REF}"
|
||||
|
||||
docker buildx build \
|
||||
--push \
|
||||
--tag ${REGISTRY}:${IMAGE_TAG} \
|
||||
--cache-from type=registry,ref=${CACHE_REF} \
|
||||
--cache-to type=registry,ref=${CACHE_REF},mode=max \
|
||||
.
|
||||
|
||||
echo "Build complete. Image: ${REGISTRY}:${IMAGE_TAG}"
|
||||
|
||||
# Optional: instruct devs how to run locally with a local cache
|
||||
cat <<'EOF'
|
||||
Local test with BuildKit and local cache:
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--tag myapp:staging \
|
||||
--cache-to=type=local,dest=/tmp/docker-cache \
|
||||
--cache-from=type=local,src=/tmp/docker-cache .
|
||||
|
||||
Prune local builder cache older than 72 hours:
|
||||
docker builder prune --filter "until=72h" --force
|
||||
EOF
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app.css",
|
||||
"baseColor": "gray"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# Plan: Fix IDAA Jitsi Breakout Links
|
||||
|
||||
IDAA Jitsi meetings are embedded in an iframe on the `idaa.org` website. To allow members to "break out" of the iframe (for a better experience on mobile or to use full-tab features), the app provides an "Open Meeting Externally" link.
|
||||
|
||||
Currently, this link is generated from `$page.url.href`, which often lacks the `key` (site access key) and `uuid` (Novi identity token) required for Aether's authentication gate, especially if the user has navigated internally within SvelteKit.
|
||||
|
||||
## 1. Objective
|
||||
Ensure all "Breakout" and "Copy Link" actions on the IDAA Video Conferences page include the necessary `key` and `uuid` parameters.
|
||||
|
||||
## 2. Implementation Steps
|
||||
|
||||
### Step 1: Update `src/routes/idaa/(idaa)/video_conferences/+page.svelte`
|
||||
- Create a reactive `breakout_url` derived from `$page.url.href`.
|
||||
- In the derivation logic:
|
||||
- Instantiate a `new URL`.
|
||||
- Check if `key` is present in `searchParams`. If missing, pull from `$ae_loc.allow_access` or `$ae_loc.site_access_key`.
|
||||
- Check if `uuid` is present in `searchParams`. If missing, pull from `$idaa_loc.novi_uuid`.
|
||||
- Return the resulting `href`.
|
||||
- Update the following UI elements to use `breakout_url`:
|
||||
- `copy_meeting_link` function (uses `navigator.clipboard.writeText`).
|
||||
- "Open in New Tab" anchor tag (`href`).
|
||||
- "Copy Link" fallback textarea (`value`).
|
||||
- "Copy Break-out Link" in the Jitsi Tools panel (`value`).
|
||||
|
||||
### Step 2: Update `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- Add a note in the "Authentication: Novi UUID System" or "Iframe Integration" section about the requirement for `key` and `uuid` in breakout links.
|
||||
- Document that the frontend now automatically re-injects these for external links to ensure persistent session access outside the iframe.
|
||||
|
||||
## 3. Verification
|
||||
- Manually verify the logic:
|
||||
- If `key` and `uuid` are already in the URL (e.g., initial load), the derived URL should remain unchanged (or correctly deduplicated).
|
||||
- If they are missing (e.g., after navigating from another IDAA page), they should be added to the generated link.
|
||||
- Run `npx svelte-check` to ensure no syntax regressions.
|
||||
@@ -1,53 +0,0 @@
|
||||
# Plan: Launcher Config UX Refinement (Cohesion & Stability)
|
||||
|
||||
The goal of this plan is to address the visual "bouncing", layering overload, and the misplaced close button in the new Launcher configuration modal.
|
||||
|
||||
## 1. Dimensional Stability
|
||||
- **Problem:** Switching tabs causes the modal to resize vertically and horizontally, leading to a "bouncing" feel.
|
||||
- **Solution:**
|
||||
- Set a fixed height for the `Launcher_cfg` container (e.g., `h-[750px]`).
|
||||
- Use `overflow-y-auto` only for the right-hand content pane.
|
||||
- Ensure the sidebar has a stable width.
|
||||
|
||||
## 2. Visual Hierarchy & Layering
|
||||
- **Problem:** Too many nested backgrounds (Page > Launcher > Modal > Inner Pane > Section Pane > Section Content).
|
||||
- **Solution:**
|
||||
- Flatten the background of the main content pane.
|
||||
- Simplify `Launcher_Cfg_Section.svelte`:
|
||||
- Remove `shadow-xl` from individual sections.
|
||||
- Use subtler borders instead of strong "preset-outlined" colors.
|
||||
- Remove the secondary background (`bg-white/5`) from the section content area.
|
||||
- Standardize on a single, clean surface color for the right-hand pane.
|
||||
|
||||
## 3. The "Centered Close Button" Bug
|
||||
- **Problem:** A close button is appearing in the middle of the screen.
|
||||
- **Investigation:**
|
||||
- Check for absolute-positioned elements in `Launcher_cfg.svelte` or `+layout.svelte`.
|
||||
- Verify if Flowbite's `Modal` default close button is clashing with internal buttons.
|
||||
- **Solution:**
|
||||
- Consolidate all "Close" actions.
|
||||
- Use the Modal's built-in top-right close button (if available) or a single, well-positioned button in the sidebar.
|
||||
|
||||
## 4. Implementation Steps
|
||||
|
||||
### Step 1: Update `Launcher_cfg.svelte`
|
||||
- Set stable dimensions: `h-[750px] max-h-[90vh] w-[1000px] max-w-[95vw]`.
|
||||
- Remove internal shadows and borders that conflict with the Modal container.
|
||||
- Clean up the sidebar "Close" button.
|
||||
|
||||
### Step 2: Update `Launcher_cfg_section.svelte`
|
||||
- Simplify the styling to reduce visual clutter.
|
||||
- Remove `shadow-xl`.
|
||||
- Use consistent padding and margins.
|
||||
|
||||
### Step 3: Update `+layout.svelte`
|
||||
- Ensure the `Modal` is configured for a stable, large view without default padding issues.
|
||||
- Verify the `modal_cfg_open` logic.
|
||||
|
||||
### Step 4: Add `Launcher_cfg_field.svelte` (Helper)
|
||||
- Implement a unified field helper to standardize Label/Description/Input layouts across all tabs.
|
||||
|
||||
## 5. Verification
|
||||
- Toggle between all 7 tabs. Verify zero layout shift (height/width remains constant).
|
||||
- Check appearance in Light and Dark modes.
|
||||
- Verify "Technical Mode" transitions.
|
||||
@@ -1,30 +0,0 @@
|
||||
# AE Docker CI Cache Policy (recommendation)
|
||||
|
||||
Purpose
|
||||
- Provide a straightforward policy to keep build caches useful but bounded.
|
||||
|
||||
Recommendations
|
||||
- Primary CI cache: **registry-based buildx cache** (preferred). Use a single cache ref (e.g. `ghcr.io/ORG/REPO:cache`) reused by CI builds.
|
||||
- Local dev cache: use `--cache-to type=local` for fast iteration but prune periodically.
|
||||
- Retention: keep registry cache for 30 days by default. Implement registry GC or lifecycle rule to delete older cache blobs.
|
||||
|
||||
Rotation strategy
|
||||
- Option A (simple): CI always writes to the same cache ref `:cache`. Periodically (monthly) run a job to `docker pull` and `docker image rm` older tags if you use date-based tagging.
|
||||
- Option B (date-tag): CI writes cache to `cache-YYYYMMDD` and a small scheduled job deletes tags older than 30 days.
|
||||
|
||||
Pruning commands (developer)
|
||||
- Remove local build cache older than 72 hours:
|
||||
```bash
|
||||
docker builder prune --filter "until=72h" --force
|
||||
```
|
||||
- Remove all builder cache (aggressive):
|
||||
```bash
|
||||
docker builder prune --all --force
|
||||
```
|
||||
|
||||
CI runner requirements
|
||||
- `docker` and `docker buildx` available in runner environment.
|
||||
- Registry credentials provided via CI secrets with permission to push/pull images.
|
||||
|
||||
Security & Secrets
|
||||
- Do not store registry credentials in repo. Use CI secret storage.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,177 +0,0 @@
|
||||
# Aether Project Architecture
|
||||
|
||||
This document outlines the overall architecture and key technologies used in the Aether SvelteKit frontend project.
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
The Aether project is a Svelte and SvelteKit based application, utilizing Tailwind CSS and Skeleton for styling and UI elements. It serves as the frontend UI/UX for the Aether system, which interacts with a Python FastAPI backend.
|
||||
|
||||
## 2. Core Technologies
|
||||
|
||||
- **Frontend Framework:** Svelte 5 and SvelteKit v2
|
||||
- **Routing:** SvelteKit's file-system based routing.
|
||||
- **Styling:** Tailwind CSS v4
|
||||
- **UI Component Libraries:**
|
||||
- Skeleton (Design System, Tailwind Components, Functional Components - being phased out due to conflicts with Tailwind CSS v4)
|
||||
- Flowbite (Tailwind Components)
|
||||
- Custom Components (a growing library of `ae_comp__*` and `element_*` components)
|
||||
- **Text/Code Editors:**
|
||||
- CodeMirror 6.x (`element_editor_codemirror.svelte`) — source/code editing, markdown
|
||||
- TipTap (`element_editor_tiptap.svelte`) — WYSIWYG rich-text for content fields (IDAA, Journals, Leads notes)
|
||||
- **Icons:** Lucide Icons (SVG Icons)
|
||||
- **Markdown Parsing:** `marked` library
|
||||
- **State Management:** Svelte stores, potentially with `liveQuery` from Dexie for reactive IndexedDB interactions.
|
||||
|
||||
### 2.1. Journals as the Canonical Frontend Pattern
|
||||
|
||||
The Journals module is the current frontend reference for configuration modal structure and journal-entry field semantics. When other docs disagree, Journals should be treated as the implementation target until proven otherwise.
|
||||
|
||||
- Entry Config modal sections now follow `Metadata`, `Status & Security`, `Privacy Flags`, `Alerts & Messaging`, and `Admin`.
|
||||
- `summary` is a first-class journal-entry field and belongs with metadata.
|
||||
- `alert` and `alert_msg` are separate fields: the flag and its text payload.
|
||||
- `priority` is a boolean flag in the object model, while `sort` remains the numeric ordering field.
|
||||
|
||||
## 3. Module Structure
|
||||
|
||||
The Aether project is organized into several modules, categorized as Core, Extended, and Custom.
|
||||
|
||||
### 3.1. Official Modules
|
||||
|
||||
#### Core Modules
|
||||
|
||||
These are foundational modules essential for the application's basic functionality.
|
||||
|
||||
- **Accounts:** Minimal implementation.
|
||||
- **Files:** Manages hosted files.
|
||||
- **People:** Minimal implementation for person records.
|
||||
- **Sites:** Minimal implementation for site configurations.
|
||||
- **Users:** Minimal implementation for user management.
|
||||
|
||||
#### Extended Modules
|
||||
|
||||
These modules provide additional features and functionalities.
|
||||
|
||||
- **Archives:** Minimal implementation.
|
||||
- **Events:** Includes features for Badges and Presentation Management.
|
||||
- **Posts:** Minimal implementation.
|
||||
- **Journals:** Manages journal entries.
|
||||
|
||||
#### Custom Modules
|
||||
|
||||
These modules are tailored for specific client needs.
|
||||
|
||||
- **IDAA:** Includes Archives, Bulletin Board (BB), and Recovery Meetings functionalities.
|
||||
|
||||
## 4. Data Storage Mechanisms
|
||||
|
||||
### 4.1. Local Storage
|
||||
|
||||
Used for client-side persistence of various application states and configurations.
|
||||
|
||||
- `api`: API-related settings.
|
||||
- `app`: Global application settings.
|
||||
- `core`: Settings and data specific to core modules.
|
||||
- `<module>`: Settings and data specific to extended modules.
|
||||
- `<custom>`: Settings and data specific to custom modules.
|
||||
|
||||
### 4.2. IndexedDB (Dexie.js)
|
||||
|
||||
Used for more structured client-side data storage, often for caching and offline capabilities.
|
||||
|
||||
- `ae_core_db`: Core database instance.
|
||||
- `<module>`: Module-specific database instances.
|
||||
- `<custom>`: Custom module-specific database instances (none currently defined).
|
||||
|
||||
## 5. Data Sorting
|
||||
|
||||
Standardized sorting orders are applied across various data lists.
|
||||
|
||||
- **Default/General:** `group > priority (flag) > sort > updated_on/created_on`
|
||||
- **Specific (e.g., Events):** `type > start_date/time > code or name`
|
||||
|
||||
## 6. Object Properties and Fields
|
||||
|
||||
A set of standardized field names and types are used across Aether objects.
|
||||
|
||||
### 6.1. Core Standard Fields
|
||||
|
||||
These fields are expected to be present in most Aether objects.
|
||||
|
||||
- `id`: Primary key for an object (internal use, often a UUID).
|
||||
- `id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
|
||||
- `code`: Short, unique identifier.
|
||||
- `name`: Display name.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
- `hide`: Boolean for visibility.
|
||||
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||
- `sort`: Numeric value for ordering within a priority group.
|
||||
- `group`: Categorization string.
|
||||
- `notes`: General notes/comments.
|
||||
- `created_on`: Timestamp of creation.
|
||||
- `updated_on`: Timestamp of last update.
|
||||
|
||||
### 6.2. Special Use Fields
|
||||
|
||||
Fields with specific purposes or conditional usage.
|
||||
|
||||
- `for_type`: Indicates the type of object this object is linked to.
|
||||
- `for_id`: The ID of the object this object is linked to.
|
||||
- `archive_on`: Timestamp for archiving.
|
||||
- `passcode`: Password or access code.
|
||||
- `external_id`: ID from an external system.
|
||||
|
||||
### 6.3. Configuration and JSON Fields
|
||||
|
||||
Fields designed to store JSON data.
|
||||
|
||||
- `cfg_json`: Configuration data in JSON format.
|
||||
- `data_json`: General data in JSON format.
|
||||
- `linked_li_json`: List of linked items in JSON format.
|
||||
|
||||
### 6.4. Special Generated Fields (Client-side)
|
||||
|
||||
Fields generated on the client-side, primarily for sorting or UI logic.
|
||||
|
||||
- `tmp_sort_1`
|
||||
- `tmp_sort_2`
|
||||
- `tmp_sort_3`
|
||||
|
||||
### 6.5. Future Standard Fields
|
||||
|
||||
A list of potential future standard fields, often prefixed with `obj_`. These are currently conceptual and not yet fully integrated.
|
||||
|
||||
- `obj_id`, `obj_ext_uid`, `obj_ext_id`, `obj_import_id`, `obj_code`, `obj_account_id`, `obj_passcode`, `obj_type`, `obj_type_ver_id`, `obj_name`, `obj_summary`, `obj_outline`, `obj_description`, `obj_enable`, `obj_enable_on`, `obj_archive_on`, `obj_hide`, `obj_priority`, `obj_sort`, `obj_group`, `obj_cfg_json`, `obj_notes`, `obj_created_on`, `obj_updated_on`.
|
||||
|
||||
## 7. Runtime Environment: Browser vs Electron
|
||||
|
||||
The Aether SvelteKit frontend runs in a standard web browser in almost all cases. The Electron native app is a **very specialized exception** with a narrow, specific purpose.
|
||||
|
||||
### 7.1. Standard Browser (Default)
|
||||
|
||||
All Aether modules run in a regular browser (Chrome, Chromium, Firefox, Safari). This includes:
|
||||
- Badge printing — `window.print()` works well across Chrome, Chromium, and Firefox. Chrome is recommended for onsite badge printing stations.
|
||||
- All CRUD operations, event management, journals, IDAA, reports, etc.
|
||||
- No special browser configuration required.
|
||||
|
||||
### 7.2. Electron Native App (Specialized — Pres Mgmt Launcher Only)
|
||||
|
||||
The Electron app (`aether_app_native_electron/`) exists **solely** to support the **Events Presentation Management Launcher**. It provides OS-level capabilities that a browser sandbox cannot:
|
||||
- Control of third-party presentation software (PowerPoint, Keynote, LibreOffice Impress)
|
||||
- Local filesystem access for slide file management
|
||||
- Hardware telemetry for connected devices
|
||||
|
||||
**What Electron is NOT used for:**
|
||||
- Badge printing (browser works well)
|
||||
- Any other Aether module
|
||||
- Any general-purpose Aether functionality
|
||||
|
||||
The bridge is exposed as `window.aetherNative` (set by Electron's preload script). All code that calls `window.aetherNative` should degrade gracefully when it is `undefined` (i.e., in a normal browser). See: `src/lib/electron/electron_relay.ts`.
|
||||
|
||||
**When to assume Electron is available:** Only inside the `/events/[event_id]/(launcher)/` route group, and only when the page was loaded from the native app.
|
||||
|
||||
## 8. IndexedDB LiveQuery Usage
|
||||
|
||||
- `lq__xyz_obj`: Used for general read-only access to liveQuery results.
|
||||
- `lqw__xyz_obj`: Used for forms and binding values, representing a writable snapshot of liveQuery results.
|
||||
- **Note:** Care must be taken when binding to `lqw__xyz_obj` to manage updates and potential conflicts with the underlying liveQuery.
|
||||
@@ -1,146 +0,0 @@
|
||||
# Aether Project Components
|
||||
|
||||
This document details the various UI components used throughout the Aether SvelteKit frontend project, categorized by their scope and functionality.
|
||||
|
||||
## 1. Aether Components (UI/UX)
|
||||
|
||||
### 1.1. System Components
|
||||
|
||||
These components are part of the core application shell and provide global functionalities.
|
||||
|
||||
- **`header`**: Application-wide header.
|
||||
- **`main/module`s**: Main content area for modules.
|
||||
- **`footer`**: Application-wide footer.
|
||||
- **`app`**: Provides global application functionalities such as:
|
||||
- Refresh application state.
|
||||
- Clear IndexedDB.
|
||||
- Clear local storage (settings).
|
||||
- Toggle iframe visibility (also updates URL parameter).
|
||||
- Copy current URL.
|
||||
- Generate and display QR codes.
|
||||
- **`menu`**: Various menus for different purposes:
|
||||
- **`mode`**: Edit mode toggle, more options (all or details).
|
||||
- **`access_type`**: Passcode input, clear access.
|
||||
- **`user`**: Sign in/out, reset password, email link, change username and email.
|
||||
- **`theme`**: Mode (light/dark), name (theme list).
|
||||
- **`debug`**: Developer-facing tools:
|
||||
- Toggle debug mode (also updates URL parameter).
|
||||
- Show core and module storages.
|
||||
- Manually set initial timestamp.
|
||||
- **`scroll_to`**: Navigation controls for scrolling:
|
||||
- Scroll to top of the page.
|
||||
- Scroll page up.
|
||||
- Scroll page down.
|
||||
- Scroll to bottom of the page.
|
||||
|
||||
### 1.2. Core Components
|
||||
|
||||
These are reusable components that provide common functionalities across different modules.
|
||||
|
||||
- **`copy_btn`**: A button to copy content to the clipboard.
|
||||
- Properties: `clipboard`, `bind:value`, `btn_text`, `btn_html`.
|
||||
- **`txt_editor`**: A basic text area editor.
|
||||
- **`md_editor`**: Markdown/rich-text editing handled by two active components:
|
||||
- `element_editor_codemirror.svelte` — CodeMirror 6, used for source/code editing
|
||||
- `element_editor_tiptap.svelte` — TipTap (WYSIWYG), used for rich-text content fields
|
||||
- **`html_editor`**: HTML editor.
|
||||
- **`media_player`**: Component for playing media files.
|
||||
- Properties: `hosted_file`, `archive_content`, `media_player`.
|
||||
- Bindings: `bind:host_id`, `bind:media_type`.
|
||||
- Status: `stopped`, `paused`, `playing`.
|
||||
- **`hosted_file_li`**: Manages a list of hosted files, making them available for selection.
|
||||
- **`hosted_file_link_to`**: Lists links per object, with bindings to add/remove links.
|
||||
- **`upload_to_host`**: Component for uploading files to the host.
|
||||
- Handles multiple files.
|
||||
- Properties: `link_type`, `link_id`, `inner fragment` (label html).
|
||||
- Bindings: `bind:trigger`, `bind:show_spinner`, `bind:show_percent`.
|
||||
- Status: `started`, `uploading`, `finished`.
|
||||
- **`upload_file_tbl`**: Table for uploaded files, includes checks for duplicate file hashes and removal from the list.
|
||||
- **`download_from_host`**: Component for downloading files from the host.
|
||||
- Bindings: `bind:host_file_id`, `bind:filename`, `bind:file_ext`.
|
||||
- Properties: `btn inner fragment`.
|
||||
- Bindings: `bind:trigger`, `bind:show_spinner`, `bind:show_percent`.
|
||||
- Status: `started`, `downloading`, `finished`.
|
||||
- **`data_store`**: Component for interacting with data stores.
|
||||
- **`element_ae_obj_field_editor`**: Standard single-field inline editor. Replaces retired `ae_crud` v1/v2 components.
|
||||
- Props: `object_type`, `object_id`, `field_name`, `field_type`, `current_value`
|
||||
- Field types: `text`, `textarea`, `select`, `tiptap`, `checkbox`, `date`, `datetime`, `number`
|
||||
- Callbacks: `on_success`, `on_error`
|
||||
- Respects `$ae_loc.edit_mode` — edit trigger hidden when edit mode is off.
|
||||
- **`sql_qry`**: Component for executing SQL queries.
|
||||
- **`obj_tbl`**: Object SQL results table or similar.
|
||||
- **`qr_scanner`**: Component for scanning QR codes.
|
||||
- **`websocket`**: Component for WebSocket communication.
|
||||
|
||||
### 1.3. Main / Module Components
|
||||
|
||||
These are components specific to main application sections or individual modules.
|
||||
|
||||
- **`menu`**:
|
||||
- **`options`**: Various settings, show/hide content and options, limit, sorting options.
|
||||
- **`actions`**: Various actions, sign in/out, email.
|
||||
|
||||
### 1.4. Object Menu
|
||||
|
||||
A standardized menu for interacting with objects.
|
||||
|
||||
- **Properties Displayed:** `id`, `name`, `group`, `priority`, `sort`, `alert`, `hide`, `enable`, `note`.
|
||||
- **Future Properties:** `ext_id`, `ext_sys_id`, `code` (not yet ready).
|
||||
- **Actions:** `create`, `view`, `edit`, `update`, `hide`, `disable`, `delete`, `alert` (message), `archive` (not yet ready).
|
||||
- **Future Actions:** `copy`, `import`.
|
||||
- **Sort Options:**
|
||||
- `[default]`: `group > priority (flag) > sort (ASC/DESC) > alert > name`
|
||||
- `[sort_updated]`: `group > priority (flag) > sort (ASC/DESC) > alert > updated_on > created_on`
|
||||
- `[priority_updated]`: `group > priority (flag) > updated_on (ASC/DESC) > created_on`
|
||||
- `[priority_name]`: `group > priority (flag) > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||
- `[name]`: `priority (flag) > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||
- `[created_on]`: `priority (flag) > created_on (ASC/DESC)`
|
||||
- `[updated_on]`: `priority (flag) > updated_on (ASC/DESC) > created_on`
|
||||
|
||||
## 2. Pop-ups
|
||||
|
||||
Standardized structure for various types of pop-up elements.
|
||||
|
||||
- **`modal_header`**:
|
||||
- `title`
|
||||
- `close` button
|
||||
- **`modal_main`**: Main content area of the modal.
|
||||
- **`modal_meta`**: Meta-information section.
|
||||
- **`modal_footer`**:
|
||||
- `close` button
|
||||
- **`Pop-up Modal (blocking)`**: A modal that blocks interaction with the rest of the page.
|
||||
- `modal position`
|
||||
- **`Pop-up Modal Inline`**: A modal that appears inline with content.
|
||||
- `inline`, `inline-block`, `block` display options.
|
||||
- **`Pop-up Dialog`**: A dialog box.
|
||||
- `dialog position`
|
||||
|
||||
## 3. Containers
|
||||
|
||||
Generic container types used for layout and grouping.
|
||||
|
||||
### 3.1. Navigation
|
||||
|
||||
- `link`
|
||||
- `download`
|
||||
|
||||
### 3.2. Forms
|
||||
|
||||
- `save` button/action
|
||||
- `clear value` action
|
||||
- `set null value` action
|
||||
|
||||
### 3.3. Other Containers
|
||||
|
||||
- `help`: Blue themed container.
|
||||
- `info`: Blue themed container.
|
||||
- `alert`: Yellow themed container.
|
||||
- `warning`: Orange themed container.
|
||||
- `error`: Red themed container.
|
||||
- `message`: Green themed container.
|
||||
|
||||
## 4. CSS Styling for UI Elements
|
||||
|
||||
- **Warning/Hide Buttons:** `preset-tonal-warning hover:preset-filled-warning-500`
|
||||
- **Error/Delete/Disable Buttons:** `preset-tonal-error hover:preset-filled-error-500`
|
||||
- **Submenu:** `flex flex-row items-center justify-center gap-1`
|
||||
@@ -1,100 +0,0 @@
|
||||
# Aether Project Data Structures
|
||||
|
||||
This document outlines the key data structures and their properties used within the Aether SvelteKit frontend project. It covers object properties, field definitions, and how data is managed.
|
||||
|
||||
## 1. Object Properties and Fields
|
||||
|
||||
### 1.1. Core Standard Fields
|
||||
|
||||
These fields are expected to be present in most Aether objects, providing a consistent base structure.
|
||||
|
||||
- `id`: Primary key for an object (internal use, often *returned* by the API as a randomized string value in place of the actual DB autonum).
|
||||
- `id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
|
||||
- `code`: Short, unique identifier.
|
||||
- `name`: Display name.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
- `hide`: Boolean for visibility.
|
||||
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||
- `sort`: Numeric value for ordering within a priority group.
|
||||
- `group`: Categorization string.
|
||||
- `notes`: General notes/comments.
|
||||
- `created_on`: Timestamp of creation.
|
||||
- `updated_on`: Timestamp of last update.
|
||||
|
||||
### 1.2. Journal Entry Fields
|
||||
|
||||
Journal entries use the shared object fields plus a few content-specific fields that matter in the UI and config modal.
|
||||
|
||||
- `summary`: Short entry summary shown in metadata and list contexts.
|
||||
- `content`: Main body text for the entry.
|
||||
- `alert`: Boolean flag used to highlight an entry as an alert.
|
||||
- `alert_msg`: Supporting alert text shown when the alert flag is enabled.
|
||||
- `private` / `public` / `personal` / `professional`: Visibility and audience flags used by the Entry Config modal.
|
||||
|
||||
### 1.3. Special Use Fields
|
||||
|
||||
Fields with specific purposes or conditional usage across different object types.
|
||||
|
||||
- `for_type`: Indicates the type of object this object is linked to (e.g., 'account', 'event').
|
||||
- `for_id`: The ID of the object this object is linked to.
|
||||
- `archive_on`: Timestamp indicating when an object was archived.
|
||||
- `passcode`: A password or access code associated with an object.
|
||||
- `external_id`: An identifier from an external system.
|
||||
|
||||
### 1.4. Configuration and JSON Fields
|
||||
|
||||
Fields designed to store structured data in JSON format.
|
||||
|
||||
- `cfg_json`: Configuration data for an object, stored as a JSON string.
|
||||
- `data_json`: General purpose data for an object, stored as a JSON string.
|
||||
- `linked_li_json`: A list of linked items, stored as a JSON string.
|
||||
|
||||
### 1.5. Special Generated Fields (Client-side)
|
||||
|
||||
These fields are generated on the client-side, primarily for facilitating UI logic, such as sorting. They are not typically stored in the backend database.
|
||||
|
||||
- `tmp_sort_1`: Temporary sort field 1.
|
||||
- `tmp_sort_2`: Temporary sort field 2.
|
||||
- `tmp_sort_3`: Temporary sort field 3.
|
||||
|
||||
### 1.6. Future Standard Fields
|
||||
|
||||
A list of potential future standard fields, often prefixed with `obj_`. These are conceptual and represent planned expansions to the data model.
|
||||
|
||||
- `obj_id`, `obj_ext_uid`, `obj_ext_id`, `obj_import_id`, `obj_code`, `obj_account_id`, `obj_passcode`, `obj_type`, `obj_type_ver_id`, `obj_name`, `obj_summary`, `obj_outline`, `obj_description`, `obj_enable`, `obj_enable_on`, `obj_archive_on`, `obj_hide`, `obj_priority`, `obj_sort`, `obj_group`, `obj_cfg_json`, `obj_notes`, `obj_created_on`, `obj_updated_on`.
|
||||
|
||||
## 2. Data Sorting
|
||||
|
||||
Standardized sorting orders are applied across various data lists to ensure consistent presentation.
|
||||
|
||||
- **Default/General Sorting:** `group > priority (flag) > sort > updated_on/created_on`
|
||||
- **Specific Sorting (e.g., for time-based events):** `type > start_date/time > code or name`
|
||||
|
||||
## 3. Data Storage Mechanisms
|
||||
|
||||
### 3.1. Local Storage
|
||||
|
||||
Used for client-side persistence of various application states and configurations.
|
||||
|
||||
- `api`: Stores API-related settings and tokens.
|
||||
- `app`: Stores global application settings and preferences.
|
||||
- `core`: Stores settings and data specific to core modules.
|
||||
- `<module>`: Stores settings and data specific to extended modules (e.g., `journals`, `events`).
|
||||
- `<custom>`: Stores settings and data specific to custom modules (e.g., `idaa`).
|
||||
|
||||
### 3.2. IndexedDB (Dexie.js)
|
||||
|
||||
Used for more structured client-side data storage, often for caching, offline capabilities, and larger datasets.
|
||||
|
||||
- `ae_core_db`: The primary Dexie database instance for core application data.
|
||||
- `<module>`: Module-specific database instances (e.g., `db_journals` for journal data).
|
||||
- `<custom>`: Custom module-specific database instances (none currently defined, but reserved for future use).
|
||||
|
||||
### 3.3. IndexedDB LiveQuery Usage
|
||||
|
||||
Dexie's `liveQuery` is used to provide reactive data streams from IndexedDB.
|
||||
|
||||
- `lq__xyz_obj`: Represents a read-only liveQuery result for a single object.
|
||||
- `lqw__xyz_obj`: Represents a writable liveQuery result, typically used for forms and data binding.
|
||||
- **Note:** When using `lqw__xyz_obj`, developers must carefully manage updates to avoid conflicts with the underlying liveQuery and ensure data integrity.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Aether Project Naming Conventions
|
||||
|
||||
## 1. General Principles
|
||||
|
||||
- **Clarity:** Names should clearly convey their purpose and meaning.
|
||||
- **Consistency:** Adhere strictly to these guidelines across the entire codebase.
|
||||
- **Readability:** Prioritize names that are easy to read and understand.
|
||||
- **Conciseness:** Avoid unnecessary verbosity, but not at the expense of clarity.
|
||||
|
||||
## 2. File Naming
|
||||
|
||||
- **Logic/Service Files:** `ae_<module>__<concept>.ts` (e.g., `ae_core__account.ts`, `ae_events__event.ts`)
|
||||
- **Database Definition Files:** `db_<module>.ts` (e.g., `db_core.ts`, `db_journals.ts`)
|
||||
- **Svelte Store Files:** `ae_<module>_stores.ts` (e.g., `ae_core_stores.ts`, `ae_journals_stores.ts`)
|
||||
- **Svelte Components:**
|
||||
- **Module-specific components:** `ae_comp__<module>__<component_name>.svelte` (e.g., `ae_comp__events__event_card.svelte`)
|
||||
- **Generic/reusable components:** `element_<component_name>.svelte` (e.g., `element_input_file.svelte`, `element_qr_scanner_v2.svelte`)
|
||||
- **SvelteKit Routes:** Follow SvelteKit's standard routing conventions (e.g., `+page.svelte`, `+layout.svelte`, `[id]/+page.svelte`).
|
||||
- **CSS Files:** `ae-<module>-<purpose>.css` (e.g., `ae-c-idaa-light.css`, `ae-osit-default.css`)
|
||||
|
||||
## 3. Function and Variable Naming
|
||||
|
||||
- **Style:** Strictly `snake_case` for all function and variable names.
|
||||
- **Deprecated:** `camelCase` should be refactored to `snake_case`.
|
||||
- **Prefixes:**
|
||||
- `load_ae_obj_id__<object_type>`: For loading a single Aether object by ID.
|
||||
- `load_ae_obj_li__<object_type>`: For loading a list of Aether objects.
|
||||
- `create_ae_obj__<object_type>`: For creating an Aether object.
|
||||
- `update_ae_obj__<object_type>`: For updating an Aether object.
|
||||
- `delete_ae_obj_id__<object_type>`: For deleting an Aether object by ID.
|
||||
- `db_save_ae_obj_li__<object_type>`: For saving a list of Aether objects to IndexedDB.
|
||||
- `db_update_ae_obj_id__<object_type>`: For updating an Aether object in IndexedDB.
|
||||
- `process_ae_obj__<object_type>_props`: For module-specific data transformation functions.
|
||||
- **Deprecated:** Ambiguous `handle_` prefixes should be replaced with more descriptive `snake_case` names (e.g., `handle_submit_form` -> `submit_form`).
|
||||
|
||||
## 4. Object and Property Naming
|
||||
|
||||
- **Singularity:** Use singular nouns for objects and properties (e.g., `example.id`, not `examples.id`).
|
||||
- **IDs:**
|
||||
- `id`: Primary key for an object (internal use, often a UUID).
|
||||
- `<object_type>_id`: Specific ID for an object (e.g., `person_id`).
|
||||
- `<object_type>_id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `account_id`, `site_id`, `user_id`, etc.: Foreign keys.
|
||||
- **Common Properties:**
|
||||
- `code`: Short, unique identifier.
|
||||
- `name`: Display name.
|
||||
- `description`: Longer text description.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
- `hide`: Boolean for visibility.
|
||||
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||
- `sort`: Numeric value for ordering within a priority group.
|
||||
- `group`: Categorization string.
|
||||
- `notes`: General notes/comments.
|
||||
- `created_on`: Timestamp of creation.
|
||||
- `updated_on`: Timestamp of last update.
|
||||
- **Special Use Properties:** `for_type`, `for_id`, `archive_on`, `passcode`, `external_id`.
|
||||
- **Config/JSON Properties:** `cfg_json`, `data_json`, `linked_li_json`.
|
||||
- **Special Generated Fields (Client-side):** `tmp_sort_1`, `tmp_sort_2`, `tmp_sort_3` (for client-side sorting).
|
||||
|
||||
## 5. List Suffixes
|
||||
|
||||
- **Simple Arrays:** Use `_li` suffix for simple, unordered arrays (e.g., `user_li`, `hosted_file_id_li`).
|
||||
- **Key-Value Maps/Objects:** Use `_kv` suffix for key-value objects/maps (e.g., `user_kv`, `hosted_file_obj_kv`).
|
||||
|
||||
## 6. Interface and Type Naming
|
||||
|
||||
- **Style:** Use `PascalCase` for interface and type names (e.g., `Account`, `HostedFile`, `GenericCrudArgs`).
|
||||
|
||||
## 7. Constants
|
||||
|
||||
- **Style:** Use `SCREAMING_SNAKE_CASE` for constants (e.g., `MAX_RETRIES`, `DEFAULT_TIMEOUT`).
|
||||
|
||||
## 8. CSS Classes and IDs
|
||||
|
||||
- **Style:** Use `kebab-case` for CSS classes and IDs (e.g., `my-component-class`, `main-header-id`).
|
||||
|
||||
## 9. Data Sorting
|
||||
|
||||
- **Standard Order:** `group > priority (flag) > sort > updated_on/created_on`
|
||||
- **Specific Order:** `type > start_date/time > code or name`
|
||||
|
||||
## 10. Local Storage and IndexedDB Keys
|
||||
|
||||
- **Local Storage:**
|
||||
- `api`
|
||||
- `app` (global)
|
||||
- `core` (core modules)
|
||||
- `<module>` (extended modules)
|
||||
- `<custom>` (custom modules)
|
||||
- **IndexedDB:**
|
||||
- `ae_core_db`
|
||||
- `<module>`
|
||||
- `<custom>`
|
||||
@@ -1,240 +0,0 @@
|
||||
# Performance Guidelines: Non-Blocking Load Pattern (SvelteKit + Dexie)
|
||||
|
||||
## Overview
|
||||
To ensure instant page transitions and a high-performance feel, the Aether platform utilizes a **Non-Blocking Load Pattern** (also known as Stale-While-Revalidate or SWR). This pattern leverages Dexie's `liveQuery` for reactive UI and SvelteKit's `load` functions for background data synchronization.
|
||||
|
||||
## 🚀 The Core Principle
|
||||
**Never block the `load` function with API calls if the data is already being observed by a `liveQuery`.**
|
||||
|
||||
The page should render *instantly* using cached data from IndexedDB. Fresh data from the API should settle in the background and update the UI automatically via reactivity.
|
||||
|
||||
---
|
||||
|
||||
## ❌ Anti-Pattern (Blocking)
|
||||
This pattern causes a "white screen" or "frozen UI" while the browser waits for the API response.
|
||||
|
||||
```typescript
|
||||
// +page.ts
|
||||
export async function load({ params, parent }) {
|
||||
const data = await parent();
|
||||
const event_id = params.event_id;
|
||||
|
||||
// BAD: This blocks the navigation until the API responds.
|
||||
const fresh_data = await events_func.load_ae_obj_id__event({
|
||||
event_id: event_id,
|
||||
try_cache: true
|
||||
});
|
||||
|
||||
return { ...data, event_obj: fresh_data };
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ Best Practice (Non-Blocking / SWR)
|
||||
This pattern completes the navigation immediately.
|
||||
|
||||
```typescript
|
||||
// +page.ts
|
||||
export async function load({ params, parent }) {
|
||||
const data = await parent();
|
||||
const event_id = params.event_id;
|
||||
|
||||
if (browser) {
|
||||
// GOOD: Fire and forget.
|
||||
// This function updates IndexedDB in the background.
|
||||
events_func.load_ae_obj_id__event({
|
||||
event_id: event_id,
|
||||
try_cache: true
|
||||
});
|
||||
}
|
||||
|
||||
return data; // Navigation completes instantly
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
|
||||
// UI reacts automatically when the background task finishes.
|
||||
let lq__event_obj = $derived(
|
||||
liveQuery(() => db_events.event.get(event_id))
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if $lq__event_obj}
|
||||
<h1>{$lq__event_obj.name}</h1>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ When to use Await
|
||||
Use `await` in `load` functions ONLY for:
|
||||
1. **Critical Auth Checks:** If you must verify a session before even showing a layout.
|
||||
2. **Parent Data:** `const data = await parent();` is necessary to build the context.
|
||||
3. **Server-Side Rendering (SSR):** If the data *must* be present in the initial HTML for SEO (rare for Aether feature modules).
|
||||
|
||||
## 📈 Performance Gains
|
||||
By adopting this pattern across the Events module, we achieved:
|
||||
- **~200-500ms reduction** in perceived page load time.
|
||||
- **Elimination of waterfalls** (sequential API calls).
|
||||
- **Better offline support**, as the UI is always ready to show what's in the local cache.
|
||||
|
||||
---
|
||||
|
||||
## Svelte 5 Runes + liveQuery: Critical Patterns
|
||||
|
||||
These rules apply to all Svelte 5 runes-mode components (the entire Aether frontend). Violations here are a common source of subtle reactivity bugs and unnecessary re-renders.
|
||||
|
||||
### Rule 1: Use `$derived.by()` when liveQuery depends on a reactive value
|
||||
|
||||
**The problem:** `$derived(liveQuery(callback))` looks like it should re-run when a store value inside `callback` changes. It does NOT. Svelte tracks reactive dependencies synchronously during the expression evaluation. The `liveQuery` callback is called later inside Dexie's async context — Svelte's tracking is already finished. The dependency is never registered.
|
||||
|
||||
```svelte
|
||||
<!-- ❌ WRONG: $events_slct.event_session_id is read inside the async callback.
|
||||
Svelte never tracks it. The liveQuery is created once and never recreates
|
||||
when event_session_id changes. -->
|
||||
let lq__session = $derived(
|
||||
liveQuery(() => db_events.session.get($events_slct.event_session_id))
|
||||
);
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- ✅ CORRECT: $derived.by() captures the ID in the outer synchronous closure.
|
||||
Svelte tracks it. When event_session_id changes, $derived.by() re-runs,
|
||||
creating a new liveQuery with the updated ID. -->
|
||||
let lq__session = $derived.by(() => {
|
||||
const id = $events_slct.event_session_id; // tracked here, synchronously
|
||||
return liveQuery(() => db_events.session.get(id));
|
||||
});
|
||||
```
|
||||
|
||||
**Rule of thumb:** If the liveQuery result changes based on a reactive value (store property, `$state`, `$props`), always use `$derived.by()`. Reserve `$derived(liveQuery(...))` only for liveQueries that watch a table broadly and don't filter by a reactive value.
|
||||
|
||||
---
|
||||
|
||||
### Rule 2: Keep liveQuery closures pure (data-only)
|
||||
|
||||
**The problem:** Writing to a Svelte store inside a liveQuery callback runs inside Dexie's async transaction context. Svelte's reactive tracking is undefined there. The write may fire at unpredictable times and create hard-to-debug reactivity loops.
|
||||
|
||||
```svelte
|
||||
<!-- ❌ WRONG: Store side-effect inside liveQuery async callback. -->
|
||||
let lq__event_obj = liveQuery(async () => {
|
||||
const obj = await db_events.event.get($events_slct.event_id);
|
||||
if (obj) $events_slct.event_obj = obj; // BAD: side-effect in async context
|
||||
return obj;
|
||||
});
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- ✅ CORRECT: liveQuery is pure data-only. Store sync happens in a $effect. -->
|
||||
let lq__event_obj = liveQuery(async () => {
|
||||
const id = $events_slct.event_id;
|
||||
if (!id) return null;
|
||||
return await db_events.event.get(id);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const result = $lq__event_obj;
|
||||
if (result) {
|
||||
untrack(() => {
|
||||
// Cheap equality guard — only write if something actually changed.
|
||||
if (result.updated_on !== $events_slct.event_obj?.updated_on ||
|
||||
result.id !== $events_slct.event_obj?.id) {
|
||||
$events_slct.event_obj = { ...result };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rule 3: Use cheap equality guards in `$effect` before writing to stores
|
||||
|
||||
Every store write in a `$effect` triggers downstream reactivity. Always guard with a comparison before writing. The cost of the comparison is always less than the cost of spurious re-renders.
|
||||
|
||||
**For single objects** — compare `id` + `updated_on` (O(1)):
|
||||
```typescript
|
||||
if (result.id !== $store.obj?.id || result.updated_on !== $store.obj?.updated_on) {
|
||||
$store.obj = { ...result };
|
||||
}
|
||||
```
|
||||
|
||||
**For arrays** — join IDs into a string (O(n)), not `JSON.stringify` (O(n × field_count)):
|
||||
```typescript
|
||||
const new_ids = results.map(r => r.id).join(',');
|
||||
const cur_ids = ($store.list ?? []).map(r => r.id).join(',');
|
||||
if (new_ids !== cur_ids) {
|
||||
$store.list = [...results];
|
||||
}
|
||||
```
|
||||
|
||||
**For flat objects** (e.g., merged config) — shallow key-by-key comparison (O(n keys)):
|
||||
```typescript
|
||||
function shallow_equal(a, b) {
|
||||
const keys_a = Object.keys(a);
|
||||
const keys_b = Object.keys(b);
|
||||
if (keys_a.length !== keys_b.length) return false;
|
||||
for (const k of keys_a) { if (a[k] !== b[k]) return false; }
|
||||
return true;
|
||||
}
|
||||
if (!shallow_equal(current, new_val)) { $store = new_val; }
|
||||
```
|
||||
|
||||
**Never use `JSON.stringify` for equality.** It serializes the full object tree on every reactive cycle and is O(total serialized bytes).
|
||||
|
||||
---
|
||||
|
||||
### Rule 4: Always use `untrack()` when writing to stores inside `$effect`
|
||||
|
||||
Without `untrack()`, reading a store to check its current value inside `$effect` registers it as a dependency — the effect re-runs whenever it writes, creating an infinite loop.
|
||||
|
||||
```svelte
|
||||
<!-- ❌ WRONG: Reading $store.obj inside $effect creates a dependency loop. -->
|
||||
$effect(() => {
|
||||
const result = $lq__obj;
|
||||
if (result.id !== $store.obj?.id) { // Reading $store.obj here is a dependency!
|
||||
$store.obj = result; // This write re-triggers the effect.
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- ✅ CORRECT: untrack() reads current store values without registering them
|
||||
as reactive dependencies of the $effect. -->
|
||||
$effect(() => {
|
||||
const result = $lq__obj; // Tracked: effect re-runs when liveQuery emits
|
||||
if (result) {
|
||||
untrack(() => {
|
||||
// Not tracked: reading $store.obj here won't cause a re-run.
|
||||
if (result.id !== $store.obj?.id) {
|
||||
$store.obj = result;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rule 5: Guard `console.log` calls with `log_lvl`
|
||||
|
||||
Raw `console.log(obj)` eagerly serializes objects (even large ones) on every call, blocking the main thread. All debug logging must be guarded.
|
||||
|
||||
```typescript
|
||||
let log_lvl: number = $state(0); // Set to 0 in production; raise locally to debug.
|
||||
|
||||
// ❌ WRONG: Always runs, always serializes.
|
||||
console.log('Result:', result_obj);
|
||||
|
||||
// ✅ CORRECT: Zero-cost when log_lvl is 0.
|
||||
if (log_lvl) console.log('Result:', result_obj);
|
||||
if (log_lvl > 1) console.log('Verbose:', result_obj); // Extra-verbose tier
|
||||
```
|
||||
|
||||
**Never hardcode `log_lvl: 2` in a call-site or override `log_lvl` inside a function body.** The parameter default exists so callers can control verbosity. Overriding it forces debug logging regardless of what the caller passed.
|
||||
@@ -1,230 +0,0 @@
|
||||
# Aether — Permissions and Security
|
||||
|
||||
**Last updated:** 2026-02-27
|
||||
**Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts`
|
||||
|
||||
---
|
||||
|
||||
## Access Level Hierarchy
|
||||
|
||||
Highest to lowest. Each level **inherits all access from every level below it**.
|
||||
|
||||
| Level | `access_type` string | Typical Use |
|
||||
| --- | --- | --- |
|
||||
| Super | `super` | OSIT internal — full system access |
|
||||
| Manager | `manager` | Account managers |
|
||||
| Administrator | `administrator` | Event/account admins |
|
||||
| Trusted | `trusted` | **Onsite staff** — site passcode or AE login |
|
||||
| Public | `public` | Site-wide passcode granted |
|
||||
| Authenticated | `authenticated` | Identity verified (e.g. IDAA Novi UUID) |
|
||||
| Anonymous | `anonymous` | Default — not signed in |
|
||||
|
||||
> **Note on Public vs Authenticated:** `public` is a *site-wide* unlock (anyone with the passcode). `authenticated` verifies a *specific identity*. In the hierarchy, public outranks authenticated because it implies broader site access.
|
||||
|
||||
---
|
||||
|
||||
## `$ae_loc` Store — Permission Flags
|
||||
|
||||
`$ae_loc` is a `persisted()` store (backed by localStorage). Key fields:
|
||||
|
||||
```typescript
|
||||
$ae_loc.access_type // string: current access type ('anonymous', 'trusted', etc.)
|
||||
|
||||
// Cumulative boolean flags (true = "you have AT LEAST this level")
|
||||
$ae_loc.anonymous_access // always true
|
||||
$ae_loc.authenticated_access // true from authenticated and above
|
||||
$ae_loc.public_access // true from public and above
|
||||
$ae_loc.trusted_access // true from trusted and above ← most-used gate
|
||||
$ae_loc.administrator_access // true from administrator and above
|
||||
$ae_loc.manager_access // true from manager and above
|
||||
$ae_loc.super_access // true only at super
|
||||
|
||||
// Exclusive check flags (true = "you are EXACTLY this level")
|
||||
$ae_loc.trusted_check // true only if access_type === 'trusted'
|
||||
$ae_loc.administrator_check // etc.
|
||||
// (rarely needed — prefer the _access flags)
|
||||
|
||||
// Behavior flags
|
||||
$ae_loc.edit_mode // boolean — user preference, see below
|
||||
$ae_loc.adv_mode // boolean — advanced mode toggle
|
||||
```
|
||||
|
||||
### Additional intermediate levels (in permission checks, not in hierarchy order)
|
||||
`support`, `assistant`, `verified`, `provisional` — appear in `_access` flags but are not part of the canonical `access_level_order`. Treat as internal/intermediate.
|
||||
|
||||
---
|
||||
|
||||
## Edit Mode — Critical Rules
|
||||
|
||||
`$ae_loc.edit_mode` is a **user preference**, not a permission level.
|
||||
|
||||
**Rules that must never be broken:**
|
||||
1. **Components must never write to `$ae_loc.edit_mode`** — only the system menu toggle and sign-out/permission-drop handlers may change it.
|
||||
2. Edit mode is only available to `trusted` and above in 95% of modules (the toggle is hidden from lower-access users).
|
||||
3. Edit mode persists across navigation — it is NOT reset by page loads or component mounts.
|
||||
4. Sign-out and permission drops to below `authenticated` should reset `edit_mode` to `false`.
|
||||
|
||||
> **Background:** A bug was fixed (2026-02-27) where `ae_comp__badge_obj_view.svelte` was writing `$ae_loc.edit_mode = false` in a data-loading `$effect`, silently overriding the user's preference on every navigation to the badge print page.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
| Method | Grants | Used For |
|
||||
| --- | --- | --- |
|
||||
| Site passcode (`site_access_code_kv`) | `trusted`, `public`, or `authenticated` | Onsite staff and event attendees |
|
||||
| AE Username + Password | `trusted` and above | Staff with AE accounts |
|
||||
| Novi UUID | `authenticated` | IDAA members (Novi membership system) |
|
||||
|
||||
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`:
|
||||
```typescript
|
||||
site_access_code_kv: {
|
||||
administrator: null, // highest passcode tier
|
||||
trusted: null, // onsite staff passcode
|
||||
public: 'public1980', // example
|
||||
authenticated: 'auth1980'
|
||||
}
|
||||
```
|
||||
|
||||
### `x-no-account-id` — Narrow Transport Exception
|
||||
|
||||
`x-no-account-id` is a transport-level escape hatch that strips account context before the request leaves the frontend. It is not a permission grant and it is not a replacement for JWT or `x-account-id`.
|
||||
|
||||
Use it only when the request truly cannot be made account-scoped. Current legitimate cases should stay narrow:
|
||||
|
||||
1. Bootstrap / site-domain discovery before the account is known.
|
||||
2. Explicit public or guest endpoints that do not have an account context.
|
||||
3. Helper paths that intentionally need a global-default fallback.
|
||||
|
||||
If a request already has a valid account context, prefer `x-account-id` and let the JWT carry session identity. Treat any new `x-no-account-id` use as temporary until it is reviewed and either replaced or justified.
|
||||
|
||||
---
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### `process_permission_checks(access_type: string)`
|
||||
Returns a full permission object (`_check` and `_access` flags) for a given access type string. Used when access type changes to update `$ae_loc`.
|
||||
|
||||
```typescript
|
||||
import { process_permission_checks } from '$lib/ae_utils/ae_utils__perm_checks';
|
||||
const checks = process_permission_checks('trusted');
|
||||
// checks.trusted_access === true
|
||||
// checks.administrator_access === false
|
||||
```
|
||||
|
||||
### `compare_access_levels(level_a, level_b)`
|
||||
Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for threshold comparisons.
|
||||
|
||||
---
|
||||
|
||||
## Privacy and Security Rules
|
||||
|
||||
### IDAA — International Doctors in Alcoholics Anonymous
|
||||
- **ALL IDAA content is private. Always. No exceptions.**
|
||||
- BB (Bulletin Board / Posts), Archives, Recovery Meetings — all require authentication.
|
||||
- IDAA users authenticate via Novi UUID at `authenticated` level or higher.
|
||||
- A prior agent accidentally exposed IDAA BB data publicly — treat any IDAA exposure as Sev-1.
|
||||
|
||||
#### IDAA IndexedDB (IDB) Caching — Auth-Before-Cache Rule
|
||||
|
||||
**Root cause discovered 2026-04:** SvelteKit `+page.ts`/`+layout.ts` load functions run *before* layout `$effect` hooks and fire during link prefetch (hover). `if (browser)` guards do NOT prevent this — they only prevent SSR. This means API calls inside these files execute before Novi auth completes, writing private IDAA data to the user's IndexedDB even for unauthenticated sessions.
|
||||
|
||||
**The fix — established pattern for all IDAA routes:**
|
||||
|
||||
1. **Load/layout `.ts` files = thin shells.** Pass URL params only. No API calls. No `if (browser)` data fetching.
|
||||
2. **Data loading = `$effect` in `.svelte` files**, gated on:
|
||||
```svelte
|
||||
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
||||
```
|
||||
3. **Three IDB purge paths** in `(idaa)/+layout.svelte` (auth failure, anonymous no-UUID, Reset & Retry button) clear `db_posts`, `db_archives`, and `db_events` tables.
|
||||
|
||||
**Auth path matrix:**
|
||||
|
||||
| User type | `novi_verified` | `trusted_access` | Can load data? | Purge fires? |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Anonymous / unauthenticated | false | false | No | Yes (Case 1) |
|
||||
| Novi-verified IDAA member | true | false | Yes | No |
|
||||
| Manager / trusted access | false | true | Yes | No (Case 3 exemption) |
|
||||
|
||||
**Applied to routes (as of 2026-04-19):**
|
||||
- `idaa/bb/+page.svelte` — `$effect` gate added; `bb/+page.ts` stripped
|
||||
- `idaa/bb/[post_id]/+page.ts` — stripped; loading handled by trigger in `bb/+layout.svelte`
|
||||
- `idaa/archives/+page.svelte` — `$effect` gate added; `archives/+layout.ts` stripped
|
||||
- `idaa/archives/[archive_id]/+page.svelte` — `$effect` gate added; `[archive_id]/+page.ts` stripped
|
||||
- `idaa/recovery_meetings/+page.svelte` — `$effect` gate already present; `+layout.ts` stripped
|
||||
- `idaa/recovery_meetings/[event_id]/+page.svelte` — `$effect` gate added; `+page.ts` stripped
|
||||
|
||||
**When adding a new IDAA route:** never put API calls in `+page.ts`/`+layout.ts`. Always gate data fetching with the `$effect` pattern above.
|
||||
|
||||
### Journals
|
||||
- Private personal data. Always authenticated. Passcode/encryption features exist.
|
||||
- Never expose journal content publicly.
|
||||
|
||||
### `PUBLIC_AE_API_SECRET_KEY`
|
||||
- Audit closed 2026-03-11. `PUBLIC_*` prefix is by design — key is always in the client bundle.
|
||||
- Anonymous site-domain lookup uses the limited-permission `PUBLIC_AE_BOOTSTRAP_KEY` instead.
|
||||
- Security model: API key is one layer; JWT + `x-account-id` scoping provides the primary auth.
|
||||
- Do not introduce new usages. Prefer `PUBLIC_AE_BOOTSTRAP_KEY` for unauthenticated lookups.
|
||||
|
||||
### JWT usage guidance
|
||||
- JWTs are the preferred proof of an established session. Keep them attached to authenticated flows instead of leaning on transport-level bypasses.
|
||||
- If a route or helper can work with a JWT and an account ID, it should not need `x-no-account-id`.
|
||||
- If a helper still needs the bypass today, document the reason and add a removal target.
|
||||
|
||||
### Email Display
|
||||
Non-trusted users must never see a full email address. Obscure using:
|
||||
```typescript
|
||||
// joh***@example.com
|
||||
function obscure_email(email: string): string {
|
||||
const at = email.indexOf('@');
|
||||
if (at < 0) return email;
|
||||
return `${email.slice(0, Math.min(3, at))}***${email.slice(at)}`;
|
||||
}
|
||||
```
|
||||
This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if needed elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## Module-Specific Permission Patterns
|
||||
|
||||
### Journals — Entry Config Admin Actions
|
||||
- Entry configuration admin controls are gated to `trusted_access` and above.
|
||||
- `manager_access` and `administrator_access` see the Delete action, which performs a hard delete.
|
||||
- `trusted_access` users see Remove instead, which follows disable semantics rather than a hard delete.
|
||||
- The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; the template toggle belongs in Metadata, while visibility/audience flags remain separate.
|
||||
|
||||
### Events — Badges
|
||||
|
||||
| Scenario | Visibility | Print Action | Review Actions |
|
||||
| --- | --- | --- | --- |
|
||||
| Anonymous / below trusted | Unprinted only | None (name display only) | Email Review Link button (→ email API) |
|
||||
| Trusted, not Edit Mode | Unprinted only | Clickable (first print) | Email Review Link button |
|
||||
| Trusted, Edit Mode | All non-hidden | Clickable incl. reprint; shows `Nx` count | Email Review Link + direct Review Link (clipboard) |
|
||||
|
||||
- Print count badge: shown as `Nx` (e.g. `2×`) next to the printer icon when `print_count >= 1`
|
||||
- Edit mode for badges: limited to `trusted_access` users (toggle hidden from lower levels)
|
||||
- `person_passcode` field (for attendee-gated review URL): **not yet in DB** as of 2026-02-27
|
||||
|
||||
### IDAA
|
||||
- Auth gate test must be the **first test** in any test file — privacy enforcement is a hard requirement.
|
||||
- Default required permission: `trusted_access` or higher for module access.
|
||||
|
||||
---
|
||||
|
||||
## Common Template Patterns
|
||||
|
||||
```svelte
|
||||
<!-- Gate on trusted access -->
|
||||
{#if $ae_loc.trusted_access}
|
||||
|
||||
<!-- Gate on edit mode (always check trusted too — edit mode alone is insufficient) -->
|
||||
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
|
||||
|
||||
<!-- Gate on administrator -->
|
||||
{#if $ae_loc.administrator_access}
|
||||
|
||||
<!-- Show full vs obscured email -->
|
||||
{$ae_loc.trusted_access ? email : obscure_email(email)}
|
||||
```
|
||||
|
||||
> Never gate purely on `$ae_loc.edit_mode` without also checking a permission level. Edit mode is a UI preference, not a permission grant.
|
||||
@@ -1,414 +0,0 @@
|
||||
# Aether UI — Component Style Patterns
|
||||
> **Version:** 1.0 (2026-03-06)
|
||||
> **Author:** One Sky IT / Scott Idem
|
||||
> **Scope:** All Aether SvelteKit frontend components
|
||||
> **Related:** `GUIDE__AE_UI_Style_Guidelines.md` (color rules, token definitions, a11y)
|
||||
|
||||
This document is a recipe book. Copy these patterns directly. Deviate only when a component's specific purpose genuinely requires it — and document why in a comment.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hero Card
|
||||
*Used for: session identity, presenter identity, location identity — the top-of-page "Is this the right one?" card.*
|
||||
|
||||
```svelte
|
||||
<div class="rounded-xl border border-surface-200-800 bg-surface-50-900 shadow-sm overflow-hidden">
|
||||
<div class="px-4 pt-4 pb-3 flex flex-col gap-3">
|
||||
<!-- primary heading (h1 / h2) -->
|
||||
<h1 class="text-2xl font-bold leading-snug">{name}</h1>
|
||||
|
||||
<!-- info chips row -->
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<!-- time chip → primary color -->
|
||||
<span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-primary-500/10 text-primary-700 dark:text-primary-300 transition-colors duration-200">
|
||||
<span class="fas fa-clock text-xs" aria-hidden="true"></span>
|
||||
Mon, Jan 1 – 2:00 PM
|
||||
</span>
|
||||
<!-- room/location chip → tertiary color -->
|
||||
<span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 transition-colors duration-200">
|
||||
<span class="fas fa-map-marker-alt text-xs" aria-hidden="true"></span>
|
||||
Room 201
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Skeleton loading variant:**
|
||||
```svelte
|
||||
<!-- While liveQuery resolves -->
|
||||
<div class="h-7 w-2/3 bg-surface-200-800 animate-pulse rounded"></div>
|
||||
<div class="h-5 w-1/2 bg-surface-200-800 animate-pulse rounded-full"></div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Standard Content Card
|
||||
*Used for: description text, notes, secondary info panels.*
|
||||
|
||||
```svelte
|
||||
<div class="rounded-lg border border-surface-200-800 bg-surface-50-900 px-4 py-3">
|
||||
<!-- optional eyebrow label -->
|
||||
<span class="text-xs font-bold uppercase tracking-wide opacity-40 block mb-1">Description</span>
|
||||
<p class="whitespace-pre-wrap text-sm leading-relaxed">{description}</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Variant — inner secondary panel:**
|
||||
```svelte
|
||||
<div class="bg-surface-100-900 rounded-lg px-3 py-2">
|
||||
<!-- inner content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Table Row
|
||||
*Used for: session search results tables, any `<tbody><tr>` list.*
|
||||
|
||||
```svelte
|
||||
<tr
|
||||
class="relative transition-colors duration-200"
|
||||
class:opacity-50={obj?.hide}
|
||||
class:preset-tonal-warning={!obj?.enable}
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
href="/path/to/{obj.id}"
|
||||
class="font-bold text-lg hover:text-primary-500 transition-colors duration-200"
|
||||
>
|
||||
{obj.name}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
- `opacity-50` for hidden/archived records
|
||||
- `preset-tonal-warning` for disabled (not enabled) records — amber background
|
||||
- `transition-colors duration-200` on both `<tr>` and `<a>`
|
||||
|
||||
---
|
||||
|
||||
## 4. List Item Card
|
||||
*Used for: presentation list items, session details lists, any vertical card stack.*
|
||||
|
||||
```svelte
|
||||
<ul class="space-y-4">
|
||||
<li class="space-y-3 border border-surface-200-800 bg-surface-50-900 p-4 rounded-xl shadow-sm transition-colors duration-200">
|
||||
|
||||
<!-- Card heading bar -->
|
||||
<h4 class="text-lg font-bold rounded-lg px-3 py-2 bg-surface-100-900 flex flex-wrap items-center gap-2">
|
||||
{name}
|
||||
<!-- code/tag badge -->
|
||||
<span class="text-xs preset-tonal-warning px-2 py-0.5 rounded-md leading-none">
|
||||
{code}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<!-- Description block -->
|
||||
<pre class="whitespace-pre-wrap p-3 bg-surface-100-900 rounded-lg text-sm">{description}</pre>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Background goes on `<li>`, NOT on `<ul>`
|
||||
- `<ul>` gets only spacing: `space-y-4` — never a background color
|
||||
- The `<ul>` container in components should have `overflow-x-auto`, not `overflow-x-scroll`
|
||||
|
||||
---
|
||||
|
||||
## 5. Info Chips
|
||||
|
||||
### Time / Date chip (Primary — Teal)
|
||||
```svelte
|
||||
<span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-primary-500/10 text-primary-700 dark:text-primary-300 transition-colors duration-200">
|
||||
<span class="fas fa-clock text-xs" aria-hidden="true"></span>
|
||||
Monday, March 6 – 2:00 PM
|
||||
</span>
|
||||
```
|
||||
|
||||
### Location / Room chip (Tertiary — Indigo)
|
||||
```svelte
|
||||
<span class="inline-flex items-center gap-1.5 text-sm font-semibold px-3 py-1 rounded-full bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 transition-colors duration-200">
|
||||
<span class="fas fa-map-marker-alt text-xs" aria-hidden="true"></span>
|
||||
Main Hall B
|
||||
</span>
|
||||
```
|
||||
|
||||
### Code / Tag badge
|
||||
```svelte
|
||||
<span class="text-xs preset-tonal-warning px-2 py-0.5 rounded-md leading-none">
|
||||
{code}
|
||||
</span>
|
||||
```
|
||||
|
||||
### Status badge (edit mode only)
|
||||
```svelte
|
||||
{#if $ae_loc.edit_mode}
|
||||
<span class="badge preset-tonal-surface text-xs">code: {obj.code}</span>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Success count badge
|
||||
```svelte
|
||||
<span class="badge preset-tonal-success" class:hidden={!fileCount}>
|
||||
<span class="fas fa-file-alt m-1" aria-hidden="true"></span>
|
||||
{fileCount}×
|
||||
</span>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Empty State Panel
|
||||
*Used for: "No results found", "No sessions match your search", "Nothing to show yet".*
|
||||
|
||||
```svelte
|
||||
<section
|
||||
class="preset-tonal-warning p-6 rounded-xl shadow-sm lg:max-w-lg mx-auto"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2 text-center">
|
||||
<span class="fas fa-search text-3xl opacity-50" aria-hidden="true"></span>
|
||||
<strong class="text-xl">No sessions found</strong>
|
||||
<p class="text-base opacity-80">
|
||||
Use the search bar above to find your session.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- optional details card -->
|
||||
<div class="bg-surface-50-900/60 rounded-lg p-3 mt-4">
|
||||
<span class="text-xs font-bold uppercase tracking-wide opacity-50 block mb-2">Search by any of:</span>
|
||||
<ul class="space-y-1 text-sm">
|
||||
<li class="flex items-center gap-1.5">
|
||||
<span class="fas fa-angle-right text-xs opacity-50" aria-hidden="true"></span>
|
||||
Session name
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Warning / Error Inline Banners
|
||||
*Used for: disabled records, agreement-required gates.*
|
||||
|
||||
```svelte
|
||||
<!-- Warning (amber — disabled/inactive) -->
|
||||
<div class="bg-warning-100 p-4 border border-warning-300 rounded-md">
|
||||
<h2 class="h3">
|
||||
<span class="fas fa-exclamation-triangle text-warning-500 m-1" aria-hidden="true"></span>
|
||||
Location Disabled
|
||||
</h2>
|
||||
<p>This location is currently disabled.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error (red — blocked/failed) -->
|
||||
<div class="bg-error-100 p-4 border border-error-300 rounded-md">
|
||||
<h2 class="h3">
|
||||
<span class="fas fa-exclamation-triangle text-error-500 m-1" aria-hidden="true"></span>
|
||||
Presenter Disabled
|
||||
</h2>
|
||||
<p>This presenter is currently disabled.</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. File Upload Zone (`Comp_event_files_upload`)
|
||||
|
||||
The `class_li` prop styles the outer upload drop zone container:
|
||||
|
||||
```svelte
|
||||
<Comp_event_files_upload
|
||||
class_li="border border-surface-200-800 rounded-xl p-4 bg-surface-50-900 hover:bg-surface-100-900 transition-colors duration-200"
|
||||
link_to_type="event_presenter"
|
||||
link_to_id={presenter_id}
|
||||
>
|
||||
{#snippet label()}
|
||||
<span>
|
||||
<div class="text-lg">
|
||||
<span class="fas fa-upload" aria-hidden="true"></span>
|
||||
<strong>Upload presenter files</strong>
|
||||
</div>
|
||||
<div class="text-sm opacity-60 italic">
|
||||
Supported: pptx, key, mp4, pdf, docx, xlsx, txt
|
||||
</div>
|
||||
</span>
|
||||
{/snippet}
|
||||
</Comp_event_files_upload>
|
||||
```
|
||||
|
||||
**Note:** The label sub-description text uses `opacity-60 italic` — never `text-gray-600 dark:text-gray-400`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Section Component Wrapper
|
||||
*Used for: `ae_comp__event_*_obj_li.svelte` outer `<section>` elements.*
|
||||
|
||||
```svelte
|
||||
<section
|
||||
class="ae_comp event_X_obj_li px-0.5 py-2 space-y-2 min-w-full w-full container overflow-x-auto {container_class_li}"
|
||||
>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `overflow-x-auto` — never `overflow-x-scroll`
|
||||
- **Never include debug breakpoint borders** — remove before committing:
|
||||
```
|
||||
sm:border-l-red-400 md:border-l-yellow-400 lg:border-l-gray-100
|
||||
sm:dark:border-l-red-600 md:dark:border-l-yellow-600 lg:dark:border-l-gray-700
|
||||
border-dashed border-y-transparent border-r-transparent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Agreement / Consent Form Layout
|
||||
*Used for: `ae_comp__event_presenter_form_agree.svelte`, `ae_comp__event_session_poc_form_agree.svelte`.*
|
||||
|
||||
```svelte
|
||||
<!-- Consent text container -->
|
||||
<div class="bg-surface-100-900 p-4 border border-surface-200-800 rounded-lg space-y-4">
|
||||
<Element_data_store ds_code="consent_text" ds_type="html" class_li="p-2" />
|
||||
</div>
|
||||
|
||||
<!-- Presenter name/identity highlight line -->
|
||||
<p class="text-lg preset-tonal-warning p-2 rounded-t-md">
|
||||
<strong>{presenter_name} ({email})</strong>
|
||||
agrees to the following terms and conditions:
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Modal Usage (Flowbite-Svelte)
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Correct — no manual color class; theme handles styling -->
|
||||
<Modal title="Host Profile" bind:open={show_modal}>
|
||||
<ProfileComponent />
|
||||
{#snippet footer()}
|
||||
<button onclick={() => show_modal = false} class="btn preset-tonal-warning">
|
||||
Close
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<!-- ❌ Wrong — manual gray overrides bypass the theme -->
|
||||
<Modal class="bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 ...">
|
||||
```
|
||||
|
||||
**Rule:** Never set `bg-*` or `text-*` color classes on `<Modal>`. Let the Flowbite component + active theme handle it. Only structural layout classes (`shadow-md`, `relative`, `flex`, etc.) belong on the `class` prop if needed.
|
||||
|
||||
---
|
||||
|
||||
## 12. Muted / Secondary Text
|
||||
|
||||
Replace all `text-gray-*` patterns with opacity wrappers on inherited text color:
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Theme-aware muted text -->
|
||||
<span class="text-sm opacity-60">Secondary label</span>
|
||||
<span class="text-sm opacity-40 italic">Hint or placeholder text</span>
|
||||
<span class="text-xs font-bold uppercase tracking-wide opacity-40">Section eyebrow</span>
|
||||
|
||||
<!-- ❌ Fixed-color muted text -->
|
||||
<span class="text-sm text-gray-500">Secondary label</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 italic">Hint text</span>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. QR Code (Async Toggle)
|
||||
|
||||
```svelte
|
||||
<!-- Gate on typeof === 'string', not truthy.
|
||||
The store holds boolean `true` as a loading placeholder, which would
|
||||
render as a broken <img src="true"> if not guarded. -->
|
||||
{#if $lq__obj && typeof $store.qr_url?.[$lq__obj.id] === 'string'}
|
||||
<div class="float-right ml-3 mb-1 flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => $store.qr_bigger = !$store.qr_bigger}
|
||||
class="rounded focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||
title="Toggle QR code size"
|
||||
aria-label="Toggle QR code size"
|
||||
>
|
||||
<img
|
||||
src={$store.qr_url[$lq__obj.id]}
|
||||
class="transition-all duration-500 rounded border border-surface-200-800"
|
||||
class:h-20={!$store.qr_bigger}
|
||||
class:w-20={!$store.qr_bigger}
|
||||
class:h-40={$store.qr_bigger}
|
||||
class:w-40={$store.qr_bigger}
|
||||
alt="QR code link to this page"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. POC / Host Button Pattern
|
||||
|
||||
```svelte
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-semibold opacity-60">Host:</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-primary transition-colors duration-200"
|
||||
onclick={() => show_profile = true}
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<span class="fas fa-id-card mr-1" aria-hidden="true"></span>
|
||||
{full_name}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Icon Usage Rules
|
||||
|
||||
| Context | Pattern |
|
||||
|---|---|
|
||||
| Decorative / visual only | `<span class="fas fa-clock" aria-hidden="true"></span>` |
|
||||
| Icon with visible adjacent text | `aria-hidden="true"` on icon, text provides meaning |
|
||||
| Icon-only button (no visible text) | `aria-label="Description"` on the `<button>` |
|
||||
| Icon used as bullet point | `aria-hidden="true"` on icon |
|
||||
|
||||
**Never use `<i>` tags.** Always `<span class="fas ...">`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Native `<select>` Dark Mode
|
||||
|
||||
Browser-native `<select>` and `<option>` elements **cannot be reliably styled** with Tailwind `dark:` utilities — the browser controls `<option>` rendering and ignores most CSS overrides. This causes the "light on light hover" bug in dark mode.
|
||||
|
||||
**Fix — add `color-scheme` directive to force OS-level dark styling:**
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { ae_loc } from '$lib/ae_core/ae_stores';
|
||||
</script>
|
||||
|
||||
<!-- Forces browser to render the select widget in dark/light OS mode matching your theme -->
|
||||
<select
|
||||
class="select text-xs p-1"
|
||||
style:color-scheme={$ae_loc.dark_mode ? 'dark' : 'light'}
|
||||
>
|
||||
{#each options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
```
|
||||
|
||||
**Why this works:** `color-scheme: dark` instructs the browser to use its native dark-mode widget rendering (dark `<select>`, dark `<option>` backgrounds). It's the only cross-browser mechanism that affects `<option>` hover colors.
|
||||
|
||||
**Alternative — replace with custom Skeleton/Flowbite component** if you need full styling control (e.g., color-coded options, icons). Native `<select>` is acceptable for simple purpose dropdowns with the `color-scheme` fix above.
|
||||
|
||||
**Store reference:** `$ae_loc.dark_mode` — boolean, set by the theme engine in `ae_stores.ts`.
|
||||
@@ -1,569 +0,0 @@
|
||||
# Aether UI/UX — Future Ideas
|
||||
|
||||
> Collection of concrete UX improvements for the Aether frontend. Each entry includes
|
||||
> the rationale, current behavior, proposed change, and implementation notes.
|
||||
> **Date:** 2026-05-17
|
||||
|
||||
---
|
||||
|
||||
## IDAA Recovery Meetings
|
||||
|
||||
### 1. Guided empty state with active filters — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** When filters return 0 results, the page shows:
|
||||
"No recovery meetings found matching your criteria."
|
||||
The member has no indication whether this is a bug, genuinely no data, or just
|
||||
overly narrow filters.
|
||||
|
||||
**Proposed change:** When filters are active AND the result count is 0, show a
|
||||
helpful prompt instead of the bare message:
|
||||
|
||||
```
|
||||
No meetings found for these filters.
|
||||
Try broadening your search or [Clear all filters →]
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `has_active_filters` derived in `+page.svelte` that checks whether any of
|
||||
`qry__physical`, `qry__virtual`, `qry__type`, or `qry__fulltext_str` is set.
|
||||
- In the template's `{:else}` block (line ~443), branch on `has_active_filters`:
|
||||
- `true` → show the guided message + "Clear Filters" button
|
||||
- `false` → show the existing escape-hatch flow (timed "Refresh Meeting Cache" button
|
||||
after 8 seconds, since zero unfiltered results always indicates a problem)
|
||||
- The "Clear Filters" button resets all four filter fields to `null`/`''` and bumps
|
||||
`search_version` to trigger a fresh unfiltered search.
|
||||
- Distinct from the `error` state — this is a successful search (`qry__status === 'done'`)
|
||||
with an empty result set.
|
||||
|
||||
**Implemented:** `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. `has_active_filters`
|
||||
derived checks `qry__physical`, `qry__virtual`, `qry__type`, and `qry__fulltext_str`. Empty
|
||||
state branches on `has_active_filters`: active filters → guided message + "Clear Filters"
|
||||
button; no active filters → existing escape-hatch flow (timed "Refresh Meeting Cache" after
|
||||
8 seconds).
|
||||
|
||||
---
|
||||
|
||||
### 2. Quick-filter chips below the search bar — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** Members toggle filters via small checkboxes (Virtual, In-person)
|
||||
and radio buttons (All, IDAA, Caduceus, Family Recovery). These require precise
|
||||
mouse/tap targeting and scanning several lines of filter UI to discover and use.
|
||||
|
||||
**Proposed change:** Add a row of preset chip buttons directly below the search input:
|
||||
|
||||
```
|
||||
[🖥 Virtual] [🏠 In-Person] [🩺 IDAA] [Caduceus] [Family Recovery] [All Types]
|
||||
```
|
||||
|
||||
- Each chip toggles the corresponding filter (`qry__virtual`, `qry__physical`, `qry__type`)
|
||||
and triggers an immediate search.
|
||||
- Selected chips get a filled/pressed style; unselected chips are outlined.
|
||||
- "All Types" is the default selected state (no type filter). Clicking another type
|
||||
chip deselects "All Types" (radio behavior for the type dimension). Virtual and
|
||||
In-person are independent toggles (checkbox behavior — can select both).
|
||||
- The existing checkboxes/radio buttons remain as the underlying state storage
|
||||
(`$idaa_loc.recovery_meetings.*`). The chips are a convenience layer — they write
|
||||
to the same store fields and call `handle_search_trigger()`.
|
||||
|
||||
**Implementation notes:**
|
||||
- Place in `ae_idaa_comp__event_obj_qry.svelte` between the search input row and the
|
||||
current filter rows.
|
||||
- Optionally hide the existing checkbox/radio filter rows when the chips are present
|
||||
(or keep both — the checkboxes serve as accessible form controls; the chips are
|
||||
the primary visual interaction).
|
||||
- On mobile, chips wrap to a second row naturally with `flex-wrap`.
|
||||
|
||||
**Implemented:** `ae_idaa_comp__event_obj_qry.svelte`. Chips replaced the old
|
||||
checkbox/radio/select UI entirely rather than layering on top. Two chip rows:
|
||||
Row 1 — My Meetings (first), Virtual, In-Person. Row 2 — All / IDAA / Caduceus /
|
||||
Family Recovery type chips. Cycling sort button replaces separate sort options
|
||||
(see item below). Max Results uses a +/− stepper. Sort and max are in a third
|
||||
row below the chips, inside the same `<form>` constraint.
|
||||
|
||||
---
|
||||
|
||||
### 3. Language: "Searching..." vs "Loading..."
|
||||
|
||||
**Current behavior:** The loading state always shows the same message:
|
||||
|
||||
```
|
||||
🔄 Searching...
|
||||
```
|
||||
|
||||
This appears on initial page load (when the user hasn't typed anything) and after
|
||||
the user clicks Search or toggles a filter. The word "Searching" implies the user
|
||||
initiated a search, which is misleading on initial page load — it's a cold cache
|
||||
load, not an active search.
|
||||
|
||||
**Proposed change:** Distinguish the two loading contexts:
|
||||
|
||||
| Context | Message |
|
||||
|---------|---------|
|
||||
| Initial page load (no filters, no search text) | "Loading meetings..." |
|
||||
| User clicked Search or toggled a filter | "Searching..." (keep current) |
|
||||
|
||||
**Implementation notes:**
|
||||
- In `+page.svelte` template around line 422, check whether `qry__fulltext_str` is
|
||||
empty AND no filter checkboxes/radios are active. If so, show "Loading meetings...";
|
||||
otherwise show "Searching...".
|
||||
- This is purely a label change — no logic changes needed. The condition can be the
|
||||
same `has_active_filters` derived from item #1.
|
||||
- Also update the list component's standalone loading state in
|
||||
`ae_idaa_comp__event_obj_li.svelte` line 556-558 to use the same distinction.
|
||||
|
||||
---
|
||||
|
||||
### 4. Filter row collapsing on mobile
|
||||
|
||||
**Current behavior:** The query bar has three filter rows (Location checkboxes,
|
||||
Type radios, Max/Sort selects) plus the search input row and the action button row.
|
||||
Combined, this takes roughly 200px of vertical space. On mobile — especially inside
|
||||
the Novi iframe on a phone — meeting cards are pushed below the fold.
|
||||
|
||||
**Proposed change:** On viewports below `md` (768px), collapse the Location and Type
|
||||
filter rows behind a "Filters ▾" toggle. The Max Results and Sort selects stay visible
|
||||
since they're used frequently. The action buttons (Show Hidden, Create Meeting, Export)
|
||||
move inside the collapsed panel or stay visible based on available width.
|
||||
|
||||
```
|
||||
[Search input............................] [Search]
|
||||
|
||||
[Filters ▾] [Max: 150 ▾] [Sort: Last Updated ▾]
|
||||
```
|
||||
|
||||
Clicking "Filters ▾" expands the panel with Location checkboxes and Type radios.
|
||||
|
||||
**Implementation notes:**
|
||||
- Use a `$state` boolean `show_filters` (session-only, resets on page load).
|
||||
- Wrap the filter rows in a `{#if show_filters}` block.
|
||||
- Persist in `$idaa_sess.recovery_meetings.show_filters_expanded` if you want the
|
||||
state to survive navigation within the module (same tab session).
|
||||
- The Tailwind `md:` breakpoint works for the collapse trigger: `class:hidden={!show_filters}`
|
||||
combined with `class:md:block` to always show on desktop.
|
||||
- Test inside the Novi iframe — Bootstrap v3 may add its own `hidden` behavior on
|
||||
`md` breakpoints that conflicts with Tailwind's.
|
||||
|
||||
---
|
||||
|
||||
### 5. Human-readable schedule line on cards
|
||||
|
||||
**Current behavior:** The meeting card displays weekdays as a flat, dense span list:
|
||||
|
||||
```
|
||||
Sunday Monday Wednesday Friday
|
||||
```
|
||||
|
||||
The timezone is shown separately as `(America/Chicago)`, and the start time is in
|
||||
a compact `7:00 PM` format. These three pieces of information are visually separated
|
||||
and require the member to mentally assemble the schedule.
|
||||
|
||||
**Proposed change:** Render a computed one-liner that combines them:
|
||||
|
||||
```
|
||||
🕐 Mondays, Wednesdays, Fridays at 7:00 PM CT
|
||||
```
|
||||
|
||||
- Weekday names are built from the `weekday_*` booleans on the event object.
|
||||
- "Mondays, Wednesdays" uses the range-joining convention (comma-separated, "and"
|
||||
before the last item for two days; "Mondays through Fridays" for consecutive spans
|
||||
of 3+ days).
|
||||
- Timezone abbreviation is extracted from `timezone` (e.g., `America/Chicago` → `CT`,
|
||||
`America/New_York` → `ET`). A small lookup table handles the common ones; fall back
|
||||
to the raw timezone string for unknown values.
|
||||
- If `timezone` is null/missing, fall back to the current flat display — don't
|
||||
silently drop information.
|
||||
- Today's meetings could optionally get a subtle "Today" badge or highlight (extra
|
||||
polish, not required for the initial version).
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$derived` in `ae_idaa_comp__event_obj_li.svelte` that computes the schedule
|
||||
string from the event object's `weekday_*` fields, `recurring_start_time`, and
|
||||
`timezone`.
|
||||
- Helper function in `ae_util` for the weekday list → natural language string
|
||||
(e.g., `['Monday', 'Wednesday', 'Friday']` → `"Mondays, Wednesdays, and Fridays"`).
|
||||
- Helper function or small lookup for timezone → abbreviation.
|
||||
- Fall back to the current flat display when `timezone` is missing to avoid losing
|
||||
information.
|
||||
|
||||
---
|
||||
|
||||
### 6. Show result count during search, not just after
|
||||
|
||||
**Current behavior:** The result count badge ("Results: 25") only appears inside the
|
||||
list wrapper component (`ae_idaa_comp__event_obj_li.svelte` line 98-108) when the
|
||||
visible result list is non-empty. During loading, the user sees only a spinner with
|
||||
no indication of how many meetings exist or what the search is operating on.
|
||||
|
||||
**Proposed change:** Show a result count line at the page level (in `+page.svelte`)
|
||||
that is always visible once the first search completes:
|
||||
|
||||
```
|
||||
25 of 140 meetings ← after search completes, with result count + total
|
||||
Searching 140 meetings... ← during initial load (cold cache, no prior result)
|
||||
0 results for these filters ← empty but filters are active (ties into item #1)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Lift the count display from the list component to `+page.svelte`, placed between
|
||||
the query bar (`Comp__event_obj_qry`) and the list wrapper.
|
||||
- The total count is available from the IDB fast path: after the initial unfiltered
|
||||
search populates `db_events.event`, the total is `db_events.event.count()` (or
|
||||
the count of records matching `account_id`).
|
||||
- The visible count is `event_id_li.length` after search completes.
|
||||
- Store the last known total in a `$state` variable so it persists across searches
|
||||
(the total changes infrequently). Refresh the total on the first search after
|
||||
page load.
|
||||
- Format: `{visible} of {total} meetings` when filters/search are active;
|
||||
`{visible} meetings` when browsing all (no active filters).
|
||||
- During loading with no prior results: show "Loading meetings..." (from item #3)
|
||||
rather than a count.
|
||||
|
||||
---
|
||||
|
||||
### 7. "Live Now" and "Starting Soon" indicators
|
||||
|
||||
**Current behavior:** Meetings are shown in a static list. To find one happening
|
||||
now, a member must scan the "When" line of multiple cards and compare the time
|
||||
to their own clock.
|
||||
|
||||
**Proposed change:** Add a high-visibility badge or pulse indicator for meetings
|
||||
that are currently in progress or starting in the next 15 minutes.
|
||||
|
||||
- "LIVE NOW" (Green pulse badge) → if `current_time` is within `[start, start + 1 hour]`.
|
||||
- "STARTING SOON" (Yellow badge) → if `current_time` is within `[start - 15 min, start]`.
|
||||
- On the card, move the "Join Zoom" or "Join Jitsi" button to the very top or
|
||||
make it significantly larger when the meeting is live.
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$derived` state `is_live` and `is_starting_soon` to the card component.
|
||||
- Requires calculating "current time in meeting's timezone" using `Temporal` or
|
||||
a date helper.
|
||||
- Ensure the pulse animation is subtle and respects `prefers-reduced-motion`.
|
||||
|
||||
---
|
||||
|
||||
### 8. Local Timezone Conversion
|
||||
|
||||
**Current behavior:** Meetings show their native timezone (e.g., "7:00 PM America/Chicago").
|
||||
The "Your TZ" line is currently a placeholder and doesn't perform conversion.
|
||||
|
||||
**Proposed change:** Automatically detect the member's browser timezone and
|
||||
show the converted time if it differs from the meeting's native timezone.
|
||||
|
||||
```
|
||||
🕐 7:00 PM CT (8:00 PM ET your time)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Use `Intl.DateTimeFormat().resolvedOptions().timeZone` to get the user's TZ.
|
||||
- If `user_tz !== meeting_tz`, perform the conversion.
|
||||
- If the conversion results in a different day (e.g., late night ET vs early morning Europe),
|
||||
prefix with "Tomorrow at..." or "Yesterday at...".
|
||||
|
||||
---
|
||||
|
||||
### 9. Favorites / "My Meetings" — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** Members scan the full list every time they want to find
|
||||
their regular weekly meeting.
|
||||
|
||||
**Proposed change:** Add a "Star" icon to every meeting card.
|
||||
- Starring a meeting adds it to a `favorites` list stored in `$idaa_loc`.
|
||||
- Favorited meetings are pinned to the top of the list by default, regardless
|
||||
of other sort orders.
|
||||
- Add a "Favorites" filter toggle in the query bar to show *only* starred meetings.
|
||||
|
||||
**Implementation notes:**
|
||||
- Store as an array of `event_id` strings in `$idaa_loc.recovery_meetings.favorites`.
|
||||
- Update the `visible_event_obj_li` derived in `+page.svelte` to prioritize
|
||||
these IDs in the sort logic.
|
||||
|
||||
**Implemented:** Star toggle on the `[event_id]` detail page
|
||||
(`src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte`).
|
||||
"My Meetings" filter chip is first in the filter chip row on the list page.
|
||||
**Implementation differs from proposal:** favorites stored server-side in a
|
||||
`data_store` record (code: `idaa_meetings_favorites`) as a UUID-keyed JSON map
|
||||
rather than in `$idaa_loc` — this means favorites persist across browsers and
|
||||
devices without Novi write capability. Pinning favorites to the top of the list
|
||||
was not implemented; the filter chip shows only favorites instead.
|
||||
|
||||
---
|
||||
|
||||
### 10. "Add to Calendar" (iCal / Google)
|
||||
|
||||
**Current behavior:** Members must manually create calendar events if they
|
||||
want reminders for recurring meetings.
|
||||
|
||||
**Proposed change:** Add an "Add to Calendar" dropdown button on the meeting
|
||||
detail page (and optionally the card).
|
||||
- Generates a `.ics` file or a Google Calendar URL with the recurring rule
|
||||
(e.g., "Every Wednesday at 7pm").
|
||||
- Includes the meeting name, description, and the Zoom/Jitsi link in the location field.
|
||||
|
||||
**Implementation notes:**
|
||||
- Use a helper to generate RFC 5545 `RRULE` strings from the `weekday_*` and
|
||||
`recurring_pattern` fields.
|
||||
- Include the `attend_url` in the calendar event description for one-tap join
|
||||
from phone lock screens.
|
||||
|
||||
---
|
||||
|
||||
### 11. Geographic Search for In-Person Meetings
|
||||
|
||||
**Current behavior:** The only location filter is a binary "Physical" checkbox.
|
||||
Members must use fulltext search (e.g., "Chicago") to find local meetings.
|
||||
|
||||
**Proposed change:** Add a "City/State" search input or a map view.
|
||||
- When `Physical` is checked, show a "Near [City, State]" input.
|
||||
- Map view (optional): A toggle to switch from "List" to "Map" view, plotting
|
||||
meetings on a map using their `location_address_json` coordinates.
|
||||
|
||||
**Implementation notes:**
|
||||
- The `event` table has `location_address_json` which often contains city/state.
|
||||
- Simple implementation: a city-picker dropdown populated from the distinct
|
||||
`location_address_json->>'$.city'` values in the current result set.
|
||||
|
||||
---
|
||||
|
||||
### 12. Prominent "Join" button for virtual meetings
|
||||
|
||||
**Current behavior:** On each meeting card, the Zoom or Jitsi join link is rendered
|
||||
as a small `btn-sm` inside the content area, visually equivalent to other label/value
|
||||
rows. The "Meeting Details" button at the top of the card is rendered *larger* than the
|
||||
join link — meaning the primary action for a member who wants to attend a meeting right
|
||||
now is visually subordinate to a navigation link.
|
||||
|
||||
The copy-to-clipboard button for the join link is gated behind `$ae_loc.manager_access`,
|
||||
so regular members have no easy way to share the link with a sponsee.
|
||||
|
||||
**Proposed change:** For virtual meetings, elevate the join button to a full-width
|
||||
prominent CTA inside the card header area, directly below the meeting name and badges:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 📅 Monday Night IDAA Discussion 🖥 Virtual │
|
||||
│ │
|
||||
│ [ 🎥 Join Zoom Meeting ] ← full-width │
|
||||
│ [ 📋 Meeting Details ] │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- The Join button uses `preset-filled` (solid) styling; Meeting Details uses
|
||||
`preset-outlined` (hollow). This makes the action hierarchy visually clear.
|
||||
- On mobile especially, a full-width join button is much easier to tap than a
|
||||
small inline link buried inside label rows.
|
||||
- Replace manager-only clipboard with a Web Share API button for all members on
|
||||
virtual meetings: `navigator.share({ title, url })` on mobile triggers the native
|
||||
OS share sheet. Fall back to clipboard copy on desktop (where `navigator.share`
|
||||
is often unavailable). This lets members easily send a meeting link to a sponsee.
|
||||
- Passcode, if present, moves to the Meeting Details page — exposing it in the
|
||||
list view is unnecessary and clutters the card.
|
||||
|
||||
**Implementation notes:**
|
||||
- In `ae_idaa_comp__event_obj_li.svelte`, move the Zoom/Jitsi attend block from
|
||||
the `event__content` section up into the `ae_options` div (line ~200), rendered
|
||||
only when `idaa_event_obj?.virtual` is true and an attend URL exists.
|
||||
- Keep the existing small label/link in `event__content` as a fallback for when
|
||||
the prominent button is not shown (non-virtual meetings may still have a URL).
|
||||
- Web Share: `{#if navigator?.share}` guard; wrap in a try/catch (user cancels
|
||||
the share sheet throws `AbortError`).
|
||||
- The Live Now / Starting Soon badges from item #7, when implemented, should also
|
||||
interact with this button — e.g., a pulsing green border when the meeting is live.
|
||||
|
||||
---
|
||||
|
||||
### 13. "Today's Meetings" section at the top of the list
|
||||
|
||||
**Current behavior:** The meeting list shows all results sorted by the selected sort
|
||||
order. To find a meeting happening today, a member must scan every card's "When" line
|
||||
and mentally compare it to the current day and time. There is no at-a-glance view
|
||||
of what's available right now or later today.
|
||||
|
||||
This is distinct from the "Live Now" badge in item #7 (which marks individual cards
|
||||
after they're already displayed in a long list). This is a dedicated section pinned
|
||||
above the main results.
|
||||
|
||||
**Proposed change:** Add a collapsible "Today" section at the very top of the results
|
||||
list that shows only meetings scheduled on the current day of the week, sorted by
|
||||
start time:
|
||||
|
||||
```
|
||||
▼ Today — Sunday, May 17 3 meetings
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Sunday Serenity Discussion 7:00 AM ET 🔴Live │
|
||||
│ IDAA Sunday Big Book 2:00 PM CT │
|
||||
│ Sunday Night IDAA 8:00 PM ET │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
All Meetings (140)
|
||||
...
|
||||
```
|
||||
|
||||
- The section is collapsed by default if it's empty (no meetings today).
|
||||
- Meetings in the "Today" section also appear in the main list below — this is a
|
||||
quick-access shortcut, not a filter.
|
||||
- Past meetings (start time has already passed today) are dimmed but still shown;
|
||||
a meeting may still be in progress.
|
||||
|
||||
**Implementation notes:**
|
||||
- Compute current day-of-week in the browser: `new Date().getDay()` → 0=Sunday, 6=Saturday.
|
||||
Map to the `weekday_*` boolean fields on the event object (e.g., day 0 → `weekday_sunday`).
|
||||
- Filter `visible_event_obj_li` (already computed in the list wrapper) for items where
|
||||
the matching `weekday_*` field is truthy. Sort by `recurring_start_time`.
|
||||
- Store collapse state in `$idaa_sess.recovery_meetings.today_section_expanded` (session
|
||||
only; default true so it's visible on first load).
|
||||
- Renders correctly in the Novi iframe since it's just a filtered sub-list of existing
|
||||
data — no additional API calls needed.
|
||||
- If item #7 (Live Now) is implemented, the "Today" section naturally becomes the host
|
||||
for the live/starting-soon badges, since that's where members will look first.
|
||||
|
||||
---
|
||||
|
||||
### 14. Data freshness indicator *(low priority — deprioritized)*
|
||||
|
||||
**Note:** Meeting records change infrequently — once established, a meeting's schedule,
|
||||
type, and contact info are typically stable for months or years. The occasional update is
|
||||
usually minor wording. Surfacing a freshness indicator for data this static would add
|
||||
visual noise with very little member benefit. The existing error state (item #4 in the
|
||||
bug fix, distinct "Unable to load meetings") and the escape-hatch cache-reset button
|
||||
already handle the reliability-concern case. This idea is recorded for completeness but
|
||||
is not recommended for implementation.
|
||||
|
||||
---
|
||||
|
||||
### 15. "Confirmed meetings only" default filter
|
||||
|
||||
**Current behavior:** All meetings are shown by default, including those with
|
||||
`status === 'unknown'` (not yet confirmed by IDAA Central Office). The "Not Confirmed
|
||||
by IDAA" warning badge on those cards is alarming-looking but does nothing to prevent
|
||||
unverified meetings from dominating the list.
|
||||
|
||||
There is currently no filter to hide unconfirmed meetings. Members have no choice but
|
||||
to see them all.
|
||||
|
||||
**Business rationale:** IDAA staff want meeting chairs to submit their meeting info for
|
||||
verification. Defaulting to "confirmed only" creates a natural incentive: unconfirmed
|
||||
meetings disappear from the default member view, which encourages chairs to contact
|
||||
IDAA staff and get their meeting verified. It also gives members a cleaner, higher-
|
||||
confidence list by default — they're not seeing meetings that may be outdated or
|
||||
inactive.
|
||||
|
||||
**Proposed change:** Add a `qry__confirmed` filter field with three states:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `'confirmed_only'` (default) | Hide meetings where `status === 'unknown'` |
|
||||
| `'all'` | Show all meetings, confirmed and unconfirmed |
|
||||
| `'unconfirmed_only'` | Show only unconfirmed (admin/staff use) |
|
||||
|
||||
The filter UI shows a simple toggle in the query bar:
|
||||
```
|
||||
[✓ Confirmed Only] ← default, shown as active chip or checkbox
|
||||
```
|
||||
|
||||
When the member switches to "All", the unconfirmed meetings appear with their warning
|
||||
badge (see item #15 for making that badge useful on mobile).
|
||||
|
||||
For trusted/admin users, the default should remain `'all'` so staff can see the full
|
||||
picture without having to change a setting.
|
||||
|
||||
**Implementation notes:**
|
||||
- Add `qry__confirmed: 'confirmed_only' | 'all' | 'unconfirmed_only'` to
|
||||
`$idaa_loc.recovery_meetings` defaults, defaulting to `'confirmed_only'`.
|
||||
Trusted users default to `'all'`.
|
||||
- Apply the filter in both the IDB fast path (`db_events.event.filter()`) and the
|
||||
API revalidation secondary filter in `handle_search_refresh`. IDB: check
|
||||
`ev.status !== 'unknown'` when `qry__confirmed === 'confirmed_only'`. API: same
|
||||
post-fetch client-side filter.
|
||||
- Pass `qry__confirmed` to `events_func.search__event` if the API supports a
|
||||
`status` filter param; otherwise handle it client-side only.
|
||||
- The `no_results_no_filters` derived (used for the escape-hatch button) should NOT
|
||||
treat `qry__confirmed === 'confirmed_only'` as an active filter — it's the default
|
||||
state, not a narrowing choice the member made. Only count it as a filter if the
|
||||
member explicitly switched it to `'all'` or `'unconfirmed_only'`.
|
||||
- Add a count badge to the toggle: "Confirmed Only (132 of 140)" so members can see
|
||||
how many unconfirmed meetings exist without having to switch the filter.
|
||||
|
||||
---
|
||||
|
||||
### 16. "Not Confirmed" status — inline explanation on mobile
|
||||
|
||||
**Current behavior:** Meetings with `status === 'unknown'` show a warning badge:
|
||||
`⚠ Not Confirmed by IDAA ⚠`. The badge has a `title` attribute with a full explanation
|
||||
(~2 sentences). Title tooltips are invisible on mobile — tapping the badge does nothing.
|
||||
Members on phones see a alarming-looking warning with no explanation of what it means
|
||||
or what they should do.
|
||||
|
||||
**Proposed change:** Make the badge tappable. On tap (or hover on desktop), show an
|
||||
inline explanation panel directly below the badge:
|
||||
|
||||
```
|
||||
⚠ Not Confirmed by IDAA [?]
|
||||
|
||||
↓ (on tap)
|
||||
|
||||
This meeting has not been confirmed by IDAA Central Office.
|
||||
Please reach out to the chair for current information.
|
||||
If this meeting is active, email info@idaa.org to confirm it.
|
||||
[✕ Close]
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$state show_unconfirmed_info = false` per card (scoped to the `{#each}` block).
|
||||
- Replace the `title` attribute with an `onclick` toggle that sets `show_unconfirmed_info`.
|
||||
- The explanation renders in a `{#if show_unconfirmed_info}` block directly below the
|
||||
badge row — a simple div with a rounded border and the existing tooltip text.
|
||||
- The `mailto:info@idaa.org` link in the explanation is already in the tooltip text;
|
||||
making it a real clickable link here rather than plain text in a tooltip is a direct
|
||||
improvement for mobile members who want to report a confirmed meeting.
|
||||
- This pattern also applies to other `title`-only tooltips on the page if they appear.
|
||||
- Note: with item #15 defaulting to "confirmed only", most members will never encounter
|
||||
this badge unless they switch to the "All" view. The inline explanation is still worth
|
||||
implementing for that audience, but the default filter reduces how often it's seen.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
### 17. Cycling sort button — ✅ Implemented 2026-05-18
|
||||
|
||||
**Problem:** Three separate sort chip buttons (Last Updated / Name A→Z / Name Z→A) took
|
||||
too much horizontal space and caused layout bounce as the selected chip changed width.
|
||||
|
||||
**Implemented:** Single cycling button in `ae_idaa_comp__event_obj_qry.svelte`.
|
||||
Clicking advances through `sort_modes` array (Last Updated → Name A→Z → Name Z→A → repeat)
|
||||
using `$derived` index + `cycle_sort()` function. Button has `min-w-36` to prevent bounce.
|
||||
Icon changes per mode (fa-clock / fa-sort-alpha-down / fa-sort-alpha-up-alt). A small
|
||||
fa-redo icon indicates it's a cycling control.
|
||||
|
||||
---
|
||||
|
||||
### 18. Collapsible "Meeting Info" data store panel — ✅ Implemented 2026-05-18
|
||||
|
||||
**Problem:** The `Element_data_store` panel (code: `recovery_meetings_info`) displays
|
||||
between the filter bar and the meeting results list. Once a member has read it, it
|
||||
consumes vertical space on every page load and pushes results below the fold, especially
|
||||
in the Novi iframe on mobile.
|
||||
|
||||
**Implemented:** Toggle button wrapping the `<Element_data_store>` in
|
||||
`src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. Button shows
|
||||
"Meeting Info" with a chevron (up = expanded, down = collapsed). Collapse state
|
||||
persisted in `$idaa_loc.recovery_meetings.ds_info_collapsed` (localStorage) so the
|
||||
user's preference survives page reloads. New field added to `idaa_local_data_struct`
|
||||
in `ae_idaa_stores.ts` — no version bump needed (existing users without the field
|
||||
get `undefined` which is falsy = expanded, the correct default).
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The fulltext search (`qry__fulltext_str`) searches against the `default_qry_str`
|
||||
field, which is a server-side composite that already includes contact info, day-of-week
|
||||
text, meeting type, location, and other metadata. The placeholder text in the search
|
||||
input is accurate — it genuinely searches contacts and schedule information despite
|
||||
those fields being stored in separate columns.
|
||||
- All changes must render correctly inside the Novi iframe context (Bootstrap v3.4.1
|
||||
CSS conflicts — see `CLIENT__IDAA_and_customized_mods.md` for known issues).
|
||||
- Mobile testing should cover Android Chrome specifically — the original "no meetings
|
||||
found" bug disproportionately affected mobile users with intermittent connections.
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
# Aether SvelteKit — AI Agent Bootstrap / Quickstart
|
||||
> **Read this first.** This doc is the fast path to being productive on this project.
|
||||
> It covers the rules, patterns, and gotchas that matter most.
|
||||
> Deep dives are in the linked docs at the bottom.
|
||||
|
||||
---
|
||||
|
||||
## 1. What This Project Is
|
||||
|
||||
**Aether** is an event management platform built by One Sky IT (Scott Idem).
|
||||
This repo is the frontend: **Svelte 5 (runes mode) + SvelteKit v2**.
|
||||
|
||||
The backend is a separate repo (`aether_api_fastapi`) — a FastAPI + MariaDB app
|
||||
running in Docker. The frontend talks to it exclusively via the V3 REST API.
|
||||
|
||||
**Key clients:**
|
||||
- **Conference organizers** — Presentation Management (pres_mgmt), Launcher, Badges
|
||||
- **Exhibitors** — Leads capture
|
||||
- **IDAA** — International Doctors in Alcoholics Anonymous (private medical/recovery community)
|
||||
|
||||
**Stack at a glance:**
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Framework | Svelte 5 (runes mode) + SvelteKit v2 |
|
||||
| Styling | Tailwind CSS v4 + Flowbite (Skeleton UI being phased out) |
|
||||
| State | `$state`/`$derived` runes + Dexie.js IndexedDB (`liveQuery`) |
|
||||
| Icons | Lucide (`@lucide/svelte`) |
|
||||
| Editors | CodeMirror 6 (primary), Edra/TipTap (secondary) |
|
||||
| Native | Electron app for onsite launcher (`src/lib/electron/electron_relay.ts`) |
|
||||
| Backend | FastAPI + MariaDB, V3 API (`/v3/crud/`, `/v3/lookup/`) |
|
||||
| Auth | Custom headers: `x-aether-api-key` + `x-account-id`; JWT Bearer is auto-injected when a session exists |
|
||||
|
||||
---
|
||||
|
||||
## 2. Critical Rules — Read Before Touching Any Code
|
||||
|
||||
### Privacy (Sev-1 class failures if violated)
|
||||
- **IDAA content is ALWAYS private.** All routes under `/idaa/` require authentication.
|
||||
A previous AI agent accidentally made IDAA bulletin board data publicly accessible.
|
||||
This is the single most serious class of mistake on this project. When in doubt — it's private.
|
||||
- **Journals** are private personal data. Always authenticated.
|
||||
|
||||
### File Safety
|
||||
- **Never use `rm`** to delete files. Move to `~/tmp/agents_trash` instead.
|
||||
- Never commit `.env` files, API keys, or passwords.
|
||||
|
||||
### Before Every Commit
|
||||
- Run `npx svelte-check` — zero errors, zero warnings. No exceptions.
|
||||
- Atomic commits: one component or one fix per commit.
|
||||
|
||||
### Before Starting Any Task
|
||||
- Read `documentation/TODO__Agents.md` — it has active tasks, known bugs, and context
|
||||
about what was recently changed and why.
|
||||
|
||||
### V3 API — Never Include the Object ID in PATCH Body Fields
|
||||
The ID is in the URL. Including it in `data_kv` causes a `400: Unknown column in SET`.
|
||||
```ts
|
||||
// WRONG — causes 400 error:
|
||||
update_ae_obj__event_file({ event_file_id, data_kv: { event_file_id, file_purpose: 'final' } })
|
||||
|
||||
// CORRECT:
|
||||
update_ae_obj__event_file({ event_file_id, data_kv: { file_purpose: 'final' } })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Environment & Deploy Cheat Sheet
|
||||
|
||||
There are **two separate `.env` systems** — do not confuse them:
|
||||
|
||||
| System | File | Controls |
|
||||
|---|---|---|
|
||||
| `aether_container_env/.env` | Docker orchestration | Ports, `AE_CFG_ID`, replicas, paths |
|
||||
| `aether_app_sveltekit/.env.*` | Vite/SvelteKit build | `PUBLIC_*` API vars baked into the JS bundle |
|
||||
|
||||
**The 4 commands you run and which env file each uses:**
|
||||
|
||||
| Command | Env file read |
|
||||
|---|---|
|
||||
| `npm run dev` | `aether_app_sveltekit/.env.local` (Vite dev server, localhost:5173) |
|
||||
| `npm run build:docker:dev` | `aether_app_sveltekit/.env.dev` (baked into local Docker image) |
|
||||
| `npm run deploy:remote:test` | `/srv/apps/test_aether_app_sveltekit/.env.test` on Linode |
|
||||
| `npm run deploy:remote:prod` | `/srv/apps/prod_aether_app_sveltekit/.env.prod` on Linode |
|
||||
|
||||
**The `.env.*` files are gitignored** (only `.default` templates are tracked). They must be
|
||||
placed manually on each server during initial setup. On the workstation you only need
|
||||
`.env.local` and `.env.dev`. The Linode servers each have exactly one env file for their environment.
|
||||
|
||||
**What goes in every SvelteKit env file** (same 8 vars, different values per env):
|
||||
```env
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=<api server hostname>
|
||||
PUBLIC_AE_API_BAK_SERVER=<bak api hostname>
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=<key>
|
||||
PUBLIC_AE_CRUD_SUPER_KEY=<key>
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=<key>
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Svelte 5 Runes Mode — Key Patterns & Gotchas
|
||||
|
||||
This codebase is **fully Svelte 5 runes mode**. No Svelte 4 syntax.
|
||||
|
||||
### The basics
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// Props — with optional two-way binding
|
||||
interface Props { count?: number; label: string; }
|
||||
let { count = $bindable(0), label }: Props = $props();
|
||||
|
||||
// Reactive state
|
||||
let value = $state('');
|
||||
let upper = $derived(value.toUpperCase());
|
||||
|
||||
// Side effects (replaces onMount + $: reactive)
|
||||
$effect(() => {
|
||||
console.log('value changed:', value);
|
||||
return () => { /* cleanup */ };
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### What NOT to use (Svelte 4 patterns — do not introduce)
|
||||
```ts
|
||||
// ❌ No writable() stores for component state
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// ❌ No reactive declarations
|
||||
$: doubled = count * 2;
|
||||
|
||||
// ❌ No onDestroy for cleanup — use $effect return instead
|
||||
onDestroy(() => cleanup());
|
||||
```
|
||||
|
||||
### `$bindable()` vs `$state()`
|
||||
- Use `$bindable()` when the parent needs two-way binding on a prop.
|
||||
- Use `$state()` for local component state with no external binding.
|
||||
|
||||
### Store reactivity trap (important for `$effect`)
|
||||
The app uses `svelte-persisted-store` (Svelte 4 contract) for `$ae_loc`, `$ae_api`,
|
||||
`$ae_sess`, etc. In Svelte 5 `$effect`, reading **any field** of a Svelte 4 store
|
||||
subscribes to the **entire store**. This means unrelated writes to `$ae_loc`
|
||||
(e.g. iframe height, SWR reload) will re-trigger your effect. Be conservative about
|
||||
what you read from these stores inside `$effect` blocks. See `PROJECT__Stores_Svelte5_Migration.md`
|
||||
for the long-term fix plan.
|
||||
|
||||
For search pages specifically, this usually means:
|
||||
- keep true user preferences in persisted local state
|
||||
- keep transient triggers, loading flags, and last-executed search keys in session state when possible
|
||||
- let the page effect schedule the search, but put the duplicate-execution guard inside the search executor so page-load auto-search still runs after hydration
|
||||
- if the search text or filters are mirrored from localStorage on mount, expect that mount-time writes can re-trigger the effect unless the executor has its own guard
|
||||
|
||||
### `{#await}` blocks
|
||||
```svelte
|
||||
{#await somePromise}
|
||||
<LoadingSpinner />
|
||||
{:then result}
|
||||
<div>{result}</div>
|
||||
{:catch error}
|
||||
<ErrorMessage {error} />
|
||||
{/await}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. V3 API Patterns
|
||||
|
||||
### SWR (Stale-While-Revalidate) — the standard load pattern
|
||||
Return cached Dexie data immediately, refresh from API in background.
|
||||
```ts
|
||||
async function load_ae_obj_id__my_obj({ api_cfg, obj_id }) {
|
||||
// 1. Return stale cache immediately (fast)
|
||||
const cached = await db.my_obj.get(obj_id);
|
||||
if (cached) my_obj_state = cached;
|
||||
|
||||
// 2. Fetch fresh from API in background
|
||||
_refresh_my_obj_background({ api_cfg, obj_id });
|
||||
}
|
||||
```
|
||||
|
||||
### Shared/Common Aether object fields
|
||||
The core fields for almost all Aether objects are:
|
||||
* id/id_random
|
||||
* code - string
|
||||
* name - string
|
||||
* summary - string
|
||||
* content - string
|
||||
* alert - boolean
|
||||
* alert_msg - text
|
||||
* priority - boolean
|
||||
* sort - int
|
||||
* group - string
|
||||
* hide - boolean
|
||||
* enable - boolean
|
||||
* default_qry_str - special concat string index
|
||||
* notes - text
|
||||
* created_on - timestamp
|
||||
* updated_on - timestamp
|
||||
|
||||
### ID convention — never use `_id_random` fields
|
||||
The V3 API uses random string IDs (e.g. `event_file_id = "aBc123"`). The `*_id_random`
|
||||
fields are legacy aliases. The integer version of the ID is never returned by the API. Always use the short form:
|
||||
```ts
|
||||
// ✅ Correct
|
||||
event_file_obj.event_file_id
|
||||
|
||||
// ❌ Wrong — legacy alias, don't use
|
||||
event_file_obj.event_file_id_random
|
||||
```
|
||||
The short ".id" is also the randomized string, **not an integer** (autonum).
|
||||
|
||||
### PATCH — only field values in the body
|
||||
```ts
|
||||
// The obj_id goes in the URL (handled by update_ae_obj__* function).
|
||||
// Only the fields you want to update go in data_kv.
|
||||
await events_func.update_ae_obj__event_file({
|
||||
api_cfg: $ae_api,
|
||||
event_file_id: 'aBc123', // → becomes the URL path param
|
||||
data_kv: { file_purpose: 'final' } // → only changed fields
|
||||
});
|
||||
```
|
||||
|
||||
### Auth headers (set automatically by `api.ts`)
|
||||
```
|
||||
x-aether-api-key: <PUBLIC_AE_API_SECRET_KEY>
|
||||
x-account-id: <account_id>
|
||||
```
|
||||
|
||||
**Do not treat `params.key` as an auth bypass.**
|
||||
Only explicit `x-no-account-id: bypass` means "drop account context".
|
||||
If `key` is present for business logic, keep `x-account-id` intact.
|
||||
|
||||
### Dexie queries — always use the object ID index, not `.get()`
|
||||
All `db_core` (and other module) Dexie tables define their schema with `id` as the first
|
||||
field (primary key), followed by the object's string ID (e.g. `person_id`). V3 **never**
|
||||
returns `id`, so every record stored in Dexie has `id = undefined`. Calling `.get(value)`
|
||||
does a primary key lookup — it will always miss when passed a string object ID.
|
||||
|
||||
```ts
|
||||
// ❌ Wrong — .get() uses the primary key (id), which V3 never populates:
|
||||
liveQuery(() => db_core.person.get(person_id))
|
||||
|
||||
// ✅ Correct — use .where() on the indexed object ID field:
|
||||
liveQuery(() => db_core.person.where('person_id').equals(person_id).first())
|
||||
```
|
||||
|
||||
This applies to every table in every module (`db_core`, `db_events`, etc.).
|
||||
When looking up a single object by its string ID, always use `.where().equals().first()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Naming Conventions (snake_case; no camelCase)
|
||||
|
||||
| Pattern | Example | Used for |
|
||||
|---|---|---|
|
||||
| `ae_comp__*` | `ae_comp__event_badge.svelte` | Route-level components |
|
||||
| `ae_<module>_comp__*` | `ae_events_comp__session_list.svelte` | Module-scoped components |
|
||||
| `element_*` | `element_input_files_tbl.svelte` | Reusable library primitives |
|
||||
| `lq__*` | `lq__journal_obj` | Read-only liveQuery |
|
||||
| `lqw__*` | `lqw__journal_obj` | Writable form snapshot liveQuery |
|
||||
| `ae_<module>__<obj>.ts` | `ae_journals__journal.ts` | Object type + functions |
|
||||
| `db_<module>.ts` | `db_journals.ts` | Dexie instance per module |
|
||||
|
||||
The **canonical pattern reference** is the Journals module (`src/lib/ae_journals/`).
|
||||
When building anything new, model it after Journals.
|
||||
|
||||
---
|
||||
|
||||
## 7. Mistakes Agents Have Made on This Project
|
||||
|
||||
These are real incidents — know them before you start.
|
||||
|
||||
1. **IDAA BB exposed publicly** — an agent removed an auth guard from the bulletin board
|
||||
route. All IDAA content must be behind authentication. Always check route guards when
|
||||
touching `/idaa/` routes.
|
||||
|
||||
2. **`event_file_id` in PATCH body (400 error)** — including the object ID in `data_kv`
|
||||
when calling `update_ae_obj__*`. The V3 API tries to `SET event_file_id = ...` which
|
||||
fails because it's a view alias, not a DB column. See Section 2 above.
|
||||
|
||||
3. **Bad `.d.ts` declaration silently hid 1368 errors** — a `declare module` in `app.d.ts`
|
||||
(a script-context file) replaced the entire `@lucide/svelte` type exports instead of
|
||||
merging. `svelte-check` showed 0 errors, masking real problems. If `svelte-check`
|
||||
suddenly drops to 0 errors, verify it's not because a bad declaration wiped a module.
|
||||
|
||||
4. **Coarse store reactivity loop** — an `$effect` that read `$ae_loc.some_field` was
|
||||
re-triggering repeatedly because unrelated writes to `$ae_loc` (e.g. SWR config reload)
|
||||
fired the effect. In Svelte 5, any read of a Svelte 4 store inside `$effect` subscribes
|
||||
to the whole store. Scope what you read carefully.
|
||||
|
||||
5. **`file_purpose == 'admin'` not hidden in Launcher** — the `hide_draft` prop hid
|
||||
`outline` and `draft` files but not `admin` files. Gaps like this happen when a new
|
||||
enum value is added to a field without auditing all the places that filter on it.
|
||||
|
||||
6. **Deleting files with `rm`** — always move to `~/tmp/agents_trash`. A deleted file may
|
||||
contain context that's not recoverable from git if it was gitignored.
|
||||
|
||||
7. **Dexie `.get()` with a string object ID returns `undefined`** — Dexie `.get(value)`
|
||||
looks up by the table's **primary key**, which is `id` (the first schema field). The V3
|
||||
API never returns `id`, so it is always `undefined` in stored records. Passing a string
|
||||
object ID (e.g. `person_id`) to `.get()` will silently return nothing. Always use
|
||||
`.where('person_id').equals(person_id).first()` instead. This has caused liveQuery
|
||||
blocks to always produce `undefined` even when the record exists in Dexie.
|
||||
|
||||
8. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child
|
||||
component cannot bypass a parent `+layout.svelte` auth gate. Children only mount if
|
||||
the parent calls `{@render children?.()}`. Adding redundant auth guards to `$effect`
|
||||
blocks that can only run after the parent gate already passed is unnecessary — and
|
||||
misleads future readers into thinking the parent gate is not sufficient on its own.
|
||||
The **real** pre-gate risk is `+page.ts` / `+layout.ts`: universal load functions run
|
||||
before any layout mounts and also fire during SvelteKit link prefetch. Keep those files
|
||||
clean of data loads in private modules. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` →
|
||||
"SvelteKit Layout Hierarchy: Security and Execution Order" for the full explanation.
|
||||
|
||||
9. **Using query `key` as a proxy for bypass stripped `x-account-id`** — this caused
|
||||
valid account-scoped requests to lose account context and 403. `key` can be a valid
|
||||
endpoint/business param, but it is not equivalent to `x-no-account-id: bypass`. Keep
|
||||
`x-no-account-id` usage narrow and temporary; do not expand it without a documented
|
||||
allowlist case.
|
||||
|
||||
10. **Pre-stringifying `*_json` fields before passing to API wrappers** — the API wrappers
|
||||
(`api_post__crud_obj.ts` for V3, `api.ts` for legacy CRUD) automatically serialize any
|
||||
field ending in `_json` (e.g. `cfg_json`, `data_json`). Pass these as plain JS objects.
|
||||
Pre-stringifying with `JSON.stringify()` before calling the wrapper will double-encode
|
||||
the value in the legacy path (stringify sees a string and escapes it), and is at best
|
||||
redundant on the V3 path. Both paths now pretty-print with 2-space indent.
|
||||
See `GUIDE__AE_API_V3_for_Frontend.md` → section 3C for the full explanation.
|
||||
|
||||
11. **Broad Dexie result windows get silently clipped** — if a broad "All" view shows fewer
|
||||
rows than a narrower filter, check for a page-level limit or an API revalidation step
|
||||
replacing the local IDB result set. For empty text searches, the full local result set
|
||||
should drive the display; server refreshes should update cache, not shrink visibility.
|
||||
|
||||
12. **Not bumping `IDB_CONTENT_VERSIONS` when changing `properties_to_save`** — this caused
|
||||
the IDAA Recovery Meetings "no meetings found" bug for approximately one year (2025–2026).
|
||||
|
||||
**What happened:** A deploy changed `properties_to_save` in `ae_events__event.ts`, but no
|
||||
one bumped `IDB_CONTENT_VERSIONS.events.event` in `store_versions.ts`. Existing users kept
|
||||
the old stale event records in IndexedDB indefinitely. On the Recovery Meetings page, the
|
||||
fast path (IDB search) returned those stale records, which all failed the `account_id`
|
||||
filter and returned 0 results. The API call then either errored silently or was filtered
|
||||
to 0 by the secondary client-side filter. Critically, the error state and the genuinely
|
||||
empty state showed the **same** "No meetings found" message — users and staff had no
|
||||
indication a failure had occurred. The manual Full Reset (via the `?` help panel) always
|
||||
fixed it, but no one knew why it worked, making the root cause impossible to track down.
|
||||
|
||||
**The fix (2026-05-16):** `check_and_clear_idb_table()` in `store_versions.ts` is now
|
||||
wired in `src/routes/idaa/(idaa)/+layout.svelte` for `db_events.event`. On a version
|
||||
match it costs one localStorage read. On a mismatch it silently clears the table; the
|
||||
SWR pattern then repopulates from the API on next load.
|
||||
|
||||
**The rule going forward:**
|
||||
- When you change `properties_to_save` in any `ae_events__*.ts` file (or any other
|
||||
object file) in a way that makes existing cached records stale — fields added, removed,
|
||||
renamed, or where a computed field's behavior changes — **bump the matching entry in
|
||||
`IDB_CONTENT_VERSIONS` in `src/lib/stores/store_versions.ts`**.
|
||||
- If the table is not yet wired, wire it first (see the wiring instructions in the
|
||||
`IDB_CONTENT_VERSIONS` comment block in `store_versions.ts`).
|
||||
- Currently wired: `events.event`. All other tables are not yet wired.
|
||||
|
||||
**Also:** Never show the same UI message for both a failed API call and a genuinely empty
|
||||
result. Always distinguish `qry__status === 'error'` from `qry__status === 'done'` with
|
||||
0 results in your templates. Silent failures look like data problems and are extremely
|
||||
difficult to diagnose.
|
||||
|
||||
13. **Breaking the API retry loop by returning errors instead of throwing them** — all four
|
||||
`api_*_object.ts` files (`api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts`,
|
||||
`api_delete_object.ts`) use a `.catch()` that returns the error as a value, followed by a
|
||||
classification block. That block **must throw** for transient network failures (`TypeError`)
|
||||
so they enter the retry loop. If you change it to `return false`, retries are silently
|
||||
bypassed for the most common failure mode in hotel/conference WiFi — and nothing warns you.
|
||||
|
||||
**What happened (commit a10accfaa, Jan 2026):** A "silence background fetch noise" commit
|
||||
changed `.catch()` to explicitly `return error`, then the classification block was changed
|
||||
from a `throw` to `return false`. `TypeError` from `ERR_NETWORK_CHANGED` — the most common
|
||||
failure on crowded WiFi — stopped retrying. The `retry_count = 5` parameter became dead
|
||||
code for network errors. Went undetected for ~4 months.
|
||||
|
||||
**The retry classification these files must honor:**
|
||||
- `TypeError` (ERR_NETWORK_CHANGED, WiFi blip) → **`throw`** → enters retry loop with backoff
|
||||
- `AbortError` where `did_timeout_abort = true` (helper's own timer) → **`throw`** → retries
|
||||
- `AbortError` where `did_timeout_abort = false` (navigation/unmount abort) → `return false`
|
||||
- HTTP 400/401/403/422 → `return false` immediately (client errors are deterministic)
|
||||
- HTTP 5xx → **`throw`** → retries with backoff
|
||||
|
||||
**How to verify after any change to the error block:** confirm that a `TypeError` still
|
||||
produces up to 5 retry attempts with 2s→4s→6s→8s delays before returning false. A single
|
||||
`return false` after the first network failure means the retry loop is broken.
|
||||
|
||||
**Also:** when reviewing these files, check that all four have:
|
||||
- `ae_auth_error.set()` triggered on 401/403 (shows session-expired banner to the user)
|
||||
- `timeout = 20000` default (was 60s in PATCH/DELETE until 2026-05-21 — 5-min worst case)
|
||||
- `did_timeout_abort` flag per attempt (separates helper timeouts from caller aborts)
|
||||
|
||||
14. **Account-scoped `liveQuery` trigger firing before bootstrap completes** — components
|
||||
that load account-specific data via `liveQuery` must not trigger the API fetch until the
|
||||
bootstrap Sync Effect in `+layout.svelte` has set the real `account_id`.
|
||||
|
||||
**What happened:** `element_data_store.svelte` triggered its load when `entry` was falsy.
|
||||
On a fresh load with no IDB cache, `$ae_api.account_id` was still `null` (bootstrap hadn't
|
||||
run yet). The `localStorage` scavenge in `api_get_object.ts` then read the stale
|
||||
`account_id = 1` from a previous dev/demo session and made the API call with the wrong
|
||||
account. The response was cached in IDB, and the next page load showed the wrong account's
|
||||
record.
|
||||
|
||||
A second failure mode: if IDB _did_ have a cached record from a previous session with a
|
||||
different account, `liveQuery` returned it as a valid hit (`entry` truthy), so the trigger
|
||||
never fired to fetch the correct record.
|
||||
|
||||
**The fix pattern** for any trigger `$effect` that depends on bootstrapped account context:
|
||||
```typescript
|
||||
$effect(() => {
|
||||
// Use $slct.account_id (non-persisted), NOT $ae_loc.account_id (persisted, stale).
|
||||
// $slct is initialized to null and set only by the bootstrap Sync Effect, so it
|
||||
// reliably gates the fetch until bootstrap has completed.
|
||||
const account_id = $slct.account_id;
|
||||
const api_ready = !!$ae_api?.base_url;
|
||||
const entry = $lq__ds_obj as SomeType | null | undefined;
|
||||
|
||||
if (!browser || !account_id || !api_ready) return;
|
||||
|
||||
// Also re-fetch when IDB holds a record from a different (non-null) account.
|
||||
// null account_id = global/shared fallback — that is still a valid cache hit.
|
||||
const entry_is_stale_account =
|
||||
entry !== undefined &&
|
||||
entry !== null &&
|
||||
entry.account_id !== null &&
|
||||
entry.account_id !== account_id;
|
||||
|
||||
if (!entry || entry_is_stale_account) {
|
||||
trigger = 'load...';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Why `$slct` not `$ae_loc`:**
|
||||
`$ae_loc` is a `svelte-persisted-store` — it hydrates from `localStorage` before any
|
||||
effects run, so its `account_id` may be a stale value from a previous session. `$slct`
|
||||
is a plain writable store initialized to `null`; the bootstrap Sync Effect is the only
|
||||
thing that sets it. Until that runs, `$slct.account_id` is `null`, providing a reliable
|
||||
gate. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → "Bootstrap Race" for the Dexie-side
|
||||
context.
|
||||
|
||||
15. **`tmp_sort_*` comparators written descending instead of ascending** — `build_tmp_sort()` encodes `priority=true` as `'0'` and `priority=false` as `'1'`, designed for **ascending** sort so priority items appear first. Writing a JS `.sort()` comparator as `b.localeCompare(a)` (descending) inverts the encoding and sends priority items to the bottom.
|
||||
|
||||
Found in journals (2026-06), IDAA recovery meetings fast-path and API re-sort (2026-06), and as a Dexie anti-pattern in BB post comments.
|
||||
|
||||
```ts
|
||||
// ❌ Wrong — descending puts priority=false ('1') before priority=true ('0')
|
||||
list.sort((a, b) => (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? ''));
|
||||
|
||||
// ✅ Correct — ascending matches build_tmp_sort encoding
|
||||
list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
|
||||
```
|
||||
|
||||
**Companion Dexie trap:** `collection.reverse().sortBy('tmp_sort_*')` — Dexie ignores a collection-level `.reverse()` when `.sortBy()` is called. The sort is always ascending. To reverse the result, call `.reverse()` on the returned array after `await`. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → `build_tmp_sort` section.
|
||||
|
||||
**Exception — legacy `ae_events__event.ts` encoding:** `ae_events__event.ts` (and `ae_events__event_session.ts`) do NOT use `build_tmp_sort`. They use `priority ? 1 : 0` (priority=true→`'1'`), which requires **descending** sort to put priority items first. `ae_events__event_presentation.ts` DOES use `build_tmp_sort` (it overrides the generic encoding in its `specific_processor`). Do not apply the ascending rule to raw event or session sorts until those modules are migrated to `build_tmp_sort`.
|
||||
|
||||
16. **Service worker without `skipWaiting()` + `clients.claim()` silently serves stale code to long-lived tabs** — The default SvelteKit service worker template does NOT include these calls. Without them, a new SW installs in the background but waits in a **"waiting"** state until every tab running the old version is closed before it activates. Users who leave a page open all day (especially IDAA members in the Novi iframe on idaa.org) run old buggy JS indefinitely after a fix is deployed.
|
||||
|
||||
**Symptom that should trigger this check:** Bug reports from users that developers cannot reproduce. Developers constantly refresh and open/close tabs — the new SW activates immediately for them. End users with persistent tabs never get it.
|
||||
|
||||
**The fix** (already applied to `src/service-worker.js` as of 2026-06-03):
|
||||
```js
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(addFilesToCache());
|
||||
self.skipWaiting(); // activate immediately, don't wait for tabs to close
|
||||
});
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(deleteOldCaches());
|
||||
self.clients.claim(); // take control of all open tabs right away
|
||||
});
|
||||
```
|
||||
|
||||
**Trade-off:** A tab mid-session gets new JS without a page reload. For a read-heavy app like IDAA (browsing meetings) this is harmless. For a form-heavy app the risk is higher — weigh accordingly.
|
||||
|
||||
---
|
||||
|
||||
## 8. Source Layout (Quick Reference)
|
||||
|
||||
```text
|
||||
src/lib/
|
||||
ae_api/ — API helpers (V3 preferred)
|
||||
ae_core/ — Account, User, Person, Site, hosted files
|
||||
ae_events/ — Events, sessions, presenters, badges, locations, files
|
||||
ae_journals/ — Journals (canonical/frontier model — copy patterns from here)
|
||||
ae_idaa/ — IDAA custom module (PRIVATE — always authenticated)
|
||||
elements/ — Reusable UI: V3 field editor, data store, CodeMirror, QR scanner
|
||||
electron/ — Native Electron bridge (electron_relay.ts)
|
||||
stores/ — ae_stores.ts, ae_events_stores.ts, ae_idaa_stores.ts
|
||||
|
||||
src/routes/
|
||||
/core/ — Admin (accounts, people, sites, users)
|
||||
/events/[id]/
|
||||
/(pres_mgmt)/ — Presentation management
|
||||
/(launcher)/ — Event launcher (kiosk display)
|
||||
/(badges)/ — Badge printing
|
||||
/(leads)/ — Exhibitor leads
|
||||
/journals/ — Journals
|
||||
/idaa/ — IDAA module (PRIVATE)
|
||||
/hosted_files/ — File management
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Reading Order for Deeper Dives
|
||||
|
||||
Start here, then go deeper as needed:
|
||||
|
||||
| What you need | Read |
|
||||
|---|---|
|
||||
| Active tasks + known bugs | `documentation/TODO__Agents.md` ← always first |
|
||||
| Dev workflow + commit rules | `documentation/GUIDE__Development.md` |
|
||||
| V3 API reference | `documentation/GUIDE__AE_API_V3_for_Frontend.md` |
|
||||
| Dexie / liveQuery patterns | `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` |
|
||||
| Svelte 5 patterns + pitfalls | `documentation/GEMINI__Svelte_and_Me.md` |
|
||||
| Permissions + auth levels | `documentation/AE__Permissions_and_Security.md` |
|
||||
| Electron / native launcher | `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` |
|
||||
| Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.md` |
|
||||
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Exhibitor_Leads.md` |
|
||||
| Naming conventions | `documentation/AE__Naming_Conventions.md` |
|
||||
@@ -1,902 +0,0 @@
|
||||
# CLIENT: IDAA — International Doctors in Alcoholics Anonymous
|
||||
|
||||
**Client:** International Doctors in Alcoholics Anonymous (IDAA)
|
||||
**Module Path:** `src/routes/idaa/`
|
||||
**State Stores:** `src/lib/stores/ae_idaa_stores.ts`
|
||||
**Last Updated:** 2026-05-18 (Default limit and stepper update)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ CRITICAL PRIVACY REQUIREMENT
|
||||
|
||||
**ALL IDAA content is PRIVATE. Authentication is required for ALL modules.**
|
||||
|
||||
IDAA serves a sensitive population — physicians in addiction recovery. Content exposure to the public is a **severe security failure** and a violation of member trust.
|
||||
|
||||
- A previous AI agent accidentally exposed IDAA Bulletin Board content publicly. This must never happen again.
|
||||
- Every route, component, and API call in this module must enforce authentication.
|
||||
- When in doubt: **it's private**.
|
||||
|
||||
**Required access level:** `trusted_access` or higher for all IDAA content.
|
||||
|
||||
---
|
||||
|
||||
## What IDAA Is
|
||||
|
||||
IDAA is a private membership organization for physicians in recovery. They use the Aether platform for:
|
||||
- A private document archive (historical materials, meeting records)
|
||||
- A members-only bulletin board (community posts and discussion)
|
||||
- A searchable directory of in-person and virtual recovery meetings
|
||||
- Video conferencing (Jitsi-based)
|
||||
|
||||
IDAA's Aether instance is embedded as an **iframe inside their existing Novi-powered website** (`idaa.org`). Novi is their external Association Management System (AMS) — it handles membership records and authentication. Aether receives the member context via URL parameters on iframe load.
|
||||
|
||||
### Breakout Links and Iframe Persistence
|
||||
|
||||
Members often need to open Jitsi meetings outside the Novi iframe (e.g., for full-screen features or on mobile). These are referred to as **Breakout Links**.
|
||||
|
||||
- **The Problem:** SvelteKit client-side navigation within the iframe often drops "bootstrap" query parameters like `?key=...` (site access key) and `?uuid=...` (Novi identity token).
|
||||
- **The Requirement:** When a member breaks out of the iframe into a new browser tab, these keys **must** be present in the URL. Without them, the member will hit the site-domain gate or the IDAA auth gate and see "Access Denied."
|
||||
- **The Solution:** The Video Conferences page uses a derived `breakout_url` that proactively re-injects the missing `key` (from `$ae_loc.allow_access`) and `uuid` (from `$idaa_loc.novi_uuid`) before generating the external link.
|
||||
|
||||
**Example Breakout URL:**
|
||||
`https://client.oneskyit.com/idaa/video_conferences?uuid=...&key=...&room=...`
|
||||
|
||||
---
|
||||
|
||||
## Architecture: Composite Module
|
||||
|
||||
IDAA is **not a standalone module** — it is a **composition of three existing Aether modules**, access-gated and branded for the IDAA client.
|
||||
|
||||
| IDAA Feature | Aether Module Used | Library |
|
||||
|---|---|---|
|
||||
| Archives | Archives module | `src/lib/ae_archives/` |
|
||||
| Bulletin Board (BB) | Posts module | `src/lib/ae_posts/` |
|
||||
| Recovery Meetings | Events module (repurposed) | `src/lib/ae_events/` |
|
||||
| Video Conferences | Jitsi (external embed) | External |
|
||||
|
||||
There is **no `src/lib/ae_idaa/`** library directory. IDAA-specific state and logic lives in `ae_idaa_stores.ts` and the route components only.
|
||||
|
||||
This design allows the IDAA module to be removed or updated without touching core modules.
|
||||
|
||||
---
|
||||
|
||||
## Route Structure
|
||||
|
||||
```
|
||||
src/routes/idaa/
|
||||
├── +layout.svelte # Root layout: Novi UUID extraction, iframe height sync
|
||||
├── (idaa)/
|
||||
│ ├── +layout.svelte # Access gate: blocks render if unauthorized; permission upgrade
|
||||
│ ├── +page.svelte # IDAA dashboard — 3-module selector
|
||||
│ ├── archives/ # Archives submodule
|
||||
│ │ ├── +page.svelte # Archive list (LiveQuery)
|
||||
│ │ └── [archive_id]/
|
||||
│ │ ├── +page.svelte # Archive detail + content viewer
|
||||
│ │ ├── ae_idaa_comp__archive_obj_id_view.svelte
|
||||
│ │ ├── ae_idaa_comp__archive_obj_id_edit.svelte
|
||||
│ │ ├── ae_idaa_comp__archive_content_obj_id_edit.svelte
|
||||
│ │ └── ae_idaa_comp__modal_media_player.svelte
|
||||
│ ├── bb/ # Bulletin Board (Posts) submodule
|
||||
│ │ ├── +page.svelte # Post list (LiveQuery, archive-filtered)
|
||||
│ │ └── [post_id]/
|
||||
│ │ ├── +page.svelte # Post detail + comments
|
||||
│ │ ├── ae_idaa_comp__post_obj_id_view.svelte
|
||||
│ │ ├── ae_idaa_comp__post_obj_id_edit.svelte
|
||||
│ │ └── ae_idaa_comp__post_comment_obj_id_edit.svelte
|
||||
│ ├── recovery_meetings/ # Recovery Meetings (Events repurposed)
|
||||
│ │ ├── +layout.ts # Layout loader (auth, stores)
|
||||
│ │ ├── +layout.svelte # Layout wrapper
|
||||
│ │ ├── +page.svelte # Meeting list + search filters
|
||||
│ │ ├── ae_idaa_comp__event_obj_li_wrapper.svelte # List container/modal host
|
||||
│ │ ├── ae_idaa_comp__event_obj_li.svelte # Individual list item card
|
||||
│ │ ├── ae_idaa_comp__event_obj_qry.svelte # Query/filter bar
|
||||
│ │ ├── ae_idaa_comp__event_obj_id_view.svelte # Meeting detail (read-only)
|
||||
│ │ ├── ae_idaa_comp__event_obj_id_edit.svelte # Meeting edit form (active)
|
||||
│ │ └── [event_id]/
|
||||
│ │ ├── +page.svelte # Meeting detail page — renders view OR edit based on session flag
|
||||
│ │ └── +page.ts
|
||||
│ ├── video_conferences/ # Jitsi video conference integration
|
||||
│ └── jitsi_reports/ # Jitsi meeting activity log report (trusted_access only)
|
||||
```
|
||||
|
||||
> **Note:** Recovery Meetings has **two UI entry points**:
|
||||
> 1. **Modal pattern** (primary list flow) — list, view, and edit components live at `recovery_meetings/`
|
||||
> level, toggled via `$idaa_sess.recovery_meetings` session flags (`show__modal_view`, `show__modal_edit`).
|
||||
> 2. **Direct page** (`[event_id]/+page.svelte`) — navigating to `/idaa/recovery_meetings/<id>` renders
|
||||
> the same view/edit components gated by `$idaa_sess.recovery_meetings.edit__event_obj`.
|
||||
>
|
||||
> Both patterns use `ae_idaa_comp__event_obj_id_edit.svelte`. The edit form clears **both**
|
||||
> `show__modal_edit` and `edit__event_obj` on save/cancel so it works correctly from either entry point.
|
||||
|
||||
---
|
||||
|
||||
## Authentication: Novi UUID System
|
||||
|
||||
IDAA members do not log in through Aether — they log in through Novi (idaa.org), and Novi passes their identity to the Aether iframe via URL parameters.
|
||||
|
||||
### URL Parameters (on iframe load)
|
||||
```
|
||||
?uuid=<36-char-uuid>
|
||||
&iframe=true
|
||||
&key=<site-access-key>
|
||||
```
|
||||
|
||||
> **Security note (2026-03-09):** The iframe HTML files previously also passed `email` and `full_name`
|
||||
> via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed.
|
||||
> The SvelteKit layout now verifies identity via the Aether server-side Novi proxy — the Novi API
|
||||
> call originates from the server, not the member's browser.
|
||||
> See "Iframe Integration" → "Novi UUID Verification Flow" below.
|
||||
|
||||
### Verification Flow (`(idaa)/+layout.svelte`)
|
||||
|
||||
When a `uuid` param is present in the URL, the layout performs an **async call to the Aether server-side endpoint** (`GET /v3/action/idaa/novi_member/{uuid}`), which proxies to Novi server-to-server:
|
||||
|
||||
1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
|
||||
2. Gets verified name and email — these can't be forged via URL
|
||||
3. Sets `$idaa_loc.novi_uuid`, `$idaa_loc.novi_email`, `$idaa_loc.novi_full_name`
|
||||
4. Sets `$idaa_loc.novi_verified = true` on success
|
||||
|
||||
A `novi_verifying` UI state prevents the "Access Denied" screen from flashing during the API round-trip.
|
||||
|
||||
**All or nothing:** If the Novi API key is not configured on the site, or the verification call fails, access is denied. There is no URL-param fallback.
|
||||
|
||||
**Required `site_cfg_json` fields:**
|
||||
```json
|
||||
{
|
||||
"novi_idaa_api_key": "Base64-encoded-key-from-Novi",
|
||||
"novi_api_root_url": "https://www.idaa.org/api", // optional, this is the default
|
||||
"novi_admin_li": ["uuid-1", "uuid-2"],
|
||||
"novi_trusted_li": ["uuid-3", "uuid-4"],
|
||||
"novi_idaa_group_guid_li": ["group-uuid"] // Jitsi moderators only
|
||||
}
|
||||
```
|
||||
|
||||
## Novi API Integration — How We Use It
|
||||
|
||||
This section documents the exact way Aether uses the Novi API for the IDAA integration so future maintainers can recreate the flow.
|
||||
|
||||
- **Purpose:** Verify a Novi-provided `uuid` received via iframe URL parameters, obtain a verified name/email from Novi, and upgrade Aether permissions for that session when appropriate.
|
||||
|
||||
- **All-or-nothing policy:** If the Novi API key is not configured or the verification call fails, the Novi-based access path is denied. The layout explicitly prevents child routes from rendering while verification is in-flight to avoid flashing "Access Denied".
|
||||
|
||||
- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The Aether backend handles 429 responses; the frontend receives a `429` and retries once after 10 seconds. The 12-hour TTL cache on successful verification (Redis server-side + `$idaa_loc` client-side) prevents repeated calls during normal use. A `503` (Novi unreachable) is auto-retried once after 3 seconds before surfacing an error to the user.
|
||||
|
||||
### Verification Flow (implementation)
|
||||
|
||||
1. The IDAA iframe loads Aether pages with a `?uuid=<uuid>&iframe=true` param.
|
||||
2. When the `uuid` param is present the IDAA layout calls the Aether server-side proxy:
|
||||
|
||||
```js
|
||||
// simplified
|
||||
fetch(`${aether_api_url}/v3/action/idaa/novi_member/${uuid}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-aether-api-key': api_key,
|
||||
'x-account-id': account_id
|
||||
}
|
||||
})
|
||||
// Aether calls Novi server-to-server; member's browser IP is never in the Novi call path.
|
||||
```
|
||||
|
||||
3. On success (`200`), the layout reads `data.full_name` and `data.email` from the response and writes them to the IDAA store, marking verification success.
|
||||
|
||||
4. The layout then determines a target Novi permission level (`authenticated`, `trusted`, `administrator`) by checking configured UUID lists (`novi_trusted_li`, `novi_admin_li`) and upgrades the Aether session only if the Novi-derived level is higher than the current global level.
|
||||
|
||||
5. The layout also resets a few IDAA-specific query defaults (BB filters, etc.) to safe values after verification.
|
||||
|
||||
### Key `site_cfg_json` fields and where they are used
|
||||
|
||||
- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Used by the Aether **server** to authenticate against Novi — the frontend never touches the key itself. The frontend checks only for its *presence* in `site_cfg_json` as a guard meaning "IDAA is configured for this site". If missing, Novi-based access is denied.
|
||||
|
||||
- **`novi_api_root_url`**: Optional Novi API root (defaults to `https://www.idaa.org/api`). Read by the Aether server, not the frontend.
|
||||
|
||||
- **`novi_admin_li`**: Array of UUIDs treated as administrators for IDAA. Merged into `$idaa_loc.novi_admin_li` during layout initialization and used to set `administrator` level.
|
||||
|
||||
- **`novi_trusted_li`**: Array of UUIDs treated as trusted members. Merged into `$idaa_loc.novi_trusted_li` and used to set `trusted` level.
|
||||
|
||||
- **`novi_jitsi_mod_li` / `novi_idaa_group_guid_li`**: Lists used to map Jitsi moderator privileges and group GUIDs (where applicable).
|
||||
|
||||
- **`novi_bb_base_url`**: (optional) Base URL used to build links for Bulletin Board notification emails.
|
||||
|
||||
- **`jitsi_exclude_uuids`**: (optional) Array of Novi UUIDs to exclude from Jitsi Reports.
|
||||
This is the canonical staff/test filter. UUIDs are matched case-insensitively against
|
||||
`final_participants[].novi_uuid` when present. Example: `["uuid-1", "uuid-2"]`.
|
||||
|
||||
- **`jitsi_known_meetings`**: (optional) Array of meeting names / room names to keep in the report.
|
||||
When this list is non-empty, only matching `room_name` values are shown. Matching is
|
||||
case-insensitive.
|
||||
|
||||
- **Legacy fallback:** `jitsi_exclude_names` is still honored for older configs, but it should be
|
||||
migrated to UUIDs.
|
||||
|
||||
- **Email config values** (`noreply_email`, `noreply_name`, `admin_email`, `admin_name`): used by functions that send notification emails (BB posts, comments, recovery meetings).
|
||||
|
||||
### Stores / runtime fields set by verification
|
||||
|
||||
- `$idaa_loc.novi_uuid` — the verified UUID
|
||||
- `$idaa_loc.novi_email` — verified email (normalized)
|
||||
- `$idaa_loc.novi_full_name` — display name built from Novi fields
|
||||
- `$idaa_loc.novi_verified` — boolean flag indicating successful verification
|
||||
- `$idaa_loc.novi_admin_li`, `$idaa_loc.novi_trusted_li` — merged lists from site config
|
||||
|
||||
These fields are read elsewhere in the IDAA UI to enable flows for verified users (for example: creating meetings, posting comments, or auto-populating contact info in notifications).
|
||||
|
||||
### Where in the codebase this runs (examples)
|
||||
|
||||
- The Novak UUID verification and permission-upgrade logic is implemented in the IDAA layout: [src/routes/idaa/(idaa)/+layout.svelte](src/routes/idaa/(idaa)/+layout.svelte).
|
||||
- UI elements that permit actions for verified Novi users or trusted members check these values. Example: the "Create New Meeting" button allows creation when either the session has `trusted_access` or a `novi_uuid` is present — see [src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte](src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte).
|
||||
|
||||
### Security notes and operational guidance
|
||||
|
||||
- The previous implementation leaked `email` and `full_name` via URL params — this was removed because those values are unauthenticated and can be spoofed.
|
||||
- The API key is sensitive — keep it only in site `cfg_json` and do not expose it in client-side code or public repositories. The key is read and used exclusively by the Aether backend; it is never sent to the browser.
|
||||
- If Novi changes their customer API shape, update `app/methods/idaa_novi_verify_methods.py` in the backend (display name/email normalization) and this documentation.
|
||||
|
||||
If you need a compact checklist for re-creating this flow in another integration, ask and I will add a small runbook with exact request/response field mappings.
|
||||
|
||||
### ~~Planned: Server-Side Novi Verification~~ ✅ Implemented (2026-05-19)
|
||||
|
||||
**Problem solved:** The previous client-side Novi API call originated from the member's browser.
|
||||
Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering
|
||||
could block these calls and produce false "Access Denied" for legitimate members.
|
||||
|
||||
**Solution implemented:** A FastAPI endpoint proxies the Novi call server-to-server
|
||||
(Aether → Novi), with Redis caching. Members' browser IPs are no longer in the call path.
|
||||
|
||||
**Endpoint:** `GET /v3/action/idaa/novi_member/{uuid}`
|
||||
- Standard Aether auth headers (`x-aether-api-key`, `x-account-id`)
|
||||
- Server reads `novi_idaa_api_key` / `novi_api_root_url` from site `cfg_json`
|
||||
- Redis cache: `idaa:novi_member:{uuid}` — 4-hour TTL, only 200s cached
|
||||
- `404` results never cached (recently-joined members not incorrectly denied)
|
||||
|
||||
**Frontend:** `verify_novi_uuid()` in `(idaa)/+layout.svelte` now calls this endpoint with
|
||||
standard Aether headers. The `novi_idaa_api_key` is still checked for presence in
|
||||
`site_cfg_json` as a proxy for "is IDAA configured for this site" (server holds the key itself).
|
||||
|
||||
**Full API spec:** `GUIDE__AE_API_V3_for_Frontend.md` §12.
|
||||
|
||||
### Permission Levels (Ascending)
|
||||
| Level | Condition | Access |
|
||||
|---|---|---|
|
||||
| Anonymous | No UUID, unrecognized UUID, or verification failure | No access |
|
||||
| Authenticated | UUID verified against Novi API | View own content, limited actions |
|
||||
| Trusted | Verified UUID in `novi_trusted_li` | Full member access to all IDAA content |
|
||||
| Administrator | Verified UUID in `novi_admin_li` | Full access + edit/manage |
|
||||
|
||||
`novi_trusted_li` and `novi_admin_li` are managed in Aether site config (not in Novi directly).
|
||||
|
||||
## Identity Linkage: The Novi UUID Rule (Triple Linkage)
|
||||
|
||||
**CRITICAL ARCHITECTURAL STANDARD:**
|
||||
All member-generated content in the IDAA module MUST be explicitly linked to the member's Novi UUID via the `external_person_id` field. This linkage is the primary mechanism for ownership, edit permissions, and auditing.
|
||||
|
||||
### 1. Mandatory at Creation
|
||||
Linkage MUST happen at the moment of initial object creation (POST). Shell records created without an `external_person_id` are considered orphaned and may be inaccessible to the creator.
|
||||
|
||||
### 2. Triple Linkage Scope
|
||||
The following objects require mandatory `external_person_id` linkage:
|
||||
- **Recovery Meetings** (`ae_Event`)
|
||||
- **Bulletin Board Posts** (`ae_Post`)
|
||||
- **Post Comments** (`ae_PostComment`)
|
||||
|
||||
### 3. Implementation Patterns
|
||||
- **Buttons:** Creation buttons (e.g., "Create New Meeting") must include `external_person_id: $idaa_loc.novi_uuid` in their initial `create_ae_obj` payload.
|
||||
- **Edit Forms:** Edit components must provide robust fallbacks to `$idaa_loc.novi_uuid` for new or incomplete records, ensuring identity is captured even if the initial creation call was narrow.
|
||||
- **Identity Sync:** Along with the UUID, `full_name` and `email` should also be synced from `$idaa_loc` to provide human-readable context in notifications and admin views.
|
||||
- **Race Condition Defense:** `$idaa_loc` may be briefly null on mount before the store hydrates from localStorage. Creation buttons and edit submit handlers must scavenge identity directly from `localStorage.getItem('ae_idaa_loc')` as a fallback when the store value is missing.
|
||||
|
||||
### 4. Staff Editing Rules (IDAA Trusted/Admin Staff)
|
||||
|
||||
IDAA staff have their own Novi UUID. When they edit member content, their identity must **not** overwrite the member's `external_person_id`, `full_name`, or `email`.
|
||||
|
||||
| Content Type | `external_person_id` for staff | `full_name` / `email` for staff |
|
||||
|---|---|---|
|
||||
| BB Post | **Readonly** (unless `administrator_access`) — member's UUID preserved | Same — rendered from existing record, not staff identity |
|
||||
| Post Comment | **Preserved** — form state initializes from existing record first | Same |
|
||||
| Recovery Meeting | **Intentionally editable** for trusted staff — staff can reassign meeting ownership | Contact 1 renders from existing `contact_li_json[0]` first; staff identity only fills if blank |
|
||||
|
||||
The fallback to `$idaa_loc.novi_uuid` (the current user's UUID) only fires when the record has **no** existing `external_person_id`. For any record properly created after the 2026-04-07 triple-linkage enforcement, this fallback should never be reached.
|
||||
|
||||
### 5. Recovery Meetings — Contact 1 Convention
|
||||
|
||||
In 99% of cases, **Contact 1 should be the same person linked via `external_person_id`** — the IDAA member who owns and runs the meeting. These are two separate fields:
|
||||
|
||||
- `external_person_id` — the ownership/identity link (Novi UUID). Determines who may edit the meeting.
|
||||
- `contact_li_json[0]` — the displayed contact info (name, email, phone). Shown to members searching for meetings.
|
||||
|
||||
They are expected to match but are set independently. Members unlock Contact 1 via confirm dialog if they need to list a different contact. Staff can edit both fields directly.
|
||||
|
||||
### Permission Upgrade Rule
|
||||
```
|
||||
// RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade.
|
||||
// If a user has a higher global Aether role (site manager, super),
|
||||
// their global role is preserved and not overwritten by Novi auth.
|
||||
```
|
||||
|
||||
This ensures that OSIT staff with `super` or `manager` roles retain full access regardless of Novi UUID status.
|
||||
|
||||
### Non-Novi Sign-in Paths (unaffected)
|
||||
- **User/Pass or Auth Link:** No `uuid` in URL → layout Novi block does not run
|
||||
- **Shared Passcode:** No `uuid` in URL → layout Novi block does not run
|
||||
|
||||
### Access Gate (`(idaa)/+layout.svelte`)
|
||||
The inner layout blocks ALL rendering if the user is not authorized:
|
||||
- `novi_verifying = true` → "Verifying identity..." spinner (message updates during retry)
|
||||
- `verify_error_type === 'rate_limited'` → yellow "Identity Verification Unavailable" panel with:
|
||||
- **Try Again** — calls `handle_verify_retry()` (respects retry_count, waits 10 s before re-calling Novi)
|
||||
- **Clear Cache & Reload** — clears IDB + localStorage + sessionStorage, then reloads
|
||||
- **Full Reset** — same clear but also navigates to `/` with `invalidateAll`
|
||||
- `verify_error_type === 'api_error'` → same yellow panel (API returned non-2xx, not a rate limit)
|
||||
- Verification failed or no UUID → "Access Denied" error page
|
||||
- Access check runs before any child routes render
|
||||
|
||||
---
|
||||
|
||||
## Module 1: Archives
|
||||
|
||||
**Route:** `/idaa/archives/`
|
||||
**Library:** `src/lib/ae_archives/`
|
||||
**Types:** `ae_Archive`, `ae_ArchiveContent`
|
||||
|
||||
The Archives module stores IDAA historical content — meeting records, conference proceedings, historical documents, and media.
|
||||
|
||||
### Object Types
|
||||
|
||||
**Archive (Container)**
|
||||
- Represents a collection (e.g., "2019 Conference Proceedings")
|
||||
- Key fields: `name`, `description`, `original_datetime`, `original_location`, `archive_on`
|
||||
- `archive_on` — date when this archive collection is auto-hidden (scheduled visibility control)
|
||||
|
||||
**ArchiveContent (Items)**
|
||||
- Individual items within an archive
|
||||
- Supports multiple content types: `'text'`, `'file'`, `'url'`, `'video'`
|
||||
- Key fields: `archive_content_type`, `content_html`, `url`, `hosted_file_id`, `duration`
|
||||
- Video/audio content has a dedicated media player component
|
||||
|
||||
### Database (Dexie)
|
||||
```
|
||||
db_archives.archive — Archive containers
|
||||
db_archives.content — Archive content items (linked by archive_id)
|
||||
```
|
||||
|
||||
### Demo / Test IDs
|
||||
- Archive: `nAA2bHLv8RK` (id: 1) "One Sky Test Archive"
|
||||
- Archive Content: `UjKzrk-GKu5` (id: 1) "Hosted File Test"
|
||||
|
||||
---
|
||||
|
||||
## Module 2: Bulletin Board (BB)
|
||||
|
||||
**Route:** `/idaa/bb/`
|
||||
**Library:** `src/lib/ae_posts/`
|
||||
**Types:** `ae_Post`, `ae_PostComment`
|
||||
|
||||
The BB is the IDAA members-only community discussion board. It is the **most sensitive module** — public exposure must never occur.
|
||||
|
||||
### Object Types
|
||||
|
||||
**Post (Thread)**
|
||||
- Key fields: `title`, `content`, `anonymous`, `full_name`, `email`
|
||||
- `archive_on` — date after which the post is hidden from all views
|
||||
- `archive` — boolean flag for immediate archival
|
||||
- `enable_comments` — controls whether replies are allowed
|
||||
- `post_comment_count` — cached count of replies
|
||||
|
||||
**PostComment (Reply)**
|
||||
- Key fields: `post_id`, `content`, `anonymous`, `full_name`, `email`
|
||||
- Replies inherit the parent post's visibility rules
|
||||
|
||||
### Post Visibility / Archival Filter
|
||||
Posts with `archive_on` set to a past date are **automatically hidden** from all queries. This is enforced at the component level via a LiveQuery filter:
|
||||
|
||||
```typescript
|
||||
// This filter is REQUIRED — do not remove it
|
||||
filter((x) => !x.archive_on || archiveDate > now)
|
||||
```
|
||||
|
||||
Archived posts are soft-deleted — they remain in the database for audit purposes but are not shown to members.
|
||||
|
||||
Most recent first (sorted `updated_on DESC`).
|
||||
|
||||
### Database (Dexie)
|
||||
```
|
||||
db_posts.post — Posts (threads)
|
||||
db_posts.comment — Post comments (linked by post_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module 3: Recovery Meetings
|
||||
|
||||
**Route:** `/idaa/recovery_meetings/`
|
||||
**Library:** `src/lib/ae_events/` (standard Events module, repurposed)
|
||||
**Types:** `ae_Event` (standard event type, filtered for meeting context)
|
||||
|
||||
Recovery Meetings reuses the Aether Events object to represent AA recovery meetings. These are NOT conferences — they are regular ongoing meetings (weekly, monthly, etc.) available to IDAA members.
|
||||
|
||||
### Search Filters
|
||||
Members can filter meetings by:
|
||||
- **Fulltext search** — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
|
||||
- **Virtual** — online meetings (Zoom, Jitsi, other)
|
||||
- **In-person** — physical location meetings
|
||||
- **Meeting type** — IDAA / Caduceus / Family Recovery
|
||||
- **My Meetings** — star toggle; shows only meetings the member has starred (favorites)
|
||||
|
||||
**Sort options:** Last Updated (default), Meeting Name A–Z, Meeting Name Z–A.
|
||||
|
||||
**Empty state behavior:**
|
||||
- Zero results with active filters → "No meetings found for these filters" + "Clear all filters" button
|
||||
- Zero results with no filters → bare message shown, then after 8s a "Refresh Meeting Cache" escape hatch appears (clears IDB and re-fetches from API — indicates a stale-cache problem, not a real empty set)
|
||||
|
||||
Search uses the standard Aether SWR pattern (IDB cache returned immediately, then API refreshes in background).
|
||||
|
||||
### Search Architecture — What Is and Isn't Searched
|
||||
|
||||
The fulltext search runs against the `default_qry_str` field (backend-computed STORED GENERATED
|
||||
column, contains: `id_random`, type, name, description, timezone, recurring pattern/text,
|
||||
location text, **contact name and email**).
|
||||
|
||||
**Contact names and emails ARE searchable via the API path.** `default_qry_str` includes
|
||||
contact data, so the API `lk_qry` LIKE search on that field covers contacts automatically.
|
||||
|
||||
**IDB fast-path gap:** The local cache (Dexie) fast-path returns all cached meetings without
|
||||
text filtering — users see the unfiltered list immediately, then the API result (with contacts
|
||||
filtered) replaces it after the background refresh completes. The IDB path does not parse
|
||||
`contact_li_json` for instant local text matching.
|
||||
|
||||
**Known history (2026-05-19):** Contact search appeared broken due to two issues now resolved:
|
||||
1. The backend STORED GENERATED columns (`default_qry_str`, `contact_li_json_ext`) had stale
|
||||
values; forced a rebuild via fake updates on each event record.
|
||||
2. The recovery meetings page secondary filter was re-running text matching against response
|
||||
fields — silently dropping results that matched only via `default_qry_str` (e.g. by contact
|
||||
name, since that field may not appear in the response body). Fix: removed text re-filtering
|
||||
from the secondary filter (type / physical / virtual OR-logic only).
|
||||
|
||||
**Remaining enhancement (tracked in TODO__Agents.md):**
|
||||
- Add `contact_li_json_ext` to the IDB fast-path filter in `search__event()` and the recovery
|
||||
meetings page so contact matches appear instantly from cache, not only after API refresh.
|
||||
|
||||
### Sort Encoding — Events Use Legacy (Not `build_tmp_sort`)
|
||||
|
||||
`ae_events__event.ts` builds `tmp_sort_1` with the **legacy encoding**: `priority ? 1 : 0`
|
||||
(priority=true → `'1'`). This is the **opposite** of `build_tmp_sort` (priority=true → `'0'`).
|
||||
|
||||
| Module | Encoding | Correct comparator |
|
||||
| --- | --- | --- |
|
||||
| `ae_events__event.ts` (Recovery Meetings) | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` |
|
||||
| `ae_events__event_session.ts` | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` |
|
||||
| `ae_events__event_presentation.ts` | `build_tmp_sort` (overrides legacy in `specific_processor`) | **Ascending** `a.localeCompare(b)` |
|
||||
| Journals, Posts, Archives | `build_tmp_sort` | **Ascending** `a.localeCompare(b)` |
|
||||
|
||||
**Do not apply the `build_tmp_sort` ascending rule to raw event or session sorts** until
|
||||
`ae_events__event.ts` is migrated (tracked in TODO__Agents.md under IDB Sort rollout).
|
||||
|
||||
### Search Trigger — Use `$slct.account_id`, Not `$ae_loc.account_id`
|
||||
|
||||
The recovery meetings search `$effect` gates on `$slct.account_id` (set only by the bootstrap
|
||||
Sync Effect, non-persisted). Do NOT change this back to `$ae_loc.account_id`.
|
||||
|
||||
**Why:** `$ae_loc` is a persisted store that hydrates from localStorage on page load. Its
|
||||
`account_id` may be stale from a previous session (e.g., a dev/demo account_id left behind).
|
||||
Using it as the gate fires the API call with the wrong account before bootstrap has run,
|
||||
producing either a 403 or wrong-account data. `$slct.account_id` is null until bootstrap
|
||||
sets it — a reliable gate. See mistake #14 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
|
||||
### My Meetings (Favorites)
|
||||
|
||||
Members can star meetings to build a personal "My Meetings" list. The star toggle appears:
|
||||
- On each card in the meeting list (`ae_idaa_comp__event_obj_li.svelte`)
|
||||
- On the meeting detail page nav bar (`[event_id]/+page.svelte`)
|
||||
|
||||
Favorites are stored in the `data_store` table (code: `idaa_meetings_favorites`, scoped to the
|
||||
IDAA account). The record's `json` field holds `{ [novi_uuid]: [event_id, ...] }` — one shared
|
||||
record per account containing all members' favorites. This means:
|
||||
- Favorites persist across browsers and devices (server-side)
|
||||
- Does **not** write to `ae_event` rows (avoiding the `ON UPDATE current_timestamp()` side effect)
|
||||
- Known last-write-wins race condition if two members toggle simultaneously — acceptable for ~1000 members
|
||||
- Pre-created DB records: ID 150 (`gaTKSVPagFj`, account_id=1, dev/demo), ID 151 (`knJh8zhyKT0`, account_id=13, live IDAA)
|
||||
|
||||
The star button uses inline styles (not `.btn`) to avoid Bootstrap v3 box-model overrides in the iframe.
|
||||
|
||||
### Edit Form — Sections and Key Fields
|
||||
|
||||
The edit form (`ae_idaa_comp__event_obj_id_edit.svelte`) is organized into these sections.
|
||||
All fields map directly to the `ae_Event` object; none are IDAA-specific custom fields.
|
||||
|
||||
| Section | Key Fields |
|
||||
| --- | --- |
|
||||
| **General Information** | `name` (required), `description` (TipTap rich text), `type` (IDAA / Caduceus / Family Recovery) |
|
||||
| **How to Attend** | `physical` (bool), `virtual` (bool) toggles; conditionally shows: |
|
||||
| → Physical | `location_address_json` (name, line_1–3, city, state, postal, country), `location_text` (TipTap) |
|
||||
| → Virtual | Platform toggle: **Zoom** (`attend_url_code` meeting ID, `attend_url_passcode`, `attend_json.zoom.passcode_enc`, `attend_json.zoom.domain`, `attend_json.zoom.full_url`), **Jitsi** (`attend_json.jitsi.*`), **Other** (`attend_url`, `attend_url_passcode`, `attend_phone`, `attend_phone_passcode`) |
|
||||
| → Both | `attend_text` (TipTap — additional attendance instructions) |
|
||||
| **Schedule** | `recurring_pattern` (weekly/every other week/monthly/other), `weekday_*` (Sun–Sat booleans), `timezone`, `recurring_start_time`, `recurring_end_time`, `recurring_text` (optional TipTap, auto-generated with `*gen*` prefix if blank) |
|
||||
| **Contacts** | `external_person_id` (Novi UUID link), `contact_li_json[0]` (Contact 1: name, email, phone_mobile, phone_home, phone_office — name/email locked to Novi user by default), `contact_li_json[1]` (Contact 2: same fields, optional) |
|
||||
| **Admin Options** | `status`, `hide`, `priority`, `sort`, `group`, `enable`, `notes` (TipTap) — **trusted_access only** |
|
||||
|
||||
**Rich text fields** all use `AE_Comp_Editor_TipTap` with separate `*_new_html` state variables
|
||||
(not bound to `$idaa_slct.event_obj` directly) to track change state for the save-button logic.
|
||||
|
||||
**Zoom URL auto-generation:** Triggered by `$idaa_trig = 'update_zoom_full_url'`. An `$effect`
|
||||
reconstructs `attend_json.zoom.full_url` from domain + meeting_id + passcode_enc whenever
|
||||
the Meeting ID, Passcode, Encrypted Passcode, or Domain fields change.
|
||||
|
||||
**Recurring text auto-generation:** If `recurring_text` is blank or contains the `*gen*` prefix,
|
||||
the submit handler generates a human-readable string (e.g., `*gen* weekly: Monday, Wednesday at 7:00 PM America/Chicago`).
|
||||
Members can opt into a custom text via "Add More Details?" (admin/trusted only).
|
||||
|
||||
**Contact 1 lock:** Contact 1 name and email default to the logged-in Novi member's identity
|
||||
(`$idaa_loc.novi_full_name`, `$idaa_loc.novi_email`). They are `readonly` unless the user
|
||||
explicitly unlocks them via confirm dialog (or has administrator access).
|
||||
|
||||
### Jitsi Integration
|
||||
Some virtual meetings are hosted via Jitsi. Members with a Jitsi moderator UUID (`novi_jitsi_mod_li`) have elevated permissions in video sessions.
|
||||
|
||||
### Edit Form — Implementation Notes (v2)
|
||||
|
||||
- The v2 edit form uses a `<style>` block with `@apply`. Tailwind v4 requires
|
||||
`@reference "../../../../app.css";` at the top of any component `<style>` block that uses `@apply`.
|
||||
- The country subdivision lookup list (`lu_country_subdivision_list`) contains duplicate entries —
|
||||
specifically Puerto Rico (`PR`) has two rows with `code = '-'`. The `{#each}` key must use
|
||||
the array index (`i`) rather than `sub.code` to avoid a Svelte `each_key_duplicate` error.
|
||||
The duplicate entries are a **backend data quality issue** that should be cleaned up in the DB.
|
||||
|
||||
### Demo / Test IDs
|
||||
No dedicated IDAA recovery meeting demo records — uses the standard Event demo record for dev:
|
||||
- Event: `pjrcghqwert` (id: 1) "Demo One Sky IT Conference"
|
||||
|
||||
---
|
||||
|
||||
## Module 4: Video Conferences (Jitsi)
|
||||
|
||||
**Route:** `/idaa/video_conferences/`
|
||||
|
||||
Embeds Jitsi video conferences directly in the IDAA module. Separate from Recovery Meetings — this is for IDAA board meetings or special sessions, not regular AA meetings.
|
||||
|
||||
Moderation permissions are controlled by `novi_jitsi_mod_li` in the IDAA store.
|
||||
|
||||
---
|
||||
|
||||
## Module 5: Jitsi Reports
|
||||
|
||||
**Route:** `/idaa/jitsi_reports/`
|
||||
**Access:** `trusted_access` or `novi_verified` — same gate as the rest of `(idaa)/`
|
||||
**Data source:** `activity_log` table — `jitsi_meeting_event` and `jitsi_meeting_stats` log types
|
||||
**Library function:** `qry__jitsi_report()` in `src/lib/ae_reports/reports_functions.ts`
|
||||
|
||||
An admin/staff reporting tool that aggregates raw Jitsi activity logs into human-readable meeting sessions. It is **not** a member-facing page — IDAA members do not see it.
|
||||
|
||||
**Reminder:** this page now filters staff by Novi UUID and can whitelist known meeting names from site config.
|
||||
|
||||
### View Modes
|
||||
|
||||
Two display modes, toggled via a button in the page header:
|
||||
|
||||
| Mode | Description |
|
||||
| --- | --- |
|
||||
| **Grouped by Room** (default) | One collapsible section per `room_name`. Each section contains a compact table: Date / Time / Duration / Attendees / Participant List. Mirrors the output of the offline Python script (`create_jitsi_report.py`). |
|
||||
| **Flat List** | Original card-per-session accordion layout. Better for drilling into event timelines and raw participant lists. |
|
||||
|
||||
Both modes use the same filtered data set — switching views does not reset filters.
|
||||
|
||||
### Dark Mode / Surface Safety
|
||||
|
||||
The page now uses explicit page and row surfaces so dark mode does not collapse into white-on-white
|
||||
text in either the regular app or the Novi iframe.
|
||||
|
||||
### Filters
|
||||
|
||||
| Filter | Default | Logic |
|
||||
| --- | --- | --- |
|
||||
| **Min. Participants** | 2 | Minimum `real_participant_count` to display a session. Used as the only size filter. |
|
||||
| **Room Name** | edit mode only | Case-insensitive substring match against `room_name`. Hidden unless AE global edit mode is on. |
|
||||
| **From / To** | last 60 days / today | Date range applied to `start_time`. "To" date includes the full end of day. |
|
||||
|
||||
A "Reset Filters" button appears whenever any filter is non-default.
|
||||
|
||||
In edit mode, two extra toggles appear:
|
||||
- **Show excluded IDs** — temporarily include the UUIDs listed in `jitsi_exclude_uuids`
|
||||
- **Show all meetings** — temporarily ignore `jitsi_known_meetings`
|
||||
|
||||
An "Active Exclusions" panel below the filter bar shows the currently applied Novi UUID exclusions
|
||||
and known meeting-name whitelist values. Each list is collapsible so the page stays compact.
|
||||
|
||||
### Staff / Meeting Filtering
|
||||
|
||||
**Problem:** Staff/test accounts and one-off test rooms distort the reports.
|
||||
|
||||
**Site config keys:**
|
||||
```json
|
||||
{
|
||||
"jitsi_exclude_uuids": ["uuid-1", "uuid-2"],
|
||||
"jitsi_known_meetings": ["IDAA-BIPOC-Meeting", "IDAA-Sunday-Meeting"]
|
||||
}
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. The page reads `$ae_loc.site_cfg_json?.jitsi_exclude_uuids` and excludes matching participants by Novi UUID.
|
||||
The UUID comes from the Jitsi log `url_params.uuid` field. `g_uuid` is the meeting/group UUID and is not used here.
|
||||
2. If a participant record does not include a UUID in the activity log, it is left visible; UUIDs are used whenever available.
|
||||
3. `real_participant_count = real_participants.length` drives filters, exports, and the per-meeting attendee count.
|
||||
4. Room-level unique participant counts are computed from Novi UUIDs when present, with display-name fallback only for UUID-less records.
|
||||
5. If `$ae_loc.site_cfg_json?.jitsi_known_meetings` is non-empty, only meetings whose `room_name` matches one of the listed names are shown.
|
||||
6. The Room Name filter is only shown when global edit mode is enabled.
|
||||
|
||||
**Temporary stopgap:** the report also hides these staff display names through the same UUID-exclusion toggle until the long-term logging fix lands:
|
||||
`Scott I.`, `Brie P.`, `Michelle V.`
|
||||
|
||||
**Note:** matching is case-insensitive on the stored `room_name` / meeting name.
|
||||
|
||||
### Summary Stats
|
||||
|
||||
Shown above the meeting list when data is loaded. Stats reflect the **filtered + exclusion-applied** view:
|
||||
|
||||
- **Meetings Shown** — count of sessions passing all filters
|
||||
- **Total Participants** — sum of `real_participant_count` across all shown sessions
|
||||
- **Avg Duration** — mean session duration (HH:MM:SS)
|
||||
- **Total Duration** — sum of all session durations (HH:MM:SS)
|
||||
|
||||
In grouped view, each room header also shows its own subtotals (meeting count, unique participants by Novi UUID when available).
|
||||
Each meeting instance keeps the full participant list visible; the **Copy names** button is edit-mode only so staff can grab the list for follow-up reports without exposing extra controls to normal viewers.
|
||||
|
||||
### Caching / Load Behavior
|
||||
|
||||
The page now reads cached `activity_log` rows from IndexedDB first, renders that result immediately,
|
||||
then refreshes from the API in the background. That keeps the report usable even when the network
|
||||
round-trip is slow.
|
||||
|
||||
Both the cache path and the API refresh now page through the matching activity-log set in
|
||||
`created_on DESC` order with a 1000-row page size before building the report. That avoids the old
|
||||
"first 500 rows" behavior that could hide newer sessions if the log table grew large.
|
||||
|
||||
The report page keeps the newest session first in both the flat list and the grouped-by-room view;
|
||||
grouped room rows are also sorted newest session first within each room.
|
||||
|
||||
### Jitsi URL Builder
|
||||
|
||||
Collapsible panel, visible to `trusted_access` users only. Generates properly-formatted Jitsi meeting URLs for IDAA rooms. Component: `ae_idaa_comp__jitsi_url_builder.svelte`.
|
||||
|
||||
### Video Conferences → Reports Link
|
||||
|
||||
Trusted Access users now get a footer link on the Video Conferences page that jumps back to the Jitsi Reports page. It preserves the current iframe context so the staff workflow stays inside the Novi embed.
|
||||
|
||||
**Future idea:** make that link include a `room=` query param for the current meeting so Jitsi Reports can auto-filter to that meeting instance, and have Reset clear that param again.
|
||||
|
||||
### Export
|
||||
|
||||
CSV and JSON export buttons in the page header export the **currently filtered + exclusion-applied** data set.
|
||||
|
||||
### Room Name Fragmentation
|
||||
|
||||
The same logical meeting can appear as multiple rooms (e.g. `IDAA-BIPOC-Meeting`, `IDAA-BIPOC-Meeting-2026`, `IDAA-BIPOC-Meeting-March-31`) because the Jitsi URL builder appends a date suffix to generate unique per-session room names. In grouped view, these appear as separate groups. A future normalization pass (strip trailing date suffixes) could optionally merge them — not implemented yet.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```text
|
||||
activity_log table
|
||||
└── qry__jitsi_report() # reports_functions.ts — fetches + aggregates by meeting_id
|
||||
└── MeetingReport[] # { meeting_id, room_name, start_time, final_duration,
|
||||
# final_participants, final_participant_count, events }
|
||||
└── jitsi_reports/+page.svelte
|
||||
├── apply exclusion list → real_participants / real_participant_count
|
||||
├── apply filters → meetings_filtered
|
||||
├── derive grouped view → Map<room_name, MeetingReport[]>
|
||||
└── render flat or grouped
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management (`ae_idaa_stores.ts`)
|
||||
|
||||
Four stores manage all IDAA state:
|
||||
|
||||
### `idaa_loc` (localStorage — persistent across sessions)
|
||||
Stores Novi auth context and per-submodule query settings:
|
||||
```typescript
|
||||
{
|
||||
novi_uuid: string | null // Member UUID (set on verification success)
|
||||
novi_email: string | null // Verified email from Novi API
|
||||
novi_full_name: string | null // Verified name from Novi API
|
||||
novi_verified: boolean // true after successful Novi API verification
|
||||
novi_admin_li: string[] // Admin UUID list (from site config)
|
||||
novi_trusted_li: string[] // Trusted member UUID list
|
||||
novi_jitsi_mod_li: string[] // Jitsi moderator UUIDs
|
||||
|
||||
archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_obj }
|
||||
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj,
|
||||
qry__enabled, qry__hidden, qry__limit, qry__offset, qry__order_by, qry__order_by_li }
|
||||
recovery_meetings: {
|
||||
qry__enabled, qry__hidden, qry__limit, qry__offset,
|
||||
qry__fulltext_str, qry__physical, qry__virtual, qry__type,
|
||||
qry__order_by, qry__order_by_li,
|
||||
qry__favorites_only, // true = show only starred meetings (My Meetings filter)
|
||||
edit__event_obj // null or event_id string when edit form is open
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `idaa_sess` (in-memory only — resets on page load)
|
||||
UI state per submodule:
|
||||
```typescript
|
||||
{
|
||||
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id,
|
||||
show__modal_edit__archive_content_id, show__modal_view__archive_content_id, obj_changed }
|
||||
bb: { qry__status, edit__post_obj, show__inline_edit__post_obj, show__modal_edit__post_id,
|
||||
show__modal_view__post_id, obj_changed }
|
||||
recovery_meetings: {
|
||||
qry__status, // null | 'loading' | 'done' | 'error'
|
||||
qry__fulltext_str, // session-only copy (separate from persisted loc copy)
|
||||
search_version, // incremented to trigger a new search cycle
|
||||
edit__event_obj, // null | event_id — controls edit form visibility
|
||||
show__modal_edit, show__modal_view,
|
||||
show__modal_edit__event_id, show__modal_view__event_id,
|
||||
attend_platform, // 'Zoom' | 'Jitsi' | null — platform selected in virtual attend section
|
||||
obj_changed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `idaa_slct` (sessionStorage — selection tracking)
|
||||
```typescript
|
||||
{
|
||||
event_id: string | null
|
||||
archive_id: string | null
|
||||
archive_content_id: string | null
|
||||
post_id: string | null
|
||||
post_comment_id: string | null
|
||||
}
|
||||
```
|
||||
|
||||
### `idaa_trig` / `idaa_prom`
|
||||
Trigger flags and promise tracking for async operations (standard Aether pattern).
|
||||
|
||||
---
|
||||
|
||||
## Iframe Integration
|
||||
|
||||
The IDAA module is embedded in `idaa.org` via iframe. This requires:
|
||||
|
||||
1. **Height sync** — The root layout posts `message` events to the parent frame for dynamic height adjustment (content length varies)
|
||||
2. **URL parameter auth** — Novi passes member context via query string on load
|
||||
3. **No standard navigation** — Members navigate within the iframe; Aether's nav chrome is hidden or minimal in this context
|
||||
|
||||
### Novi UUID Verification Flow
|
||||
|
||||
**Iframe HTML files** (in `static/`): Pass only `uuid` to the iframe src — no Novi API calls in the browser:
|
||||
```text
|
||||
idaa_novi_iframe_archives.html
|
||||
idaa_novi_iframe_bulletin_board.html
|
||||
idaa_novi_iframe_recovery_meetings.html
|
||||
idaa_novi_iframe_jitsi_meeting.html ← reference pattern (unchanged)
|
||||
```
|
||||
|
||||
**SvelteKit layout** (`(idaa)/+layout.svelte`): Calls `GET /customers/{uuid}` on the Novi API using the `novi_idaa_api_key` from `site_cfg_json`. Sets verified name/email in `$idaa_loc` and grants permissions. Shows a "Verifying identity..." spinner during the async call.
|
||||
|
||||
**Jitsi page** (`video_conferences/+page.svelte`): Checks `$idaa_loc.novi_verified` in `fetch_novi_data()`. If the layout already verified the user, it reuses `$idaa_loc.novi_email` / `$idaa_loc.novi_full_name` and skips the duplicate member details API call. The group moderator check (`get_novi_group_moderators`) always runs — it is Jitsi-specific.
|
||||
|
||||
### ⚠️ Iframe CSS Conflicts (Bootstrap v3)
|
||||
|
||||
When `$ae_loc.iframe = true`, the root layout (`+layout.svelte`) injects two external stylesheets from Novi's CDN:
|
||||
|
||||
```text
|
||||
https://assets-staging.noviams.com/novi-core-assets/css/fontawesome.css — safe, icon-only
|
||||
https://assets-staging.noviams.com/novi-core-assets/css/c/idaa/idaa.css — Bootstrap v3.4.1 ⚠️
|
||||
```
|
||||
|
||||
`idaa.css` is a full **Bootstrap v3.4.1** bundle. It applies global styles to bare HTML elements
|
||||
(`input`, `select`, `textarea`, `h1–h6`) and commonly named classes (`.btn`, `.badge`, `.active`,
|
||||
`.text-*`, `.bg-*`). These will compete with Tailwind v4 + Skeleton UI.
|
||||
|
||||
**Known consequences:**
|
||||
- Bare form elements (`<input>`, `<select>`) receive Bootstrap's height/padding resets on top of Tailwind
|
||||
- `.btn` class gets Bootstrap button colors, potentially overriding `preset-*` Skeleton classes
|
||||
- `<section>` and heading elements may get unexpected margins/padding from Bootstrap's typography reset
|
||||
- Class names `.field-input` and `.field-label` (used in the v2 edit form's scoped `<style>` block)
|
||||
also exist in `idaa.css`'s date picker — Svelte's scoped attribute selector wins, but be aware
|
||||
- In iframe widths near Tailwind `sm`, avoid hiding critical button labels behind breakpoint classes
|
||||
and do not depend on color-only active states; Bootstrap's `.active`/button styling can make the
|
||||
selected state nearly invisible unless the control uses an obvious fill/ring change plus
|
||||
`aria-pressed`
|
||||
|
||||
**Mitigation:** The iframe CSS conflicts existed before v2 and are not new. The v2 form uses the
|
||||
same Skeleton/Tailwind component classes as the rest of the app. Avoid using bare `<section>`,
|
||||
`<article>`, or block-level HTML5 elements as style hooks; use `<div>` with explicit classes instead.
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Auth Gate Tests Come First
|
||||
**For every IDAA submodule, the first test written must be an authentication enforcement test.**
|
||||
|
||||
```typescript
|
||||
// ✅ Required test pattern for each IDAA module
|
||||
test('Archives - unauthenticated user cannot access content', async ({ page }) => {
|
||||
// Inject localStorage WITHOUT trusted_access
|
||||
// Navigate to /idaa/archives/
|
||||
// Assert: access denied message shown, no archive content visible
|
||||
});
|
||||
|
||||
test('Archives - trusted member can access content', async ({ page }) => {
|
||||
// Inject localStorage WITH trusted_access + novi_uuid
|
||||
// Navigate to /idaa/archives/
|
||||
// Assert: archive list renders
|
||||
});
|
||||
```
|
||||
|
||||
### Privacy in Test Data
|
||||
- Never use real member data in test fixtures
|
||||
- Use canonical demo IDs from `tests/_helpers/env.ts` only
|
||||
- Test names should document the privacy rule being enforced, not just the behavior
|
||||
|
||||
### Trusted Access State Injection
|
||||
Tests that need authenticated IDAA access must set `trusted_access: true` and `novi_uuid` in the injected `ae_loc` localStorage:
|
||||
```typescript
|
||||
// In addInitScript or env helper
|
||||
ae_loc.trusted_access = true;
|
||||
ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
||||
```
|
||||
|
||||
### Current Test Coverage (as of 2026-04-07)
|
||||
| Module | State | Notes |
|
||||
|---|---|---|
|
||||
| Archives | ⚠️ Smoke only | `archive_content.test.ts` — no auth gate test |
|
||||
| Bulletin Board | ❌ None | Priority — most sensitive module |
|
||||
| Recovery Meetings | ✅ Substantial | `tests/idaa_recovery_meeting_edit.test.ts` — form render, field interactions, PATCH payload verification (all sections), real backend save, creation linkage (Novi UUID in POST body) |
|
||||
| Video Conferences | ❌ None | Jitsi complexity, lower priority |
|
||||
| Jitsi Reports | ❌ None | Admin-only tool; lower privacy risk than member modules |
|
||||
|
||||
**Pending:** BB Post and Post Comment creation linkage tests (pattern established in Recovery Meetings test).
|
||||
|
||||
---
|
||||
|
||||
## External Links (idaa.org)
|
||||
- Archives: `https://www.idaa.org/idaa-archives`
|
||||
- Bulletin Board: `https://www.idaa.org/idaa-bulletin-board`
|
||||
- Meetings: `https://www.idaa.org/idaa-meetings`
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md)
|
||||
- [Development Guide](./GUIDE__Development.md)
|
||||
- [Naming Conventions](./AE__Naming_Conventions.md)
|
||||
- [Playwright Test README](../tests/README.md)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## IDAA Novi Groups and Moderators
|
||||
|
||||
### "IDAA Association Admins Group" = "409e91dc-f5a3-486c-a964-71b7d19e6841"
|
||||
|
||||
* Scott
|
||||
* Michelle
|
||||
* Brie
|
||||
|
||||
### "IDAA Couples Meeting" = "e9e162f0-3d03-4241-9682-340135ec3fb8"
|
||||
|
||||
* "Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
|
||||
* "Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
|
||||
* "Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
|
||||
* "Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
|
||||
* "Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
|
||||
* "Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"
|
||||
|
||||
### "Student/Resident Meeting Moderators" "d76d2c00-962d-40f6-a2e8-ed9c85594d96"
|
||||
|
||||
* "Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
|
||||
* "Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"
|
||||
|
||||
### "IDAA BIPOC Meeting" "873d3ad0-2605-4ccf-824c-638c16b2b9cf"
|
||||
|
||||
* "Paula Lynn Bailey-Walton" "68383ba2-0989-4860-9ea6-073f9698df67"
|
||||
* "Tasha Hudson" "03d5408c-3c13-4c3a-a93f-49871f9050b1"
|
||||
|
||||
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Current
|
||||
**Last Verified:** 2026-06-03 — Recovery Meetings: documented legacy `tmp_sort_1` encoding for events (requires descending sort, not ascending); documented `$slct.account_id` gate pattern for search trigger; noted service worker `skipWaiting`/`clients.claim` requirement for long-lived IDAA iframe sessions (root cause of user-reported loading failures that could not be reproduced in dev)
|
||||
@@ -1,99 +0,0 @@
|
||||
# Gemini's Svelte 5 Learnings and Best Practices
|
||||
|
||||
This document outlines key insights and strategies developed during Svelte 5 (with runes mode) refactoring and bug-fixing tasks. It specifically addresses common pitfalls and effective patterns for an AI agent working with Svelte.
|
||||
|
||||
## 1. Async/Await in Svelte (Runes Mode)
|
||||
|
||||
The most frequent source of errors has been related to asynchronous operations and the `await` keyword.
|
||||
|
||||
- **Rule:** Any function or block containing `await` _must_ be explicitly marked `async`.
|
||||
- This applies to callbacks within promise chains (`.then()`, `.catch()`, `.finally()`), DOM event handlers (`onclick`, `onchange`), and Svelte lifecycle functions (`onMount`).
|
||||
- **Common Error:** `Cannot use keyword 'await' outside an async function`. This happens when `await` is used in a function/block that is not `async`.
|
||||
- **Solution:** Ensure the surrounding function or the callback itself is declared `async`.
|
||||
- `somePromise.finally(async () => { await someAsyncOperation(); });`
|
||||
- `onclick={async () => { await someAsyncOperation(); }}`
|
||||
- `onMount(async () => { await someAsyncOperation(); });`
|
||||
|
||||
- **Asynchronous Navigation (`goto`):**
|
||||
- The correct pattern for asynchronous navigation using `goto` in Svelte 5 is `await goto(await resolve(path), options);`.
|
||||
- `import { resolve } from '$app/paths';` is crucial when using `resolve()`. A missing import will lead to `no-undef` errors for `resolve`.
|
||||
- `resolve(path)` is crucial to ensure the path is correctly resolved before navigation, especially in universal applications.
|
||||
- The `await` before `goto` is necessary if subsequent code depends on the navigation completing or if `invalidateAll` is used.
|
||||
- **Clearing Stale Caches:** When encountering confusing linting or build errors that don't seem to match the current code, especially after significant refactorings or dependency changes, always try running `npm run clean` to clear stale build artifacts and then re-lint/re-build.
|
||||
|
||||
- **Refactoring Promise Chains:**
|
||||
- When converting `.then().catch().finally()` chains to `async/await` structure, encapsulate the asynchronous operation within a `try...catch...finally` block.
|
||||
- **Incorrect:**
|
||||
```javascript
|
||||
someAsyncFunc()
|
||||
.then(() => { await anotherAsyncFunc(); }) // ERROR: .then() callback is not async
|
||||
.catch(() => { /* ... */ })
|
||||
.finally(() => { await lastAsyncFunc(); }); // ERROR: .finally() callback is not async
|
||||
```
|
||||
- **Correct:**
|
||||
```javascript
|
||||
async () => {
|
||||
try {
|
||||
const result = await someAsyncFunc();
|
||||
await anotherAsyncFunc(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
await lastAsyncFunc();
|
||||
}
|
||||
};
|
||||
```
|
||||
- **Note:** If the entire handler (e.g., `onclick`) is already `async`, you can `await` the promise directly within it.
|
||||
|
||||
## 2. Reactive Declarations & Scoping
|
||||
|
||||
Svelte 5's runes mode introduces new ways of managing reactivity, and understanding variable lifecycles is key.
|
||||
|
||||
- **`$state` for Reactive Variables:**
|
||||
- Variables that are expected to trigger re-renders when their values change, or whose changes need to be observed, should be declared with `$state`.
|
||||
- **Common Error:** Warnings like "This reference only captures the initial value of `data`. Did you mean to reference it inside a closure instead?" or "Variable `x` is updated, but is not declared with `$state(...)`."
|
||||
- **Solution:** Declare the variable using `$state(initialValue)`.
|
||||
- **Context for `data` prop:** When a `data` prop (from a SvelteKit `load` function) is accessed outside of reactive declarations (like `$effect` or event handlers), Svelte might warn that it only captures its initial value. To ensure reactivity or to correctly process it, use it within `$effect` or derive a `$state` variable from it.
|
||||
|
||||
- **Function Scoping and Redeclaration:**
|
||||
- **Common Error:** `Identifier 'function_name' has already been declared`. This occurs when functions with the same name are defined in overlapping scopes.
|
||||
- **Solution:**
|
||||
- If a function is only needed within a specific block (e.g., an `onMount` callback), define it _inside_ that block to limit its scope.
|
||||
- If a function is truly global and needs to be accessible from templates and multiple `onMount` blocks, define it once outside of any `onMount` and ensure it doesn't conflict with other definitions.
|
||||
- Be mindful of helper functions that might be implicitly pulled into global scope by the Svelte compiler if not correctly encapsulated.
|
||||
|
||||
## 3. `replace` Tool Usage Strategy (Critical for AI)
|
||||
|
||||
My efficiency heavily relies on the `replace` tool, and precision is paramount.
|
||||
|
||||
- **Exact `old_string` Matching:**
|
||||
- The `old_string` parameter _must_ precisely match the target text in the file, including all whitespace, indentation, newlines, and comments. Even a single character difference will cause the tool to fail with "0 occurrences found".
|
||||
- For multi-line replacements, always copy the exact block from the `read_file` output.
|
||||
- **Contextual Specificity:**
|
||||
- Avoid generic `old_string` patterns (e.g., `onclick={() => {`) if there are many such occurrences in a file. Instead, expand the `old_string` to include enough surrounding unique context (e.g., the entire button element or parent `div`) to ensure it matches only one instance (`expected_replacements: 1`).
|
||||
- **Iterative Refinement:**
|
||||
- For complex refactorings involving multiple changes in a file, perform changes in small, atomic steps.
|
||||
- **Always `read_file` before each `replace` operation.** This ensures the `old_string` is based on the absolute latest content of the file, preventing mismatches due to previous modifications or unexpected formatting.
|
||||
- After each `replace` operation, immediately run `npm run build` (or `npm run lint`) to validate the change and catch new errors early. This is crucial for catching cascading issues introduced by partial refactorings.
|
||||
|
||||
## 4. Error Debugging Workflow
|
||||
|
||||
- **Prioritize Compiler/Build Errors:** These are blocking issues that prevent the application from running. Address them first.
|
||||
- **Analyze Error Messages:** Read the full error message carefully, including line numbers, and look for keywords (e.g., `await`, `async`, `declared`, `undefined`).
|
||||
- **Consult Svelte Documentation:** The Svelte compiler often provides helpful links (`https://svelte.dev/e/js_parse_error`) which should be a first point of reference if the error is unfamiliar.
|
||||
- **Re-read File Content:** If a `replace` operation fails or produces unexpected results, immediately use `read_file` to verify the exact state of the file before attempting another change.
|
||||
|
||||
## 5. Safe Property Binding (Preventing `props_invalid_value`)
|
||||
|
||||
Svelte 5 enforces strict contracts for bound properties (`bind:prop`). If a component expects a property to have a fallback/default value, you cannot bind `undefined` to it.
|
||||
|
||||
- **The Error:** `Uncaught Svelte error: props_invalid_value. Cannot do bind:prop={undefined} when prop has a fallback value`.
|
||||
- **The Cause:** Attempting to bind a variable that is currently `undefined` to a component prop that has a default value (e.g., `let { prop = false } = $props()`). Svelte cannot determine whether to use the bound `undefined` or the component's internal default, so it throws an error.
|
||||
- **The Fix:** **Always initialize bound variables.**
|
||||
- **Initialization:** Ensure the variable you are binding to is initialized to a valid value (matching the prop's type) *before* the component mounts.
|
||||
- Example: `let myVar = $state(false);` instead of `let myVar = $state();`.
|
||||
- **Data Fetching:** If the data comes from an asynchronous source (API, DB), ensure the object properties have default values *immediately* upon assignment, even before the data is fully populated.
|
||||
- **Good:** `$slct.obj = { ...apiResult, myBoundProp: apiResult.myBoundProp ?? false };`
|
||||
- **Bad:** `$slct.obj = apiResult;` (if `apiResult.myBoundProp` is missing/undefined).
|
||||
- **Updates/resets:** When resetting or updating the object, explicitly re-initialize the bound properties.
|
||||
- `obj = {};` -> `obj = { myBoundProp: false };`
|
||||
@@ -1,704 +0,0 @@
|
||||
# Aether API V3 Frontend Integration Guide (Svelte/TypeScript)
|
||||
|
||||
This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints.
|
||||
|
||||
---
|
||||
|
||||
## 1. Authentication and Security (Mandatory)
|
||||
|
||||
V3 architecture enforces strict **Multi-Tenant Isolation** and **Machine Authorization**. Requests require two levels of validation.
|
||||
|
||||
### A. The "Entry Ticket" (API Key)
|
||||
**Mandatory for all requests.** identifies the application or client.
|
||||
* **Header:** `x-aether-api-key: <your_app_key>`
|
||||
* **Status Code:** `403 Forbidden` if missing or invalid.
|
||||
|
||||
### B. The "Visa" (Account Context)
|
||||
Required for any non-public data (Journals, Badges, Users, etc.).
|
||||
1. **Standard Access**: Provide the `x-account-id` (the random string ID).
|
||||
* **Header:** `x-account-id: <account_id>`
|
||||
2. **Administrative Bypass**: For authorized scripts needing global access.
|
||||
* **Header:** `x-no-account-id: bypass`
|
||||
* **Scope:** Narrow escape hatch only. Keep it limited to allowlisted bootstrap/public/global-default paths and prefer `x-account-id` or JWT-backed requests everywhere else.
|
||||
3. **Token Access**: Provide a **JWT** in the query string.
|
||||
* **Query Param:** `?jwt=<token>`
|
||||
4. **Important Distinction:** A query parameter named `key` is **not** an account-context bypass signal.
|
||||
* `key` may be used by specific endpoints/business logic, but it must **not** cause the frontend to remove `x-account-id`.
|
||||
* Only explicit `x-no-account-id: bypass` should strip account context.
|
||||
|
||||
> [!NOTE]
|
||||
> The `x-no-account-id` path should continue to shrink over time. If you need a new use, document why `x-account-id` or JWT cannot cover it and mark the use as temporary unless it is a hard bootstrap/global-default requirement.
|
||||
|
||||
> [!CAUTION]
|
||||
> **UNSUPPORTED HEADERS:** The header `x-aether-api-token` is **NOT recognized** by the V3 API. If you send it, the backend will treat you as a guest and block access to private data.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bootstrapping (The FQDN Handshake)
|
||||
|
||||
When the frontend first loads and doesn't know the `account_id`, it performs a "handshake" using its domain name.
|
||||
|
||||
**Endpoint:** `POST /v3/crud/site_domain/search`
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{ "field": "fqdn", "op": "eq", "value": "demo.oneskyit.com" }
|
||||
]
|
||||
}
|
||||
```
|
||||
**Results:**
|
||||
* Returns 200 + a list containing the `account_id` (random string ID) and `site_id` (random string ID).
|
||||
* ** デザイン Choice:** If the domain is not found, it returns **200 OK with an empty list `[]`**. It is NOT a 404.
|
||||
|
||||
> **Access Key Support**
|
||||
>
|
||||
> Some client deployments restrict their domain via an access key passed in the browser URL (e.g. `?key=abc123`). The frontend reads this param and forwards it as `access_key` in the POST body.
|
||||
>
|
||||
> **How to pass the key:**
|
||||
> ```json
|
||||
> {
|
||||
> "and": [
|
||||
> { "field": "fqdn", "op": "eq", "value": "client.example.com" },
|
||||
> { "field": "access_key", "op": "eq", "value": "abc123" }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
> If `key` is absent, empty, or falsy — **omit `access_key` from the payload entirely**. Do not send `"access_key": ""`.
|
||||
>
|
||||
> **Server behavior:**
|
||||
> - `site_access_key` (site-level key) takes priority. If set, all domains under that site require it.
|
||||
> - `site_domain_access_key` (domain-level key) is used as fallback when `site_access_key` is not set.
|
||||
> - A domain is **public** only when **both** key columns are NULL/empty.
|
||||
> - Falsy `access_key` values are ignored server-side as a safety net.
|
||||
> - Match → `200` with the record. No match → `200` with empty list `[]`.
|
||||
> - Do **not** use `access_code_kv_json` for this — that field is for UI features only.
|
||||
>
|
||||
> | Browser URL | `access_key` in payload | Result |
|
||||
> |---|---|---|
|
||||
> | `https://dev-demo.oneskyit.com` | *(omit)* | ✅ Returns record (public) |
|
||||
> | `https://client.example.com/?key=correct` | `"correct"` | ✅ Returns record |
|
||||
> | `https://client.example.com/` | *(omit)* | ❌ Empty (key required) |
|
||||
> | `https://client.example.com/?key=wrong` | `"wrong"` | ❌ Empty (wrong key) |
|
||||
> | `https://client.example.com/?key=` | *(omit — strip empty)* | ❌ Empty (key required) |
|
||||
>
|
||||
---
|
||||
|
||||
## 3. Standard CRUD Patterns
|
||||
|
||||
### A. GET by ID
|
||||
Used when the ID is known.
|
||||
* **Endpoint:** `GET /v3/crud/{obj_type}/{id}`
|
||||
* **Security:** Returns 403 if the record doesn't belong to your `x-account-id`.
|
||||
|
||||
### B. POST Search
|
||||
The primary way to retrieve data.
|
||||
* **Endpoint:** `POST /v3/crud/{obj_type}/search`
|
||||
* **Security:** Automatically filters results to only show records belonging to your `x-account-id`. If no account context is provided, it will return **0 records** for private objects.
|
||||
|
||||
### C. POST Create / PATCH Update
|
||||
Modify data in the system.
|
||||
* **Endpoints:**
|
||||
* `POST /v3/crud/{obj_type}/`
|
||||
* `PATCH /v3/crud/{obj_type}/{id}`
|
||||
* **Strict Mode (Default):** The API validates your payload against the Pydantic model. If you send fields that do not exist in the model, the database might return a 400 "Unknown column" error.
|
||||
* **Permissive Mode (Header):** To allow the frontend to send "extra" fields (like local UI state) without causing errors, use the following header:
|
||||
* **Header:** `x-ae-ignore-extra-fields: true`
|
||||
* **Behavior:** When set to `true`, the backend will automatically strip any fields from the payload that are not defined in the object's model before attempting to save to the database.
|
||||
|
||||
#### `*_json` field serialization — do NOT pre-stringify in route/component code
|
||||
|
||||
The frontend API wrappers (`src/lib/ae_api/api_post__crud_obj.ts` for V3, `src/lib/api/api.ts` for legacy CRUD) automatically serialize any field whose name ends in `_json` (e.g. `cfg_json`, `data_json`) before sending. They pretty-print with 2-space indent via an internal `serialize_json_field_pretty()` helper.
|
||||
|
||||
**Pass `*_json` fields as plain JS objects from routes and components.** The serialization layer handles the rest.
|
||||
|
||||
```ts
|
||||
// ✅ Correct — pass as plain object; V3 wrapper serializes it
|
||||
await update_ae_obj__site({ site_id, data_kv: { cfg_json: { jitsi_token_endpoint: url } } });
|
||||
|
||||
// ❌ Wrong — double-encodes the JSON string (the wrapper would stringify an already-stringified value)
|
||||
await update_ae_obj__site({ site_id, data_kv: { cfg_json: JSON.stringify({ jitsi_token_endpoint: url }) } });
|
||||
```
|
||||
|
||||
The V3 wrapper (`api_post__crud_obj.ts`) only serializes when `typeof value === 'object'`, so it will not double-encode a plain string. The legacy wrapper (`api.ts`) stringifies unconditionally, so pre-stringifying there **will** produce double-encoded JSON. In both cases, the right answer is to pass the raw object and let the layer handle it.
|
||||
|
||||
### D. ID Fields in Responses (Vision ID Convention)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **V3 responses always use random string IDs — never database integers.**
|
||||
|
||||
All V3 responses — `POST` create, `GET` single, `GET` list, search, and `PATCH` update — contain:
|
||||
|
||||
| Field | Type | Use |
|
||||
| :--- | :--- | :--- |
|
||||
| `{obj_type}_id` | `string` | **Primary public ID.** Use this for subsequent `PATCH` calls and UI routing. |
|
||||
| `{obj_type}_id_random` | `string` | Legacy alias. Same value as `{obj_type}_id`. Present for backward compat only. |
|
||||
|
||||
**Example — create then immediately PATCH:**
|
||||
```ts
|
||||
const created = await postArchiveContent(archiveId, payload);
|
||||
const newId = created.data.archive_content_id; // random string e.g. "xK9mP3qRtL2"
|
||||
|
||||
// Use it directly in the PATCH URL — no lookup needed
|
||||
await patchArchiveContent(newId, { name: 'Updated Name' });
|
||||
// PATCH /v3/crud/archive/{archive_id}/archive_content/{newId}
|
||||
```
|
||||
|
||||
> **Note on `_id_random` suffix:** The `{obj_type}_id_random` field is a legacy artifact from the pre-Vision model. Once you confirm `{obj_type}_id` is a random string (length 11–22), you do not need `_id_random` as a fallback. New code should only read `{obj_type}_id`.
|
||||
|
||||
---
|
||||
|
||||
## 4. V3 Uniform Lookup System
|
||||
|
||||
The V3 Lookup system provides a hierarchical, deduplicated interface for standardized reference tables (Countries, Timezones, etc.). It supports global defaults, account-level overrides, and object-level overrides, with optional site-specific whitelisting.
|
||||
|
||||
### How the hierarchy works
|
||||
|
||||
Each lookup table (`lu_v3_country`, `lu_v3_time_zone`, etc.) can hold multiple rows for the same logical item at different scopes:
|
||||
|
||||
| Scope | `account_id` | `for_type` / `for_id` | Wins over |
|
||||
|---|---|---|---|
|
||||
| Global default | `NULL` | `NULL` / `NULL` | nothing |
|
||||
| Account override | set | `NULL` / `NULL` | Global default |
|
||||
| Object override | set | set | Account override + Global default |
|
||||
|
||||
The API uses `ROW_NUMBER() PARTITION BY group` to collapse all rows for the same item down to the single highest-priority winner before returning results. **`group` is the identity key** — it is what makes two rows "the same item competing for priority."
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **The `group` field is not a display label.** It is the deduplication key. Each lookup type uses a different natural key for `group`:
|
||||
>
|
||||
> | Lookup type | `group` value | Example |
|
||||
> |---|---|---|
|
||||
> | `country` | ISO alpha-2 code | `"US"`, `"CA"`, `"GB"` |
|
||||
> | `country_subdivision` | subdivision code | `"US-NY"`, `"CA-ON"` |
|
||||
> | `time_zone` | IANA timezone name | `"America/New_York"`, `"US/Eastern"` |
|
||||
>
|
||||
> For `time_zone`, `group` and `name` must always be identical — there is no concept of "override all US timezones as a group." Each timezone is its own identity.
|
||||
|
||||
### A. List Lookups
|
||||
|
||||
Retrieve the deduplicated, ranked list for a lookup type.
|
||||
|
||||
* **Endpoint:** `GET /v3/lookup/{lu_type}/list`
|
||||
* **Available Types:** `country`, `country_subdivision`, `time_zone`
|
||||
* **Parameters:**
|
||||
* `site_id` (Optional): Random ID of the site — applies a **Whitelist Policy** (see §C).
|
||||
* `only_priority` (Optional): `true` returns only `priority=1` items (e.g., common time zones).
|
||||
* `for_type` / `for_id` (Optional): Object context — activates object-level override matching.
|
||||
* `include_disabled` (Optional): `true` includes shadowed/disabled records (useful for admin views).
|
||||
|
||||
**Frontend keying:** Always key Svelte `{#each}` blocks on `group`, not `id` or `name`. `group` is guaranteed unique in the response. Keying on `id` will break if an account override wins (different `id`, same logical item).
|
||||
|
||||
### B. Resolve Identity
|
||||
|
||||
Resolves a string to a single lookup record.
|
||||
|
||||
* **Endpoint:** `GET /v3/lookup/{lu_type}/resolve?q=VALUE`
|
||||
* **Usage:** Use when you have an external code (e.g., ISO `"US"`) and need the full Aether record. Scans `name`, `group`, and other identity fields.
|
||||
|
||||
### C. Site Whitelist Policy
|
||||
|
||||
To restrict which lookup items appear for a specific site, add a `lookup_policy` to `site.cfg_json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"lookup_policy": {
|
||||
"country": ["US", "CA", "GB"],
|
||||
"time_zone": ["America/New_York", "US/Eastern"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Whitelist values must match the `group` field** — i.e., the natural key for that type (ISO code for country, IANA name for time zone). Using a display name will silently return no results for that item.
|
||||
|
||||
### D. Adding and managing client overrides
|
||||
|
||||
When a client needs a customized label or wants to hide/reorder lookup items, create override records rather than modifying global defaults.
|
||||
|
||||
**Rules:**
|
||||
1. **Never modify global default rows** (`account_id = NULL`). Those are shared across all accounts. Any change there affects every client.
|
||||
2. **Set `group` to the exact same value as the global default row** for the item you are overriding. If `group` doesn't match, the override creates a new item instead of replacing the existing one.
|
||||
3. **Set `account_id`** to the client's account ID. Leave `for_type` / `for_id` null unless the override is specific to a single object (e.g., one site).
|
||||
|
||||
**Example — rename "US/Eastern" for one account:**
|
||||
|
||||
```sql
|
||||
INSERT INTO lu_v3_time_zone
|
||||
(account_id, name, name_override, `group`, enable, priority, sort)
|
||||
VALUES
|
||||
(42, 'US/Eastern', 'Eastern Time (Client Label)', 'US/Eastern', 1, 1, 50);
|
||||
```
|
||||
|
||||
The `name_override` field is the display label the frontend should prefer when set. `group = 'US/Eastern'` ensures this row competes with — and wins over — the global default in the `PARTITION BY group` deduplication.
|
||||
|
||||
**To disable an item for one account** (hide it from their dropdowns):
|
||||
|
||||
```sql
|
||||
INSERT INTO lu_v3_time_zone
|
||||
(account_id, name, `group`, enable)
|
||||
VALUES
|
||||
(42, 'US/Samoa', 'US/Samoa', 0);
|
||||
```
|
||||
|
||||
Setting `enable = 0` on an account-scoped row shadows the global default for that account only.
|
||||
|
||||
**To remove a client override** (revert to global default):
|
||||
|
||||
Simply delete the row where `account_id = <client>` and `group = '<item>'`. The global default row is unaffected and immediately resumes winning.
|
||||
|
||||
### E. Adding new global lookup items
|
||||
|
||||
When seeding new lookup data (e.g., adding timezones in bulk):
|
||||
|
||||
1. Set `group = name` for every row (for `time_zone`). This is a hard invariant — if `group` is set to a regional label like `"United States"` instead of the timezone name, the entire group collapses to a single winner and all but one entry disappear from the API response.
|
||||
2. Set `account_id = NULL` and `for_type = NULL` / `for_id = NULL` for global defaults.
|
||||
3. After seeding, verify with:
|
||||
```sql
|
||||
-- Should return 0 rows; any result means multiple items will collapse into one
|
||||
SELECT `group`, COUNT(*) AS cnt
|
||||
FROM lu_v3_time_zone
|
||||
WHERE account_id IS NULL
|
||||
GROUP BY `group`
|
||||
HAVING cnt > 1;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Event File Data Retrieval (Hosted Files)
|
||||
|
||||
Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File itself is a metadata record for binary content (files), which is accessed via separate Action endpoints (e.g., `/v3/action/hosted_file/download`). This API endpoint provides metadata about the associated hosted file. To retrieve this additional metadata:
|
||||
|
||||
* **Endpoint:** `GET /v3/crud/event_file/{event_file_id}`
|
||||
* **Query Parameter:** Add `inc_hosted_file=true`
|
||||
* Example: `/v3/crud/event_file/<event_file_id>?inc_hosted_file=true`
|
||||
|
||||
**Response Impact:**
|
||||
|
||||
1. **Top-Level Convenience Fields:** The response will include top-level fields for commonly needed hosted file data. These are populated directly from the SQL view via JOINs.
|
||||
* `hosted_file_hash_sha256` (string)
|
||||
* `hosted_file_subdirectory_path` (string)
|
||||
* `hosted_file_content_type` (string)
|
||||
* `hosted_file_size` (string - in bytes)
|
||||
2. **Nested Hosted File Object:** A full `hosted_file` object will be nested under the `hosted_file` key. This object (`Hosted_File_Base` model) will contain all its standard fields, including `id` (random string ID), `hash_sha256`, `content_type`, `size`, etc.
|
||||
|
||||
---
|
||||
|
||||
## 6. Hosted File Actions: Convert & Clip (Frontend Notes)
|
||||
|
||||
These helper endpoints let the frontend request small server-side transformations without uploading new blobs. They return a newly-created `hosted_file` metadata object on success.
|
||||
|
||||
- **Convert (PDF → Image)**
|
||||
- Method: `GET`
|
||||
- Path: `/v3/action/hosted_file/{hosted_file_id}/convert_file`
|
||||
- Required query params: `link_to_type`, `link_to_id`
|
||||
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_conversion`), `to_type` (defaults to `webp`)
|
||||
- Auth: standard V3 headers (`x-aether-api-key`, `x-account-id` / `x-no-account-id` / `?jwt=`)
|
||||
- Behavior: converts the first page of a PDF to `webp` or `png`, saves a new `hosted_file`, and returns its metadata. Returns 400 on failure.
|
||||
|
||||
- **Clip Video**
|
||||
- Method: `GET`
|
||||
- Path: `/v3/action/hosted_file/{hosted_file_id}/clip_video`
|
||||
- Required query params: `link_to_type`, `link_to_id`, `start_time`, `end_time` (format `HH:MM:SS`)
|
||||
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool)
|
||||
- Auth: standard V3 headers
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`. Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize. Returns 400 on failure.
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
|
||||
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize.
|
||||
- For longer-running clips you can schedule the job in the background by adding `?background=true`. When scheduled the API returns `202 Accepted` and the clip runs asynchronously on the server; check the returned `hosted_file` record later via the standard V3 `hosted_file` endpoints.
|
||||
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
|
||||
|
||||
Frontend guidance:
|
||||
|
||||
- Call these routes with the same `link_to_type` / `link_to_id` you plan to associate the resulting hosted_file with — the server resolves random IDs for you.
|
||||
- After a successful response, use the V3 `hosted_file` action endpoints (download/delete) to manage or retrieve the new file.
|
||||
- These endpoints run synchronously and can take time for large inputs; for heavy or batch workloads use a queued job pattern instead.
|
||||
- These endpoints may take time for large inputs. Prefer using `?background=true` to schedule work and receive a `202 Accepted` response for async processing. For heavy or batch workloads use a queued job pattern instead.
|
||||
|
||||
---
|
||||
|
||||
## 8. Email Send Action
|
||||
|
||||
Send a transactional email via the Aether API.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/email/send`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id` (or `x-no-account-id` / `?jwt=`)
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"from_email": "noreply@example.com",
|
||||
"from_name": "Example App",
|
||||
"to_email": "user@example.com",
|
||||
"to_name": "Alice Smith",
|
||||
"subject": "Your login link",
|
||||
"body_html": "<p>Click <a href=\"...\">here</a> to log in.</p>",
|
||||
"body_text": "Visit ... to log in.",
|
||||
"cc_email": null,
|
||||
"bcc_email": null
|
||||
}
|
||||
```
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `test` | bool | `false` | Simulate send without delivering |
|
||||
|
||||
**Response:** `data` contains `{ from_email, to_email, subject }` (first 40 chars of subject). `400` if delivery failed.
|
||||
|
||||
> **Replaces:** `POST /util/email/send` (disabled as of May 2026).
|
||||
|
||||
---
|
||||
|
||||
## Axonius Zoom CSV Upload (Temporary — Apr 2026, EXPIRED)
|
||||
|
||||
Purpose: Staff-only quick upload to upsert Event Person + Event Badge records from a Zoom Events registrant CSV.
|
||||
|
||||
- **Endpoint:** `POST /event/{event_id}/badge/import/zoom_csv`
|
||||
- **Auth:** include `x-aether-api-key` (if required) and account context via `x-account-id: <ACCOUNT_ID>`. Admin bypass (`x-no-account-id: bypass`) or `?jwt=<token>` are accepted per site policy.
|
||||
- **Request:** `multipart/form-data` with single file field `file` (Zoom CSV). Query params:
|
||||
- `begin_at` (int, default `0`)
|
||||
- `end_at` (int, default `20000`)
|
||||
- `return_detail` (bool, default `false`)
|
||||
- Delimiter is auto-detected; Zoom CSV layout: row 1 = metadata, row 2 = blank, row 3 = headers (the backend skips the first two rows).
|
||||
|
||||
Behavior / notes:
|
||||
- The handler forces `Registrant email` to be used as the `external_id`. `Unique identifier` is used as `external_registration_id` only when it is meaningful (placeholders like `N/A`, `NA`, `UNKNOWN` are ignored).
|
||||
- Per-ticket custom fields are parsed (Organization, Job title, Phone, Address lines, City, State/Province, Postal/Zip, Country, etc.).
|
||||
- Marketing-consent values are mapped to `agree_to_tc` and `allow_tracking`.
|
||||
- TEMP AXONIUS MAPPING: the import temporarily defaults `event_badge_template_id` to `21` and `event_badge_template_id_random` to `RKYp2HcQm9o`. Ticket-name → `badge_type_code` mapping is applied for some labels (e.g., contains "sponsor" → `sponsor`; contains "attend"/"attendee" → `attendee`). This mapping is temporary (April 2026) — surface this to staff.
|
||||
- Rows missing `Registrant email` are skipped.
|
||||
- The server upserts via existing backend methods and creates/updates `event_person`, `event_person_profile`, and `event_badge` records as needed.
|
||||
|
||||
Frontend guidance:
|
||||
- UI must be staff-only and should validate an `event_id` is selected.
|
||||
- For large files, use `begin_at`/`end_at` to process in chunks.
|
||||
- Prefer `return_detail=false` for large imports to reduce payload size.
|
||||
|
||||
Common errors:
|
||||
- `403` — missing/invalid account context or API key.
|
||||
- `404` — event not found.
|
||||
- `500` — file save or processing error.
|
||||
|
||||
Example curl (replace placeholders):
|
||||
```bash
|
||||
curl -v -X POST "https://api.example.com/event/<EVENT_ID>/badge/import/zoom_csv?begin_at=0&end_at=20000&return_detail=false" \
|
||||
-H "x-aether-api-key: <API_KEY>" \
|
||||
-H "x-account-id: <ACCOUNT_ID>" \
|
||||
-F "file=@/path/to/zoom_export.csv"
|
||||
```
|
||||
|
||||
Sample success (summary mode, `return_detail=false`):
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"event_id": "xK9mP3qRtL2",
|
||||
"event_id_random": "xK9mP3qRtL2",
|
||||
"external_id": "alice@example.com",
|
||||
"given_name": "Alice",
|
||||
"family_name": "Smith",
|
||||
"email": "alice@example.com"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"status_code": 200,
|
||||
"status_name": "OK",
|
||||
"success": true,
|
||||
"data_type": "list",
|
||||
"data_list_count": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sample success (detailed, `return_detail=true`) — `data` contains full `event_person` objects with nested `event_badge` (may include temporary `event_badge_template_id`: `21` and `event_badge_template_id_random`: `RKYp2HcQm9o`).
|
||||
|
||||
Paste this section into the guide as a temporary Axonius-specific note (April 2026). Consider linking staff to a sample Zoom CSV for QA.
|
||||
|
||||
---
|
||||
|
||||
## 7. User Actions (`/v3/action/user/`)
|
||||
|
||||
Stateful user account operations that are not standard CRUD. All require `x-aether-api-key`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Migration from legacy `/user/*` routes:** The table below maps each legacy endpoint to its V3 replacement. Run both in parallel during transition; remove legacy routes once traffic logs confirm they are quiet.
|
||||
>
|
||||
> | Legacy | V3 Replacement |
|
||||
> |---|---|
|
||||
> | `GET /user/authenticate` | `POST /v3/action/user/authenticate` |
|
||||
> | `POST /user/verify_password` | `POST /v3/action/user/verify_password` |
|
||||
> | `PATCH /user/{id}/change_password` | `POST /v3/action/user/{id}/change_password` |
|
||||
> | `GET /user/{id}/new_auth_key` | `GET /v3/action/user/{id}/new_auth_key` |
|
||||
> | `GET /user/{id}/email_auth_key_url` | `GET /v3/action/user/{id}/email_auth_key_url` |
|
||||
> | `GET /user/lookup` | `POST /v3/crud/user/search` |
|
||||
> | `GET /user/lookup_email` | `POST /v3/crud/user/search` |
|
||||
> | `GET /user/lookup_username` | `POST /v3/crud/user/search` |
|
||||
|
||||
### A. Authenticate
|
||||
|
||||
Authenticate a user by **username + password** or **user_id + auth_key**.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/user/authenticate`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id` (scopes username lookups to the correct account)
|
||||
- **Security improvement:** Credentials are in the **POST body**, not query params — safe from URL logging.
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "username": "scott", "password": "MyPassword123!" }
|
||||
```
|
||||
or:
|
||||
```json
|
||||
{ "user_id": "<user_id_random>", "auth_key": "<one_time_key>", "valid_email": true }
|
||||
```
|
||||
|
||||
- `valid_email` (optional `bool`): if `true`, marks `email_verified = true` on success.
|
||||
- `inc_user_role_list` (optional query param, default `false`): include role list in the returned user object.
|
||||
|
||||
**Response on success:** Full user object (same shape as `GET /v3/crud/user/{id}`).
|
||||
|
||||
**Errors:** `400` missing credentials, `403` wrong password / account disabled / account not yet enabled / account expired, `404` user not found.
|
||||
|
||||
> **Auth key flow:** Auth keys are one-time-use — the key is cleared from the DB immediately on successful authentication. Request a new one via `GET /v3/action/user/{id}/new_auth_key`.
|
||||
|
||||
---
|
||||
|
||||
### B. Verify Password
|
||||
|
||||
Check a user's current password without changing it.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/user/verify_password`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "user_id": "<user_id_random>", "current_password": "MyPassword123!" }
|
||||
```
|
||||
or use `"username"` instead of `"user_id"` to look up by username within the account.
|
||||
|
||||
**Response:** `data: true` on match. `400` if the user has no password set, `403` on mismatch, `404` if user not found.
|
||||
|
||||
---
|
||||
|
||||
### C. Change Password
|
||||
|
||||
Change a user's password. Optionally verify the current password first.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/user/{user_id}/change_password`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "new_password": "NewPassword456!", "current_password": "MyPassword123!" }
|
||||
```
|
||||
|
||||
- `new_password` is required (minimum 10 characters).
|
||||
- `current_password` is optional. If provided, it is verified before the change is applied. Omit it for admin-driven resets.
|
||||
|
||||
**Response:** `data: true` on success. `403` if `current_password` provided but wrong.
|
||||
|
||||
---
|
||||
|
||||
### D. Generate New Auth Key
|
||||
|
||||
Generate a fresh one-time-use auth key for the user and write it to the DB.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/user/{user_id}/new_auth_key`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "data": { "auth_key": "<new_key>" } }
|
||||
```
|
||||
|
||||
The returned key can then be passed to `/authenticate` (as `auth_key`) or embedded in a login URL. The user record must have `allow_auth_key = true` for key-based authentication to work.
|
||||
|
||||
---
|
||||
|
||||
### E. Email Auth Key URL
|
||||
|
||||
Generate a new auth key and email a one-time login link to the user's email address.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/user/{user_id}/email_auth_key_url`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `root_url` | `string` | *(required)* | Base URL the login link is built from. Must be provided — if omitted the link in the email will be malformed (`None?...`). |
|
||||
| `key_param_name` | `string` | `auth_key` | Query param name used for the auth key in the generated link. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `root_url` is **required in practice**. The FastAPI query param accepts `null` but the email builder does not guard against it — omitting it produces a broken link in the email.
|
||||
|
||||
**Magic link URL format (default `key_param_name`):**
|
||||
```
|
||||
{root_url}?user_id={user_id_random}&auth_key={auth_key}&valid_email=True
|
||||
```
|
||||
The frontend at `root_url` should read these query params and call `POST /v3/action/user/authenticate` with `{ "user_id": "...", "auth_key": "..." }`. Note that `valid_email=True` is **always** injected — authenticating via a magic link automatically marks the user's email as verified.
|
||||
|
||||
**Response:** `data: true` on success (email sent). `404` if user not found. `500` if delivery failed — common causes: account email not configured, user `enable = false`, or `allow_auth_key = false`.
|
||||
|
||||
---
|
||||
|
||||
### F. User Lookups via V3 CRUD Search
|
||||
|
||||
The three legacy lookup routes (`lookup`, `lookup_email`, `lookup_username`) are replaced by standard V3 CRUD search:
|
||||
|
||||
```typescript
|
||||
// Look up by user_id (Vision ID)
|
||||
POST /v3/crud/user/search
|
||||
{ "and": [{ "field": "id_random", "op": "eq", "value": "<user_id>" }] }
|
||||
|
||||
// Look up by email
|
||||
POST /v3/crud/user/search
|
||||
{ "and": [{ "field": "email", "op": "eq", "value": "user@example.com" }] }
|
||||
|
||||
// Look up by username
|
||||
POST /v3/crud/user/search
|
||||
{ "and": [{ "field": "username", "op": "eq", "value": "scott" }] }
|
||||
```
|
||||
|
||||
Results are automatically scoped to the `x-account-id` provided in the request.
|
||||
|
||||
---
|
||||
|
||||
## 10. Event Exhibit Tracking Export (Leads Export)
|
||||
|
||||
Allows an exhibitor to download all lead-capture records for their exhibit as a CSV or XLSX file.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/event_exhibit/{exhibit_id}/tracking_export`
|
||||
- **Auth:** Standard V3 headers (`x-aether-api-key` + `x-account-id` or `?jwt=`)
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `file_type` | `CSV` \| `XLSX` | `CSV` | Output format. |
|
||||
| `return_file` | bool | `true` | `true` → file download response. `false` → JSON body with row data. |
|
||||
|
||||
### Response
|
||||
|
||||
- `Content-Type: text/csv` (CSV) or `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (XLSX)
|
||||
- `Content-Disposition: attachment; filename="leads_export_<timestamp>.csv"`
|
||||
- If there are no tracking records, a valid file with headers only is returned (not a 404).
|
||||
|
||||
### Columns Returned
|
||||
|
||||
Fixed columns (always present), followed by any custom question columns flattened from `responses_json`:
|
||||
|
||||
`event_exhibit_tracking_id`, `created_on`, `updated_on`, `event_exhibit_name`, `event_badge_full_name`, `event_badge_email`, `event_badge_professional_title`, `event_badge_affiliations`, `event_badge_location`, `event_badge_country`, `external_person_id`, `exhibitor_notes`, `priority`, `enable`, `hide`, `[custom question codes…]`
|
||||
|
||||
> **Note:** `exhibitor_notes` has HTML tags stripped automatically for clean CSV output.
|
||||
|
||||
### Permission Requirement — `leads_api_access`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This endpoint enforces a **per-exhibit permission flag**. The `event_exhibit` record **must** have `leads_api_access = true` set in the database, OR the caller must have manager-level account access (JWT with `manager: true`).
|
||||
>
|
||||
> If `leads_api_access` is `false` or `null` on the exhibit, the API returns:
|
||||
> ```json
|
||||
> { "detail": "Access denied: leads API access is not enabled for this exhibit." }
|
||||
> ```
|
||||
> **Fix:** Enable the flag on the exhibit record via `PATCH /v3/crud/event_exhibit/{id}` with `{ "leads_api_access": true }`, or set it directly in the database/admin panel.
|
||||
|
||||
#### Dual purpose of `leads_api_access`
|
||||
|
||||
This flag serves two related but distinct roles:
|
||||
|
||||
1. **3rd-party API access (original intent):** Controls whether external systems (exhibitor apps, badge-scanning devices, etc.) are permitted to push or pull lead data for this exhibit via the API.
|
||||
2. **UI export gate (new):** The frontend should read `leads_api_access` from the exhibit record and use it to show or hide the export/download button. Only render the button when the flag is `true` — this prevents users from triggering a request that will always 403.
|
||||
|
||||
The recommended pattern is to fetch the exhibit record first and gate the UI on this field before the user ever sees the export option. The API enforces the same check server-side as a safety net.
|
||||
|
||||
### Example Request
|
||||
|
||||
```ts
|
||||
const resp = await fetch(
|
||||
`https://dev-api.oneskyit.com/v3/action/event_exhibit/${exhibitId}/tracking_export?file_type=CSV&return_file=true`,
|
||||
{
|
||||
headers: {
|
||||
'x-aether-api-key': API_KEY,
|
||||
'x-account-id': accountId,
|
||||
},
|
||||
}
|
||||
);
|
||||
// resp is a file blob — use URL.createObjectURL() or trigger a download
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. IDAA: Server-Side Novi Member Verification
|
||||
|
||||
Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether backend. This eliminates false "Access Denied" failures for members on hotel/conference WiFi, VPNs, and Cloudflare-filtered networks — the Novi call originates from the server's IP, not the member's browser IP.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/idaa/novi_member/{uuid}`
|
||||
- **Auth:** Standard V3 (`x-aether-api-key` + `x-account-id` or `?jwt=`)
|
||||
|
||||
### Request
|
||||
|
||||
| Parameter | Location | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `uuid` | Path | Yes | Novi member UUID (from Novi AMS) |
|
||||
|
||||
### Response on success (`200 OK`)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"verified": true,
|
||||
"full_name": "Alice S.",
|
||||
"email": "alice+member@idaa.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `full_name`: `"{FirstName} {LastName[0]}."` format. Falls back to the Novi `Name` field if first/last are absent.
|
||||
- `email`: Novi `Email` field with space → `+` normalization applied (Novi quirk — `alice member@idaa.org` → `alice+member@idaa.org`).
|
||||
|
||||
### Error responses
|
||||
|
||||
| Status | Meaning | Frontend action |
|
||||
|---|---|---|
|
||||
| `404` | UUID not found in Novi, or Novi returned 200 with no identity data (empty-member anti-pattern — member may have just joined) | Treat as denied / not a member |
|
||||
| `429` | Novi rate limit hit | Surface as `'rate_limited'`; advise retry |
|
||||
| `503` | Novi unreachable or Novi 5xx error | Auto-retry once after 3s; if retry also fails, surface as `'api_error'` |
|
||||
|
||||
### Migration from direct Novi call — ✅ Complete (2026-05-19)
|
||||
|
||||
`+layout.svelte:verify_novi_uuid()` now calls this endpoint instead of Novi directly. Response code mapping (for reference):
|
||||
|
||||
| Direct Novi result | This endpoint returns | Frontend behavior |
|
||||
|---|---|---|
|
||||
| `200` with identity data | `200` | `verified` |
|
||||
| `200` with no identity data | `404` | `denied` |
|
||||
| `404` | `404` | `denied` |
|
||||
| `429` | `429` | Auto-retry after 10s; `'rate_limited'` if retry fails |
|
||||
| Network error / Novi 5xx | `503` | Auto-retry after 3s; `'api_error'` if retry fails |
|
||||
|
||||
### Caching
|
||||
|
||||
Verified results are cached in Redis (`idaa:novi_member:{uuid}`, 4-hour TTL). `404` results are **never** cached so recently-joined members are not incorrectly denied on their next attempt.
|
||||
|
||||
---
|
||||
|
||||
## 11. Troubleshooting 403 Forbidden
|
||||
|
||||
If you receive a 403 on a valid ID:
|
||||
1. Verify `x-aether-api-key` is correct.
|
||||
2. Ensure you are sending `x-account-id` and NOT `x-aether-api-token`.
|
||||
3. Verify the record actually belongs to the account ID you are sending.
|
||||
4. Check if the object is marked `public_read: True` in the registry. (Posts and Archive Content allow guest access; Journals and Badges do not).
|
||||
5. Confirm the frontend is not treating `params.key` as an implicit bypass and stripping `x-account-id`.
|
||||
6. If list/search endpoints work but `GET /v3/crud/{obj_type}/{id}` still returns 403, this is likely endpoint-level policy (e.g., requires stronger auth like JWT) rather than a transport/header bug.
|
||||
@@ -1,201 +0,0 @@
|
||||
# Aether API V3 WebSocket Integration Guide
|
||||
|
||||
This guide explains how to implement real-time communication using the **Aether API V3 WebSocket** protocol. V3 introduces granular routing, strict message schemas, and improved multi-tenant isolation compared to previous versions.
|
||||
|
||||
---
|
||||
|
||||
## 1. Key Improvements (V2 vs V3)
|
||||
|
||||
| Feature | WebSocket V2 (Legacy) | WebSocket V3 (Modern) |
|
||||
| :--- | :--- | :--- |
|
||||
| **URL Prefix** | `/ws/` or `/ws_redis/` | `/v3/ws/` |
|
||||
| **Routing** | **Global**: Every client receives every message. | **Granular**: Redis filters messages before sending. |
|
||||
| **Performance**| Low efficiency at scale (Python filtering). | High efficiency (Redis native pub/sub). |
|
||||
| **Schema** | Loose JSON objects. | Strict Pydantic-validated models. |
|
||||
| **Presence** | None / Manual. | Automatic Redis-backed presence sets. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Connection Strategy
|
||||
|
||||
### A. Endpoint URL
|
||||
The V3 WebSocket path requires both a `group_id` and a `client_id`. Both are treated as **opaque unique strings** by the backend — no specific format is enforced.
|
||||
|
||||
```text
|
||||
wss://[api_domain]/v3/ws/group/{group_id}/client/{client_id}
|
||||
```
|
||||
|
||||
**`group_id`** — identifies the shared channel (e.g., an event ID, a room name, or a Vision ID random string). All clients using the same `group_id` receive group-targeted messages together.
|
||||
|
||||
**`client_id`** — uniquely identifies this specific connection. The backend accepts any unique string (UUID, timestamp, Vision ID — no format validation). The **recommended pattern** is a UUID v4 generated once and persisted in `localStorage` so the same identity is reused across page reloads and sessions on that browser.
|
||||
|
||||
> Use `ws://` for local development and `wss://` in production (any HTTPS site). The Nginx config must include the Upgrade block — see Section 6.
|
||||
|
||||
### B. Authentication
|
||||
Browsers **cannot** set custom HTTP headers on WebSocket connections. Pass the API Key and account context as **query parameters** instead:
|
||||
|
||||
| Parameter | Purpose | Example |
|
||||
| :--- | :--- | :--- |
|
||||
| `api_key` | Entry Ticket (machine auth) | `?api_key=<your_app_key>` |
|
||||
| `jwt` | Visa (user / account context) | `&jwt=<token>` |
|
||||
| `x_account_id` | Alt account context | `&x_account_id=<account_id>` |
|
||||
|
||||
**Full example URL:**
|
||||
```text
|
||||
wss://dev-api.oneskyit.com/v3/ws/group/{group_id}/client/{client_id}?api_key=<key>&jwt=<token>
|
||||
```
|
||||
|
||||
### C. Connection Example (TypeScript)
|
||||
```ts
|
||||
// client_id: generated once, persisted in localStorage for stable identity across reloads
|
||||
if (!localStorage.getItem('controller_client_id')) {
|
||||
localStorage.setItem('controller_client_id', crypto.randomUUID());
|
||||
}
|
||||
const client_id = localStorage.getItem('controller_client_id')!; // UUID v4, e.g. "550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
const group_id = "event_abc123"; // Any unique string identifying the shared channel
|
||||
const api_key = import.meta.env.VITE_API_KEY;
|
||||
const jwt = getSessionToken(); // your JWT helper
|
||||
|
||||
const ws_url = `wss://dev-api.oneskyit.com/v3/ws/group/${group_id}/client/${client_id}?api_key=${api_key}&jwt=${jwt}`;
|
||||
|
||||
const socket = new WebSocket(ws_url);
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log("Connected to Aether WS V3");
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. The V3 Message Schema
|
||||
|
||||
All messages sent and received over V3 must follow the standardized **WS_Message_V3** structure.
|
||||
|
||||
### Message Fields
|
||||
| Field | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `version` | string | Auto | Always `"3"`. Set by server. |
|
||||
| `msg_type` | string | Yes | `'msg'`, `'cmd'`, `'heartbeat'`, `'presence'` |
|
||||
| `target` | string | Yes | `'direct'`, `'group'`, `'broadcast'`, `'echo'` |
|
||||
| `from_id` | string | Auto | **Server fills this from the URL path** — do not send. |
|
||||
| `to_id` | string | Conditional | Target Client ID. Required when `target` is `'direct'`. |
|
||||
| `group_id` | string | Auto | **Server fills this from the URL path** — do not send. |
|
||||
| `cmd` | string | No | Specific action keyword (e.g., `'RELOAD'`). |
|
||||
| `msg` | string | No | Human-readable text content. |
|
||||
| `payload` | object | No | Flexible key-value data. |
|
||||
| `sent_at` | string | Auto | ISO 8601 Timestamp. Set by server. |
|
||||
|
||||
> **Frontend tip:** Only send `msg_type`, `target`, and whatever content fields you need (`msg`, `cmd`, `payload`, `to_id`). The server enforces `from_id`, `group_id`, and `sent_at` from the connection context, preventing spoofing.
|
||||
|
||||
---
|
||||
|
||||
## 4. Message Targeting Logic
|
||||
|
||||
V3 uses the `target` field to determine which Redis channel to use, ensuring only the intended recipients receive the data.
|
||||
|
||||
### A. Group Broadcast
|
||||
Sends the message to every client connected to the same `group_id`.
|
||||
```json
|
||||
{
|
||||
"msg_type": "msg",
|
||||
"target": "group",
|
||||
"msg": "Hello team!"
|
||||
}
|
||||
```
|
||||
|
||||
### B. Direct Message (DM)
|
||||
Sends the message to one specific client ID, regardless of their group.
|
||||
```json
|
||||
{
|
||||
"msg_type": "msg",
|
||||
"target": "direct",
|
||||
"to_id": "target_client_random_id",
|
||||
"msg": "Private message just for you."
|
||||
}
|
||||
```
|
||||
|
||||
### C. System Broadcast
|
||||
Sends the message to **every** connected client on the platform (use sparingly).
|
||||
```json
|
||||
{
|
||||
"msg_type": "cmd",
|
||||
"target": "broadcast",
|
||||
"cmd": "MAINTENANCE_WARNING"
|
||||
}
|
||||
```
|
||||
|
||||
### D. Echo
|
||||
Sends the message back only to the sender (useful for testing round-trip latency).
|
||||
```json
|
||||
{
|
||||
"msg_type": "msg",
|
||||
"target": "echo",
|
||||
"msg": "Ping!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Specialized Message Types
|
||||
|
||||
### Commands (`cmd`)
|
||||
Used for remote control or orchestration.
|
||||
```json
|
||||
{
|
||||
"msg_type": "cmd",
|
||||
"target": "group",
|
||||
"cmd": "RELOAD_UI",
|
||||
"payload": { "force": true }
|
||||
}
|
||||
```
|
||||
|
||||
### Heartbeats (`heartbeat`)
|
||||
Keep the connection alive and **refresh presence** in the backend. Should be sent every 30-60 seconds.
|
||||
|
||||
- The server intercepts `heartbeat` messages and refreshes the Redis presence TTL (1 hour window) before echoing back.
|
||||
- Without periodic heartbeats, a client idle for >1 hour may disappear from the presence set even while still connected.
|
||||
- Use `target: 'echo'` so the server sends the heartbeat straight back — useful for measuring round-trip latency.
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_type": "heartbeat",
|
||||
"target": "echo"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Infrastructure Requirements (Nginx)
|
||||
|
||||
Unlike standard REST endpoints, WebSockets require explicit "Upgrade" handling in the Nginx gateway. If you are deploying to a new server, ensure the following block is present in your Nginx configuration:
|
||||
|
||||
```nginx
|
||||
location /v3/ws {
|
||||
proxy_pass http://fastapi_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_read_timeout 2100s; # Match your app's max heartbeat/session time
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Common Pitfalls & Troubleshooting
|
||||
|
||||
- **HTTP 404 Errors**: This almost always means Nginx is missing the `location /v3/ws` block and is trying to serve the request as a static file from the disk.
|
||||
- **HTTP 400 Errors**: Check your `Host` header. Nginx routes requests based on the `server_name` directive. If you connect to an IP or a non-standard hostname (like `localhost`), ensure it is explicitly listed in your Nginx config.
|
||||
- **Connection Drops**: If the connection drops exactly after 60 seconds, check your Nginx `proxy_read_timeout`. It should be set high (e.g., `2100s`) to allow for long-lived WebSocket sessions.
|
||||
|
||||
---
|
||||
|
||||
## 8. Migration Guide (V2 to V3)
|
||||
|
||||
If you are upgrading from the legacy V2 WebSocket (`/ws/group/...`):
|
||||
|
||||
1. **Change the URL**: Prepend `/v3/` to your WebSocket path.
|
||||
2. **Wrap your JSON**: In V2, you might have sent `{"msg": "hi"}`. In V3, this must be `{"msg_type": "msg", "target": "group", "msg": "hi"}`.
|
||||
3. **Use unique string IDs**: Both `group_id` and `client_id` in the path are opaque strings — any unique value works (timestamp, UUID, Vision ID random string). Just don't use raw database integer IDs. For `to_id` in direct messages, use whatever `client_id` that target client registered with.
|
||||
4. **Listen for `msg_type`**: Update your frontend handlers to switch logic based on the `msg_type` field instead of proprietary keys.
|
||||
@@ -1,239 +0,0 @@
|
||||
# Aether Events — Onsite Badge Printing
|
||||
|
||||
Notes on setup, process, hardware, and browser behavior for onsite badge printing at events.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aether badge printing uses the browser's native `window.print()` — no special software or print
|
||||
server needed. The badge render page (`/events/[event_id]/badges/print/[badge_id]`) outputs
|
||||
print-ready HTML/CSS, and the browser sends it directly to the connected printer via CUPS (Linux)
|
||||
or the OS print system (macOS/Windows).
|
||||
|
||||
Chrome (Chromium) is the recommended browser for onsite kiosk stations.
|
||||
Firefox is a solid alternative, especially for Save-to-PDF workflows.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Workflow — Onsite Kiosk
|
||||
|
||||
1. Open the event's badge printing page: `/events/[event_id]/badges`
|
||||
2. Search for the attendee (name, badge ID, or QR scan)
|
||||
3. Open the badge print page — review the rendered badge
|
||||
4. Click **Print Badge** in the controls panel (or use keyboard shortcut)
|
||||
5. In the browser print dialog:
|
||||
- Set Margins to **None** (Chrome) or leave defaults (Firefox)
|
||||
- Confirm paper/card size matches the stock loaded in the printer
|
||||
- Print
|
||||
6. `print_count` increments automatically on each print via the Print Badge button
|
||||
|
||||
For high-volume events, consider the **rapid QR scan** mode in the Leads module or using a
|
||||
dedicated kiosk session where the operator only handles physical card handoff.
|
||||
|
||||
---
|
||||
|
||||
## Browser Settings
|
||||
|
||||
### Chrome / Chromium (Recommended for kiosk use)
|
||||
|
||||
Chrome is recommended for onsite badge printing stations. Key print dialog settings:
|
||||
|
||||
| Setting | Correct value | Notes |
|
||||
|---|---|---|
|
||||
| Margins | **None** or **Minimum** | Default margins add URL/date headers — breaks badge centering |
|
||||
| Paper size | Match card stock (e.g. 3.5" × 5.5") | Zebra driver may override this automatically |
|
||||
| Background graphics | **On** | Required for colored header/footer stripe to print |
|
||||
| Pages | 1 | PVC single-sided — only front should print |
|
||||
|
||||
**Important:** Chrome ignores CSS `@page { size }` for Save to PDF — it defaults to letter/A4.
|
||||
For physical printer output, the printer driver controls paper size. This is expected behavior.
|
||||
|
||||
To lock Chrome settings for a kiosk, set Margins to "None" once and Chrome remembers per-printer.
|
||||
|
||||
### Firefox
|
||||
|
||||
Firefox honors CSS `@page { size }` which makes it ideal for PDF generation.
|
||||
For physical printing, Firefox generally "just works" without margin adjustments.
|
||||
|
||||
| Setting | Notes |
|
||||
|---|---|
|
||||
| Paper size | Can be set in dialog, but CSS `@page { size }` is honored |
|
||||
| Margins | Default is usually fine; remove headers/footers if they appear |
|
||||
| Background graphics | Enable for colored stripes and header images to print |
|
||||
|
||||
### General Notes
|
||||
|
||||
- **Background graphics must be enabled** in any browser — otherwise header images, footer
|
||||
color stripes, and tonal backgrounds will not print.
|
||||
- Private/incognito mode blocks PWA install prompts — use normal browser sessions for kiosk.
|
||||
- For highest reliability, set the kiosk machine to auto-login and open Chrome to the event URL.
|
||||
|
||||
---
|
||||
|
||||
## Linux / CUPS Setup
|
||||
|
||||
For Linux workstations and dedicated kiosk machines running Linux:
|
||||
|
||||
1. Install CUPS if not already present: `sudo pacman -S cups` (Arch) or equivalent
|
||||
2. Start the CUPS service: `sudo systemctl enable --now cups`
|
||||
3. Open the CUPS web UI: `http://localhost:631`
|
||||
4. Add the printer and install the appropriate driver (see per-printer sections below)
|
||||
5. Print a test page from CUPS to confirm card feed and quality
|
||||
6. In Chrome: select the CUPS printer name under Destination in the print dialog
|
||||
|
||||
On macOS and Windows, use the vendor-provided driver installer.
|
||||
|
||||
---
|
||||
|
||||
## Printers
|
||||
|
||||
---
|
||||
|
||||
### Zebra ZC10L — PVC Card Printer
|
||||
|
||||
**Card stock:** 3.5" × 5.5" PVC cards (CR80 extended)
|
||||
**Tested:** 2026-03-17 (rental test day, Arch Linux)
|
||||
**Status:** Working. Confirmed suitable for Axonius NYC (mid-April 2026).
|
||||
|
||||
#### Physical Setup
|
||||
|
||||
- Connect via USB (the ZC10L supports USB and Ethernet)
|
||||
- Load PVC card stock per the Zebra loading instructions — cards face-up, landscape
|
||||
- The ZC10L prints one side (single-sided dye-sub thermal); do not attempt duplex on PVC stock
|
||||
|
||||
#### Linux Driver
|
||||
|
||||
- Download the Zebra ZC10L CUPS driver from zebra.com (ZC Series Linux support)
|
||||
- Install the `.deb` or extract the PPD file and add to CUPS manually
|
||||
- In CUPS (`http://localhost:631`), add the printer and select the ZC10L PPD
|
||||
- Set default paper size to **3.5" × 5.5"** (or CR80 Extended if listed)
|
||||
- Print a blank test page from CUPS before using Chrome
|
||||
|
||||
> **Note:** Driver version tested: *(update here after confirming)*
|
||||
> CUPS printer name used: *(update here after setup)*
|
||||
|
||||
#### Chrome Print Settings (ZC10L)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Destination | Zebra ZC10L (CUPS name) |
|
||||
| Paper size | 3.5 × 5.5 in (or as set in CUPS) |
|
||||
| Margins | **None** |
|
||||
| Background graphics | On |
|
||||
| Pages | 1 (front only) |
|
||||
|
||||
#### CSS Layout
|
||||
|
||||
The ZC10L uses the `badge_3.5x5.5_pvc` layout. The PVC layout CSS is at:
|
||||
`src/routes/events/[event_id]/(badges)/badges/print/badge_layout_zebra_zc10l_pvc.css`
|
||||
|
||||
This layout hides `.badge_back` in `@media print` — only the front face prints.
|
||||
`@page { size: 3.5in 5.5in; margin: 0; }` is set in the CSS.
|
||||
|
||||
#### Known Behaviors / Watch-outs
|
||||
|
||||
- Chrome with **Default** margins: inserts URL/date headers, offsets badge — use **None**
|
||||
- Chrome with **None** or **Minimum** margins: correct output
|
||||
- Firefox: works correctly out of the box with this layout
|
||||
- Physical card alignment: if the badge appears offset on the card, a CSS margin tweak may
|
||||
be needed in the PVC layout file — note the offset and adjust `print_margin_cfg` once that
|
||||
field is wired to the UI
|
||||
- Font sizes: if name/affiliation text appears too small at physical scale, adjust via the
|
||||
font size controls (+ / −) in the print controls panel; note the preferred values for this
|
||||
event's template
|
||||
|
||||
#### Test Results (2026-03-19)
|
||||
|
||||
- Card feeds and prints without jam: ✅
|
||||
- Single-sided PVC confirmed (back does not print): ✅
|
||||
- Chrome margins None/Minimum: correct output ✅
|
||||
- Firefox: correct output ✅
|
||||
- QR code scannable from printed card: ✅
|
||||
- Print tracking (`print_count` increment): ✅
|
||||
- Font sizes / visual quality: tested; specific calibration pending client design direction
|
||||
- Driver version: *(record here)*
|
||||
- Physical offset needed: *(record if any)*
|
||||
|
||||
---
|
||||
|
||||
### Epson ColorWorks C3500 — Fan-Fold Label Printer
|
||||
|
||||
**Card stock:** 4" × 6" fan-fold paper label stock
|
||||
**Layout code:** `badge_4x6_fanfold`
|
||||
**Status:** Configured. First live use: Axonius Adapt DC — June 9, 2026.
|
||||
|
||||
The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
|
||||
not individual cards. Badges are separated along the perforation after printing.
|
||||
|
||||
#### Physical Setup
|
||||
|
||||
- Connect via USB or Ethernet
|
||||
- Load 4" × 6" fan-fold stock per Epson instructions
|
||||
- The C3500 is single-sided — only the front face prints. Back section is suppressed in CSS.
|
||||
- The badge has a lanyard hole punch: 5/8" × 1/8", centered, 1/4" from the top.
|
||||
Most fan-fold stock for badge use includes a pre-punched lanyard slot — verify stock matches.
|
||||
|
||||
#### Driver
|
||||
|
||||
- Epson ColorWorks C3500 CUPS driver available from epson.com (ColorWorks section)
|
||||
- On Linux/CUPS: install the provided PPD and add the printer at `http://localhost:631`
|
||||
- Set default paper size to **4" × 6"** in CUPS
|
||||
- Print a test page from CUPS before going live
|
||||
|
||||
#### Chrome Print Settings (C3500)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Destination | Epson C3500 (CUPS name) |
|
||||
| Paper size | 4 × 6 in (set in CUPS driver) |
|
||||
| Margins | **None** |
|
||||
| Background graphics | On |
|
||||
| Pages | 1 (single-sided) |
|
||||
|
||||
#### CSS Layout
|
||||
|
||||
The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
|
||||
`src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
|
||||
|
||||
Created 2026-05-15 for Axonius Adapt DC. Key specs:
|
||||
- `badge_front` 4" × 6", portrait orientation
|
||||
- `badge_header` max-height 1.5in
|
||||
- Lanyard hole: 5/8" × 1/8", centered, 1/4" from top
|
||||
- `@page { size: 4in 6in; margin: 0; }` set in the print page dynamically
|
||||
- `.badge_back` suppressed in `@media print` (single-sided)
|
||||
|
||||
#### Known Behaviors
|
||||
|
||||
- Same Chrome margin rules apply: **Margins → None** prevents URL/date header clipping
|
||||
- Firefox honors `@page { size: 4in 6in }` for PDF proofing — use it to verify layout
|
||||
- Fan-fold stock separates along the perforation — no cutting needed, but verify the
|
||||
perforation lands outside the badge content area
|
||||
|
||||
---
|
||||
|
||||
## Print Tracking
|
||||
|
||||
The badge print page tracks print counts per badge:
|
||||
|
||||
- `print_count` — increments on each **Print Badge** button click
|
||||
- `print_first_datetime` — timestamp of first print
|
||||
- An amber "Printed N×" chip appears in the print page header after the first print
|
||||
|
||||
The reprint shortcut (trusted access + edit mode) does **not** increment the count.
|
||||
Only the **Print Badge** button path increments the count. This is intentional — reprints
|
||||
for alignment or quality checks should not inflate the print count.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| White border around printed badge | Chrome Default margins | Change to None or Minimum |
|
||||
| URL / date printed at top or bottom | Chrome Default margins | Change to None |
|
||||
| Header image / stripe not printing | Background graphics disabled | Enable in print dialog |
|
||||
| Badge appears on wrong-size output | Paper size mismatch | Set correct size in CUPS and/or print dialog |
|
||||
| Card jams | Card stock misloaded | Re-seat cards per printer manual; check stock orientation |
|
||||
| Badge content clipped | Layout overflow | Check font size — use − control to reduce if needed |
|
||||
| Second blank card ejected | Duplex triggered on PVC | Confirm `.badge_back { display: none }` in print CSS for this layout |
|
||||
@@ -1,132 +0,0 @@
|
||||
# Guide — Aether Events: Onsite Runbook
|
||||
|
||||
This guide covers the human-centric logistics and "In the Heat of the Moment" support for onsite event operations.
|
||||
|
||||
---
|
||||
|
||||
## Badge Printing
|
||||
|
||||
Aether badge printing uses the browser's native `window.print()` — no special software or print
|
||||
server needed.
|
||||
|
||||
### Kiosk Station Setup
|
||||
- **Browser:** Use **Chrome (Chromium)** for all kiosk stations.
|
||||
- **Settings:** Set Margins to **None**. Enable **Background Graphics**.
|
||||
- **Mode:** Use normal browser sessions (not Incognito) to allow PWA caching.
|
||||
|
||||
### Printer Reference: Zebra ZC10L (PVC)
|
||||
- **Stock:** 3.5" × 5.5" PVC cards.
|
||||
- **Orientation:** Cards face-up, landscape in the hopper.
|
||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||
- **Layout code:** `badge_3.5x5.5_pvc`
|
||||
|
||||
### Printer Reference: Epson ColorWorks C3500 (Fan-Fold)
|
||||
- **Stock:** 4" × 6" fan-fold paper label stock.
|
||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||
- **Layout code:** `badge_4x6_fanfold`
|
||||
- **Lanyard hole:** Pre-punched 5/8" × 1/8" slot at top center — verify stock matches.
|
||||
- **First live use:** Axonius Adapt DC, June 9, 2026.
|
||||
|
||||
### Printing Workflow
|
||||
1. **Search:** Find the attendee by name or QR scan in the Badges module.
|
||||
2. **Review:** Open the print page and confirm the layout looks correct.
|
||||
3. **Print:** Click **Print Badge**. `print_count` increments automatically.
|
||||
4. **Handoff:** Verify the card print quality before handing it to the attendee.
|
||||
|
||||
---
|
||||
|
||||
## Exhibitor Leads (Lead Retrieval)
|
||||
|
||||
Exhibitors use a PWA (Progressive Web App) to scan badges and capture leads.
|
||||
|
||||
### Exhibitor Support Workflow
|
||||
1. **Booth Lookup:** Help the exhibitor find their booth in the Leads landing page.
|
||||
2. **Sign-In:** Assist with the **Shared Passcode** or individual **Licensed User** login.
|
||||
3. **App Install:** Encourage them to "Add to Home Screen" (iOS) or click the Install button (Android/Chrome) for offline stability.
|
||||
4. **Scanning Demo:** Show them the **Rapid Scan** mode. Remind them that attendees must have `allow_tracking = true` on their record to be scanned.
|
||||
|
||||
### Managing Licenses
|
||||
- License counts are managed in the **Manage** tab (Admin or Shared Passcode only).
|
||||
- If an exhibitor needs more staff slots, update the `license_max` in the Exhibit record.
|
||||
|
||||
---
|
||||
|
||||
## Speaker Ready Room (SRR)
|
||||
... (rest of the file) ...
|
||||
The SRR is the central hub for content management and presenter support.
|
||||
|
||||
### SRR Practice Stations
|
||||
Stations mirror the session room setup exactly:
|
||||
- Same Mac laptop model and adapter/dongle configuration as the podiums.
|
||||
- Projector and screen (where possible).
|
||||
- Launcher running in **Native** mode — ensures verification matches the podium experience.
|
||||
|
||||
### Staffing Roles
|
||||
|
||||
| Role | Access Level | Typical Tasks |
|
||||
|---|---|---|
|
||||
| **OSIT Staff** | `trusted_access` | Manage devices, monitor via VNC, deep troubleshooting. |
|
||||
| **Client Staff** | `authenticated_access` | Upload files, view session lists, assist presenters. |
|
||||
| **Presenter** | `authenticated_access` | Self-upload via QR link (if enabled). |
|
||||
|
||||
### SRR Workflow — Day-of-Show
|
||||
1. **Check-in:** Staff looks up the presenter's session in Presentation Management.
|
||||
2. **Upload:** File is uploaded to the presenter/session record.
|
||||
3. **Verification:** Staff opens the file on a practice station to confirm rendering.
|
||||
4. **Launcher Sync:** File propagates to the podium. Use **Force Sync Location** in the Launcher config if immediate full-room caching is needed.
|
||||
5. **Proceed:** Presenter walks to the room; the podium kiosk already has the file cached.
|
||||
|
||||
---
|
||||
|
||||
## Onsite Operation (Managing Parallel Rooms)
|
||||
|
||||
### SRR Overview Page
|
||||
The Pres Mgmt overview (`/events/[id]/pres_mgmt`) is the "Command Center":
|
||||
- Monitor file status per session.
|
||||
- Filter by location and time block to stay ahead of active sessions.
|
||||
|
||||
### Per-Room Monitoring
|
||||
- Use **VNC or RustDesk** to monitor all podium screens in real time from the SRR.
|
||||
- Confirm "Native Sync" status chip in the bottom-left of the Launcher is green/idle before sessions start.
|
||||
|
||||
### Session Transitions
|
||||
- **Timing:** Ideally, sessions show/hide based on `datetime_start`.
|
||||
- **Manual Control:** In looser schedules, use Launcher controls to manually select the current session.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Show Checklist
|
||||
|
||||
### 1–2 Weeks Before
|
||||
- [ ] Event created with correct dates and timezone.
|
||||
- [ ] `mod_pres_mgmt_json` configured for client needs.
|
||||
- [ ] Locations (rooms) created and named.
|
||||
- [ ] Sessions created, assigned to locations, and timed.
|
||||
- [ ] Launcher devices (`event_device`) registered with correct codes.
|
||||
- [ ] Device-to-location assignments confirmed.
|
||||
|
||||
### Day Before (SRR Setup)
|
||||
- [ ] Mac laptops at podiums booted; Electron app running.
|
||||
- [ ] Each podium confirms it loaded the correct room's Launcher.
|
||||
- [ ] SRR practice stations confirmed (matching hardware).
|
||||
- [ ] Run **Force Sync Location** on all podiums to pre-cache all day-1 content.
|
||||
- [ ] VNC/RustDesk connections established to all podiums.
|
||||
|
||||
### Day of Show
|
||||
- [ ] Confirm all session times are accurate before the first block.
|
||||
- [ ] Monitor SRR queue and verify every file on a practice station.
|
||||
- [ ] Check VNC wall to ensure all podiums are online and synced.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Session not in Launcher | Datetime wrong or Location unassigned. | Verify session metadata in Pres Mgmt. |
|
||||
| File uploaded but missing | Polling lag or attached at wrong level. | Wait 30s; check if file is at Session vs Presenter level. |
|
||||
| File opens slowly | Not in native cache yet. | Check "Native Sync" chip; use Force Sync in config. |
|
||||
| File won't open | Corrupt upload or missing Mac codec. | Test on SRR station; convert or re-upload. |
|
||||
| Drifted schedule | Room timing shifted. | Use Launcher controls to manually select the active session. |
|
||||
| `lock_config` resets changes | Remote config is forced. | Edit the master `mod_pres_mgmt_json` in Event Settings. |
|
||||
| Move laptop to new room | Hardware reassignment. | Update `location_id` in `event_device` record; restart Electron. |
|
||||
@@ -1,276 +0,0 @@
|
||||
# Aether UI — Design System Style Guidelines
|
||||
> **Version:** 1.2 (2026-03-20)
|
||||
> **Author:** One Sky IT / Scott Idem
|
||||
> **Scope:** All Aether SvelteKit frontend components
|
||||
> **Related:** `AE__UI_Component_Patterns.md`, `ae-firefly.css`, `documentation/AE__Components.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Philosophy
|
||||
|
||||
**"Shiny serenity, like a firefly."**
|
||||
|
||||
The Aether UI is calm, focused, and softly luminous. It must be immediately readable under conference-room lighting, at a glance, by presenters who are nervous and in a hurry. Staff in the Speaker Ready Room need scan-speed identity confirmation. Remote presenters uploading files from home need a clear, unambiguous interface that doesn't waste their time.
|
||||
|
||||
Core principles:
|
||||
- **Identity first.** The user's first question is always *"Am I in the right place?"* Answer it with the hero card — name, time, room — before anything else is shown.
|
||||
- **Progressive disclosure.** Admin fields (codes, IDs, passcodes) are hidden unless `edit_mode` is active.
|
||||
- **Theme-aware always.** Zero hardcoded colors. Every background, border, and text color must respond to light/dark mode and the active theme via CSS variables.
|
||||
- **Transitions, not pops.** Every interactive state change is smoothed with `transition-colors duration-200`.
|
||||
- **Section 508 / WCAG 2.1 AA** compliance is non-negotiable. Contrast ratios, focus indicators, ARIA labels, and screen-reader regions are required everywhere.
|
||||
|
||||
---
|
||||
|
||||
## 2. Technical Stack Mandates (2026 Standard)
|
||||
|
||||
To maintain codebase health and performance, all new development must adhere to the following stack:
|
||||
|
||||
### 🚀 Svelte 5 Runes
|
||||
- **Mandatory**: Use `$state`, `$derived`, and `$effect`.
|
||||
- **Snippet pattern**: Use `{@render snippet()}` for reusable UI blocks within components.
|
||||
- **Avoid**: Legacy `export let` (use `$props()`), `onMount` for reactive derived state (use `$derived` or `$effect`), and `$$slots` (use Snippets).
|
||||
|
||||
### 🎨 Tailwind 4 + Skeleton v4
|
||||
- **Mandatory**: Use `preset-*` classes for interactive elements (e.g., `preset-tonal-primary`).
|
||||
- **Forbidden**: Legacy Skeleton v3 `variant-*` classes.
|
||||
- **Customization**: Use Tailwind 4 `@theme` blocks for project-wide overrides.
|
||||
- **URLs**: Skeleton for Svelte for LLMs docs: https://www.skeleton.dev/llms-svelte.txt
|
||||
|
||||
### 🔣 Lucide Icons
|
||||
- **Mandatory**: Use `@lucide/svelte` components (e.g., `<Calendar size="1em" />`).
|
||||
- **Migration**: Replaced all FontAwesome `fas fa-*` icons in general modules.
|
||||
- **🚨 Exception: IDAA Module**: The IDAA module **must** retain FontAwesome and Bootstrap classes. It integrates with Novi CMS which relies on these legacy standards. **Do not migrate IDAA icons.**
|
||||
|
||||
---
|
||||
|
||||
## 3. The AE_Firefly Theme
|
||||
|
||||
**App default since 2026-03-06.** Set in `ae_stores.ts` as `theme_name = 'AE_Firefly'`.
|
||||
File: `src/ae-firefly.css` | Activated by: `data-theme="AE_Firefly"`
|
||||
|
||||
| Role | Palette Name | Hue | Use Case |
|
||||
|---|---|---|---|
|
||||
| **Primary** | Luminescent Teal | ~184° | Primary actions, date/time chips, focus rings, anchor links |
|
||||
| **Secondary** | Warm Amber-Gold | ~90° | Secondary actions, copy/email buttons, soft highlights |
|
||||
| **Tertiary** | Night-Sky Indigo | ~277° | Location/room chips, depth accents |
|
||||
| **Surface** | Moonlit Slate | ~215–233° | All backgrounds — off-white (light) → midnight slate (dark) |
|
||||
| **Warning** | Amber semantic | n/a | Disabled/inactive records, "no results" states, code badges |
|
||||
| **Success** | Green semantic | n/a | Active/complete states, file count badges |
|
||||
| **Error** | Red semantic | n/a | Errors, disabled-presenter states |
|
||||
|
||||
### Contrast Guarantees
|
||||
- Body text (`surface-950` on `surface-50`): > 15:1 in light mode ✓
|
||||
- Primary buttons: ≥ 3:1 for interactive component threshold ✓
|
||||
- Designed using OKLCH perceptual lightness — not HSL estimates
|
||||
|
||||
---
|
||||
|
||||
## 4. Color Token Rules
|
||||
|
||||
### ✅ Always Use Theme Tokens
|
||||
|
||||
**Backgrounds / surfaces:**
|
||||
```
|
||||
bg-surface-50-900 ← card faces (light: near-white, dark: deep slate)
|
||||
bg-surface-100-900 ← inner sections, code blocks, secondary panels
|
||||
bg-surface-50-950 ← page-level containers
|
||||
bg-surface-200-800 ← skeleton pulse placeholders, dividers
|
||||
```
|
||||
|
||||
**Borders:**
|
||||
```
|
||||
border-surface-200-800 ← standard card/panel borders
|
||||
border-primary-500 ← focus rings (via focus-visible:ring)
|
||||
border-warning-500 ← warning/caution interactive zones
|
||||
border-surface-500/20 ← subtle section dividers
|
||||
```
|
||||
|
||||
**Primary color accents (teal):**
|
||||
```
|
||||
bg-primary-500/10 ← tinted chip background (time chips)
|
||||
text-primary-700 dark:text-primary-300 ← chip text with auto dark mode
|
||||
hover:text-primary-500 ← link hover color
|
||||
```
|
||||
|
||||
**Tertiary color accents (indigo):**
|
||||
```
|
||||
bg-tertiary-500/10 ← tinted chip background (room/location chips)
|
||||
text-tertiary-700 dark:text-tertiary-300
|
||||
```
|
||||
|
||||
**Skeleton presets:**
|
||||
```
|
||||
preset-tonal-warning ← "no results", disabled rows, code tag badges
|
||||
preset-tonal-primary ← primary action buttons
|
||||
preset-tonal-surface ← neutral/secondary actions, status badges
|
||||
preset-tonal-success ← success states, file count badges
|
||||
preset-tonal-error ← error/blocked states
|
||||
preset-filled-*-500 ← solid-fill buttons (hover state for tonal)
|
||||
```
|
||||
|
||||
**Warning/error semantic backgrounds:**
|
||||
```
|
||||
bg-warning-100 border border-warning-300 ← inline warning banners
|
||||
bg-error-100 border border-error-300 ← inline error banners
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Never Use These
|
||||
|
||||
| Forbidden | Reason | Replace With |
|
||||
|---|---|---|
|
||||
| `bg-gray-*` | Fixed color, breaks dark mode | `bg-surface-*` tokens |
|
||||
| `bg-neutral-*` | Same — fixed hue | `bg-surface-*` tokens |
|
||||
| `bg-white` | Light-mode only | `bg-surface-50-900` |
|
||||
| `text-gray-*` | Breaks dark mode | `opacity-60` on inherited text |
|
||||
| `text-neutral-*` | Same | `opacity-*` |
|
||||
| `border-gray-*` | Non-theme border | `border-surface-200-800` |
|
||||
| `bg-yellow-*`, `text-yellow-*` | Bypasses warning semantic | `preset-tonal-warning` |
|
||||
| `bg-red-*`, `text-red-*` | Bypasses error semantic | `bg-error-100` / `preset-tonal-error` |
|
||||
| `bg-white dark:bg-gray-800` | Manual light/dark pair | Let theme tokens handle it — remove entirely |
|
||||
| `text-gray-600 dark:text-gray-400` | Manual light/dark pair | `opacity-60` |
|
||||
| `rounded-container-token` | Skeleton v3 class | `rounded-xl` |
|
||||
| `variant-*` (v3) | Skeleton v3 class | `preset-*` (v4) |
|
||||
| `preset-filled-surface-300-700` | v3 dual-shade notation | `bg-surface-200-800` or `bg-surface-100-900` |
|
||||
| `preset-filled-surface-400-600` | v3 dual-shade notation | `bg-surface-100-900` |
|
||||
| `overflow-x-scroll` | Forces scrollbar visible | `overflow-x-auto` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Transitions & Animation
|
||||
|
||||
All interactive state changes must be smoothed — no hard pops.
|
||||
|
||||
| Element | Classes |
|
||||
|---|---|
|
||||
| Table row | `transition-colors duration-200` on `<tr>` |
|
||||
| List item card | `transition-colors duration-200` on `<li>` |
|
||||
| Link hover | `transition-colors duration-200` on `<a>` |
|
||||
| Info chips | `transition-colors duration-200` on `<span>` |
|
||||
| QR code toggle (size) | `transition-all duration-500` on `<img>` |
|
||||
| Collapsible sections | Use Skeleton `Accordion` or CSS `transition-all` — don't add custom unless needed |
|
||||
| Buttons | Handled automatically by Skeleton preset classes |
|
||||
|
||||
---
|
||||
|
||||
## 6. Loading / Skeleton States
|
||||
|
||||
When `liveQuery` data is still resolving, show pulse placeholders instead of nothing:
|
||||
|
||||
```svelte
|
||||
<!-- Title placeholder -->
|
||||
<div class="h-7 w-2/3 bg-surface-200-800 animate-pulse rounded"></div>
|
||||
|
||||
<!-- Chip/pill placeholder -->
|
||||
<div class="h-5 w-1/2 bg-surface-200-800 animate-pulse rounded-full"></div>
|
||||
|
||||
<!-- Icon placeholder -->
|
||||
<div class="h-5 w-5 bg-surface-200-800 animate-pulse rounded-full"></div>
|
||||
```
|
||||
|
||||
Always wrap in `{#if $lq__obj}{...}{:else}...skeleton...{/if}` — **never** show real content structure before data exists.
|
||||
|
||||
---
|
||||
|
||||
## 7. Dark Mode Rules
|
||||
|
||||
- **Never write `dark:` overrides for background or text colors.** The Firefly theme handles both modes through CSS variables. Writing `dark:bg-gray-800` or `dark:text-gray-400` bypasses the theme and breaks if the user switches themes.
|
||||
- **Exception allowed:** `dark:text-primary-300` and `dark:text-tertiary-300` in info chips are intentional — they reference theme variables that gracefully degrade.
|
||||
- **Exception allowed:** `dark:border-surface-700` in fine-grained border work when `border-surface-200-800` isn't strong enough.
|
||||
|
||||
---
|
||||
|
||||
## 8. Accessibility (Section 508 / WCAG 2.1 AA)
|
||||
|
||||
| Requirement | Implementation |
|
||||
|---|---|
|
||||
| Decorative icons | `aria-hidden="true"` on all icons |
|
||||
| Icon-only buttons | `aria-label="..."` or `title="..."` + visible context |
|
||||
| Async content regions | `role="status" aria-live="polite"` on loading/empty sections |
|
||||
| Focus indicators | `focus-visible:ring-2 focus-visible:ring-primary-500` on custom interactive elements |
|
||||
| Interactive dialogs | `aria-haspopup="dialog"` on trigger buttons |
|
||||
| Form inputs | Visible `<label>` linked via `for` / `id`, or explicit `aria-label` |
|
||||
| Color-only information | Always pair color coding with icon or text — never color alone |
|
||||
| Minimum touch target | 44×44px effective hit area for all tap targets |
|
||||
| Button label + icon | All buttons should include **both a Lucide icon and text label**. Icon-only is acceptable for space-constrained toolbar/header actions (with `title` attribute); text-only is acceptable when layout is extremely tight. The icon+text combination aids non-English-native users who may not read the label fluently. |
|
||||
|
||||
---
|
||||
|
||||
## 9. Debug Code — Remove Before Committing
|
||||
|
||||
These patterns are breakpoint debuggers added during development. **Never commit them:**
|
||||
|
||||
```html
|
||||
<!-- Breakpoint border debugger — REMOVE before commit -->
|
||||
sm:border-l-red-400 md:border-l-yellow-400 lg:border-l-gray-100
|
||||
sm:dark:border-l-red-600 md:dark:border-l-yellow-600 lg:dark:border-l-gray-700
|
||||
```
|
||||
|
||||
Also flag on code review:
|
||||
- `console.log(...)` that isn't behind a `log_lvl` guard
|
||||
- `border-dashed border-y-transparent border-r-transparent` left on production components
|
||||
- `overflow-x-scroll` (should be `overflow-x-auto`)
|
||||
|
||||
---
|
||||
|
||||
## 10. QR Code Pattern — Critical Bug Prevention
|
||||
|
||||
The async QR generation code uses a boolean `true` as a loading placeholder:
|
||||
|
||||
```typescript
|
||||
$events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = true; // ← loading
|
||||
// ... async ...
|
||||
$events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = result; // ← URL string
|
||||
```
|
||||
|
||||
**Always gate display on `typeof ... === 'string'`**, not just truthy:
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Correct -->
|
||||
{#if typeof $events_sess.pres_mgmt.session_qr_url?.[$lq__obj.id] === 'string'}
|
||||
|
||||
<!-- ❌ Wrong — renders broken <img src="true"> during load -->
|
||||
{#if $events_sess.pres_mgmt.session_qr_url[$lq__obj.id]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Tailwind Utility Usage Notes
|
||||
|
||||
- **`opacity-*` for muted text**: Use `opacity-60` (secondary) or `opacity-40` (tertiary/hint) instead of `text-gray-*`. This works in any theme and both modes.
|
||||
- **`/` opacity modifier**: `bg-primary-500/10` is preferred over separate `opacity-10` — it targets only the background.
|
||||
- **`text-sm leading-relaxed`**: Standard for body-level descriptive text in cards.
|
||||
- **`tracking-wide uppercase`**: Use for section label/eyebrow text with `opacity-40`.
|
||||
- **`whitespace-pre-wrap`**: Required for any `<pre>` or `<p>` displaying user-entered multi-line text (preserves breaks without horizontal overflow).
|
||||
|
||||
---
|
||||
|
||||
## 12. Known Issues & Workarounds
|
||||
|
||||
### `btn` + `preset-filled-*` resolves to transparent inside `card` components
|
||||
|
||||
**Symptom:** A button using `btn preset-filled-primary` (or any `preset-filled-*`) inside a `card` div renders with `background-color: transparent`, making it invisible against the card surface.
|
||||
|
||||
**Root cause:** The Skeleton v4 `btn` class sets a transparent background via a CSS variable chain. When nested inside a `card` element, the `preset-filled-*` class fails to win the specificity battle and the button appears invisible. This affects both light and dark mode.
|
||||
|
||||
**Workaround:** Skip `btn` and `preset-filled-*` entirely for buttons inside `card` elements. Use direct Tailwind token classes instead:
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Correct — works reliably inside cards -->
|
||||
<button class="w-full rounded-xl py-5 font-bold flex items-center justify-center gap-2
|
||||
bg-primary-500 text-white hover:brightness-110 transition-all cursor-pointer">
|
||||
...
|
||||
</button>
|
||||
|
||||
<!-- Secondary / cancel button inside a card -->
|
||||
<button class="w-full rounded-lg py-3 text-sm font-medium flex items-center justify-center gap-2
|
||||
border border-surface-500/40 hover:bg-surface-200-800 transition-colors cursor-pointer opacity-70">
|
||||
...
|
||||
</button>
|
||||
|
||||
<!-- ❌ Broken inside card — do not use -->
|
||||
<button class="btn btn-xl preset-filled-primary">...</button>
|
||||
```
|
||||
|
||||
**Scope:** `btn` + `preset-*` classes work correctly on standalone buttons (e.g. page headers, nav bars). The issue is specific to the `card` component context. If we migrate away from Skeleton `card`/`btn`, this issue goes away.
|
||||
@@ -1,191 +0,0 @@
|
||||
# Aether Development SOP (Frontend)
|
||||
> **Version:** 1.2 (2026-03-17)
|
||||
> **Location:** documentation/GUIDE__Development.md
|
||||
|
||||
## 1. Verification (The "Test-First" Mandate)
|
||||
**Rule:** No code is to be committed unless it has passed local verification.
|
||||
|
||||
### Required Checks
|
||||
1. **Svelte Integrity:** `npx svelte-check`
|
||||
- **Zero Tolerance:** If a task introduces even a single svelte-check warning or error, it must not be merged. Resolve all warnings before committing.
|
||||
2. **Type Safety:** Ensure interfaces in `src/lib/types/ae_types.ts` match backend schemas.
|
||||
3. **Reactivity Check:** Verify Svelte 5 runes (`$state`, `$derived`) are not creating race conditions with Dexie `liveQuery`.
|
||||
4. **Build Check:** For major changes, run `npm run build:dev` to ensure no SSR or build-time failures.
|
||||
5. **Integration Tests:** For changes to badge print, event layouts, or auth/store logic, run the relevant Playwright test file(s):
|
||||
```bash
|
||||
npx playwright test tests/event_badge_render.test.ts tests/event_badge_attendee_workflow.test.ts
|
||||
```
|
||||
Run the full suite with `npm run test:integration`. The badge tests (`event_badge_*.test.ts`) are the canonical integration test template.
|
||||
|
||||
## 2. Commit Policy
|
||||
- **Atomic Commits:** One component or one logic fix per commit. Do not batch unrelated changes.
|
||||
- **Safety:** Use `~/tmp/agents_trash` for file removal; never use `rm` directly on source files.
|
||||
- **Secrets:** Never commit `.env`, API keys, or passwords.
|
||||
|
||||
## 3. Coordination (The Handshake)
|
||||
You are not working in a vacuum. Coordinate with the Backend Agent via MCP tools.
|
||||
|
||||
### Mandatory Messaging Triggers
|
||||
- **Data Requirements:** When a UI feature requires a new field or endpoint.
|
||||
- **API Failures:** When a V3 endpoint returns unexpected data or errors.
|
||||
- **Blocked:** If stuck in a loop or lacking information, use `ae_send_message` to ask the Backend Agent, or flag for Scott.
|
||||
|
||||
### Tools
|
||||
- `ae_send_message` / `ae_inbox` — agent-to-agent messaging
|
||||
- `ae_task_list` / `ae_task_add` / `ae_task_complete` — shared Kanban board
|
||||
- `ae_log_work` — log activity to daily journal
|
||||
|
||||
## 4. Continuity (Before Starting Work)
|
||||
1. Review `documentation/TODO__Agents.md` for active tasks.
|
||||
2. Check `~/agents_sync/README.md` for fleet status and cross-agent tasks.
|
||||
3. Describe your plan before making code changes across multiple files.
|
||||
|
||||
## 5. Key Documentation
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `documentation/TODO__Agents.md` | Active task list — read first |
|
||||
| `documentation/GUIDE__AE_API_V3_for_Frontend.md` | V3 API reference (authoritative) |
|
||||
| `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` | Dexie + liveQuery patterns |
|
||||
| `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns |
|
||||
| `documentation/AE__Architecture.md` | System architecture overview |
|
||||
| `documentation/AE__Naming_Conventions.md` | Naming rules |
|
||||
| `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` | Electron/Launcher reference |
|
||||
| `tests/README.md` | Playwright test guide — shared helpers, hard-won lessons, demo IDs |
|
||||
|
||||
## 6. Inline Field Editing — `element_ae_obj_field_editor`
|
||||
|
||||
The standard component for single-field inline editing throughout the platform. Wraps a `PATCH /v3/crud/{obj_type}/{obj_id}` call behind a click-to-edit UI that respects `$ae_loc.edit_mode`.
|
||||
|
||||
```svelte
|
||||
import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte';
|
||||
```
|
||||
|
||||
### Basic usage — text field with custom display
|
||||
|
||||
Wrap the display content in the default snippet. The component renders it in view mode and swaps in the input on edit.
|
||||
|
||||
```svelte
|
||||
<Element_ae_obj_field_editor
|
||||
object_type={'event_session'}
|
||||
object_id={session.id}
|
||||
field_name={'name'}
|
||||
field_type={'text'}
|
||||
current_value={session.name}
|
||||
on_success={() => events_func.load_ae_obj_id__event_session({ api_cfg: $ae_api, event_session_id: session.id })}
|
||||
>
|
||||
<h1 class="text-2xl font-bold">{session.name}</h1>
|
||||
</Element_ae_obj_field_editor>
|
||||
```
|
||||
|
||||
### Field types
|
||||
|
||||
| `field_type` | Input rendered |
|
||||
| --- | --- |
|
||||
| `text` (default) | `<input type="text">` — Enter key saves |
|
||||
| `textarea` | `<textarea>` — use `textarea_rows` prop |
|
||||
| `select` | `<select>` — pass `select_options={{ value: 'Label' }}` |
|
||||
| `checkbox` | `<input type="checkbox">` — shows Enabled/Disabled |
|
||||
| `tiptap` | TipTap rich-text editor |
|
||||
| `date` | `<input type="date">` |
|
||||
| `datetime` | `<input type="datetime-local">` |
|
||||
| `number` | `<input type="number">` — Enter key saves |
|
||||
|
||||
### Select with nullable FK
|
||||
|
||||
```svelte
|
||||
<Element_ae_obj_field_editor
|
||||
object_type={'event_presenter'}
|
||||
object_id={presenter.event_presenter_id}
|
||||
field_name={'person_id'}
|
||||
field_type={'select'}
|
||||
current_value={presenter.person_id}
|
||||
select_options={$slct.person_obj_kv}
|
||||
allow_null={$ae_loc.administrator_access}
|
||||
on_success={() => events_func.load_ae_obj_id__event_presenter({ api_cfg: $ae_api, event_presenter_id: presenter.event_presenter_id })}
|
||||
>
|
||||
{presenter.person_id ?? 'Not linked'}
|
||||
</Element_ae_obj_field_editor>
|
||||
```
|
||||
|
||||
### Key props
|
||||
|
||||
| Prop | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `current_value` | — | Required. Bound with `$bindable` — liveQuery updates flow through automatically |
|
||||
| `allow_null` | `false` | Shows a "Set Null" button in edit mode |
|
||||
| `display_block` | `false` | Makes the wrapper `display: block` instead of `inline-block` |
|
||||
| `on_success` | — | Callback after successful PATCH — use to trigger SWR cache refresh |
|
||||
| `object_reload` | `true` | Triggers internal SWR reload after patch (in addition to `on_success`) |
|
||||
|
||||
### Behavior notes
|
||||
- The edit trigger button is `visibility: hidden` (not `display: none`) when `$ae_loc.edit_mode` is off — this preserves layout so the page doesn't shift when edit mode toggles.
|
||||
- Optimistic display: draft value is shown immediately after save; cleared once liveQuery confirms the update came back from the DB.
|
||||
- `on_success` should always call the relevant `load_ae_obj_id__*` function to keep Dexie in sync.
|
||||
|
||||
---
|
||||
|
||||
## 7. URL Parameters
|
||||
|
||||
URL params consumed by the app. Params are read by layouts and applied on mount.
|
||||
|
||||
### Global (active on all routes — read by `src/routes/+layout.svelte`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `iframe` | `true` / `false` | Enables iframe mode — hides the AE system bar for all users by default, suppresses sign-in/passcode UI |
|
||||
| `show_menu` | `true` | Override: show the AE system bar inside an iframe. Intended for admins/trusted users who need menu access while testing an embed. |
|
||||
| `hide_menu` | `true` | Explicitly hide the AE system bar outside of iframe mode (e.g. fullscreen kiosk pages). |
|
||||
| `theme` | theme name | Applies a theme on load, then removes param from URL (no history entry) |
|
||||
| `theme_mode` | `light` / `dark` | Applies theme mode on load, then removes param from URL |
|
||||
|
||||
### IDAA Module (`/idaa/` routes)
|
||||
|
||||
| Param | Values | Consumed by | Effect |
|
||||
| --- | --- | --- | --- |
|
||||
| `iframe` | `true` | `idaa/+layout.svelte` | Hides IDAA nav chrome |
|
||||
| `uuid` | Novi UUID | `idaa/(idaa)/+layout.svelte` | Sets Novi UUID → triggers member auth lookup |
|
||||
|
||||
### IDAA Video Conferences (`/idaa/video_conferences`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `uuid` | Novi UUID | Member identity for Jitsi JWT |
|
||||
| `key` | site key string | Site auth key |
|
||||
| `room` | room name | Jitsi room name |
|
||||
| `moderator` | `true` | Grants moderator role + JWT, enables lobby and activity logging |
|
||||
| `domain` | hostname | Jitsi server (default: `jitsi.dgrzone.com`) |
|
||||
| `start_muted` | `true` | Start audio muted |
|
||||
| `start_hidden` | `true` | Start video off |
|
||||
| `incoming_msg_sound` | `true` | Disable incoming message sound |
|
||||
| `participant_joined_sound` | `true` | Disable participant joined sound |
|
||||
| `participant_left_sound` | `true` | Disable participant left sound |
|
||||
| `reaction_sound` | `true` | Disable reaction sound |
|
||||
| `raise_hand_sound` | `true` | Disable raise hand sound |
|
||||
|
||||
### Events Launcher (`/events/[id]/launcher`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `session_id` | session ID | Pre-selects a session on load |
|
||||
| `iframe` | `true` | Iframe mode flag |
|
||||
| `launcher_menu` | show/hide | Show/hide launcher menu chrome |
|
||||
| `launcher_header` | show/hide | Show/hide launcher header |
|
||||
| `launcher_footer` | show/hide | Show/hide launcher footer |
|
||||
|
||||
### Events Sign-In (`/events/[id]/sign_in_out`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `person_id` | person ID | Pre-fill attendee |
|
||||
| `person_pass` | passphrase | Auto-authenticate attendee |
|
||||
| `presentation_id` | ID | Pre-select presentation |
|
||||
| `presenter_id` | ID | Pre-select presenter |
|
||||
| `session_id` | ID | Pre-select session |
|
||||
|
||||
### Badges (`/events/[id]/badges/print_list`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `printed_status` | filter value | Filter badge list by print status |
|
||||
| `badge_type_code` | code string | Filter badge list by type |
|
||||
@@ -1,625 +0,0 @@
|
||||
# Stability Patterns for liveQuery + Svelte 5
|
||||
|
||||
Dexie's `liveQuery` works well with Svelte 5 runes, but the combination requires a few stable patterns so queries don't get recreated unintentionally and components render correctly on a "cold start" (empty IndexedDB).
|
||||
- Keep the observable instance stable: wrap `liveQuery` in a stable `$derived` so the observable isn't recreated on every render. Recreate the `liveQuery` only when explicit dependencies change (IDs, filters, or search keys).
|
||||
|
||||
```typescript
|
||||
// stable derived wrapper — only recreated when `id` changes
|
||||
let lq__obj = $derived(
|
||||
(() => {
|
||||
// capture the dependency(s) in a single stable closure
|
||||
const id = url_id;
|
||||
return liveQuery(async () => {
|
||||
if (!id) return null;
|
||||
console.log('[LQ] running for id=', id);
|
||||
return await db.table.get(id);
|
||||
});
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
- Use `$derived.by(() => ...)` where available in your runes shim to build queries from a computed set of inputs (IDs list, search params). This preserves a stable observable instance while still reacting to explicit dependency changes.
|
||||
|
||||
- Avoid capturing mutable objects or inline expressions in the `liveQuery` closure. If the closure captures a changing reference, the query may be recreated unexpectedly or miss the first write.
|
||||
## Common Gotchas and Fixes (Why things sometimes need multiple refreshes)
|
||||
|
||||
- Cold start (IDB empty) + non-blocking API writes: If you mount a component before data is written to IDB, `liveQuery` may run against an empty DB. The API write will populate IDB later, but sometimes a chain of dependent queries (e.g., presentations -> presenters) won't all rerun in the order you expect. The symptoms you described — session shows after one refresh, presenters only after a second — are consistent with either (a) queries recreated in the wrong order or (b) dependent store values being set only after some subscriptions are already created.
|
||||
|
||||
### Bootstrap Race: Account-scoped Loads Before `account_id` Is Set (2026-06)
|
||||
|
||||
Account-scoped `liveQuery` triggers can fire before `+layout.svelte`'s bootstrap Sync Effect
|
||||
has propagated the real `account_id`. Two failure modes:
|
||||
|
||||
1. **IDB empty:** fetch runs with `account_id = null`. The `localStorage` scavenge in
|
||||
`api_get_object.ts` reads the stale value from a previous session — possibly a different
|
||||
account — and caches that wrong record into IDB.
|
||||
2. **IDB has a stale record:** `liveQuery` returns a cached record from a different account as
|
||||
a valid hit, so the trigger condition (`!entry`) is never true and the correct record is
|
||||
never fetched.
|
||||
|
||||
**Rule:** Gate any trigger `$effect` that loads account-scoped data on `$slct.account_id`,
|
||||
not `$ae_loc.account_id`. `$slct` is a plain writable store (not persisted), initialized to
|
||||
`null` and set _only_ by the bootstrap Sync Effect. `$ae_loc` is a persisted store that
|
||||
hydrates from `localStorage` before effects run and may carry a stale `account_id`.
|
||||
|
||||
Also treat a non-null, non-matching `account_id` in an IDB record as a cache miss:
|
||||
|
||||
```typescript
|
||||
$effect(() => {
|
||||
const account_id = $slct.account_id; // null until bootstrap Sync Effect runs
|
||||
const api_ready = !!$ae_api?.base_url;
|
||||
const entry = $lq__obj as SomeType | null | undefined;
|
||||
|
||||
if (!browser || !account_id || !api_ready) return;
|
||||
|
||||
// null account_id on a record = global/shared fallback — still a valid hit.
|
||||
const entry_is_stale_account =
|
||||
entry !== undefined && entry !== null &&
|
||||
entry.account_id !== null &&
|
||||
entry.account_id !== account_id;
|
||||
|
||||
if (!entry || entry_is_stale_account) {
|
||||
trigger = 'load...';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See `BOOTSTRAP__AI_Agent_Quickstart.md` → Section 7, entry 14 for the full incident writeup.
|
||||
|
||||
### Critical Discovery (2026-02-26): The "try_cache: false" Bug
|
||||
|
||||
**Symptom:** Nested data (e.g., Session → Presentations → Presenters) requires multiple manual refreshes to display on cold-start, even when using blocking loads.
|
||||
|
||||
**Root Cause:** Two interconnected issues in nested data loaders:
|
||||
1. **Disabled caching in nested loads**: Parent loads were passing `try_cache: false` to child loads, meaning presentations and presenters were fetched from API but **never written to IndexedDB**.
|
||||
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery _before_ IndexedDB writes completed, causing race conditions.
|
||||
|
||||
**Example of the Bug:**
|
||||
```typescript
|
||||
// Session loader (BROKEN)
|
||||
await db_save_ae_obj_li__ae_obj({ table: 'session', obj_li: [session] });
|
||||
// Loads presentations but disables caching ❌
|
||||
return await load_presentations({ ..., try_cache: false });
|
||||
// Presentations fetch from API ✅
|
||||
// Presentations SKIP IndexedDB write ❌
|
||||
// Presenters SKIP IndexedDB write ❌
|
||||
// Component mounts, liveQuery finds only session ❌
|
||||
```
|
||||
|
||||
**The Fix:**
|
||||
```typescript
|
||||
// Session loader (FIXED)
|
||||
await db_save_ae_obj_li__ae_obj({ table: 'session', obj_li: [session] });
|
||||
await Promise.resolve(); // Yield to observers
|
||||
// Preserve parent's try_cache value ✅
|
||||
return await load_presentations({ ..., try_cache });
|
||||
// Presentations fetch AND write to IDB ✅
|
||||
await Promise.resolve(); // Yield to observers
|
||||
// Presenters fetch AND write to IDB ✅
|
||||
await Promise.resolve(); // Yield to observers
|
||||
// Component mounts, liveQuery finds all data ✅
|
||||
```
|
||||
|
||||
**Key Lessons:**
|
||||
1. **Always preserve `try_cache` through nested loads** unless you have a specific reason to disable caching for that operation
|
||||
2. **Add `await Promise.resolve()` after IndexedDB writes** to ensure Dexie's liveQuery observers fire before the function returns
|
||||
3. **Block on nested loads with `await Promise.all()`** instead of fire-and-forget `forEach()` when the page needs complete data for first render
|
||||
|
||||
Fixes:
|
||||
- Prefer the "Blocking Loader" when you can: `await` the API call in `+page.ts` so IDB is populated before Svelte mounts.
|
||||
- If you cannot block, return an `initial_*` object from `+page.ts` and use it as an immediate fallback in your component so the UI renders from that payload while `liveQuery` takes over for subsequent updates. Example from Aether:
|
||||
|
||||
```svelte
|
||||
<Comp_event_presentation_obj_li
|
||||
lq__event_presentation_obj_li={$lq__event_presentation_obj_li ?? data.initial_session_obj?.event_presentation_li ?? []}
|
||||
{log_lvl}
|
||||
/>
|
||||
```
|
||||
|
||||
- Ensure store IDs are set before subscribers that depend on them are created. Use `untrack()` (or an equivalent non-reactive assignment) to set IDs in stores during initialization so components subscribe to the correct IDs immediately:
|
||||
|
||||
```typescript
|
||||
$effect(() => {
|
||||
if (!ae_acct) return;
|
||||
untrack(() => {
|
||||
$events_slct.event_id = url_event_id;
|
||||
$events_slct.event_session_id = url_session_id;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- When you have chains (presentations depend on session; presenters depend on presentation.person_id), make the dependent liveQuery explicitly wait for the upstream ID and log inside each query to verify the order — adding a small `await Promise.resolve()` or `await 0` inside the `liveQuery` is sometimes useful during debugging to ensure the JS microtask queue has a chance to settle after DB writes.
|
||||
|
||||
## IDB Sort: `build_tmp_sort` Pattern (2026-05)
|
||||
|
||||
All Aether objects support `priority`, `sort`, `group`, and `name` fields. Rather than sorting in JS after a Dexie query (which requires `.reverse()` hacks and duplicated logic), pre-compute up to three `tmp_sort_*` string fields during the processing pipeline and store them in Dexie. Then `.sortBy('tmp_sort_2')` does the right thing in one call, with no `.reverse()`.
|
||||
|
||||
**Utility:** `src/lib/ae_core/core__idb_sort.ts` — `build_tmp_sort()`
|
||||
|
||||
```typescript
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
|
||||
// Inside specific_processor callback:
|
||||
const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? '0'], // always first
|
||||
priority: obj.priority, // boolean; true→'0' so ASC sorts it first
|
||||
sort: obj.sort, // zero-padded to 8 chars
|
||||
fields_1: [...], // module-specific tier-1 fields
|
||||
fields_2: [...], // tier-2 fields (tmp_sort_2 = base + tier-1 + tier-2)
|
||||
fields_3: [...] // tier-3 fields
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
obj.tmp_sort_3 = tmp_sort_3;
|
||||
```
|
||||
|
||||
**Sort chain convention:** `group → priority DESC → sort ASC → [module-specific] → name`
|
||||
|
||||
**Priority encoding:** `priority ? '0' : '1'` — inverted so that `priority=true` sorts first in ascending order. This means:
|
||||
- **Dexie `.sortBy('tmp_sort_*')`** — always call without `.reverse()` before it (Dexie ignores collection-level `.reverse()` when using `.sortBy()`). If descending is needed for non-tmp_sort fields, call `.reverse()` on the resulting array after `await`.
|
||||
- **JS `.sort()` comparators** — use **ascending** `a.localeCompare(b)`, NOT `b.localeCompare(a)`. Using descending flips the priority encoding and puts `priority=false` items first.
|
||||
|
||||
```ts
|
||||
// ✅ Correct — ascending; priority=true ('0') sorts before priority=false ('1')
|
||||
list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
|
||||
|
||||
// ❌ Wrong — descending inverts the encoding; priority=false ('1') sorts first
|
||||
list.sort((a, b) => (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? ''));
|
||||
```
|
||||
|
||||
**Modules using `build_tmp_sort`:**
|
||||
- `ae_events__event_presentation.ts` — `tmp_sort_1/2`: group → priority → sort → start_datetime → code → name
|
||||
- `ae_events__event.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on (used by IDAA recovery meetings)
|
||||
- `ae_journals__journal.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on
|
||||
- `ae_journals__journal_entry.ts` — same chain as journal
|
||||
|
||||
**Legacy encoding (not yet migrated to `build_tmp_sort`):** `ae_posts__post.ts`, `ae_posts__post_comment.ts`, `ae_archives__archive.ts`, `ae_archives__archive_content.ts`, `ae_sponsorships_functions.ts` use the opposite encoding (`priority ? '1' : '0'`, designed for descending sort). Their current route consumers sort by date/name so there is no visible priority bug today, but they must be migrated before any route starts sorting by `tmp_sort_*`. See `TODO__Agents.md`.
|
||||
|
||||
---
|
||||
|
||||
## `$derived.by` Dependency Capture for Extra Filter State
|
||||
|
||||
When a `liveQuery` has a SCENARIO 2 fallback (broad search with no IDs), it may run before the debounced search fast path populates `event_session_id_li`. If that fallback doesn't apply the same visibility filter as the fast path, hidden items will briefly appear then disappear ("blink").
|
||||
|
||||
**Fix:** capture the filter flag as a `$derived.by` dependency in the outer closure so Svelte recreates the liveQuery instance whenever it changes — SCENARIO 2 then uses the correct filter from first render.
|
||||
|
||||
```typescript
|
||||
let lq__event_session_obj_li = $derived.by(() => {
|
||||
const ids = event_session_id_li; // drives SCENARIO 1 vs 2
|
||||
const event_id = $events_slct?.event_id;
|
||||
const qry_hidden = pres_mgmt_loc.current.qry_hidden; // extra dependency
|
||||
|
||||
return liveQuery(async () => {
|
||||
// SCENARIO 1 — specific IDs (fast path or API result)
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
const results = await db.session.bulkGet(ids);
|
||||
return results.filter(Boolean);
|
||||
}
|
||||
// SCENARIO 2 — broad fallback, uses captured qry_hidden
|
||||
if (event_id && !someFilter) {
|
||||
const all = await db.session.where('event_id').equals(event_id).sortBy('name');
|
||||
return all.filter((s: any) => {
|
||||
if (qry_hidden === 'not_hidden') return !s.hide;
|
||||
if (qry_hidden === 'hidden') return !!s.hide;
|
||||
return true; // 'all'
|
||||
});
|
||||
}
|
||||
return [];
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key rule:** anything read inside `$derived.by()`'s outer closure (but outside the `liveQuery` callback) becomes a Svelte reactive dependency. Changes to it recreate the liveQuery. Use this to synchronize filter flags that Dexie doesn't track.
|
||||
|
||||
**Also fix the API call:** use the snapshot value from `params` (captured at debounce time) rather than the live store, so rapid toggling doesn't create a mismatch between fast path and API results:
|
||||
|
||||
```typescript
|
||||
// Bad — uses live store value, can race if user toggles during pending call:
|
||||
hidden: pres_mgmt_loc.current.qry_hidden ?? 'not_hidden'
|
||||
|
||||
// Good — uses snapshot captured when handle_search_refresh was called:
|
||||
hidden: params.qry_hidden ?? 'not_hidden'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings)
|
||||
- Journals: The journaling pages use SWR-style background refreshes but reliably render because either (a) the page `+page.ts` blocks to populate DB for critical views, or (b) components accept `data.initial_*` fallback values until `liveQuery` emits. This hybrid approach avoids the "refresh twice" problem while keeping navigation snappy.
|
||||
- Journals broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows.
|
||||
|
||||
- Sessions / Presentations: The session page demonstrates several best practices:
|
||||
- Use `url_*` constants (derived from `data.params`) so the `liveQuery` closure captures a stable value instead of the reactive store directly.
|
||||
- Provide `initial_session_obj` from `+page.ts` as a first-draw fallback to child components.
|
||||
- Use `$derived.by(() => liveQuery(...))` for presentation lists so the observable instance is stable across renders and recreated only when `event_session_id` or `search` changes.
|
||||
|
||||
- Search pages with persisted filters or saved query text should keep the auto-search trigger in a page-level `$effect`, but the duplicate guard should live inside the actual search executor. That preserves the first page-load search while blocking repeated identical reruns from localStorage-backed rerenders. In practice:
|
||||
- derive a single `qry_key` from the search inputs
|
||||
- debounce in the `$effect`
|
||||
- compare `qry_key` against a `last_executed_key` inside `handle_search_refresh()`
|
||||
- keep transient loading flags and trigger counters in session state when the value is only used to force a refresh, not as a persisted preference
|
||||
|
||||
Example (presentation list pattern):
|
||||
```typescript
|
||||
let lq__event_presentation_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!url_session_id) return [];
|
||||
console.log('[LQ] Querying Presentations for Session:', url_session_id);
|
||||
return await db_events.presentation.where('event_session_id').equals(url_session_id).sortBy('name');
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
- Add a small `console.log` inside each `liveQuery` closure to confirm when it runs and what `id` it sees.
|
||||
- Verify that `+page.ts` either `await`s critical loads or returns `initial_*` payloads for first-render hydration.
|
||||
- Confirm that dependent store values (selected IDs) are assigned before components subscribe — use `untrack` to prevent extra reactive cycles.
|
||||
- If a search page stops auto-loading after a localStorage change, check whether the duplicate guard was placed in the `$effect` instead of the executor. Guarding too early can suppress the initial search; guard at execution time instead.
|
||||
- If a broad Dexie-backed list shows fewer rows than a narrower filter, look for a limit or revalidation step overwriting the local IDB result set. Broad views should stay unbounded unless the user is actually narrowing by text.
|
||||
- Ensure your `liveQuery` closures return quickly and do not throw; any exception inside the query can stop updates.
|
||||
- If a dependent query appears stale, temporarily add `await 0` in the upstream query or an explicit `Promise.resolve()` after the IDB write to force the microtask queue to flush during debugging.
|
||||
|
||||
## Summary Recommendations
|
||||
|
||||
- Prefer blocking loads for primary views when first-render correctness matters.
|
||||
- Use `initial_*` fallback data when non-blocking loads are required.
|
||||
- Wrap `liveQuery` in stable `$derived` instances and only recreate when explicit inputs change.
|
||||
- Use `untrack` to set selection IDs during initialization to avoid subscribe-order bugs.
|
||||
- Add targeted logs inside `liveQuery` closures to diagnose ordering and subscription behavior.
|
||||
|
||||
These patterns are deliberately conservative — they trade minimal blocking or small explicit fallbacks for predictable first-render behaviour. The Aether app's Journals and Event session pages are working examples of these techniques in practice.
|
||||
|
||||
## Examples in this repository
|
||||
|
||||
The following files demonstrate stable `liveQuery` usage, `initial_*` fallbacks, and stable `$derived` wrappers used across the Aether app. Inspect these for copy/paste patterns and concrete implementations.
|
||||
|
||||
- Journals page (stable LQ + search patterns): [src/routes/journals/[journal_id]/+page.svelte](src/routes/journals/[journal_id]/+page.svelte#L51)
|
||||
- Journals layout (blocking background loader): [src/routes/journals/[journal_id]/+layout.ts](src/routes/journals/[journal_id]/+layout.ts#L1)
|
||||
- Session page with URL capture + initial fallback: [src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte#L41)
|
||||
- Presentation management overview (stable derived + search): [src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte#L70)
|
||||
- Event settings example (simple observable): [src/routes/events/[event_id]/settings/+page.svelte](src/routes/events/[event_id]/settings/+page.svelte#L51)
|
||||
- Badge/detail pages (examples of nested LQ): [src/routes/events/[event_id]/(badges)/badges/+page.svelte](src/routes/events/[event_id]/(badges)/badges/+page.svelte#L66)
|
||||
|
||||
Refer to these files when you need concrete code examples to adopt the patterns described above.
|
||||
|
||||
## References
|
||||
|
||||
This document provides a guide to integrating Svelte (with a focus on Runes) and Dexie.js for building reactive web applications. It covers key concepts and best practices for managing reactivity between Svelte components and the Dexie.js database.
|
||||
|
||||
## Svelte 5 Migration Guide
|
||||
|
||||
Svelte 5 introduces "runes" as a new way to manage reactivity. This is a major change from previous versions of Svelte, and it's important to understand the breaking changes before migrating.
|
||||
|
||||
### Key Breaking Changes
|
||||
|
||||
- **`let` is no longer reactive:** In Svelte 4, any `let` variable declared in the top-level scope of a component was automatically reactive. In Svelte 5, you must explicitly declare reactive state using the `$state` rune.
|
||||
- **`$:` is replaced by `$derived` and `$effect`:** The `$` label is no longer used for reactive statements. Instead, you should use the `$derived` rune for computed values and the `$effect` rune for side effects.
|
||||
- **`export let` is replaced by `$props`:** Component props are now declared using the `$props` rune, which provides a more flexible and explicit way to define component APIs.
|
||||
- **Event handling:** The `on:` directive is replaced by event attributes (e.g., `onclick`). Component events are now handled using callback props instead of `createEventDispatcher`.
|
||||
- **Slots are replaced by snippets:** The `<slot>` element is replaced by the `{#snippet ...}` block, which provides a more powerful and flexible way to pass content to components.
|
||||
|
||||
For a complete list of breaking changes, refer to the [Svelte 5 migration guide](https://svelte.dev/docs/svelte/v5-migration-guide).
|
||||
|
||||
## Dexie.js Quick Reference
|
||||
|
||||
Dexie.js is a lightweight, minimalistic wrapper for IndexedDB that makes it easier to work with client-side databases.
|
||||
|
||||
### Key Classes and Methods
|
||||
|
||||
- **`Dexie`:** The main class for creating and managing IndexedDB databases.
|
||||
- `new Dexie(databaseName)`: Creates a new database instance.
|
||||
- `version(versionNumber).stores({ ... })`: Defines the database schema.
|
||||
- **`Table`:** Represents an object store (table) in the database.
|
||||
- `add(item)`: Adds a new item to the table.
|
||||
- `put(item)`: Adds or updates an item in the table.
|
||||
- `update(key, changes)`: Updates an existing item.
|
||||
- `delete(key)`: Deletes an item by its primary key.
|
||||
- `get(key)`: Retrieves an item by its primary key.
|
||||
- `where(index)`: Starts a query using an index.
|
||||
- `toArray()`: Retrieves all items from the table as an array.
|
||||
- **`Collection`:** Represents a collection of items resulting from a query.
|
||||
- `toArray()`: Retrieves all items in the collection as an array.
|
||||
- `first()`: Retrieves the first item in the collection.
|
||||
- `last()`: Retrieves the last item in the collection.
|
||||
- `each(callback)`: Iterates over each item in the collection.
|
||||
- `modify(changes)`: Updates all items in the collection.
|
||||
- `delete()`: Deletes all items in the collection.
|
||||
|
||||
For a complete list of API methods, refer to the [Dexie.js API Reference](https://dexie.org/docs/API-Reference).
|
||||
|
||||
## Integrating Svelte Runes and Dexie.js
|
||||
|
||||
The combination of Svelte Runes and Dexie.js allows for the creation of highly reactive and efficient web applications.
|
||||
|
||||
### The `liveQuery` Function
|
||||
|
||||
Dexie.js provides a `liveQuery` function that returns an observable of the query result. This observable can be used to automatically update the UI whenever the data in the database changes.
|
||||
|
||||
### Using `liveQuery` with Svelte Runes
|
||||
|
||||
To use `liveQuery` with Svelte Runes, you can create a custom readable store that wraps the `liveQuery` observable. This store can then be used in your Svelte components to display and interact with the data.
|
||||
|
||||
**1. Create a `liveQuery` store:**
|
||||
|
||||
```typescript
|
||||
import { liveQuery } from 'dexie';
|
||||
import { readable } from 'svelte/store';
|
||||
import { db } from './db'; // Your Dexie database instance
|
||||
|
||||
export function createLiveQueryStore<T>(query: () => T | Promise<T>) {
|
||||
return readable<T | undefined>(undefined, (set) => {
|
||||
const subscription = liveQuery(query).subscribe({
|
||||
next: (result) => set(result),
|
||||
error: (error) => console.error(error)
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**2. Use the `createLiveQueryStore` in your component:**
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { createLiveQueryStore } from './stores';
|
||||
import { db } from './db';
|
||||
|
||||
const friends = createLiveQueryStore(() => db.friends.toArray());
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#if $friends} {#each $friends as friend}
|
||||
<li>{friend.name}</li>
|
||||
{/each} {/if}
|
||||
</ul>
|
||||
```
|
||||
|
||||
The `createLiveQueryStore` function creates a readable store that automatically updates whenever the data in the `friends` table changes. The `$friends` variable in the component will always contain the latest data from the database.
|
||||
|
||||
## SvelteKit Layout Hierarchy: Security and Execution Order
|
||||
|
||||
Understanding _when_ SvelteKit code runs is critical for private-data modules like IDAA.
|
||||
|
||||
### Execution order on any navigation
|
||||
|
||||
```text
|
||||
1. +layout.ts / +page.ts ← run FIRST — before any component mounts
|
||||
also fired by SvelteKit link prefetch (on hover)
|
||||
2. Parent +layout.svelte mounts → its $effect blocks run
|
||||
3. Child +layout.svelte mounts → only if parent called {#render children?.()}
|
||||
4. +page.svelte mounts → only if every parent in the chain rendered children
|
||||
5. $effect blocks in all of the above run after mount
|
||||
```
|
||||
|
||||
### The auth-gate consequence
|
||||
|
||||
A `{:else if authenticated} {@render children?.()}` block in a `+layout.svelte`
|
||||
controls whether **everything below it** ever mounts. If the gate blocks rendering,
|
||||
no child layout or page component instantiates — their `$effect` blocks, event
|
||||
handlers, and liveQuery closures never run.
|
||||
|
||||
```svelte
|
||||
<!-- (idaa)/+layout.svelte -->
|
||||
{:else if $ae_loc.trusted_access || $idaa_loc.novi_verified}
|
||||
{@render children?.()} ← children only mount if this branch runs
|
||||
{:else}
|
||||
<p>Access Denied</p> ← children never mount; their $effects never run
|
||||
{/if}
|
||||
```
|
||||
|
||||
**`$effect` blocks inside a child component cannot bypass a parent layout auth gate.**
|
||||
They are already inside the gate. Adding redundant auth guards to `$effect` blocks
|
||||
that only run after a parent has already verified access is unnecessary — and misleads
|
||||
future readers into thinking the parent gate alone is not sufficient.
|
||||
|
||||
### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts`
|
||||
|
||||
Universal load functions run _before_ components mount and _before_ layout effects
|
||||
execute. They also fire during SvelteKit link prefetch — triggered by the user
|
||||
hovering a link, even if they never navigate. This makes them unsafe for private data:
|
||||
|
||||
```text
|
||||
User hovers an /idaa/ link →
|
||||
SvelteKit prefetch fires →
|
||||
+page.ts runs (no layout has mounted yet, no auth gate has run) →
|
||||
API call / IDB write happens for an unauthenticated user
|
||||
```
|
||||
|
||||
**Rule for private modules (IDAA, Journals):** `+page.ts` and `+layout.ts` files must
|
||||
not call any data load functions that write to IDB. Move all data loading to `$effect`
|
||||
blocks in the corresponding `+page.svelte`, gated inside the auth-checked layout render.
|
||||
The comments in every `+page.ts` under `src/routes/idaa/(idaa)/` explain this pattern.
|
||||
|
||||
### The `$effect` auth guards in IDAA `+page.svelte` files
|
||||
|
||||
These ARE still useful — but for a different reason than layout bypass:
|
||||
|
||||
```ts
|
||||
// In bb/+page.svelte
|
||||
$effect(() => {
|
||||
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
||||
posts_func.load_ae_obj_li__post(...)
|
||||
});
|
||||
```
|
||||
|
||||
Because `$ae_loc` is a Svelte 4 coarse-grained store, any unrelated write to it
|
||||
(iframe height, SWR reload) re-triggers this `$effect`. The guard prevents a spurious
|
||||
API call if `$idaa_loc.novi_verified` has been cleared between re-runs (e.g. TTL
|
||||
expiry mid-session). It is a reactivity guard, not a layout-bypass guard.
|
||||
|
||||
---
|
||||
|
||||
## Page Load Strategies (Avoiding the "Waterfall")
|
||||
|
||||
When loading data for a primary page view (e.g., viewing a specific Journal, Session, or Person), you must choose a synchronization strategy to ensure the UI renders correctly on the first load.
|
||||
|
||||
### ❌ The "Fire & Forget" Anti-Pattern (AVOID)
|
||||
Triggering a background load in `+page.ts` without `await` leads to race conditions.
|
||||
1. `+page.svelte` mounts immediately.
|
||||
2. `liveQuery` runs against an empty IndexedDB.
|
||||
3. API data arrives later and writes to IndexedDB.
|
||||
4. **Failure:** Svelte 5 + Dexie `liveQuery` may not automatically detect this first "cold start" update without a manual refresh.
|
||||
|
||||
### ✅ The "Blocking Loader" Pattern (RECOMMENDED)
|
||||
Ensure the data is in IndexedDB **before** the component mounts.
|
||||
1. In `+page.ts`, `await` the API load function.
|
||||
2. In `+page.svelte`, the `liveQuery` will see the data immediately upon mount.
|
||||
|
||||
**Example (+page.ts):**
|
||||
```typescript
|
||||
export async function load({ params }) {
|
||||
// Blocking await ensures IDB is populated
|
||||
await journals_func.load_ae_obj_id__journal({
|
||||
journal_id: params.journal_id,
|
||||
try_cache: true
|
||||
});
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ The "Hydrate & Subscribe" Pattern (ADVANCED)
|
||||
If you must use non-blocking loads, you must pass the initial data to the component to "hydrate" the state before the subscription takes over.
|
||||
|
||||
1. In `+page.ts`, `await` the load and **return the object**.
|
||||
2. In `+page.svelte`, use the returned object as a fallback or initial state.
|
||||
|
||||
**Example (+page.svelte):**
|
||||
```svelte
|
||||
<script>
|
||||
let { data } = $props();
|
||||
let lq__obj = $derived(liveQuery(async () => db.table.get(id)));
|
||||
</script>
|
||||
|
||||
<!-- Use fallback to handle the gap before liveQuery emits -->
|
||||
{#if $lq__obj || data.initial_obj}
|
||||
<View object={$lq__obj ?? data.initial_obj} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
## The `untrack()` Reactive-Tracking Trap
|
||||
|
||||
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you _need_ to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
|
||||
|
||||
### Symptom
|
||||
|
||||
An effect runs once, reads a store value inside `untrack()`, takes an early-exit path (e.g. "no API key → skip"), and never retries — even after the store value is updated by a background process.
|
||||
|
||||
### Real Example (IDAA Novi Verification Bug — 2026-03-25)
|
||||
|
||||
The IDAA layout verifies Novi UUIDs. `site_cfg_json` (which contains the Novi API key) was read **inside** `untrack()`:
|
||||
|
||||
```typescript
|
||||
// BUG: site_cfg_json read inside untrack → one-shot, never retries
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const uuid = data.url.searchParams.get('uuid'); // tracked ✓
|
||||
|
||||
untrack(() => {
|
||||
const site_cfg_json = $ae_loc.site_cfg_json; // ← NOT tracked ✗
|
||||
const api_key = site_cfg_json?.novi_idaa_api_key ?? null;
|
||||
if (!api_key) return; // exits silently on first load with stale cache
|
||||
verify_novi_uuid(uuid, api_key, ...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
On first load, the Dexie cache returned a stale `site_cfg_json` missing the API key. The effect exited early. The background refresh later updated `$ae_loc.site_cfg_json`, but because `site_cfg_json` was consumed inside `untrack()`, the effect never re-ran.
|
||||
|
||||
**Fix:** Move the dependency read **outside** `untrack()`:
|
||||
|
||||
```typescript
|
||||
// FIX: site_cfg_json tracked outside untrack → effect re-runs when it changes
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const uuid = data.url.searchParams.get('uuid'); // tracked ✓
|
||||
const site_cfg_json = $ae_loc.site_cfg_json; // tracked ✓ — effect re-runs on change
|
||||
|
||||
untrack(() => {
|
||||
// Guard: already verified for this UUID — don't repeat the round-trip
|
||||
if ($idaa_loc.novi_verified && $idaa_loc.novi_uuid === uuid) return;
|
||||
|
||||
const api_key = site_cfg_json?.novi_idaa_api_key ?? null;
|
||||
if (!api_key) return;
|
||||
verify_novi_uuid(uuid, api_key, ...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The guard inside `untrack()` is important: without it, every unrelated change to `$ae_loc` would re-trigger verification.
|
||||
|
||||
### Rule of Thumb
|
||||
|
||||
Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re-run if this value changes?"**
|
||||
|
||||
- If yes → read it **outside** `untrack()`, and add a guard inside to prevent redundant work.
|
||||
- If no → `untrack()` is correct.
|
||||
|
||||
---
|
||||
|
||||
## Svelte 5 Binding Pitfalls
|
||||
|
||||
### 1. `props_invalid_value` (The "Expression Binding" Error)
|
||||
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
|
||||
|
||||
**❌ Invalid Pattern (Causes Compile Error):**
|
||||
Attempting to normalize a value _inside_ the binding will fail.
|
||||
```svelte
|
||||
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
||||
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
||||
```
|
||||
|
||||
**✅ Correct Pattern:**
|
||||
Ensure the source value is already normalized before binding, or use a reactive effect to handle the fallback.
|
||||
```typescript
|
||||
// Normalize in an effect or derivation
|
||||
$effect(() => {
|
||||
if ($events_slct.event_session_id === undefined) {
|
||||
$events_slct.event_session_id = null;
|
||||
}
|
||||
});
|
||||
```
|
||||
```svelte
|
||||
<!-- Bind directly to the normalized property -->
|
||||
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Safe Data Processing for IndexedDB Sorting
|
||||
|
||||
When preparing data for IndexedDB, especially when creating composite sort keys, it is critical to handle `null` or `undefined` values safely to prevent runtime crashes that can interrupt the data synchronization process.
|
||||
|
||||
### 1. Safe String Padding
|
||||
Attempting to call `.toString()` or `.padStart()` on a `null` or `undefined` value will throw a `TypeError`. This is a common pitfall when processing optional fields like `sort` or `group`.
|
||||
|
||||
**Bad Pattern (Crash Risk):**
|
||||
```typescript
|
||||
// Crashes if obj.sort is null or undefined
|
||||
obj.tmp_sort_1 = `${obj.sort.toString().padStart(3, '0')}`;
|
||||
obj.tmp_sort_2 = `${obj.sort?.toString().padStart(3, '0') ?? ''}`; // Still risky if chaining is misunderstood
|
||||
```
|
||||
|
||||
**Good Pattern (Safe):**
|
||||
```typescript
|
||||
// Safely handle null/undefined by defaulting to 0 or an empty string BEFORE string manipulation
|
||||
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
|
||||
```
|
||||
|
||||
### 2. Correct Sorting with Dexie
|
||||
Dexie's `sortBy()` method returns a new array sorted by the specified key. It **ignores** previous `reverse()` calls on the collection. To achieve a descending sort, you must sort first and then reverse the resulting array.
|
||||
|
||||
**Incorrect (Ascending Sort Result):**
|
||||
```typescript
|
||||
// .reverse() is ignored by .sortBy()
|
||||
let results = await db.table.where('id').equals(id).reverse().sortBy('sort_key');
|
||||
```
|
||||
|
||||
**Correct (Descending Sort Result):**
|
||||
```typescript
|
||||
// Sort ascending first, then reverse the array
|
||||
let results = await db.table.where('id').equals(id).sortBy('sort_key');
|
||||
return results.reverse();
|
||||
```
|
||||
|
||||
## References
|
||||
* https://dexie.org/llms.txt - Dexie.js and Dexie Cloud — LLM Guide and Documentation Summary
|
||||
@@ -1,61 +0,0 @@
|
||||
# Aether Journals: Configuration & Settings Map
|
||||
|
||||
This document tracks all available settings across the three levels of the Journals module.
|
||||
|
||||
## 1. Module Level (Global)
|
||||
* **Scope:** Applied across the entire journals application for the current site/user.
|
||||
* **Storage:** Browser Local Storage (via `journals_loc` persisted store).
|
||||
* **UI Location:** Journals Landing Page -> Settings Icon (Top Right).
|
||||
|
||||
| Setting | Type | Description | Save Type |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `datetime_format` | enum | Preferred display for date/time strings. | Manual (Save Changes) |
|
||||
| `time_format` | enum | Preferred display for time-only strings. | Manual (Save Changes) |
|
||||
| `entry.auto_save` | boolean | If true, entry edits are debounced and saved to DB. | Manual (Save Changes) |
|
||||
| `show_id_random` | boolean | Display technical UUIDs in metadata footers. | Manual (Save Changes) |
|
||||
| `enable_session_passcode_cache`| boolean | If true, private passcodes are held in session store. | Manual (Save Changes) |
|
||||
|
||||
## 2. Journal Level (Specific Journal)
|
||||
* **Scope:** Applied to a specific journal object and its metadata.
|
||||
* **Storage:** Remote MariaDB (via `update_ae_obj__journal`).
|
||||
* **UI Location:** Journal View -> Menu -> Edit Journal.
|
||||
|
||||
| Setting | Type | Description | Save Type |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | string | Display name of the journal. | Manual (Save Changes) |
|
||||
| `description` | markdown | Detailed purpose/notes for the journal. | Manual (Save Changes) |
|
||||
| `type_code` | enum | Category of journal (Diary, Log, etc.). | Manual (Save Changes) |
|
||||
| `group` | string | Sorting group for the journal list. | Manual (Save Changes) |
|
||||
| `sort` | integer | Manual sort order weight. | Manual (Save Changes) |
|
||||
| `priority` | boolean | Flag for "Starred" or "Pinned" status. | Manual (Save Changes) |
|
||||
| `enable` | boolean | Global activation flag for the journal. | Manual (Save Changes) |
|
||||
| `hide` | boolean | If true, journal is hidden from standard views. | Manual (Save Changes) |
|
||||
| `passcode` | string | Module-level encryption passcode. | Manual (Save Changes) |
|
||||
| `private_passcode`| string | User-specific secondary encryption secret. | Manual (Save Changes) |
|
||||
| `cfg_json.*` | JSON | UI/UX overrides (colors, default viewers, etc.). | Manual (Save Changes) |
|
||||
|
||||
## 3. Journal Entry Level (Specific Entry)
|
||||
* **Scope:** Applied to an individual entry within a journal.
|
||||
* **Storage:** Remote MariaDB (via `update_ae_obj__journal_entry`).
|
||||
* **UI Location:** Entry View -> Settings Button.
|
||||
|
||||
| Setting | Type | Description | Save Type |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | string | Entry title. | Auto-Save (if enabled) |
|
||||
| `content` | markdown | Main text body. | Auto-Save (if enabled) |
|
||||
| `category_code` | enum | User-defined category for the entry. | Auto-Save / Manual |
|
||||
| `tags` | string | Comma-separated list of tags. | Auto-Save / Manual |
|
||||
| `priority` | boolean | If true, entry is pinned or highlighted. | Manual (Done) |
|
||||
| `enable` | boolean | Activation flag for the entry. | Manual (Done) |
|
||||
| `hide` | boolean | If true, entry is hidden from standard lists. | Manual (Done) |
|
||||
| `sort` | integer | Manual sort order weight. | Manual (Done) |
|
||||
| `archive_on` | datetime | Scheduled date for automatic archiving. | Manual (Done) |
|
||||
| `private` | boolean | Trigger for E2EE (Encryption). | Manual (Done) |
|
||||
| `alert` | boolean | Trigger for visual "Alert" state. | Manual (Done) |
|
||||
| `group` | string | Grouping key for the list view. | Manual (JSON only) |
|
||||
|
||||
## 📐 Data Normalization Rules
|
||||
To prevent infinite reactivity loops and trivial save cycles, the following normalizations are applied before comparison:
|
||||
1. **Strings:** Trimmed and `null` treated as `""`.
|
||||
2. **Booleans:** Forced to `true/false` (no nulls).
|
||||
3. **JSON:** Deep stringification comparison (`JSON.stringify`).
|
||||
@@ -1,486 +0,0 @@
|
||||
# MODULE: Aether Events — Badge Templates
|
||||
|
||||
**Module Path:** `src/routes/events/[event_id]/(badges)/templates/`
|
||||
**API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts`
|
||||
**Database Table:** `event_badge_template`
|
||||
**Last Updated:** 2026-03-02
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Badge templates define the visual and structural configuration for printing event badges.
|
||||
Each template applies to one category of badge (e.g., general attendees, workshops,
|
||||
exhibitors). An event typically has 1–3 templates.
|
||||
|
||||
**Key principle:** One template per badge stock type/audience. Do not use flags on a
|
||||
single template to drive multiple layouts — create a separate template instead.
|
||||
|
||||
**Common template sets:**
|
||||
- **General Attendees** — main conference badge (most attendees)
|
||||
- **Workshops / Pre-conference** — alternate header, possibly different badge type list
|
||||
- **Exhibitors** — distinct footer stripe colors, exhibitor-specific badge types
|
||||
|
||||
Each template uses the same physical badge stock and printer configuration.
|
||||
|
||||
---
|
||||
|
||||
## DB Field Reference
|
||||
|
||||
### Core Identity
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | int | Internal PK |
|
||||
| `id_random` | str | External-facing ID (AE Triple ID pattern) |
|
||||
| `event_id` | str | Parent event |
|
||||
| `name` | str | Template display name |
|
||||
| `description` | str | Optional description |
|
||||
|
||||
### Image Assets
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `logo_path` | str (URL) | Org logo — fallback when no header image |
|
||||
| `logo_filename` | str | **Deprecated** — redundant with logo_path; do not use |
|
||||
| `header_path` | str (URL) | Front-of-badge header image — primary branding |
|
||||
| `secondary_header_path` | str (URL) | Back-of-badge header image (falls back to header_path) |
|
||||
| `footer_path` | str (URL) | Optional footer image — rarely used |
|
||||
| `header_row_1` | str/HTML | Text fallback line 1 when no header image |
|
||||
| `header_row_2` | str/HTML | Text fallback line 2 |
|
||||
| `footer_title`, `footer_left`, `footer_right` | str | Legacy Flask-era fields — not used |
|
||||
| `header_background`, `footer_background` | str | Legacy — not used; do not add to new templates |
|
||||
|
||||
### Network / WiFi
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `wireless_ssid` | str | WiFi network name — displayed on badge back |
|
||||
| `wireless_password` | str | WiFi password — displayed on badge back |
|
||||
|
||||
### QR Code Behavior
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `show_qr_front` | bool (0/1) | Show attendee QR code on front of badge |
|
||||
| `show_qr_back` | bool (0/1) | Show attendee QR code (+ ID text) on back of badge |
|
||||
|
||||
### Badge Type List
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `badge_type_list` | JSON string | List of `{code, name}` objects for this template |
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
[
|
||||
{"code": "current_member", "name": "Member"},
|
||||
{"code": "guest", "name": "Guest"},
|
||||
{"code": "staff", "name": "Staff"},
|
||||
{"code": "test", "name": "Test"}
|
||||
]
|
||||
```
|
||||
|
||||
The badge type footer stripe color is driven by CSS rules targeting the `code` value
|
||||
as a class on the footer element. Each event/template defines its own list — there is
|
||||
no global default. The component derives this list from the template at render time.
|
||||
|
||||
### Ticket Definitions
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `ticket_list` | JSON string | List of `{num, code, name}` for this template's tickets |
|
||||
| `ticket_1_text` – `ticket_8_text` | HTML | Ticket block HTML printed on badge back |
|
||||
|
||||
**ticket_list format:**
|
||||
```json
|
||||
[
|
||||
{"num": 1, "code": "foundation_reception", "name": "Foundation Reception"},
|
||||
{"num": 2, "code": "volunteer_reception", "name": "Volunteer Reception"}
|
||||
]
|
||||
```
|
||||
|
||||
The `ticket_N_code` field on the badge object references a ticket by its `code`. The
|
||||
corresponding `ticket_N_text` on the template provides the HTML rendered on the badge.
|
||||
|
||||
### Print Layout / Styling
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `layout` | str | Layout code — see Layout Codes below |
|
||||
| `style_filename` | str | CSS filename for locally-served stylesheets |
|
||||
| `style_href` | str (URL) | **Preferred** — external URL for custom CSS |
|
||||
| `script_src` | str (URL) | **Do not use** — Flask-era arbitrary script injection |
|
||||
|
||||
### Access Control
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `passcode` | str | Shared passcode for template management access |
|
||||
| `enable` | bool | Standard AE enable flag |
|
||||
| `hide` | bool | Standard AE hide flag |
|
||||
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
|
||||
| `notes` | str | Internal notes |
|
||||
|
||||
### Duplex / Single-Sided
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `duplex` | bool | When `false`, back section is hidden from print (`@media print`) |
|
||||
|
||||
The `duplex` field controls whether the back-of-badge section renders during printing.
|
||||
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
|
||||
prints. The back section still displays on screen for configuration reference.
|
||||
|
||||
`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
|
||||
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
|
||||
|
||||
Axonius events use `duplex = false` — single-sided printing only.
|
||||
|
||||
---
|
||||
|
||||
## External CSS Approach
|
||||
|
||||
### Why External
|
||||
|
||||
Badge templates may need visual adjustments mid-event (e.g., a color correction, a
|
||||
footer fix) without deploying a new SvelteKit build. Hosting the CSS at an external URL
|
||||
allows changes to take effect on next page load without any deployment.
|
||||
|
||||
### How It Works
|
||||
|
||||
The `style_href` field contains a full URL to a CSS file hosted on the static server
|
||||
(e.g., `https://static.oneskyit.com/c/ISHLT/css/badges_custom_ishlt.css`).
|
||||
|
||||
The print page (`print/+page.svelte`) or the badge view should conditionally add a
|
||||
`<link>` element via `<svelte:head>` when `style_href` is populated:
|
||||
|
||||
```svelte
|
||||
<svelte:head>
|
||||
{#if $lq__event_badge_template_obj?.style_href}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href={$lq__event_badge_template_obj.style_href}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
```
|
||||
|
||||
This is implemented — `style_href` loads via `<svelte:head>` in `print/+page.svelte` and is included in `properties_to_save`. (Verified 2026-03-18)
|
||||
|
||||
### CSS Scope
|
||||
|
||||
External badge CSS should scope all rules under `.badge_front`, `.badge_back`, etc.
|
||||
to avoid bleeding into the rest of the app. The classes used in
|
||||
`ae_comp__badge_obj_view.svelte` are the canonical hook points:
|
||||
|
||||
- `.badge_front` — entire front card
|
||||
- `.badge_back` — entire back card
|
||||
- `.badge_header` — front header area
|
||||
- `.badge_body` — front content area (name, title, affiliations, location)
|
||||
- `.badge_footer` — front footer stripe
|
||||
- `.badge_back_header` — back header area
|
||||
- `.badge_back_content` — back content area
|
||||
- `.badge_footer_center.<code>` — footer text per badge type code (for color stripes)
|
||||
|
||||
### layout field
|
||||
|
||||
The `layout` field encodes physical badge stock dimensions. Standard codes to use:
|
||||
|
||||
| Code | Dimensions | CSS file | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `badge_4x5_fanfold` | 4" × 5" (101.6 × 127mm) | `badge_layout_epson_4x5_fanfold.css` | Epson ColorWorks C3500 / ExpoBadge fanfold — preferred for general conference use (ISHLT, demos) |
|
||||
| `badge_3.5x5.5_pvc` | 3.5" × 5.5" (88.9 × 139.7mm) | `badge_layout_zebra_zc10l_pvc.css` | PVC card, Zebra ZC10L — single-sided, set `duplex=0` |
|
||||
| `badge_4x6_fanfold` | 4" × 6" (101.6 × 152.4mm) | `badge_layout_epson_4x6_fanfold.css` | Single-sided fanfold; Axonius Adapt 2026 (June 2026). Lanyard hole: 5/8in × 1/8in, centered, 1/4in from top. |
|
||||
| `badge_4x6_fanfold_tickets` | 4" × 6" + tear-offs | *(pending)* | Fanfold with ticket stubs |
|
||||
|
||||
Layout CSS files live in `src/lib/ae_events/badges/css/` and are imported by
|
||||
`ae_comp__badge_obj_view.svelte`. Rules are scoped under `[data-layout="..."]` on the
|
||||
wrapper so multiple layouts can coexist in the bundle without conflict.
|
||||
|
||||
`@page` paper size rules are injected per-layout from `print/+page.svelte <svelte:head>`
|
||||
(attribute selectors cannot scope `@page` rules, so they're handled dynamically).
|
||||
|
||||
---
|
||||
|
||||
## cfg_json Reference
|
||||
|
||||
All keys are optional. Unknown keys are preserved on save (forward-compatible). Managed via the template form's **Advanced** and **Header & Branding** sections, or directly in phpMyAdmin.
|
||||
|
||||
### Visibility
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `hide_badge_header` | bool | `false` | Hides the entire header section (image + logo/text fallback). Auto-true when `background_image_path` is set, unless explicitly overridden. |
|
||||
| `hide_badge_footer` | bool | `false` | Hides the badge type footer stripe. |
|
||||
| `hide_title` | bool | `false` | Suppresses the professional title field on the badge front. |
|
||||
| `hide_affiliations` | bool | `false` | Suppresses the affiliations field. |
|
||||
| `hide_location` | bool | `false` | Suppresses the location field. |
|
||||
|
||||
### QR Codes
|
||||
|
||||
These keys override the top-level DB fields (`show_qr_front`, `show_qr_back`) when present. Prefer setting them here rather than the top-level fields.
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `show_qr_front` | bool | `false` | Show attendee QR on badge front. |
|
||||
| `show_qr_back` | bool | `true` | Show attendee QR (+ ID text) on badge back. |
|
||||
|
||||
### Text Alignment
|
||||
|
||||
Stored under a nested `align` object.
|
||||
|
||||
```json
|
||||
"align": { "name": "left", "title": "left", "affiliations": "left", "location": "center" }
|
||||
```
|
||||
|
||||
| Key | Values | Default |
|
||||
| --- | --- | --- |
|
||||
| `align.name` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
| `align.title` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
| `align.affiliations` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
| `align.location` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
|
||||
QR alignment stored under `qr_alignment`:
|
||||
|
||||
| Key | Values | Default |
|
||||
| --- | --- | --- |
|
||||
| `qr_alignment.front` | `left` \| `center` \| `right` | `center` |
|
||||
| `qr_alignment.back` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
|
||||
### Header Image
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `header_margin_top` | CSS length | `2rem` | Vertical offset of the header image. Negative = shift up. e.g. `"-0.25in"`, `"1rem"`. |
|
||||
| `header_border_color` | hex color | none | Bottom border drawn below the header div. **Empty = no border.** e.g. `"#FE6111"`. |
|
||||
| `header_border_width` | CSS length | `2px` | Thickness of the header bottom border. Only applied when `header_border_color` is set. |
|
||||
| `header_padding_bottom` | CSS length | none | Space between the header image and the bottom border line. e.g. `"1.45in"`. |
|
||||
|
||||
### Appearance
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `body_text_color` | hex color | `#000000` | Inline color applied to all badge body text. |
|
||||
| `bleed` | CSS length | none | Extends background image past card edges on all sides. Prevents white borders on printers that clip slightly inside the card. e.g. `"0.125in"`, `"3mm"`. |
|
||||
|
||||
### Text Zone Heights (`fit_heights`)
|
||||
|
||||
Per-layout height overrides for the auto-scaling text zones. Set any subset — unset keys fall back to the layout default. Useful when `background_image_path` is set and the designed zones don't align with code defaults.
|
||||
|
||||
```json
|
||||
"fit_heights": { "grp_name_title": "1.8in", "name": "1.4in" }
|
||||
```
|
||||
|
||||
| Key | Notes |
|
||||
|---|---|
|
||||
| `grp_name_title` | Height of the name+title container |
|
||||
| `grp_name_title_flex` | Flex distribution: `around` \| `between` \| `even` \| `center` \| `start` \| `end` |
|
||||
| `name` | Height of the name text zone |
|
||||
| `title` | Height of the title text zone |
|
||||
| `grp_aff_loc` | Height of the affiliations+location container |
|
||||
| `grp_aff_loc_flex` | Flex distribution (same values as above) |
|
||||
| `affiliations` | Height of the affiliations text zone |
|
||||
| `location` | Height of the location text zone |
|
||||
|
||||
### Punch-Out Hole Markers (`punch_holes`)
|
||||
|
||||
Enables X overlays at the physical badge clip slot positions. Slots are pre-perforated on the badge stock — the markers print on the badge so attendees know where to push them out.
|
||||
|
||||
**Slot dimensions:** 5/8″ wide × 1/8″ tall, 1/4″ from top edge, 3/8″ from left/right edges. Center slot is horizontally centered.
|
||||
|
||||
```json
|
||||
"punch_holes": { "left": true, "right": true, "center": false }
|
||||
```
|
||||
|
||||
| Key | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `punch_holes.left` | `false` | Left clip slot marker |
|
||||
| `punch_holes.right` | `false` | Right clip slot marker |
|
||||
| `punch_holes.center` | `false` | Center clip slot marker (less common) |
|
||||
|
||||
---
|
||||
|
||||
### Controls Panel (`controls_cfg`)
|
||||
|
||||
Controls which fields appear in the print controls panel for non-trusted users, and which fields authenticated users may edit. Trusted + Edit Mode always sees and can edit all fields regardless of this config.
|
||||
|
||||
```json
|
||||
"controls_cfg": {
|
||||
"shown": ["name", "title", "affiliations"],
|
||||
"auth_editable": ["title", "affiliations", "location"]
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Type | Default |
|
||||
| --- | --- | --- |
|
||||
| `controls_cfg.shown` | `string[]` | `["name", "title", "affiliations", "location"]` |
|
||||
| `controls_cfg.auth_editable` | `string[]` | `["title", "affiliations", "location", "allow_tracking", "pronouns"]` |
|
||||
|
||||
Valid field keys: `name`, `title`, `affiliations`, `location`, `pronouns`, `allow_tracking`.
|
||||
|
||||
---
|
||||
|
||||
## Template-Derived Features (component behavior)
|
||||
|
||||
### badge_type_list → badge type select
|
||||
The badge type dropdown shown when editing a badge is derived from the template's
|
||||
`badge_type_list` JSON, not a hardcoded list. This was a bug (fixed 2026-03-02).
|
||||
See `ae_comp__badge_obj_view.svelte` — `badge_type_code_li` is now `$derived.by()`.
|
||||
|
||||
### "Info section" flags (exhibitor_info, presenter_info, etc.)
|
||||
These flags (`exhibitor_info`, `presenter_info`, `staff_info`, `vip_info`, `vote_info`)
|
||||
**do not exist as DB columns**. They appeared as placeholder `{#if}` blocks in the
|
||||
badge view component from Flask-era development and were never implemented.
|
||||
|
||||
The correct approach is **one template per badge audience** — an Exhibitor template will
|
||||
have exhibitor-specific `badge_type_list`, header images, and CSS. No flags needed.
|
||||
|
||||
The dead `{#if $lq__event_badge_template_obj.exhibitor_info}` blocks in
|
||||
`ae_comp__badge_obj_view.svelte` should be removed in a future cleanup pass.
|
||||
|
||||
---
|
||||
|
||||
## Properties Saved to IDB (Dexie)
|
||||
|
||||
The `properties_to_save` array in `ae_events__event_badge_template.ts` controls what
|
||||
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
|
||||
in DB and may be needed:
|
||||
|
||||
- `passcode` — not needed client-side
|
||||
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
||||
- `header_background`, `footer_background` — not needed (legacy)
|
||||
- `script_src` — do not add; this field should not be used
|
||||
- `duplex` — **add when backend adds the field**
|
||||
|
||||
---
|
||||
|
||||
## Standard Template Setup (per event)
|
||||
|
||||
### 1. General Attendees template
|
||||
- `header_path`: event-specific conference header image
|
||||
- `secondary_header_path`: back-of-badge header (often same or related image)
|
||||
- `wireless_ssid` + `wireless_password`: venue WiFi
|
||||
- `show_qr_back`: `1` (back QR is standard for most events)
|
||||
- `show_qr_front`: `0` (usually off for front)
|
||||
- `badge_type_list`: full list of member/guest/staff/test types
|
||||
- `ticket_list` + `ticket_N_text`: event-specific tickets if applicable
|
||||
- `style_href`: client-specific CSS URL
|
||||
- `layout`: appropriate layout code
|
||||
- `duplex`: `1` (or `0` for single-sided events like Axonius 2026)
|
||||
|
||||
### 2. Workshop / Pre-conference template
|
||||
- Same as above but with workshop-specific header images
|
||||
- `badge_type_list`: reduced list (workshop-relevant types only)
|
||||
- `ticket_list`: may be empty `[]`
|
||||
- `duplex`: match main template
|
||||
|
||||
### 3. Exhibitor template
|
||||
- Exhibitor-specific header images
|
||||
- `badge_type_list`: exhibitor-only types (`ex_all`, `ex_booth`, `guest`, `staff`, `test`)
|
||||
- `ticket_list`: `[]` (exhibitors typically don't have event tickets)
|
||||
- `show_qr_back`: may be `0` (exhibitors scan others, they don't need their own QR prominent)
|
||||
- `wireless_ssid` + `wireless_password`: same venue WiFi
|
||||
|
||||
---
|
||||
|
||||
## Print Layout Architecture
|
||||
|
||||
### How the print CSS works
|
||||
|
||||
The print page (`print/+page.svelte`) injects `<style>` blocks into `<svelte:head>` that
|
||||
take effect only in `@media print`. Multiple layers of the SvelteKit layout chain must
|
||||
be neutered to get a clean print surface.
|
||||
|
||||
**`#ae_main_content` — cannot dissolve, must passthrough:**
|
||||
`#ae_main_content` has `overflow: auto` (it is the events layout scroll container). CSS
|
||||
spec prohibits `display: contents` from overriding elements with overflow clipping —
|
||||
Firefox enforces this strictly, Chrome is lenient. Workaround: strip all its visual/layout
|
||||
effects with explicit `display: block; overflow: visible; position: static; width: 100%`.
|
||||
|
||||
**Wrappers dissolved via `display: contents` (safe — no overflow constraints):**
|
||||
| Selector | Source | Why dissolved |
|
||||
|---|---|---|
|
||||
| `.main_content` | `events/+layout.svelte` | `pb-48`, `pt-20+`, `grow` |
|
||||
| `#badge_render_area` | `print/+page.svelte` | Screen-only right-padding offset for controls panel |
|
||||
|
||||
**App chrome hidden via `print:hidden`:**
|
||||
- `nav.submenu` (events layout nav bar)
|
||||
- `footer.footer` (events layout footer)
|
||||
- Scroll-to-top / scroll-to-bottom button div
|
||||
- Kiosk header (`<header>` in print page)
|
||||
- Controls panel (`<div>` fixed right in print page)
|
||||
- Debug info section (edit mode only)
|
||||
- Root layout: offline banner, session expired banner, hydration overlay, sys/debug menus
|
||||
|
||||
**Badge centering — `position: fixed`:**
|
||||
`.event_badge_wrapper` uses `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%)`.
|
||||
In print, `position: fixed` anchors relative to the `@page` content area, bypassing the entire
|
||||
ancestor hierarchy (no containing-block height dependency, no overflow-clip interference).
|
||||
|
||||
**Future per-template margins:** `print_margin_cfg` is already parsed from `cfg_json`
|
||||
in `print/+page.svelte`. A dynamic `@page { margin: ... }` injection can be built from
|
||||
that value when a UI for it exists.
|
||||
|
||||
---
|
||||
|
||||
### Cross-browser print behavior — IMPORTANT
|
||||
|
||||
Verified 2026-03-12 by comparing print-to-PDF output from both browsers across multiple
|
||||
print dialog settings.
|
||||
|
||||
#### `@page { size }` — paper size
|
||||
|
||||
| Browser | Save to PDF | Physical printer |
|
||||
|---|---|---|
|
||||
| **Firefox** | Paper size locked — cannot change in dialog; CSS `@page { size }` used ✅ | Can select paper size in dialog |
|
||||
| **Chrome/Chromium** | Paper size locked — cannot change in dialog; uses system default (letter, A4, etc.) ❌ | Can select paper size under "More settings" |
|
||||
|
||||
Chrome intentionally does not honor `@page { size }` for Save as PDF. It uses the system
|
||||
default paper size. This is a Chrome design decision, not a bug in our code.
|
||||
|
||||
For actual printing to Epson/Zebra hardware: the printer driver controls paper size from
|
||||
the loaded badge stock. CSS `@page { size }` is advisory only. Real badge printing is
|
||||
unaffected by Chrome's behavior.
|
||||
|
||||
Use Firefox for accurate print-to-PDF proofing — it produces a correctly-sized PDF that
|
||||
matches the badge stock dimensions exactly.
|
||||
|
||||
#### Margins — Chrome "Default" causes layout problems
|
||||
|
||||
| Chrome margin setting | Result |
|
||||
|---|---|
|
||||
| **Default** | ❌ Adds URL, date, and page-number headers/footers into the printable area. These eat into the space that `position: fixed; top: 50%` references, making the badge appear off-center or clipped against the footer. |
|
||||
| **None** | ✅ Correct — badge centered cleanly |
|
||||
| **Minimum** | ✅ Correct — small margins, badge still centered |
|
||||
| **Custom (reasonable values)** | ✅ Correct |
|
||||
|
||||
The badge content itself is **not** distorted. Verified: Chrome "None" margins on an A4
|
||||
page produces the badge perfectly horizontally centered (page center 297.5 pts, badge
|
||||
content center 297.5 pts). The CSS centering logic is correct.
|
||||
|
||||
**Staff guidance for Chrome:**
|
||||
- Set **Margins → None** (or Minimum) in Chrome's print dialog.
|
||||
- Optionally set paper size to match badge stock under "More settings" when printing to PDF.
|
||||
- For physical printer: select correct paper size under "More settings".
|
||||
|
||||
Firefox users can use "Save to PDF" directly — it just works.
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `ae_events__event_badge_template.ts` | API + IDB functions; `properties_to_save` |
|
||||
| `db_events.ts` | Dexie schema for `badge_template` table |
|
||||
| `templates/+page.svelte` | Template list + create/edit/delete UI |
|
||||
| `templates/ae_comp__badge_template_form.svelte` | Template create/edit form |
|
||||
| `[badge_id]/ae_comp__badge_obj_view.svelte` | Badge render — consumes template data |
|
||||
| `[badge_id]/print/+page.svelte` | Print page — loads template, hosts `<svelte:head>` CSS |
|
||||
| `documentation/MODULE__AE_Events_Badges.md` | Badge object reference |
|
||||
|
||||
---
|
||||
|
||||
## Pending / TODO
|
||||
|
||||
- [x] Wire `style_href` via `<svelte:head>` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified)
|
||||
- [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified)
|
||||
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
|
||||
- [x] `badge_4x6_fanfold` layout CSS created (`badge_layout_epson_4x6_fanfold.css`), imported in badge component, `@page 4in 6in` wired in print page. (2026-05-15)
|
||||
- [x] Template form expanded — `layout`, `style_href`, `badge_type_list`, `duplex`, and all `cfg_json` keys now editable via the form. (2026-06-04)
|
||||
- [x] `cfg_json.header_margin_top`, `header_border_color`, `header_border_width`, `header_padding_bottom` added — header image position and bottom border are fully configurable without a code deploy. (2026-06-04)
|
||||
- [ ] Wire `badge_type_list` from the template into the badge search filter — currently the search form uses a hardcoded list. See `ae_comp__badge_search.svelte` TODO comment.
|
||||
- [ ] `badge_4x5_fanfold` layout CSS exists but is stale (not used in 2+ years) — review against actual hardware before next use.
|
||||
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1)
|
||||
@@ -1,130 +0,0 @@
|
||||
# Aether Events — Badges
|
||||
|
||||
The Badges module manages event attendee records and their physical badge configurations. It supports multi-source imports, field protection for onsite edits, and multi-tier access control for self-service review.
|
||||
|
||||
---
|
||||
|
||||
## Data Model & Hierarchy
|
||||
|
||||
### Core Objects
|
||||
- **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
|
||||
- **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
|
||||
|
||||
### Relationships
|
||||
- **Badge → Event:** Many-to-one.
|
||||
- **Badge → Template:** Many-to-one (via `event_badge_template_id`).
|
||||
- **Badge → Person:** Optional link to core Aether Person record for unified profiles.
|
||||
|
||||
---
|
||||
|
||||
## Critical Design Pattern: Override Fields
|
||||
|
||||
### Purpose
|
||||
The `*_override` fields pattern (established in 2018) protects data from being overwritten during scheduled cron syncs from external systems (iMIS, Novi, etc.). This ensures that staff corrections or attendee self-updates persist across multiple sync cycles.
|
||||
|
||||
### How It Works
|
||||
1. **Import:** External systems populate **REGULAR** fields only.
|
||||
2. **Display Logic:** The UI displays the `*_override` field if it has a value; otherwise, it falls back to the regular field.
|
||||
3. **HTML Rendering:** Certain display fields (Name, Title, Affiliations, Location) support HTML markup for rich text formatting (bold, italics, line breaks) on the physical badge.
|
||||
|
||||
### Standard Override Pairs
|
||||
|
||||
| Regular Field | Override Field | Editable By | HTML? |
|
||||
|---|---|---|---|
|
||||
| `full_name` | `full_name_override` | Staff, Attendee | ✅ |
|
||||
| `professional_title` | `professional_title_override` | Staff, Attendee | ✅ |
|
||||
| `affiliations` | `affiliations_override` | Staff, Attendee | ✅ |
|
||||
| `location` | `location_override` | Staff, Attendee | ✅ |
|
||||
| `email` | `email_override` | Staff Only | No |
|
||||
| `badge_type` | `badge_type_override` | Staff Only | No |
|
||||
|
||||
---
|
||||
|
||||
## External System Integration
|
||||
|
||||
Aether acts as a **Pull-Only** consumer for registration data. It does not push changes back to external systems, maintaining them as the source of truth for base registration while Aether handles the "Onsite Truth."
|
||||
|
||||
### Supported Sources
|
||||
- **iMIS**, **Novi AMS**, **Impexium** (Associations)
|
||||
- **Zoom**, **Cvent** (Registrations)
|
||||
- **Confex** (Abstracts/Presenters)
|
||||
- **Custom CSV/Excel**
|
||||
|
||||
---
|
||||
|
||||
## Access Control & Permissions
|
||||
|
||||
| Level | Access |
|
||||
|---|---|
|
||||
| **Authenticated** | View own badge, limited self-edit (overrides only). |
|
||||
| **Trusted** | Search all badges, view all, reprint existing badges. |
|
||||
| **Administrator** | Full CRUD, bulk operations, override any field. |
|
||||
| **Manager** | All Admin + Event/Template configuration. |
|
||||
|
||||
### Attendee Self-Service (`/review`)
|
||||
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`). This allows them to verify their info and provide preferred name/title overrides before printing.
|
||||
|
||||
---
|
||||
|
||||
## Search & Filter Capabilities
|
||||
|
||||
- **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
|
||||
- **Multi-Word Logic:** Queries like "Scott Idem" are split and treated as `LIKE %Scott% AND LIKE %Idem%`.
|
||||
- **QR Scan Search:** Scanning an attendee's QR code (from a confirmation email or old badge) immediately jumps to their record.
|
||||
- **Advanced Filters (Trusted + Edit Mode):** Badge Type, Printed Status, Affiliations, Sort Order.
|
||||
|
||||
### Visibility Filter (Trusted + Edit Mode)
|
||||
|
||||
Three-option select controlling which records are shown:
|
||||
|
||||
| Option | Who can set it | Effect |
|
||||
| --- | --- | --- |
|
||||
| **Default** | Any | Hides hidden and disabled badges |
|
||||
| **Show Hidden** | Trusted | Shows hidden badges alongside normal ones |
|
||||
| **Show Disabled + Hidden** | Manager only | Shows all records regardless of enable/hide flags |
|
||||
|
||||
### Result Limit Stepper (Edit Mode)
|
||||
|
||||
Controls the maximum number of results returned. Only visible in edit mode.
|
||||
|
||||
| Access Level | Range | Step |
|
||||
| --- | --- | --- |
|
||||
| Below Trusted | Fixed 25 | — |
|
||||
| Trusted | 25 – 250 | 25 |
|
||||
| Manager+ | 25 – 2550 | 25 up to 250, then 100 |
|
||||
|
||||
### Badge Type Filter — Known Limitation
|
||||
|
||||
The badge type dropdown in the search form uses a **hardcoded list**, not the template's `badge_type_list`. This means the codes shown in the filter may not match the codes used by the current event's template. This is a known gap — the fix requires passing the template object into the search component. Until resolved, staff can still search by name/email and filter results manually.
|
||||
|
||||
---
|
||||
|
||||
## Print Tracking
|
||||
|
||||
Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `print_count` | Increments on every "Print Badge" action. |
|
||||
| `print_first_datetime` | Timestamp of the very first print. |
|
||||
| `print_last_datetime` | Timestamp of the most recent print. |
|
||||
|
||||
> **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
|
||||
|
||||
---
|
||||
|
||||
## Route Map (Badges)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/badges` | Main search and attendee list. |
|
||||
| `/events/[id]/badges/templates` | Badge template management. |
|
||||
| `/events/[id]/badges/[id]/print` | The actual print-ready render page. |
|
||||
| `/events/[id]/badges/[id]/review` | Attendee-facing self-service form. |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
👉 **[MODULE__AE_Events_Badge_Templates.md](./MODULE__AE_Events_Badge_Templates.md)** (Technical reference for layouts)
|
||||
👉 **[GUIDE__AE_Events_Badges_Onsite.md](./GUIDE__AE_Events_Badges_Onsite.md)** (Hardware & station setup)
|
||||
👉 **[GUIDE__AE_Events_Onsite_Runbook.md](./GUIDE__AE_Events_Onsite_Runbook.md)** (Onsite operational checklists)
|
||||
@@ -1,79 +0,0 @@
|
||||
# Aether Events — Launcher (Podium Display)
|
||||
|
||||
The Launcher module provides the podium display interface that runs on each session room's kiosk machine. It is designed to work in standard browsers but is optimized for the **Aether Desktop (Electron)** native shell.
|
||||
|
||||
---
|
||||
|
||||
## Operational Modes
|
||||
|
||||
| Mode | Use Case | File Handling |
|
||||
|---|---|---|
|
||||
| **Default** | Browser on any machine | Files downloaded on demand via browser. |
|
||||
| **Onsite** | Browser on event network | Faster polling; browser-managed files. |
|
||||
| **Native** | Electron app on podium Mac | Background pre-cache; atomic file handover. |
|
||||
|
||||
For production onsite use, **Native mode on Mac laptops** is the target. The Electron
|
||||
app pre-caches all session files in the background so presentations open instantly without
|
||||
a network round-trip at the moment of launch.
|
||||
|
||||
---
|
||||
|
||||
## Launcher Display Views
|
||||
|
||||
| View | Shown When |
|
||||
|---|---|
|
||||
| **Session view** | Active session with session-level files. |
|
||||
| **Presentation view** | Active session with named presentations. |
|
||||
| **Presenter view** | Presentation selected; shows presenter bio/photo. |
|
||||
| **Poster/group view** | Special layout for poster sessions. |
|
||||
| **Screensaver** | No active session; idle state. |
|
||||
|
||||
---
|
||||
|
||||
## Sync Engine & File Handling
|
||||
|
||||
### Background Sync (File Warming)
|
||||
When a user navigates to a session in the Launcher UI, the background engine automatically warms the cache for that specific session by downloading all associated files.
|
||||
|
||||
### Force Sync Location
|
||||
To ensure full room readiness (e.g., during SRR setup or overnight), operators can trigger a **Force Sync Location** via the configuration menu. This performs a recursive fetch of all sessions, presentations, and presenters for the room and queues every file for the day for download.
|
||||
|
||||
### Download Priority & Room Readiness
|
||||
To ensure the podium is ready for the day's first sessions, the Launcher sync engine uses a 4-tier chronological sorting priority:
|
||||
|
||||
1. **Global Assets:** Event and Location level files (branding, walk-in slides) are cached first.
|
||||
2. **Session Schedule:** Files for the earliest sessions in the room are prioritized.
|
||||
3. **Presentation Order:** Within a session block, speakers are prioritized by their scheduled start time.
|
||||
4. **First-In Fairness:** When times are equal, older uploads are prioritized over late revisions (respecting on-time presenters).
|
||||
|
||||
### Native File Opening (Safe Handover)
|
||||
1. Verify SHA-256 hash in permanent cache.
|
||||
2. Atomic copy to system `[tmp]` directory.
|
||||
3. Rename to original filename (e.g., `Abstract_101.pptx`).
|
||||
4. OS opens the file via a **Launch Profile** (AppleScript or Shell command).
|
||||
|
||||
---
|
||||
|
||||
## Device & Native Integration
|
||||
|
||||
Each Launcher kiosk is registered as an `event_device` record in Aether. The technical specifications for the Electron bridge, hashed cache protocol, and hardware actuators are documented in:
|
||||
👉 **[MODULE__AE_Events_Launcher_Native.md](./MODULE__AE_Events_Launcher_Native.md)**
|
||||
|
||||
---
|
||||
|
||||
## Route Map (Display)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/launcher` | Launcher home — select location |
|
||||
| `/events/[id]/launcher/[location_id]` | Launcher display for a specific room |
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
| Feature | Minimum Access |
|
||||
|---|---|
|
||||
| View Launcher display | `authenticated_access` |
|
||||
| Manual session selection | `trusted_access` |
|
||||
| Advanced Config / Sync Control | `trusted_access` (via Configuration Drawer) |
|
||||
@@ -1,94 +0,0 @@
|
||||
# Aether Events — Launcher Configuration Menu (Inventory)
|
||||
|
||||
> **Status:** Current Reference (v3.0)
|
||||
> **Location:** `src/routes/events/[event_id]/(launcher)/launcher_cfg.svelte`
|
||||
|
||||
This document provides a detailed inventory of the Launcher's configuration menu settings as of May 2026. This serves as the baseline for the v3.1 reorganization into a modal-based tabbed interface.
|
||||
|
||||
---
|
||||
|
||||
## 1. UI Architecture & Visibility
|
||||
|
||||
The configuration menu currently resides in a slide-out **Drawer** (sidebar).
|
||||
|
||||
### 1.1 Visibility Modes
|
||||
- **Standard Mode:** Default view for onsite operators. Hides advanced technical and destructive controls.
|
||||
- **Technical Mode (`$ae_loc.edit_mode`):** Toggled via a subtle pencil icon. Reveals advanced diagnostic fields, manual overrides, and debug tools.
|
||||
- **Native Mode (`$ae_loc.is_native`):** Automatically detected when running in the Electron shell. Shows OS-level controls (Filesystem, Power, Apps).
|
||||
|
||||
### 1.2 Section Expansion Logic
|
||||
- **`collapsed`**: Content hidden.
|
||||
- **`auto`**: Expanded by default; collapses when another "auto" section opens.
|
||||
- **`pinned`**: Remains expanded regardless of other interactions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Menu Inventory (Tabbed View)
|
||||
|
||||
### Tab 1: Setup (Onsite Operator Focus)
|
||||
|
||||
| Section | Feature | Technical Mode Only |
|
||||
| :--- | :--- | :--- |
|
||||
| **Display & App Modes** | Session Mode Preset (Oral vs Poster Kiosk) | |
|
||||
| | Operational Env (Web / App / Onsite) | |
|
||||
| | Interface Visibility (Hide Header/Menu/Footer/Times) | |
|
||||
| | Clock Format (12/24 hour) | |
|
||||
| | WebSocket Debugger Toggle | Yes |
|
||||
| | Poster Modal Title Toggle | Yes |
|
||||
| | Native Test Mode (Simulation) | Yes |
|
||||
| **Remote Controller** | WS Connection Status Badge | |
|
||||
| | Controller Strategy (Local / Remote / Local Push) | |
|
||||
| | Connect / Disconnect Action | |
|
||||
| | Group Reload (WS trigger) | |
|
||||
| | Channel Group Code (Locked/Unlockable) | Yes |
|
||||
| **Poster Screen Saver** | Idle Timeout Summary | |
|
||||
| | Timer Overrides (Idle / Cycle / Loop) | Yes |
|
||||
|
||||
### Tab 2: Device (Technical & Native Focus)
|
||||
|
||||
| Section | Feature | Technical Mode Only |
|
||||
| :--- | :--- | :--- |
|
||||
| **Sync Engine & Timers** | Pause / Resume Sync | |
|
||||
| | Force Sync Location (Recursive fetch) | |
|
||||
| | Polling Periods (Event/Device/Loc/Sess/Pres/Presenter) | Yes |
|
||||
| | Cache Hash Prefix Length (1-3 chars) | Yes |
|
||||
| **System & Sync Health** | CPU & RAM Usage Gauges | |
|
||||
| | Heartbeat Status & Timestamp | |
|
||||
| | Sync Progress (Cached vs Total) | |
|
||||
| | Active Sync Filename (Animated) | |
|
||||
| | Hostname & IP List | Yes |
|
||||
| | Raw Device JSON Inspector | Yes |
|
||||
| **Native OS Management** | Open Cache / Temp Folders | |
|
||||
| | Window Control (Maximize / Kiosk) | |
|
||||
| | Display Mode (Extend / Mirror) | |
|
||||
| | Presentation Remote (Prev/Start/Stop/Next) | |
|
||||
| | Reset Wallpaper (Site Header) | Yes |
|
||||
| | Kill Presentation Apps (PowerPoint/Keynote/etc) | Yes |
|
||||
| | Power Actions (Reboot / Shutdown) | Yes |
|
||||
| | Manual Terminal Command Entry | Yes |
|
||||
| **Wallpaper** | Primary Display URL Preset/Input | |
|
||||
| | External Display URL Preset/Input | |
|
||||
| | Save & Apply Wallpaper | |
|
||||
| | Restore macOS Default | |
|
||||
| **Launch Timing** | Per-Profile Post-Open Delay (ms) Overrides | Yes |
|
||||
| **Application Updates** | Update Source (File / URL) | Yes |
|
||||
| | Check for Updates | |
|
||||
| | Install & Relaunch | |
|
||||
|
||||
### Tab 3: Dev (Technical/Developer Focus)
|
||||
|
||||
| Section | Feature | Technical Mode Only |
|
||||
| :--- | :--- | :--- |
|
||||
| **Local Reset & Actions** | Maintenance Select (Wipe IDB / LocalStorage) | Yes |
|
||||
| | Global Sys Menu Toggle | Yes |
|
||||
| | Global Debug Menu Toggle | Yes |
|
||||
| | Cache .tmp Cleanup (Native Only) | Yes |
|
||||
| | API Endpoint & Account ID Summary | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 3. Global Actions (Footer)
|
||||
|
||||
- **Close:** Dismisses the configuration menu.
|
||||
- **Reload:** Performs a full browser `location.reload()`.
|
||||
- **Debug Panel:** Opens the raw state inspector (Technical Mode Only).
|
||||
@@ -1,110 +0,0 @@
|
||||
# Aether Events — Unified Launcher Configuration (Vision v3.1)
|
||||
|
||||
> **Status:** Strategic Design / Unified Proposal
|
||||
> **Author:** Gemini CLI (Interactive Agent)
|
||||
> **Target:** Full consistency across all configuration modules.
|
||||
|
||||
## 1. Unified Design Language
|
||||
|
||||
To eliminate the "created by 3 different people" feel, all components must strictly adhere to this shared specification.
|
||||
|
||||
### 1.1 Color Palette & Semantics
|
||||
- **Primary (Blue):** Main actions, active tabs, and standard configuration toggles.
|
||||
- **Secondary (Green):** Safe actions (Connect, Sync, Apply).
|
||||
- **Warning (Orange):** Technical overrides that require caution (Timers, Native Shell).
|
||||
- **Error (Red):** Destructive actions (Resets, Shutdown, Kill Apps).
|
||||
- **Surface (Gray):** Containers, input backgrounds, and inactive states.
|
||||
|
||||
### 1.2 Typography & Spacing
|
||||
- **Section Headers:** `text-sm font-bold uppercase tracking-tight` (Provided by Wrapper).
|
||||
- **Field Labels:** `text-[10px] font-bold uppercase tracking-wider opacity-60 mb-1`.
|
||||
- **Sub-Descriptions:** `text-[9px] italic opacity-40 leading-snug mt-1`.
|
||||
- **Status Badges:** `text-[8px] font-bold uppercase tracking-tighter`.
|
||||
- **Grid Standard:**
|
||||
* Single Column for complex fields.
|
||||
* `grid-cols-2` with `gap-4` for standard inputs.
|
||||
* `grid-cols-3` or `grid-cols-4` only for small buttons or icon toggles.
|
||||
|
||||
---
|
||||
|
||||
## 2. Structural Reorganization (The "Aether" Layout)
|
||||
|
||||
The menu is now a **Vertical Sidebar Modal**. This allows for persistent navigation while dedicating the large right pane to content.
|
||||
|
||||
### Tab 1: 🖥️ Display (General Operator)
|
||||
*Focus: What the screen looks like.*
|
||||
- **Category: Layout & UI**
|
||||
- Presets: Oral/Default vs Poster Kiosk (One-tap setup).
|
||||
- Toggles: Header, Menu, Footer, Times visibility.
|
||||
- Formatting: Clock (12/24h), Date formats.
|
||||
- **Category: Screen Saver**
|
||||
- Idle Timeout (Minutes).
|
||||
- Mode: Image Cycle vs Video vs Custom.
|
||||
|
||||
### Tab 2: 🔌 Connectivity (Onsite Tech)
|
||||
*Focus: How it talks to the network.*
|
||||
- **Category: WebSocket Control**
|
||||
- Connection Status & Signal Strength.
|
||||
- Controller Mode: Local vs Remote vs Push.
|
||||
- Group Code: Channel sharding for multi-room management.
|
||||
- **Category: API Context**
|
||||
- Current Endpoint, Account, and Site context.
|
||||
|
||||
### Tab 3: 🔄 Sync & Health (Onsite Tech)
|
||||
*Focus: Data integrity and performance.*
|
||||
- **Category: Sync Engine**
|
||||
- Status: Active vs Paused.
|
||||
- Action: Force Sync Location (recursive metadata fetch).
|
||||
- Stats: Cached Files vs Total Files (Progress bar).
|
||||
- **Category: System Telemetry**
|
||||
- CPU & RAM usage (Visual gauges).
|
||||
- Heartbeat monitor (Last success timestamp).
|
||||
- Device Identity: Hostname, IP list, Local paths.
|
||||
|
||||
### Tab 4: 🛠️ Native Shell (Specialized / Mac)
|
||||
*Focus: OS-level capabilities.*
|
||||
- **Category: App Control**
|
||||
- Window: Maximize, Kiosk Mode, Fullscreen.
|
||||
- Automation: Kill presentation apps (Clean slate).
|
||||
- Remote: Virtual clicker (Prev/Next/Start/Stop).
|
||||
- **Category: System Action**
|
||||
- Displays: Extend vs Mirror (Native bridge).
|
||||
- Folders: Open Cache / Open Temp.
|
||||
- Power: Reboot / Shutdown (With confirmation).
|
||||
|
||||
### Tab 5: 🖼️ Wallpaper (Branding)
|
||||
*Focus: Event-specific aesthetics.*
|
||||
- **Category: Customization**
|
||||
- Primary Display: URL/Preset.
|
||||
- Secondary/Projector: URL/Preset.
|
||||
- Action: Apply to OS (Native) + Preview (Web).
|
||||
|
||||
### Tab 6: 🧪 Advanced (Developer Mode)
|
||||
*Focus: Fine-tuning and updates.*
|
||||
- **Category: Performance**
|
||||
- Polling Intervals (Event, Device, Room, Session, Presenter).
|
||||
- Cache Sharding (Prefix length).
|
||||
- **Category: Launch Logic**
|
||||
- Per-Profile Post-Open Delays (ms).
|
||||
- **Category: Updates**
|
||||
- Source: File vs URL.
|
||||
- Version: Current vs Target.
|
||||
- Action: Download/Install.
|
||||
|
||||
### Tab 7: 🧹 Maintenance (Emergency)
|
||||
*Focus: Troubleshooting.*
|
||||
- **Category: Resets**
|
||||
- Wipe IndexedDB (Module selective).
|
||||
- Clear LocalStorage (Reset config).
|
||||
- **Category: Diagnostics**
|
||||
- Raw Device JSON inspector.
|
||||
- Terminal Command Entry.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Plan: The "Cohesion" Refactor
|
||||
|
||||
1. **Standardize `Launcher_Cfg_Section.svelte`:** Ensure padding and spacing are baked into the wrapper so children don't have to define it.
|
||||
2. **Create `Launcher_Cfg_Field.svelte`:** A new helper component to handle the Label + Description + Input pattern consistently.
|
||||
3. **Audit Sub-Components:** Update all 10 components to use the new colors, grid patterns, and typography.
|
||||
4. **Polish Transitions:** Ensure the Modal entry and Tab switching are butter-smooth with Svelte 5 transitions.
|
||||
@@ -1,275 +0,0 @@
|
||||
# Aether Events — Launcher: Native Integration
|
||||
|
||||
> **Status:** Operational / Permanent Reference
|
||||
> **Last Updated:** 2026-05-21 (Reorganized)
|
||||
> **Primary Platform:** macOS (Darwin)
|
||||
> **Fallback Platform:** Linux / Windows
|
||||
|
||||
## 1. Overview
|
||||
The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide OS-level capabilities that are normally restricted by browser sandboxing. This enables persistent file caching, direct control of presentation software (Keynote, PowerPoint), and hardware telemetry.
|
||||
|
||||
### Operational Modes
|
||||
|
||||
| Mode | Purpose | File Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| **Default** | Standard web browser access. | Direct downloads; no local caching. |
|
||||
| **Onsite** | Web access on event networks. | Faster polling; browser-based file management. |
|
||||
| **Native** | Dedicated Podium Kiosk (Electron). | Full background pre-caching; atomic safe-handover. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture: The Three-Layer Bridge
|
||||
|
||||
The integration is built on a decoupled three-layer communication model to ensure security and cross-platform flexibility.
|
||||
|
||||
### 2.1 Layer 1: The Engine (Main Process)
|
||||
- **Repo:** `~/OSIT_dev/aether_app_native_electron/` (separate git repo)
|
||||
- **File:** `aether_app_native_electron/src/main/*.ts`
|
||||
- **Role:** Performs the heavy lifting (Filesystem, Shell, AppleScript).
|
||||
- **Responsibilities:**
|
||||
- Managing the **Hashed Cache** directory.
|
||||
- Executing `osascript` intents for presentation control.
|
||||
- Spawn/Kill process management.
|
||||
|
||||
### 2.2 Layer 2: The Gatekeeper (Preload Script)
|
||||
- **Namespace:** `window.aetherNative`
|
||||
- **Role:** Securely exposes whitelisted IPC channels to the Renderer.
|
||||
- **Standards:** Uses `contextBridge.exposeInMainWorld` to prevent arbitrary code execution.
|
||||
|
||||
### 2.3 Layer 3: The Messenger (SvelteKit Relay)
|
||||
- **File:** `src/lib/electron/electron_relay.ts`
|
||||
- **Role:** Provides a clean, typed API for Svelte components.
|
||||
- **Responsibilities:**
|
||||
- Mapping `camelCase` UI triggers to `snake_case` IPC calls.
|
||||
- Resolving an extension alias to a canonical Launch Profile, then to a single
|
||||
`native_template` string before crossing IPC.
|
||||
|
||||
The reason for this split is simple: Launch Profiles are policy, while Native Templates are
|
||||
executable strings. Keeping that distinction explicit prevents the bridge from mixing config
|
||||
objects with runtime commands.
|
||||
|
||||
---
|
||||
|
||||
## 3. The "Zero-Config" Lifecycle
|
||||
|
||||
To support rapid onsite deployment, the native app requires zero manual setup.
|
||||
|
||||
1. **Seed:** On launch, the Main process reads a local `seed.json` (Device ID + API Key).
|
||||
2. **Identity:** Calls `GET /v3/crud/event_device/{id}` to pull device config and extract `app_base_url` (the event FQDN) and `account_id`.
|
||||
3. **Site Context:** POSTs to `/v3/crud/site_domain/search?limit=1` with the FQDN to resolve the correct site. No JWT — auth is `x-aether-api-key` + `x-account-id` throughout.
|
||||
4. **Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route (`/events/{eventId}/launcher/{locationId}`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Podium Reliability Protocol
|
||||
|
||||
The system is designed to ensure that a presentation never fails due to network instability.
|
||||
|
||||
### 4.1 Hashed Cache Pattern
|
||||
Files are stored persistently using their SHA-256 hash to prevent filename collisions and handle versioning.
|
||||
- **Root:** `~/Library/Caches/OSIT/file_cache/`
|
||||
- **Subdirectory:** First 2 characters of hash (e.g., `ab/`)
|
||||
- **Filename:** `{hash}.file`
|
||||
|
||||
### 4.2 Background Sync (File Warming)
|
||||
When a user navigates to a session in the Launcher UI, the `LauncherBackgroundSync` component warms the cache for that specific session. To ensure full room readiness, a **Force Sync Location** trigger is available in the configuration UI.
|
||||
|
||||
1. **Metadata Fetch:** The system fetches all sessions, presentations, and presenters for the current location into the local database (Dexie).
|
||||
2. **Chronological Priority:** Missing files are added to the download queue and sorted to prioritize the event schedule:
|
||||
- **Tier 1: Global Assets** — Event and Location level files (virtual time 0).
|
||||
- **Tier 2: Session Schedule** — Earliest sessions are prioritized first.
|
||||
- **Tier 3: Presentation Order** — Within a session, speakers are prioritized by their start time.
|
||||
- **Tier 4: Integrity & Fairness** — Tie-breakers use `created_on` (oldest first) to ensure on-time uploads are cached before last-minute revisions.
|
||||
3. **Download:** Triggers background downloads via `aetherNative.download_to_cache` sequentially to preserve network bandwidth and ensure file integrity.
|
||||
|
||||
### 4.3 Safe Handover (Launch Sequence)
|
||||
When a user clicks "Open", the system follows a non-destructive sequence:
|
||||
1. **Verify:** Confirm hash exists in the permanent cache.
|
||||
2. **Copy:** Create an atomic copy in the system `[tmp]` directory.
|
||||
3. **Restore:** Rename the copy to its original filename (e.g., `Abstract_101.pptx`).
|
||||
4. **Execute:** Launch the file via the OS.
|
||||
|
||||
---
|
||||
|
||||
## 5. Automation & Actuators (Phase 5)
|
||||
|
||||
The native shell provides specialized handlers for controlling the "Podium Experience."
|
||||
|
||||
### 5.1 Presentation Acts
|
||||
|
||||
| Action | Handler | Actuator (macOS) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Launch** | `launch_presentation` | `open` or `osascript` (slideshow start) |
|
||||
| **Control** | `control_presentation` | `osascript` (next/prev slide) |
|
||||
| **Clean Up** | `kill_processes` | `killall -INT` (graceful exit) |
|
||||
|
||||
### 5.2 System Management
|
||||
- **Telemetry:** Pushes `cpu_usage`, `memory_free_gb`, and `foreground_app` via heartbeats using the `get_device_info` relay.
|
||||
- **Self-Update (Roadmap):** Plan to monitor Syncthing `admin_share` for newer `.app` versions and perform atomic swaps.
|
||||
|
||||
### 5.3 Implemented Actuators (Phase 5 Complete)
|
||||
- **Recording:** `manage_recording({action})` — Aperture session capture (`start`, `stop`, `status`). macOS only.
|
||||
- **Display Layouts:** `set_display_layout({mode, configStr?})` — Mirror / Extend displays. macOS only. **Primary:** native `display_control` binary (`resources/bin/display_control`) uses CoreGraphics APIs directly — no Homebrew dependency. Built from `scripts/display_control.m` via `scripts/build-display-control.sh` on a Mac; commit the binary to the repo. **Fallback:** [`displayplacer`](https://github.com/jakehilborn/displayplacer) (`brew install displayplacer`) used when binary is absent or `configStr` override is set. Failures are logged to the Electron console but do not block file open. A **Display Mode** toggle (Extend / Mirror) is available in the Launcher config — Native OS section, visible without Technical Mode.
|
||||
- **Power Control:** `power_control({action})` — Shutdown, reboot, sleep. macOS + Linux.
|
||||
- **Window Control:** `window_control({action})` — Maximize, minimize, fullscreen, kiosk mode.
|
||||
- **Wallpaper:** `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Downloads from URL (cached locally) or applies a local path. Per-display targeting (`'all'`/`'primary'`/`'external'`). macOS only in production; Linux returns a dev-mode preview payload.
|
||||
|
||||
> **Note:** `update_app` is implemented as a stub — downloads but does not install. Not yet functional for end users.
|
||||
|
||||
---
|
||||
|
||||
## 6. Launcher Configuration & Management
|
||||
|
||||
The Launcher features a standardized, responsive configuration interface designed for onsite technical management.
|
||||
|
||||
### 6.1 UI Architecture
|
||||
- **Tabbed Navigation:** Categorized into System, Sync, and General settings.
|
||||
- **Section Wrapper (`Launcher_Cfg_Section`):** A shared component providing a consistent header, icon, and responsive grid container.
|
||||
|
||||
### 6.2 3-Way State Logic
|
||||
To manage screen real estate on varying laptop resolutions, all configuration sections utilize a 3-way visibility state:
|
||||
- **`collapsed`**: Content is hidden.
|
||||
- **`auto`**: Expanded by default, but automatically closes if another "auto" section is opened.
|
||||
- **`pinned`**: Expanded and remains open regardless of other section interactions.
|
||||
|
||||
### 6.3 Technical Mode (`edit_mode`)
|
||||
The UI dynamically filters fields based on the user's focus. Enabling Technical Mode (`$ae_loc.edit_mode`) reveals advanced diagnostic and writeable fields.
|
||||
|
||||
| Category | Standard View (Read-Only) | Technical Mode (Read/Write) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Health** | Heartbeat, RAM Usage, Sync Stats | Hostname, IP List, Raw Device JSON |
|
||||
| **OS Bridge** | Folder Buttons, Recording Toggle | Manual Terminal Commands, Reset Wallpaper |
|
||||
| **Sync** | Sync Completion Status | Millisecond Timers, Cache Prefix Logic |
|
||||
| **Update** | Current Version Status | Manual Update Paths, URL Overrides |
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Reference (IPC Whitelist)
|
||||
|
||||
All functions below are exported from `src/lib/electron/electron_relay.ts` and safely
|
||||
no-op when `window.aetherNative` is not present (i.e., in browser/non-native mode).
|
||||
|
||||
### Config & Info
|
||||
- `get_device_config()` — Returns hydrated device settings injected by the native shell on startup.
|
||||
- `get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`).
|
||||
|
||||
### File Cache
|
||||
- `check_cache({cache_root, hash, hash_prefix_length?, verify_hash?})` — Verifies a file exists in the local hashed cache. `verify_hash: true` re-hashes to confirm integrity.
|
||||
- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check. Stale `.tmp` files (older than 5 min) from crashed downloads are cleaned up automatically on each call.
|
||||
- `copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})` — **Preferred primitive.** Copies a cached file to temp and returns `{ success, path }`. The Svelte caller decides what to do next (run a script, open it, etc.).
|
||||
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, native_template?})` — Combines copy + launch in one call. Executes the provided `native_template` string after the file is copied to temp. If no template is supplied, treat it as an error and do not rely on Electron-side defaults.
|
||||
|
||||
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
|
||||
|
||||
### Shell & OS
|
||||
- `open_folder(path)` — Opens a path in the OS file manager.
|
||||
- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution.
|
||||
- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution.
|
||||
- `run_osascript(script)` — Executes an AppleScript string. macOS only. **Hardened (2026-05-11):** writes script to a temp `.scpt` file; multi-line scripts and paths with special characters now work correctly. No shell escaping needed in the passed string.
|
||||
- `kill_processes({process_name_li})` — Terminates processes by name. macOS/Linux: `pkill -f`. Windows: `taskkill /F`.
|
||||
- `open_local_file_v2(path)` — Opens a file with its default OS application.
|
||||
|
||||
### Presentations (Phase 5)
|
||||
- `launch_presentation({path, app?, os?})` — Platform-aware launcher. macOS: PowerPoint/Keynote via AppleScript. Linux: LibreOffice Impress. Resolves `[home]`/`[tmp]` placeholders.
|
||||
- `control_presentation({app, action})` — Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript.
|
||||
|
||||
### System Management (Phase 5)
|
||||
- `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Sets desktop wallpaper. Downloads from `url` (cached to `~/Library/Caches/OSIT/wallpaper/`) or applies a local `path`. `url_external` targets the projector/second display separately. macOS only in production; Linux returns a dev-mode preview payload without applying.
|
||||
- `window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk.
|
||||
- `set_display_layout({mode, configStr?})` — Mirror or extend displays via [`displayplacer`](https://github.com/jakehilborn/displayplacer). macOS only. Auto-detects via `displayplacer list`; `configStr` overrides auto-detection when set. Binary lookup order: bundled `resources/bin/displayplacer` → `/opt/homebrew/bin/` (Apple Silicon) → `/usr/local/bin/` (Intel). Requires `brew install displayplacer` on each venue Mac if not bundled.
|
||||
- `power_control({action})` — Shutdown, reboot, or sleep the host machine. macOS + Linux.
|
||||
- `manage_recording({action, options?})` — Aperture capture control (`start`/`stop`/`status`). macOS only.
|
||||
- `open_external({url, app?})` — Opens a URL in Chrome, Firefox, or the default browser.
|
||||
- `update_app(args)` — **Stub only.** Downloads but does not install. Not yet functional.
|
||||
- `list_tools()` — Returns a self-documenting manifest of all available native bridge functions.
|
||||
|
||||
### Path Placeholders
|
||||
All paths passed to native handlers should use tokens rather than hardcoded OS paths:
|
||||
- `[home]` — Resolved to the user's home directory by the native bridge.
|
||||
- `[tmp]` — Resolved to the system temporary directory.
|
||||
|
||||
---
|
||||
|
||||
## 8. Launch Profiles and Native Templates (No-Rebuild File Handling)
|
||||
This launcher uses two related concepts:
|
||||
|
||||
- **Launch Profile**: the Svelte-side config object keyed by file extension. A profile decides
|
||||
which app to use, whether to extend or mirror displays, whether to use an explicit open
|
||||
command, whether to run post-open automation, and how long to wait before running it.
|
||||
- **Native Template**: the single AppleScript or shell command string handed to Electron after
|
||||
Svelte resolves the profile. This is what Electron actually executes.
|
||||
|
||||
The Svelte launcher resolves a profile and then passes a native template string to
|
||||
`launch_from_cache`. Electron only executes the template it receives. If Svelte has not
|
||||
resolved a template yet, it should stop before IPC and surface a missing-profile error.
|
||||
This keeps all fallback logic in Svelte, where it can be edited without rebuilding Electron.
|
||||
The native layer should not invent or guess a default launch path.
|
||||
|
||||
The built-in defaults are organized as canonical profile names plus extension aliases. That
|
||||
lets multiple file types share one profile without repeating the same app/script details.
|
||||
The profile object also carries `post_delay_ms`, and a device-specific per-profile
|
||||
`launch_profiles[profile].post_delay_ms` override can tune the delay without changing the bridge
|
||||
contract. URL-based presentations remain a special pseudo-extension handled separately from
|
||||
the cache open flow.
|
||||
|
||||
### Native Template Formats
|
||||
|
||||
| Format | Example |
|
||||
| :--- | :--- |
|
||||
| **AppleScript** (macOS) | Multi-line AppleScript string with `{{path}}` placeholder |
|
||||
| **Shell command** | String prefixed with `shell:` — e.g. `shell:open "{{path}}"` |
|
||||
|
||||
The placeholder `{{path}}` is replaced with the full resolved path to the file in the temp
|
||||
directory after the atomic copy from cache.
|
||||
|
||||
### Where to Configure
|
||||
|
||||
Launch profiles are resolved in priority order by `get_launch_profile()` in
|
||||
`launcher_file_cont.svelte`:
|
||||
|
||||
1. **`event_device.data_json.launch_profiles`** — API-driven, per-device. Highest priority.
|
||||
Set via the `event_device` record (Pres Mgmt → Device Management or direct DB edit).
|
||||
2. **`$events_loc.launcher.launch_profiles`** — Local persistent config. Editable via the
|
||||
Launcher config UI (planned) or direct `localStorage` manipulation.
|
||||
|
||||
If neither is set, the resolved native template is `null` and the launcher should not call
|
||||
Electron until an explicit template is available.
|
||||
|
||||
Why: this avoids a second hidden source of truth. The profile map can evolve independently of
|
||||
the executable string, and Electron stays a thin executor rather than a policy engine.
|
||||
|
||||
### Key Format
|
||||
|
||||
Keys are lowercase file extensions without the dot. A `"default"` key catches all
|
||||
unrecognised extensions.
|
||||
|
||||
The JSON below illustrates the `native_template` emitted after profile resolution, not the
|
||||
full Launch Profile object schema.
|
||||
|
||||
```json
|
||||
// event_device.data_json.launch_profiles example
|
||||
{
|
||||
"launch_profiles": {
|
||||
"pptx": "tell application \"Microsoft PowerPoint\"\n activate\n open (POSIX file \"{{path}}\")\n delay 3\nend tell\ntell application \"System Events\"\n keystroke return using command down\nend tell",
|
||||
"key": "tell application \"Keynote\"\n activate\n open (POSIX file \"{{path}}\")\n delay 1\n start (front document)\nend tell",
|
||||
"pdf": "shell:open \"{{path}}\"",
|
||||
"default": "shell:open \"{{path}}\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AppleScript Execution — All Handlers Hardened (2026-05-11)
|
||||
|
||||
All AppleScript execution in the native shell now writes scripts to a temp `.scpt` file and
|
||||
runs `osascript "<path>"` rather than the old `osascript -e "<inline>"` approach.
|
||||
|
||||
- **`run_osascript`** — hardened (2026-05-11, earlier batch)
|
||||
- **`launch_from_cache`** — hardened (same batch)
|
||||
- **`launch_presentation`** — hardened (2026-05-11, follow-up fix; was the last handler still using `-e`)
|
||||
- **`control_presentation`** — uses single-line scripts with no path interpolation; `-e` is safe here and retained for simplicity
|
||||
|
||||
The `-e` approach breaks on (1) multi-line scripts and (2) file paths containing spaces,
|
||||
quotes, or parentheses — common in conference presentation filenames.
|
||||
|
||||
### Not Exposed via Relay (intentional)
|
||||
- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.
|
||||
@@ -1,284 +0,0 @@
|
||||
# Aether Events — Exhibitor Leads Module (v3)
|
||||
|
||||
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
|
||||
**Platform:** PWA only — mobile-first, offline-capable.
|
||||
**Target users:** Conference exhibitors scanning attendee badges at their booths.
|
||||
|
||||
### Recent Changes (2026-04-03)
|
||||
|
||||
- Migrated Leads persisted state to Svelte‑5 PersistedState: `leads_loc` now implemented at `src/lib/stores/ae_events_stores__leads.svelte.ts` and the store version constant `AE_LEADS_LOC_VERSION` added to `src/lib/stores/store_versions.ts`.
|
||||
- Payment UI adjustments: `ae_comp__exhibit_payment.svelte` now accepts a `leads_require_payment` prop and enforces the event-level `mod_exhibits_json.leads_require_payment` flag; a loading guard was added so the component waits for the exhibit record (Dexie `liveQuery`) before deciding which UI to show.
|
||||
- Tests: update `tests/_helpers/leads_helpers.ts` to seed `leads_loc` defaults and `__version` when needed to avoid localStorage wipe caused by store version checks.
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
The Exhibitor Leads module lets conference exhibitors capture and manage attendee leads directly
|
||||
from their booth. Exhibitors scan or search attendee badges and build a list of contacts they met.
|
||||
All data is cached locally (IndexedDB / Dexie.js) for spotty or offline venue Wi-Fi, with
|
||||
background SWR revalidation against the API when the network is available.
|
||||
|
||||
Key capabilities:
|
||||
|
||||
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
|
||||
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
|
||||
- **Lead detail** — custom question responses, notes (rich text), priority boolean flag, hide/unhide
|
||||
- **Export** — CSV/XLSX download of all leads for an exhibit
|
||||
- **License management** — assign staff accounts (email + passcode) per max license count
|
||||
- **Custom questions** — configurable per-exhibit follow-up questions (ratings, dropdowns, text)
|
||||
- **Offline-first** — IndexedDB cache survives network drops; syncs on reconnect
|
||||
- **PWA install** — Chrome/Android native install prompt; iOS Safari "Add to Home Screen" nudge
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
Three sign-in levels are supported within this module:
|
||||
|
||||
| Level | How to sign in | What they can do |
|
||||
|---|---|---|
|
||||
| **Aether Platform Auth** | Standard Aether login (manager/trusted access) | Full admin bypass; all exhibit data |
|
||||
| **Shared Exhibit Passcode** | Enter booth's `staff_passcode` | Manage licenses, view/add leads |
|
||||
| **Licensed User** | Email + individual passcode from `license_li_json` | Add and manage leads for this booth |
|
||||
|
||||
Auth state is persisted in `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-backed).
|
||||
|
||||
A booth only shows in the landing page search to non-admins if it is marked `priority = true` (i.e. paid).
|
||||
|
||||
### `allow_tracking` Opt-In
|
||||
|
||||
Attendees must have `allow_tracking = true` on their badge record to be added as a lead.
|
||||
Attendees without this flag are blocked at both the QR scanner and the manual search:
|
||||
- QR scan shows a "Tracking Blocked" warning card (`ShieldOff` icon)
|
||||
- Manual search shows an "Opt-Out" badge per result row; the "Add as Lead" button is suppressed
|
||||
|
||||
---
|
||||
|
||||
## Route Structure
|
||||
|
||||
```
|
||||
/events/[event_id]/leads/
|
||||
→ Exhibit search / landing page — find your booth
|
||||
|
||||
/events/[event_id]/leads/exhibit/[exhibit_id]/
|
||||
→ Main exhibitor view — all 4 tabs
|
||||
|
||||
/events/[event_id]/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/
|
||||
→ Lead detail view — edit notes, custom responses, flags
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Tabs
|
||||
|
||||
### Tab 1 — Start / Sign In
|
||||
|
||||
The only tab visible when not signed in as a licensed leads user.
|
||||
|
||||
- **Sign in with shared passcode** — grants booth management access (license management, passcode change)
|
||||
- **Sign in as licensed user** — grants lead capture access (email + passcode)
|
||||
- **PWA install prompt** — Chrome/Android native install button; iOS "Share → Add to Home Screen" instructions
|
||||
- **License list** — shown when signed in via shared passcode or Aether admin; add/edit/remove staff slots
|
||||
|
||||
### Tab 2 — Add Leads
|
||||
|
||||
Visible only when signed in (licensed user or Aether auth).
|
||||
|
||||
- **Text search** — search by name, email, affiliations, badge ID
|
||||
- **QR scan** — three modes (persisted per exhibit in `tab_scan_qualify`):
|
||||
- **Confirm** (`rapid`) — scan, then choose per badge: **Add & Scan Next** (resets after 2s) or **Add & View Lead** (navigates to detail)
|
||||
- **Auto** — no confirmation tap; adds immediately and auto-resets (high-throughput)
|
||||
- **Multi** — BarcodeDetector batch scan; up to 4 badges in one frame as a confirm grid
|
||||
- Previously-removed leads detected on scan — shown a "Previously Removed" card with **Restore & Scan Next** / **Restore & View Lead** buttons
|
||||
- Results show "Add as Lead" or "View Lead" depending on whether already captured
|
||||
- `external_person_id` and `group` resolved by auth type — see [Capture Identity](#capture-identity) below
|
||||
|
||||
### Tab 3 — Leads List
|
||||
|
||||
The main lead management view.
|
||||
|
||||
- **Search** — full-text across name, email, notes (local IDB fast path + API revalidation)
|
||||
- **Sort** — Newest first, Oldest first, Name A→Z, Name Z→A
|
||||
- **Filter by staff member** — "All Leads" or filter by individual licensed user
|
||||
- **Show/hide hidden records** — toggles `hide` filter on IDB and API results
|
||||
- **Export** — downloads CSV/XLSX for the exhibit (`leads_api_access` required)
|
||||
|
||||
### Tab 4 — Manage / Config
|
||||
|
||||
Exhibit configuration and app settings.
|
||||
|
||||
**Admin Tools** (manager_access only):
|
||||
- Payment status toggle (`priority` boolean field)
|
||||
- Max licenses, small/large device counts
|
||||
|
||||
**Booth Profile** (all signed-in users):
|
||||
- Exhibitor name, booth description (rich text)
|
||||
|
||||
**Access & Security**:
|
||||
- View/change shared staff passcode
|
||||
- Sign out button
|
||||
|
||||
**Lead Retrieval Config**:
|
||||
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` OR signed in via shared exhibit passcode)
|
||||
- Qualifiers & Questions — custom question config
|
||||
- Licenses & Billing — Stripe payment (only shown when `event.mod_exhibits_json.leads_require_payment = true`)
|
||||
|
||||
**App Settings**:
|
||||
- Auto-hide header/footer toggle
|
||||
- Show Extra Details toggle
|
||||
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
||||
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### `event_exhibit`
|
||||
One exhibitor's presence at an event.
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `event_exhibit_id` | Primary / URL-safe ID |
|
||||
| `name` | Exhibitor display name |
|
||||
| `code` | Booth number |
|
||||
| `staff_passcode` | Shared sign-in code |
|
||||
| `priority` | `1` = paid/active |
|
||||
| `license_max` | Max licensed staff slots |
|
||||
| `license_li_json` | Array of `{ full_name, email, passcode }` |
|
||||
| `leads_custom_questions_json` | Array of question definitions |
|
||||
| `leads_device_sm_qty` / `leads_device_lg_qty` | Device count tracking |
|
||||
|
||||
### `event_exhibit_tracking`
|
||||
One captured lead — links an exhibit to a badge.
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `event_exhibit_tracking_id` | Primary key |
|
||||
| `event_exhibit_id` | Parent exhibit |
|
||||
| `event_badge_id` | Captured attendee's badge |
|
||||
| `external_person_id` | Capturing staff's email (from license) |
|
||||
| `exhibitor_notes` | Rich text notes (HTML via TipTap) |
|
||||
| `responses_json` | `{ [question_code]: { response: value } }` |
|
||||
| `priority` | Star/flag for high-priority leads |
|
||||
| `hide` | Soft-delete / hide from list |
|
||||
| Denormalized badge fields | `event_badge_full_name`, `event_badge_email`, `event_badge_affiliations`, `event_badge_professional_title` |
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
### Routes
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `leads/+page.svelte` | Exhibit search/landing |
|
||||
| `leads/exhibit/[exhibit_id]/+page.svelte` | Main exhibitor view — orchestrates all tabs |
|
||||
| `leads/exhibit/[exhibit_id]/+layout.svelte` / `+layout.ts` | Layout / data load |
|
||||
| `leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte` | Lead detail |
|
||||
|
||||
### Components
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `ae_tab__start.svelte` | Tab 1 — welcome, sign-in, license list |
|
||||
| `ae_tab__add.svelte` | Tab 2 — QR scan + text search toggle |
|
||||
| `ae_tab__manage.svelte` | Tab 4 — admin tools, booth config, app settings |
|
||||
| `ae_comp__exhibit_signin.svelte` | Sign-in UI (shared passcode + licensed user) |
|
||||
| `ae_comp__lead_qr_scanner.svelte` | QR scanner (rapid / qualify mode) |
|
||||
| `ae_comp__lead_manual_search.svelte` | Manual badge search + add |
|
||||
| `ae_comp__exhibit_tracking_search.svelte` | Lead list search/filter/sort bar |
|
||||
| `ae_comp__exhibit_tracking_obj_li.svelte` | Lead list item renderer |
|
||||
| `ae_comp__exhibit_license_list.svelte` | License slot manager |
|
||||
| `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor |
|
||||
| `ae_comp__exhibit_payment.svelte` | **STUB** — Stripe placeholder |
|
||||
| `ae_comp__exhibit_search.svelte` | Exhibit search on the landing page |
|
||||
| `lead/ae_comp__lead_detail_form.svelte` | Custom question response editor |
|
||||
|
||||
### Lib Functions
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/lib/ae_events/ae_events__exhibit.ts` | Exhibit load, search, create, update |
|
||||
| `src/lib/ae_events/ae_events__exhibit_tracking.ts` | Tracking load, search, create, update, export |
|
||||
|
||||
Both aggregated into `events_func` via `src/lib/ae_events/ae_events_functions.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Offline / PWA Notes
|
||||
|
||||
- All data is stored in `db_events` (Dexie.js) — `exhibit` and `exhibit_tracking` tables
|
||||
- SWR pattern: IDB cache returned immediately; background API fetch updates IDB and triggers UI refresh
|
||||
- Search: local IDB first pass (fast), then API revalidation via `search__exhibit_tracking`
|
||||
- `beforeinstallprompt` event captured at module load time (`src/lib/pwa/pwa_install.svelte.ts`)
|
||||
— fires within ~1 second of page load, before any Svelte `$effect` runs
|
||||
- iOS Safari: no native install prompt; shows "Share → Add to Home Screen" instructions instead
|
||||
|
||||
---
|
||||
|
||||
## Capture Identity
|
||||
|
||||
`external_person_id` and `group` on every `event_exhibit_tracking` record record who captured the lead. Resolved at capture time in all three lead capture components (single scanner, multi scanner, manual search):
|
||||
|
||||
| Auth type | `kv.type` | Value stored |
|
||||
| --- | --- | --- |
|
||||
| Licensed exhibit user | `'licensed'` | Their email address (`kv.key`) |
|
||||
| Shared exhibit passcode | `'shared'` | `'shared_passcode'` (label — raw passcode is NOT stored) |
|
||||
| Aether user (admin bypass, no kv) | `undefined` | `$ae_loc.access_type` — e.g. `'trusted'`, `'manager'`, `'super'` |
|
||||
|
||||
`kv` = `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-persisted exhibit sign-in state).
|
||||
|
||||
---
|
||||
|
||||
## Lead Soft-Delete / Re-enable
|
||||
|
||||
Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors:
|
||||
|
||||
- **Leads list** always filters out `enable = false` records (both IDB fast-path and API results) — no flash of removed records
|
||||
- **QR scanner**: if a previously-removed badge is scanned, the scanner detects it via `existing_leads_map` (IDB) or API fallback search (`search__exhibit_tracking` with `qry_badge_id` + `enabled: 'not_enabled'`) and shows the reenable card instead of an error
|
||||
- **Lead detail page**: "Remove Lead" button (two-click confirm in header) sets `enable = false` and navigates back. "Restore Lead" card appears at the bottom of the right sidebar when `enable` is falsy.
|
||||
- `search__exhibit_tracking` supports `qry_badge_id` param (added) and `enabled: 'not_enabled'` to find disabled records for a specific badge + exhibit combination
|
||||
|
||||
---
|
||||
|
||||
## Known Gaps
|
||||
|
||||
None currently. See TODO__Agents.md for remaining smoke test items.
|
||||
|
||||
## Implemented (previously listed as gaps)
|
||||
|
||||
### Payment / Stripe
|
||||
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
|
||||
confirmation card), Stripe not configured (admin hint), payment form with license tier selector.
|
||||
Visibility is event-wide: set `event.mod_exhibits_json.leads_require_payment = true` in the event
|
||||
settings JSON to enable. When `false` (default), both the header CreditCard button and the
|
||||
"Licenses & Billing" accordion in the Manage tab are hidden. The Stripe component itself is
|
||||
unchanged — gating is done in `+page.svelte` and `ae_tab__manage.svelte`.
|
||||
|
||||
### License Management — Shared Passcode Access
|
||||
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
|
||||
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
|
||||
|
||||
### "My Leads" filter for shared-passcode users
|
||||
Fixed. `external_person_id` is stored as the literal `'shared_passcode'` for shared users (not
|
||||
the raw passcode string). The `search_params` derived in `+page.svelte` now checks `kv.type ===
|
||||
'shared'` and resolves to `'shared_passcode'` instead of `kv.key`, so the "My Leads" filter
|
||||
correctly returns their captured records.
|
||||
|
||||
---
|
||||
|
||||
## OSIT Admin Notes
|
||||
|
||||
- Mark `priority = 1` on an exhibit to make it visible in public search and to enable lead capture
|
||||
- `license_max` controls how many licensed staff slots an exhibit can have
|
||||
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
|
||||
- Custom questions are stored per-exhibit in `leads_custom_questions_json` (not global)
|
||||
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id]/`
|
||||
|
||||
|
||||
## Old Files for Reference
|
||||
|
||||
@backups/legacy/events_leads_v2/exhibit/[slug]/+page.svelte
|
||||
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_manage.svelte
|
||||
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_payment.svelte
|
||||
@@ -1,139 +0,0 @@
|
||||
# Aether Events — Presentation Management
|
||||
|
||||
The Presentation Management module handles the full lifecycle of conference content: sessions, presentations, presenters, presentation files, and room/location assignments. It serves as the "Back Office" interface for event staff.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Object Hierarchy
|
||||
|
||||
```text
|
||||
Event
|
||||
├── Event File (walk-in/out, hold slides for the whole event)
|
||||
├── Location (physical room — assigned to Sessions, not the other way around)
|
||||
├── Track (optional grouping; rarely used)
|
||||
└── Session (time block; Location assigned here, but may be unset initially)
|
||||
├── Session File (moderator slides, group/hold slides for this session)
|
||||
└── Presentation (a talk within the session; must belong to exactly one Session)
|
||||
└── Presenter (belongs to exactly one Presentation)
|
||||
└── Presenter File (their slides/materials — the common case)
|
||||
```
|
||||
|
||||
> **Import note:** When program data is initially imported (sessions, presentations,
|
||||
> presenters), Locations are often not assigned to Sessions yet — rooms may not be
|
||||
> finalized or the venue's room list may not be set up in Aether. Location assignment
|
||||
> typically happens as a separate step once the room list is confirmed.
|
||||
|
||||
### Relationships
|
||||
|
||||
- **Session → Location:** Many-to-one. A Session is assigned to one Location; a Location
|
||||
hosts many Sessions across the event timeline. Location may be null initially.
|
||||
- **Presentation → Session:** Many-to-one. A Presentation belongs to exactly one Session.
|
||||
A Session can have many Presentations (or none, for session-only setups).
|
||||
- **Presenter → Presentation:** Many-to-one. A Presenter belongs to exactly one Presentation.
|
||||
Optionally linked to an `event_person_id` for cross-referencing the person record.
|
||||
- **Event File:** Can be attached at any level — Presenter, Presentation, Session,
|
||||
Location, or Event. See the File Attachment Levels table.
|
||||
|
||||
### File Attachment Levels
|
||||
|
||||
Files (`event_file`) can be attached at five levels:
|
||||
|
||||
| Level | When Used | Typical Content |
|
||||
|---|---|---|
|
||||
| **Presenter** | 99% of the time for individual speakers | Their PowerPoint/PDF/video |
|
||||
| **Session** | Moderator slides; group/hold content for a specific session | "Session 3 — Group Discussion.pptx" |
|
||||
| **Location** | Walk-in/out or hold slides for a room across all sessions | Looped PPTX playing between sessions |
|
||||
| **Event** | Walk-in/out or hold slides used everywhere | Looped PPTX; branding overlay |
|
||||
| **Presentation** | File attached to the presentation record itself (less common) | Varies |
|
||||
|
||||
### Key Objects
|
||||
|
||||
| Object | Table | Purpose |
|
||||
|---|---|---|
|
||||
| Session | `event_session` | Time block; Location and datetime range assigned here |
|
||||
| Location | `event_location` | Physical room |
|
||||
| Presentation | `event_presentation` | A talk within a session; belongs to exactly one Session |
|
||||
| Presenter | `event_presenter` | Person linked to exactly one Presentation |
|
||||
| Event Person | `event_person` | Person record within the event context |
|
||||
| Event File | `event_file` | Uploaded file; attached at Presenter, Presentation, Session, Location, or Event level |
|
||||
|
||||
---
|
||||
|
||||
## Client Setup Variation
|
||||
|
||||
There are no rigid "modes" — events are configured with as much or as little structure
|
||||
as needed. The platform handles the full range:
|
||||
|
||||
**Minimal setup (BGH):**
|
||||
Sessions have room and time info. No Presentations or Presenters defined.
|
||||
Staff upload files directly at the session or location level onsite.
|
||||
|
||||
**Mid-range setup:**
|
||||
Sessions defined with named Presentations. Presenters may or may not be tracked.
|
||||
Mix of pre-uploaded and onsite files. QR codes may be used for quick session/presenter lookup.
|
||||
|
||||
**Full setup (LCI):**
|
||||
Sessions, Presentations, Presenters all defined and managed. External ID labeling
|
||||
(e.g., "LCI Member ID"). Agreement tracking for presenters. Files managed per-presenter.
|
||||
|
||||
The config that drives this is `event.mod_pres_mgmt_json` — see the Configuration section.
|
||||
|
||||
---
|
||||
|
||||
## Configuration — `mod_pres_mgmt_json`
|
||||
|
||||
The event's Presentation Management behavior is controlled by `event.mod_pres_mgmt_json`.
|
||||
|
||||
### Convention
|
||||
|
||||
| Prefix | Default state | Meaning |
|
||||
|---|---|---|
|
||||
| `hide__` | `false` = visible | Feature is ON by default; set `true` to suppress |
|
||||
| `show__` | `false` = hidden | Feature is OFF by default; set `true` to enable |
|
||||
|
||||
### Common Config Keys
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `lock_config` | `false` | `true` = force remote→local sync; prevents user overrides of local config |
|
||||
| `hide__session_code` | `false` | Hide session code column/field |
|
||||
| `hide__session_description` | `false` | Hide session description field |
|
||||
| `hide__session_location` | `false` | Hide location field on session view |
|
||||
| `hide__session_datetime` | `false` | Hide datetime fields |
|
||||
| `hide__presentation_code` | `false` | Hide presentation code |
|
||||
| `hide__presenter_code` | `false` | Hide presenter code |
|
||||
| `hide__location_code` | `false` | Hide location code |
|
||||
| `show__launcher_link` | `false` | Show direct Launcher link in session view |
|
||||
| `show__session_qr` | `false` | Show QR code for session (SRR lookup) |
|
||||
| `show__presenter_qr` | `false` | Show QR code for presenter (SRR lookup) |
|
||||
| `label__person_external_id` | `null` | Override label for external ID field (e.g., `"Member ID"`) |
|
||||
| `label__session_poc_name` | `null` | Override label for session POC (e.g., `"Champion"`) |
|
||||
| `file_purpose_option_kv` | `{}` | Key-value map of file purpose options (e.g., `{"ppt": "PowerPoint", "pdf": "PDF"}`) |
|
||||
|
||||
---
|
||||
|
||||
## Route Map (Administration)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/pres_mgmt` | Overview — sessions list, search, filter by location |
|
||||
| `/events/[id]/pres_mgmt/config` | Config editor (admin only) |
|
||||
| `/events/[id]/session/[session_id]` | Session detail — files, presentations, timing, alert |
|
||||
| `/events/[id]/presenter/[presenter_id]` | Presenter detail — bio, files, agreement, alert |
|
||||
| `/events/[id]/location/[location_id]` | Location detail — session schedule for this room, alert |
|
||||
| `/events/[id]/locations` | All locations list |
|
||||
| `/events/[id]/reports` | Reports — sessions, presenters, files |
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
| Feature | Minimum Access |
|
||||
|---|---|
|
||||
| View pres_mgmt overview | `authenticated_access` |
|
||||
| Upload files | `authenticated_access` |
|
||||
| Edit sessions / presentations | `trusted_access` |
|
||||
| Edit config | `administrator_access` + `edit_mode` |
|
||||
| Device management | `administrator_access` |
|
||||
@@ -1,625 +0,0 @@
|
||||
# PROJECT: AE Events Badges — Review Form & Print Font Controls
|
||||
|
||||
**Created:** 2026-02-27
|
||||
**Last Updated:** 2026-03-18
|
||||
**Branch:** `ae_app_3x_llm`
|
||||
**Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026
|
||||
**Owner:** Scott Idem / One Sky IT
|
||||
**Status:** ✅ TASK 1 COMPLETE | ✅ TASK 2 COMPLETE | ✅ TASK 3 COMPLETE | ✅ TASK 4.1 COMPLETE | ⏳ TASK 4.0 OPEN
|
||||
|
||||
---
|
||||
|
||||
## Design Intent — Two Complementary Flows
|
||||
|
||||
### Flow 1: Remote Badge Review (email link)
|
||||
- Staff emails a review link to the attendee before the event.
|
||||
- Attendee opens the link on their own device, reviews their badge info, and edits permitted fields.
|
||||
- **Email address rule:** Always send to `event_badge.email` — never `email_override`.
|
||||
`email_override` is a display/badge field only. It cannot be trusted as a delivery address
|
||||
(attendee may have changed it to something different for badge display purposes).
|
||||
- Component: `ae_comp__badge_review_form.svelte` — plain form, no badge render.
|
||||
- Route: `/events/[event_id]/badges/[badge_id]/review/`
|
||||
|
||||
### Flow 2: Kiosk / Onsite Badge Station (print page)
|
||||
- Hardware: a laptop + badge printer (Epson fanfold or Zebra PVC card) at the check-in table.
|
||||
- At the event, an attendee walks up to a badge station (check-in kiosk).
|
||||
- A staff member or volunteer pulls up the attendee's badge on the print page.
|
||||
- The **print page is a kiosk tool**, not just a print queue:
|
||||
- Attendee reviews their badge info and can edit permitted fields **in real time**,
|
||||
with the live badge render updating as they make changes.
|
||||
- Staff/volunteers are present to assist with any questions.
|
||||
- Once satisfied, staff prints the badge.
|
||||
- The key differentiator vs the review form: **the live badge render** shows exactly how
|
||||
the badge will print. Attendees and staff can see changes immediately.
|
||||
- Component: `ae_comp__badge_obj_view.svelte`
|
||||
- Route: `/events/[event_id]/badges/[badge_id]/print/`
|
||||
|
||||
### Permission Model — Same Logic, Both Flows
|
||||
Both flows should respect the same permission model:
|
||||
- **Attendee-level** (basic Authenticated access): can edit `pronouns_override`,
|
||||
`full_name_override`, `professional_title_override`, `affiliations_override`,
|
||||
`location_override`, `phone_override`, `email_override`, `allow_tracking`, `agree_to_tc`.
|
||||
- **Staff-level** (trusted_access+): all attendee fields + `email`, `badge_type_code_override`,
|
||||
`badge_type_override`, `hide`, `priority`, `notes`, and font size controls.
|
||||
- Permissions are configured per-event in `event.mod_badges_json.edit_permissions`.
|
||||
Hardcoded defaults are used until that config is implemented.
|
||||
|
||||
**Current gap (TASK 4):** The print page edit button is currently gated to trusted_access only.
|
||||
It needs to be accessible to attendees at the kiosk (with appropriate field-level gating),
|
||||
matching the permission model already implemented in `ae_comp__badge_review_form.svelte`.
|
||||
|
||||
---
|
||||
|
||||
## Next Up for Badges (TASK 4)
|
||||
|
||||
### 0. Kiosk Editing — Print Page Permission Model Alignment
|
||||
**This is the most important gap before the first live event.**
|
||||
|
||||
Currently the print page edit button is staff-only (trusted_access gate). At the kiosk,
|
||||
attendees need to be able to edit their own fields (same attendee-level permissions as the
|
||||
review form), with staff-only fields gated appropriately.
|
||||
|
||||
Work needed:
|
||||
- Wire the same `can_edit_fields` / `can_edit(field)` permission logic into the print page
|
||||
that `ae_comp__badge_review_form.svelte` already uses.
|
||||
- The edit panel on the print page should show attendee-editable fields to all authenticated
|
||||
users, and staff-only fields to trusted_access+.
|
||||
- The badge render (v1 or v2) should update live as the attendee edits fields.
|
||||
- Consider whether the print page needs its own inline edit panel (sidebar or overlay)
|
||||
or whether it should share/reuse the review form component alongside the badge render.
|
||||
- **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`.
|
||||
|
||||
### 1. Auto-Scaling Badge Text — In Progress
|
||||
`ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale).
|
||||
Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button.
|
||||
Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges.
|
||||
|
||||
### 2. QR Code on Badge Front — `ae_comp__badge_obj_view.svelte`
|
||||
The badge template has a `show_qr` flag (or similar). When toggled on, the QR code should
|
||||
appear on the front face of the printed badge. Currently QR is only shown on the review form.
|
||||
|
||||
- Check the badge template fields for the QR toggle field name (`show_qr`, `qr_enabled`, etc.)
|
||||
via `ae_describe event_badge_template` and inspect `ae_comp__badge_obj_view.svelte`.
|
||||
- The QR code data URL is generated with:
|
||||
```typescript
|
||||
qr_data_url = await core_func.js_generate_qr_code('obj', {
|
||||
obj_type: 'event_badge',
|
||||
obj_id: event_badge_id
|
||||
});
|
||||
```
|
||||
See `ae_comp__badge_review_form.svelte` for the working pattern.
|
||||
- Position on badge: typically bottom-right corner of the badge face, sized to fit within
|
||||
the template's layout constraints. Do NOT alter structural badge dimensions.
|
||||
- Must be hidden on `ae_comp__badge_obj_view.svelte` when `show_qr` is falsy.
|
||||
|
||||
### 2. Badge Print Controls — UX Improvements (ae_comp__badge_print_controls.svelte)
|
||||
- Consider: keyboard shortcuts (+ / -) for font sizing while a field is active
|
||||
- Consider: "Apply to all badges" workflow for font size presets
|
||||
|
||||
### 4. Leads Module
|
||||
Next major work after badge polish. See `documentation/MODULE__AE_Events_Leads.md` (if it
|
||||
exists) for context. Exhibitor lead scanning via QR code at exhibitor booth → capture attendee
|
||||
badge data, gated by `allow_tracking` on the badge.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ⏳ TASK 4.0: Kiosk Editing — NOT STARTED (updated 2026-03-18)
|
||||
Print page edit access needs to be opened to attendee-level permissions, not just trusted_access.
|
||||
The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte`
|
||||
should be the reference. See Design Intent section above.
|
||||
|
||||
**Note (2026-03-18):** `style_href` and `duplex` are both fully implemented and verified in code —
|
||||
the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badge render gates
|
||||
`show_badge_back` on it. `style_href` loads via `<svelte:head>` in `print/+page.svelte`.
|
||||
|
||||
### ✅ TASK 4.1: Auto-Scaling Badge Text v2 — COMPLETE (2026-03-12)
|
||||
**Files created/updated:**
|
||||
- `src/lib/elements/action_fit_text.ts` — Svelte action
|
||||
- `src/lib/elements/element_fit_text.svelte` — Component wrapper
|
||||
- `src/routes/events/.../ae_comp__badge_obj_view.svelte` — V2 badge render (canonical)
|
||||
Debug blocks gated behind `$ae_loc.edit_mode` (hidden in production).
|
||||
- `print/+page.svelte` — Always uses v2 now. v1/v2 toggle removed. Header redesigned for kiosk UX.
|
||||
- `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
|
||||
"Staff adjustments" divider before badge_type field.
|
||||
- `print_list/+page.svelte` — Updated to import v2.
|
||||
- `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/agents_trash/**
|
||||
|
||||
**Kiosk UX improvements (2026-03-12):**
|
||||
- Print page header: cleaner, shows name + "Ready"/"Printed N×" status chip, event name.
|
||||
Header Print Now button removed (duplicate); only Re-print shortcut visible in trusted+edit mode.
|
||||
- Controls right panel: identity card at top confirms who the badge belongs to before printing.
|
||||
Pronouns field is now an attendee-level field (was trusted-only). Staff section labelled.
|
||||
- Debug JSON blocks in v2 badge render hidden behind global edit_mode flag.
|
||||
|
||||
**Print page CSS centering work (also 2026-03-12) — ⚠️ Chromium PDF issue pending:**
|
||||
Multiple iterations to center the badge on the printed page, working around SvelteKit layout
|
||||
hierarchy issues. Current approach: `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%)`
|
||||
on `.event_badge_wrapper`. Key findings:
|
||||
|
||||
- `#ae_main_content` has `overflow: auto` — Firefox (spec-compliant) won't let `display: contents`
|
||||
dissolve it. Workaround: explicit passthrough block (`display: block; overflow: visible; width: 100%`).
|
||||
- `app.css` global `overflow: hidden` on `html, body` creates a BFC that collapses body to badge-width.
|
||||
Override with `overflow: visible !important` in print CSS.
|
||||
- **Chrome ignores `@page { size }` for Save as PDF** — verified 2026-03-12. Firefox honors it.
|
||||
Chrome uses the system default paper size; Firefox locks to the CSS `@page { size }` value.
|
||||
Neither browser lets you change paper size in "Save to PDF" mode — only when printing to a
|
||||
physical printer. For actual Epson/Zebra printing, the driver controls paper size; unaffected.
|
||||
- **Chrome "Default" margins cause the "squish"** — Chrome's Default margin setting inserts
|
||||
URL/date/page-number headers and footers into the printable area. These eat into the space
|
||||
that `position: fixed; top: 50%` references, making the badge off-center or clipped. Fix:
|
||||
set **Margins → None** (or Minimum) in Chrome's print dialog. Content centering verified
|
||||
correct: Chrome A4 + None margins produces badge center = 297.5 pts on a 297.5 pt wide page.
|
||||
Use Firefox for accurate PDF proofing.
|
||||
- See `documentation/MODULE__AE_Events_Badge_Templates.md` → "Print Layout Architecture" for full
|
||||
technical details.
|
||||
|
||||
### ✅ TASK 3: Badge Print Controls Panel — COMPLETE (2026-03-02)
|
||||
|
||||
**Files created/modified:**
|
||||
- `ae_comp__badge_print_controls.svelte` — NEW. Right-edge control panel with per-field
|
||||
accordion sections. Font size controls + inline edit forms gated by access level.
|
||||
- `print/+page.svelte` — layout changed from `flex-row` to fixed right panel.
|
||||
|
||||
**Design decisions:**
|
||||
- Controls panel is `position: fixed right-0 top-20 bottom-0 w-64` — out of normal flow,
|
||||
always visible regardless of viewport width. `top-20` (80px) clears the page header.
|
||||
- Badge area gets `pr-64` to prevent content from hiding under the fixed panel.
|
||||
`print:pr-0` and `print:hidden` on the panel restore a clean print layout.
|
||||
- `bg-white dark:bg-zinc-900` gives the panel a solid background to prevent bleed-through.
|
||||
|
||||
**Per-field accordion structure (one open at a time):**
|
||||
|
||||
| Field | Access | Font Controls |
|
||||
| --- | --- | --- |
|
||||
| Name | Trusted+ edit | ✅ |
|
||||
| Professional Title | All auth edit | ✅ |
|
||||
| Affiliations | All auth edit (textarea) | ✅ |
|
||||
| Location | All auth edit | ✅ |
|
||||
| Lead Scanning (allow_tracking) | All auth edit | — |
|
||||
| Pronouns | Trusted+ edit | — |
|
||||
| Badge Type | Trusted+, only when template has badge_type_list | — |
|
||||
|
||||
**Access level note:**
|
||||
`is_trusted = $derived($ae_loc.trusted_access === true)` — covers Trusted, Administrator,
|
||||
Manager, Super (cascade). No need to OR in `administrator_access`.
|
||||
|
||||
**badge_type_override coupling:**
|
||||
When badge type is changed via dropdown, both `badge_type_code_override` AND
|
||||
`badge_type_override` are saved together (name comes from template list). Same behavior in
|
||||
`ae_comp__badge_obj_view.svelte` and `ae_comp__badge_review_form.svelte`.
|
||||
Edge case: custom names (e.g. code=`member`, name=`"Life Member"`) must be set manually in DB.
|
||||
|
||||
**Font size config (moved from print page to controls component):**
|
||||
|
||||
| Field | Default px | Range | Step |
|
||||
|--------------|------------|----------|------|
|
||||
| Name | 58px | 20–80px | 2px |
|
||||
| Title | 34px | 14–56px | 2px |
|
||||
| Affiliations | 38px | 14–60px | 2px |
|
||||
| Location | 34px | 14–56px | 2px |
|
||||
|
||||
Font sizes flow back to the parent via `$bindable()` props so `ae_comp__badge_obj_view`
|
||||
stays in sync without prop-drilling through a third component.
|
||||
|
||||
---
|
||||
|
||||
### ✅ TASK 1: Badge Review Form — COMPLETE
|
||||
|
||||
The badge review form (`ae_comp__badge_review_form.svelte`) is now fully functional with:
|
||||
- ✅ All editable fields with access-level gating
|
||||
- ✅ Print status display section
|
||||
- ✅ QR code generation and display (hover zoom + click expand)
|
||||
- ✅ Options and Tickets fields (staff edit / attendee view)
|
||||
- ✅ Save/Cancel with change detection
|
||||
- ✅ Override field revert buttons
|
||||
- ✅ **HTML rendering** for full_name, professional_title, affiliations, location
|
||||
- ✅ **Accessibility toggle** for text enlargement (text-2xl ↔ text-4xl)
|
||||
- ✅ **Help modal** with 6 sections of attendee guidance (Flowbite Modal component)
|
||||
- ✅ Local edit mode (never writes to `$ae_loc.edit_mode`)
|
||||
|
||||
**Bug fixed (2026-02-27):** `default_authenticated_fields` and `default_trusted_fields` in
|
||||
`review/+page.svelte` had incorrect field names causing `can_edit()` to silently drop saves.
|
||||
Fixed to use exact names matching the form's `can_edit()` checks.
|
||||
|
||||
### ✅ TASK 2: Badge Print Font Controls — COMPLETE (v1)
|
||||
|
||||
Implemented in commit `3d7279da`. This is a **first draft** — auto font scaling using
|
||||
mm/inch units is planned as a future iteration.
|
||||
|
||||
**What was built:**
|
||||
- `ae_comp__badge_obj_view.svelte`: 4 new optional props (`font_size_name`, `font_size_title`,
|
||||
`font_size_affiliations`, `font_size_location`, all `number` in px). When provided, replaces
|
||||
auto inch-based Tailwind class sizing with an inline `font-size: Npx` style. Existing
|
||||
auto-sizing behavior is completely unchanged when props are absent.
|
||||
- `print/+page.svelte`: Screen-only (`print:hidden`) control panel with 4 rows, one per field.
|
||||
Each row: label, `[−]` button, value display (`58px` or `Auto`), `[+]` button, `[↺]` reset.
|
||||
Step: 2px. `null` state = auto (uses existing inch-based auto-sizing). First `+` click
|
||||
activates at a sensible default that approximates the current auto inch values.
|
||||
|
||||
**Default px values when first activated (≈ inch equivalents at 96dpi):**
|
||||
|
||||
| Field | Default px | Approx. inch | Range |
|
||||
|--------------|------------|--------------|----------|
|
||||
| Name | 58px | ≈ .60in | 20–80px |
|
||||
| Title | 34px | ≈ .35in | 14–56px |
|
||||
| Affiliations | 38px | ≈ .40in | 14–60px |
|
||||
| Location | 34px | ≈ .35in | 14–56px |
|
||||
|
||||
**Future:** Auto font scaling using mm/inch units (physical paper stock measurements).
|
||||
Will likely need to revisit the inch ↔ mm conversion and potentially expose the auto-sizing
|
||||
logic as adjustable rather than replacing it with px overrides.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The Events Badges module is mostly complete for navigation and search. Two key pieces of
|
||||
functional UI were needed before the first show:
|
||||
|
||||
1. **Badge Review Form** ✅ — `ae_comp__badge_review_form.svelte` now has complete field
|
||||
rendering, edit inputs gated by access level, save/cancel API calls, and display-only
|
||||
sections (QR code, print status, option/ticket checkmarks). Also includes accessibility
|
||||
features (text enlargement) and help modal for attendee guidance.
|
||||
|
||||
2. **Badge Print Font Controls** ⏳ — The print page header needs screen-only controls
|
||||
(hidden during `window.print()`) to bump font sizes for the name, professional title,
|
||||
affiliations, and location sections before printing. These only affect the `ae_comp__badge_obj_view.svelte` render — not the page layout/template structural dimensions.
|
||||
|
||||
Read `documentation/MODULE__AE_Events_Badges.md` for full module context before starting.
|
||||
|
||||
---
|
||||
|
||||
## MANDATORY: Before You Start
|
||||
|
||||
1. Run `ae_describe event_badge` (MCP tool) to confirm which fields actually exist in the
|
||||
DB. Several fields in the spec below may need to be added to `properties_to_save` in
|
||||
`src/lib/ae_events/ae_events__event_badge.ts` if they are not already saved to IDB.
|
||||
|
||||
2. Fields to specifically confirm exist in `event_badge` schema:
|
||||
- `pronouns`, `pronouns_override`
|
||||
- `phone`, `phone_override`
|
||||
- `allow_tracking`
|
||||
- `agree_to_tc`
|
||||
- `other_1_code` through `other_8_code` (the "option" fields)
|
||||
- `ticket_1_code` through `ticket_8_code`
|
||||
- `registration_type`, `registration_type_code`
|
||||
- `registration_type_override`, `registration_type_code_override`
|
||||
|
||||
3. Run `npx svelte-check` before committing. Baseline is **77 errors** (all pre-existing,
|
||||
none in the badge module files). Do not introduce new errors.
|
||||
|
||||
4. Do NOT write to `$ae_loc.edit_mode` from any badge component. This was a critical
|
||||
bug (fixed 2026-02-27). See `documentation/AE__Permissions_and_Security.md`.
|
||||
|
||||
---
|
||||
|
||||
## TASK 1: Badge Review Form (HIGH PRIORITY)
|
||||
|
||||
### File to build
|
||||
`src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_review_form.svelte`
|
||||
|
||||
This component is already imported and used by `review/+page.svelte`. Props it receives:
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
event_id: string;
|
||||
event_badge_id: string;
|
||||
lq__event_badge_obj: any; // Svelte 5 store from liveQuery
|
||||
can_edit_fields: string[]; // Which fields this user can edit
|
||||
is_staff: boolean; // True if trusted_access or higher
|
||||
log_lvl?: number;
|
||||
}
|
||||
```
|
||||
|
||||
`can_edit_fields` values:
|
||||
- `['*']` — administrator (all fields)
|
||||
- Array of field names — specific editable fields
|
||||
- `[]` — read-only (shouldn't normally reach this component, but handle it)
|
||||
|
||||
### Helper
|
||||
|
||||
Use a helper derived inside the component:
|
||||
```typescript
|
||||
function can_edit(field: string): boolean {
|
||||
return can_edit_fields.includes('*') || can_edit_fields.includes(field);
|
||||
}
|
||||
```
|
||||
|
||||
### Save / Cancel Pattern
|
||||
|
||||
Follow the Journals module pattern (`src/lib/ae_journals/`). Key points:
|
||||
- Use `import { events_func } from '$lib/ae_events_functions'`
|
||||
- Call `events_func.update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })`
|
||||
- Only send changed fields in `data_kv` (compare against `$lq__event_badge_obj` values)
|
||||
- Show save/cancel buttons only when something has changed (`has_changes` derived)
|
||||
- Show a success/error state briefly after save (1-2 seconds, then reset)
|
||||
- Cancel resets local state back to `$lq__event_badge_obj` values
|
||||
- Use `data-testid="badge-review-save-btn"` and `data-testid="badge-review-cancel-btn"`
|
||||
|
||||
### Save API Call
|
||||
|
||||
```typescript
|
||||
await events_func.update_ae_obj__event_badge({
|
||||
api_cfg: $ae_api, // from ae_loc store or passed as prop — check how ae_comp__badge_obj_view.svelte does it
|
||||
event_badge_id: event_badge_id,
|
||||
event_id: event_id,
|
||||
data_kv: { /* only changed fields */ }
|
||||
});
|
||||
```
|
||||
|
||||
Check `ae_comp__badge_obj_view.svelte` for the existing save pattern — it already works
|
||||
and can be used as reference.
|
||||
|
||||
---
|
||||
|
||||
### Section 1: Display-Only Status Bar (all access levels) ✅ IMPLEMENTED
|
||||
|
||||
Always show at top of form. Read-only. No edit controls.
|
||||
|
||||
```
|
||||
Print Status: [Not yet printed] OR [Printed 3× — first: Jan 5 2026, last: Jan 5 2026]
|
||||
```
|
||||
|
||||
**Implemented:** Shows print count with first/last print datetimes. Hidden if `print_count < 1`.
|
||||
Uses `$lq__event_badge_obj.print_count`, `print_first_datetime`, `print_last_datetime`.
|
||||
Format datetimes with `ae_util.iso_datetime_formatter(dt, 'datetime_iso_12_no_seconds')`.
|
||||
Import `ae_util` from `$lib/ae_utils/ae_utils`.
|
||||
|
||||
---
|
||||
|
||||
### Section 2: QR Code (all access levels) ✅ IMPLEMENTED
|
||||
|
||||
Display the attendee's badge QR code. This is the same QR code shown on the printed badge
|
||||
itself — scanning it at the badge station triggers automatic badge search and print.
|
||||
|
||||
**Implemented using `core_func.js_generate_qr_code()`:**
|
||||
```typescript
|
||||
qr_data_url = await core_func.js_generate_qr_code('obj', {
|
||||
obj_type: 'event_badge',
|
||||
obj_id: event_badge_id
|
||||
});
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Hover: Zoom overlay effect (`qr_hovered` state)
|
||||
- Click: Expand/collapse that pushes content down (`qr_expanded` state)
|
||||
- Displays as data URL image from QR code generation
|
||||
- Reactive: automatically regenerates when `event_badge_id` changes
|
||||
|
||||
---
|
||||
|
||||
### Section 3: Editable Fields ✅ IMPLEMENTED
|
||||
|
||||
Render each field as: read-only display when `!can_edit(field)`, or an `<input>` /
|
||||
`<select>` / `<textarea>` when `can_edit(field)`.
|
||||
|
||||
Show `(overridden)` label next to override fields when the override value differs from
|
||||
the base field value.
|
||||
|
||||
**HTML Rendering (implemented 2026-02-27):**
|
||||
The following fields render HTML markup using `{@html}` when viewing (not when editing):
|
||||
- `full_name_override` / `full_name`
|
||||
- `professional_title_override` / `professional_title`
|
||||
- `affiliations_override` / `affiliations`
|
||||
- `location_override` / `location`
|
||||
|
||||
This allows for rich text formatting (bold, italic, line breaks, etc.) in badge displays.
|
||||
|
||||
**Accessibility Features (implemented 2026-02-27):**
|
||||
- Text enlargement toggle button in sticky header
|
||||
- Normal size: `text-2xl` on field values
|
||||
- Enlarged size: `text-4xl` on field values
|
||||
- Button shows visual feedback (gray → blue, "Larger" → "Normal" label)
|
||||
- Applied consistently across all text fields
|
||||
|
||||
**Help Modal (implemented 2026-02-27):**
|
||||
- Flowbite Modal component with 6 sections
|
||||
- Sections: Reviewing Badge, Editing Info, Accessibility, QR Code, Lead Scanning, Assistance
|
||||
- Triggered by Help button (BadgeQuestionMark icon) in sticky header
|
||||
- Currently has placeholder text — can be customized per event/client
|
||||
|
||||
#### Attendee-Editable Fields (shown to all access levels with link)
|
||||
|
||||
| Field | Input Type | Notes |
|
||||
|---|---|---|
|
||||
| `pronouns_override` | text input | Fallback display: `pronouns` |
|
||||
| `full_name_override` | text input | Fallback display: `full_name`; **renders HTML** when viewing |
|
||||
| `professional_title_override` | text input | Fallback display: `professional_title`; **renders HTML** when viewing |
|
||||
| `affiliations_override` | textarea | Fallback display: `affiliations`; **renders HTML** when viewing |
|
||||
| `phone_override` | text input (tel) | Fallback display: `phone` |
|
||||
| `location_override` | text input | Fallback display: `location`; **renders HTML** when viewing |
|
||||
| `allow_tracking` | checkbox | Label: "Allow exhibitor lead scanning" |
|
||||
| `agree_to_tc` | checkbox | Label: "I agree to the Terms and Conditions" + placeholder T&C text block |
|
||||
|
||||
#### Staff-Only Additional Fields (shown when `is_staff === true`) ✅ IMPLEMENTED
|
||||
|
||||
| Field | Input Type | Notes |
|
||||
|---|---|---|
|
||||
| `email_override` | email input | Fallback display: `email` |
|
||||
| `badge_type_code_override` | select | Options: member, non-member, guest, exhibitor, staff, test; also updates `badge_type_override` text |
|
||||
|
||||
> **Edge case — custom badge type name:** If an attendee needs a standard badge type code (for
|
||||
> CSS styling) but a slightly different displayed name (e.g. code=`member`, name=`"Life Member"`),
|
||||
> set `badge_type_override` directly in the DB. Do **not** use the dropdown — selecting from the
|
||||
> dropdown in the UI overwrites `badge_type_override` with the standard name from the template
|
||||
> list. This is an intentional trade-off: coded for the normal case (dropdown keeps both fields in
|
||||
> sync), special cases handled manually by Scott in the DB.
|
||||
| `registration_type_code_override` | select | Same options as badge_type for now; also updates `registration_type_override` |
|
||||
| `hide` | checkbox | Label: "Hidden from search results" |
|
||||
| `priority` | number input | |
|
||||
| `notes` | textarea | |
|
||||
|
||||
#### Staff-Only: Options & Tickets (read-edit, shown when `is_staff === true`) ✅ IMPLEMENTED
|
||||
|
||||
**Other/Options** (`other_1_code` through `other_8_code`):
|
||||
- If field has a value: show as editable text input with label "Option X"
|
||||
- If field is empty/null: show faintly as "Option X (empty)" — staff can still set it
|
||||
- These represent event-specific add-ons or membership indicators
|
||||
|
||||
**Tickets** (`ticket_1_code` through `ticket_8_code`):
|
||||
- Same pattern as options above, label "Ticket X"
|
||||
|
||||
#### Attendee-Only: Options & Tickets (display only) ✅ IMPLEMENTED
|
||||
|
||||
When `!is_staff` and the field has a value: show `[✓] Option X` or `[✓] Ticket X`.
|
||||
When the field is empty: hide entirely (attendees don't see empty slots).
|
||||
|
||||
---
|
||||
|
||||
### Section 4: Terms & Conditions Block (all, only when `agree_to_tc` in can_edit_fields) ✅ IMPLEMENTED
|
||||
|
||||
Placeholder text for now:
|
||||
```
|
||||
By checking this box, I confirm that the information on my badge is correct to the best
|
||||
of my knowledge. I agree that this badge may be used for identification purposes during
|
||||
the event and that my attendance may be recorded by exhibitors using the lead scanning
|
||||
feature if I permit it.
|
||||
```
|
||||
|
||||
Show this before the `agree_to_tc` checkbox. If `agree_to_tc` is not in `can_edit_fields`,
|
||||
hide the entire block.
|
||||
|
||||
---
|
||||
|
||||
### Field State Pattern (Svelte 5 runes)
|
||||
|
||||
```typescript
|
||||
// Initialize local editable state from badge object
|
||||
let local_full_name_override = $state($lq__event_badge_obj?.full_name_override ?? '');
|
||||
let local_pronouns_override = $state($lq__event_badge_obj?.pronouns_override ?? '');
|
||||
// ... etc for each editable field
|
||||
|
||||
// Detect changes
|
||||
let has_changes = $derived(
|
||||
local_full_name_override !== ($lq__event_badge_obj?.full_name_override ?? '')
|
||||
|| local_pronouns_override !== ($lq__event_badge_obj?.pronouns_override ?? '')
|
||||
// ... etc
|
||||
);
|
||||
|
||||
// Build changed-fields-only payload
|
||||
function build_save_payload(): Record<string, any> {
|
||||
const payload: Record<string, any> = {};
|
||||
if (local_full_name_override !== ($lq__event_badge_obj?.full_name_override ?? ''))
|
||||
payload.full_name_override = local_full_name_override || null; // empty string → null
|
||||
// ... etc
|
||||
return payload;
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Empty string inputs should save as `null` (clears the override, falls back
|
||||
to base field). Use `value || null` in the payload.
|
||||
|
||||
---
|
||||
|
||||
## TASK 2: Badge Print Font Size Controls (MEDIUM PRIORITY)
|
||||
|
||||
### Where to add
|
||||
|
||||
`src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte`
|
||||
|
||||
Add a screen-only (`print:hidden`) control panel between the header and the badge render.
|
||||
This panel lets staff adjust font sizes for the four text-heavy sections before clicking Print.
|
||||
|
||||
### Controls needed
|
||||
|
||||
```
|
||||
Font Size Controls (screen only, hidden during print):
|
||||
[Name] [−] [14px] [+]
|
||||
[Title] [−] [12px] [+]
|
||||
[Affiliations] [−] [11px] [+]
|
||||
[Location] [−] [10px] [+]
|
||||
```
|
||||
|
||||
- Start with sensible defaults (match what `ae_comp__badge_obj_view.svelte` currently uses)
|
||||
- Min/max per field (e.g., 8px–24px for name, 7px–18px for others)
|
||||
- Pass the sizes as props into `ae_comp__badge_obj_view`
|
||||
|
||||
### Props to add to `ae_comp__badge_obj_view.svelte`
|
||||
|
||||
`ae_comp__badge_obj_view.svelte` currently has internal font size logic. It needs to
|
||||
accept optional override props:
|
||||
|
||||
```typescript
|
||||
// New optional props:
|
||||
font_size_name?: number; // px
|
||||
font_size_title?: number; // px
|
||||
font_size_affiliations?: number; // px
|
||||
font_size_location?: number; // px
|
||||
```
|
||||
|
||||
When these props are provided, use them instead of the internally computed sizes.
|
||||
When not provided, fall back to existing auto-sizing behavior.
|
||||
|
||||
**IMPORTANT:** Do NOT touch structural dimensions (overall badge width/height, header/footer
|
||||
sizes, template layout). Only the text content font sizes.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `[badge_id]/ae_comp__badge_review_form.svelte` | **BUILD THIS** — review form stub |
|
||||
| `[badge_id]/ae_comp__badge_obj_view.svelte` | Badge render + print button; add font size props |
|
||||
| `[badge_id]/print/+page.svelte` | Print page; add font size control panel |
|
||||
| `[badge_id]/review/+page.svelte` | Review page; already wired, passes `can_edit_fields` |
|
||||
| `src/lib/ae_events/ae_events__event_badge.ts` | API functions: `update_ae_obj__event_badge` |
|
||||
| `src/lib/ae_events/db_events.ts` | Dexie schema — `properties_to_save` for badge |
|
||||
| `src/lib/ae_utils/ae_utils.ts` | `ae_util.iso_datetime_formatter()` |
|
||||
| `documentation/MODULE__AE_Events_Badges.md` | Full module reference |
|
||||
| `documentation/AE__Permissions_and_Security.md` | Permission flags, edit_mode rules |
|
||||
| `documentation/GUIDE__AE_API_V3_for_Frontend.md` | V3 API reference |
|
||||
|
||||
## Access Level Reference
|
||||
|
||||
```typescript
|
||||
// From $ae_loc store (persisted localStorage)
|
||||
$ae_loc.trusted_access // true = trusted and above (onsite staff)
|
||||
$ae_loc.administrator_access // true = administrator and above
|
||||
$ae_loc.edit_mode // boolean — user preference toggle (NEVER write to this from components)
|
||||
```
|
||||
|
||||
`is_staff` prop on the review form = `$ae_loc.trusted_access`.
|
||||
`trusted_access` is `true` for Trusted and every level above it (Administrator, Manager, Super)
|
||||
— no need to OR in `administrator_access` since it's already implied by the cascade.
|
||||
|
||||
---
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
- **Canonical module reference:** `src/lib/ae_journals/` — most complete, most advanced
|
||||
- **Svelte 5 runes:** `$state`, `$derived`, `$derived.by()`, `$effect` — no legacy `$:` syntax
|
||||
- **Icons:** Lucide Svelte only — `import { Save, X, Check, ... } from 'lucide-svelte'`
|
||||
- **No Font Awesome** (`fas fa-*`) anywhere in the badge module
|
||||
- **Styling:** Tailwind CSS v4 + Skeleton UI utility classes (`btn`, `preset-tonal-*`, `input`, `card`)
|
||||
- **Commits:** Atomic — one component per commit; run `npx svelte-check` before every commit
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
- Do NOT touch `@page` CSS or badge template structural dimensions — print layout is out of scope
|
||||
- Do NOT write to `$ae_loc.edit_mode` from any component
|
||||
- Do NOT connect `mod_badges_json.edit_permissions` yet — hardcoded field lists are intentional for now
|
||||
- Do NOT implement the email API — `send_review_email()` placeholder stays as `alert()`
|
||||
- Do NOT add `person_passcode` DB field — out of scope for this sprint
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Run existing badge tests after any changes:
|
||||
```bash
|
||||
npm run test:unit
|
||||
npx playwright test tests/events/badges/
|
||||
```
|
||||
|
||||
Baseline: all badge tests passing as of 2026-02-26 (`f5e98b8c`).
|
||||
|
||||
Add `data-testid` attributes to key interactive elements:
|
||||
- `badge-review-save-btn`
|
||||
- `badge-review-cancel-btn`
|
||||
- `badge-review-full-name-input`
|
||||
- `badge-review-agree-to-tc-checkbox`
|
||||
@@ -1,238 +0,0 @@
|
||||
# Project: Pres Mgmt Config Cleanup & Config UI
|
||||
|
||||
**Status:** Planning / Ready to Execute
|
||||
**Priority:** High (BGH conference in ~2 weeks; only one active event using pres_mgmt)
|
||||
**Created:** 2026-04-02
|
||||
**Related:** `TODO__Agents.md`, `PROJECT__Stores_Svelte5_Migration.md`
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The `event.mod_pres_mgmt_json` config grew organically across several conferences
|
||||
(LCI, BGH, etc.) and has accumulated serious inconsistencies:
|
||||
|
||||
- Mixed `show__` and `hide__` prefixes for the same concepts
|
||||
- Some features have BOTH `show__foo` and `hide__foo` keys active simultaneously
|
||||
- Duplicate keys with different names (`file_purpose_option_kv` = `file_purpose_option_li`)
|
||||
- Dead config (`HOLD__*` prefix)
|
||||
- Type inconsistency (`label__person_external_id: false` vs `"LCI member ID"` string)
|
||||
- Keys in the DB not consumed by `sync_config__event_pres_mgmt()`
|
||||
- Bug: `label__session_poc_name_short` is read then immediately overwritten (line 970-972 in ae_events__event.ts)
|
||||
- `hide_launcher_link` / `hide_launcher_link_legacy` missing the `__` separator (inconsistent)
|
||||
- `show_content__presentation_description` uses a third naming convention
|
||||
- Admin must edit DB records directly to change config — error-prone
|
||||
|
||||
The local config (`events_loc.pres_mgmt`) is also tangled into the main `events_loc`
|
||||
persisted store which is part of the paused Svelte 5 migration.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Canonical config schema** — define a TypeScript interface for `mod_pres_mgmt_json`
|
||||
2. **Consistent naming convention** — one rule for all `show__`/`hide__` keys
|
||||
3. **New Svelte 5 store** — break out local pres_mgmt config from `events_loc`
|
||||
4. **Config UI** — admin page within pres_mgmt to manage the remote config
|
||||
5. **No more direct DB edits** for routine pres_mgmt configuration
|
||||
|
||||
---
|
||||
|
||||
## Convention Decision
|
||||
|
||||
**Rule: the prefix reflects the default state.**
|
||||
|
||||
| Prefix | Default | Use for |
|
||||
|--------|---------|---------|
|
||||
| `hide__` | `false` = visible | Features ON by default that can be turned off |
|
||||
| `show__` | `false` = hidden | Features OFF by default that can be turned on |
|
||||
|
||||
**Never have both `show__foo` and `hide__foo` for the same concept.**
|
||||
|
||||
- Visibility controls (codes, descriptions, POC, biography) → default visible → `hide__`
|
||||
- Opt-in features (access links, launcher, QR links) → default hidden → `show__`
|
||||
|
||||
---
|
||||
|
||||
## Canonical Remote Config Schema
|
||||
|
||||
`PressMgmtRemoteCfg` — the authoritative TypeScript interface for `event.mod_pres_mgmt_json`:
|
||||
|
||||
```typescript
|
||||
interface PressMgmtRemoteCfg {
|
||||
// System
|
||||
lock_config: boolean; // true = force remote→local sync (prevent user overrides)
|
||||
|
||||
// Labels (event-specific terminology overrides)
|
||||
label__person_external_id: string | null; // default: 'External ID'
|
||||
label__presenter_external_id: string | null; // default: 'External ID'
|
||||
label__session_poc_type: string | null; // e.g. 'champion', 'poc'
|
||||
label__session_poc_name: string | null; // e.g. 'Champion', 'Point of Contact'
|
||||
|
||||
// Codes (visible by default — hide to suppress)
|
||||
hide__location_code: boolean;
|
||||
hide__presentation_code: boolean;
|
||||
hide__presenter_code: boolean;
|
||||
hide__session_code: boolean;
|
||||
|
||||
// Session fields (visible by default)
|
||||
hide__session_description: boolean;
|
||||
hide__session_location: boolean;
|
||||
hide__session_msg: boolean;
|
||||
hide__session_poc: boolean;
|
||||
hide__session_poc_biography: boolean;
|
||||
hide__session_poc_profile_pic: boolean;
|
||||
|
||||
// Presenter fields
|
||||
hide__presenter_biography: boolean;
|
||||
|
||||
// Presentation fields
|
||||
hide__presentation_datetime: boolean;
|
||||
hide__presentation_description: boolean; // replaces show_content__presentation_description
|
||||
|
||||
// Opt-in features (hidden by default — show to enable)
|
||||
show__copy_access_link: boolean;
|
||||
show__email_access_link: boolean;
|
||||
show__launcher_link: boolean;
|
||||
show__launcher_link_legacy: boolean;
|
||||
|
||||
// Requirements
|
||||
require__presenter_agree: boolean;
|
||||
require__session_agree: boolean;
|
||||
|
||||
// Navigation/UI constraints
|
||||
limit__navigation: boolean;
|
||||
limit__options: boolean;
|
||||
|
||||
// File upload config
|
||||
file_purpose_option_kv: Record<string, {
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
}> | null;
|
||||
|
||||
// Report visibility (key = report slug, value = true to hide)
|
||||
hide__report_kv: Record<string, boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
### Keys Removed vs. Current DB Records
|
||||
|
||||
| Removed Key | Reason |
|
||||
|-------------|--------|
|
||||
| `file_purpose_option_li` | Duplicate of `file_purpose_option_kv` |
|
||||
| `HOLD__file_os_selection_option` | Dead/held feature |
|
||||
| `hide__copy_access_link` | Conflicts with `show__copy_access_link` — use `show__` |
|
||||
| `hide__email_access_link` | Conflicts with `show__email_access_link` — use `show__` |
|
||||
| `hide__launcher_link` | Conflicts with `show__launcher_link` — use `show__` |
|
||||
| `hide__launcher_link_legacy` | Conflicts with `show__launcher_link_legacy` — use `show__` |
|
||||
| `hide__report_li` | Superseded by `hide__report_kv` |
|
||||
| `show__navigation` | Ambiguous — covered by `limit__navigation` |
|
||||
| `label__session_poc_name_short` | Was a bug — never applied (overwritten immediately) |
|
||||
| `show_content__presentation_description` | Renamed to `hide__presentation_description` |
|
||||
|
||||
---
|
||||
|
||||
## New Svelte 5 Local Store
|
||||
|
||||
**Do NOT touch `events_loc` or the paused Svelte 5 migration.**
|
||||
Instead, create a standalone store for pres_mgmt local config.
|
||||
|
||||
**File:** `src/lib/stores/ae_events_stores__pres_mgmt.svelte.ts`
|
||||
|
||||
```typescript
|
||||
import { PersistedState } from 'runed';
|
||||
import { pres_mgmt_loc_defaults } from './ae_events_stores__pres_mgmt_defaults';
|
||||
|
||||
export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults);
|
||||
// Usage: pres_mgmt_loc.current.hide__session_code
|
||||
```
|
||||
|
||||
- New localStorage key: `ae_pres_mgmt_loc` (separate from `ae_events_loc`)
|
||||
- Version gate: add `AE_PRES_MGMT_LOC_VERSION` to `store_versions.ts`
|
||||
- `sync_config__event_pres_mgmt()` writes to `pres_mgmt_loc.current` directly
|
||||
|
||||
Consumer syntax change:
|
||||
```
|
||||
BEFORE: $events_loc.pres_mgmt.hide__session_code
|
||||
AFTER: pres_mgmt_loc.current.hide__session_code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Config UI Page
|
||||
|
||||
**Route:** `/events/[event_id]/(pres_mgmt)/pres_mgmt/config/`
|
||||
**Access:** `$ae_loc.manager_access` only
|
||||
**Button visibility:** Edit mode only (`$ae_loc.edit_mode`)
|
||||
|
||||
### Page behavior
|
||||
- Loads `event.mod_pres_mgmt_json` fresh from API on page open
|
||||
- Displays grouped form sections (see below)
|
||||
- Save = load → merge → PATCH `/v3/crud/event/{event_id}` with `{ mod_pres_mgmt_json: updated }`
|
||||
- The existing settings form at `/events/[id]/settings` has its pres_mgmt section removed or replaced with a link
|
||||
|
||||
### Form sections (grouped)
|
||||
|
||||
1. **System** — `lock_config`
|
||||
2. **Labels** — `label__*` fields (text inputs, nullable)
|
||||
3. **Session Visibility** — `hide__session_*` toggles
|
||||
4. **Presenter Visibility** — `hide__presenter_*` toggles
|
||||
5. **Presentation Visibility** — `hide__presentation_*` toggles
|
||||
6. **Code Visibility** — `hide__*_code` toggles
|
||||
7. **Opt-in Features** — `show__*` toggles
|
||||
8. **Requirements** — `require__presenter_agree`, `require__session_agree`
|
||||
9. **Navigation Limits** — `limit__navigation`, `limit__options`
|
||||
10. **File Purpose Config** — `file_purpose_option_kv` (JSON editor or structured form)
|
||||
11. **Report Visibility** — `hide__report_kv` (key-value toggles)
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
Safe and backward compatible — old DB records fall through to `?? false` defaults.
|
||||
|
||||
1. No DB migration script needed — old keys are simply ignored by the updated sync function
|
||||
2. Active events (BGH) get updated via the new UI after it's built
|
||||
3. The `sync_config__event_pres_mgmt()` rewrite is the critical step — it must handle the
|
||||
canonical keys and clean defaults before the UI ships
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
- [ ] **Step 1** — Define `PressMgmtRemoteCfg` TypeScript interface (new file or in `ae_events__event.ts`)
|
||||
- [ ] **Step 2** — New `ae_events_stores__pres_mgmt.svelte.ts` with `PersistedState`; add version gate to `store_versions.ts`
|
||||
- [ ] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys and write to the new store
|
||||
- [ ] **Step 4** — Build config UI page at `(pres_mgmt)/pres_mgmt/config/+page.svelte` (manager_access + edit_mode gated)
|
||||
- [ ] **Step 5** — Strip `ae_comp__event_settings_pres_mgmt_form.svelte` from settings page (or replace with a link to new page)
|
||||
- [ ] **Step 6** — Migrate all `$events_loc.pres_mgmt.*` references in pres_mgmt templates to `pres_mgmt_loc.current.*`
|
||||
- [ ] **Step 7** — Update BGH (and any other active events) via new UI
|
||||
- [ ] **Step 8** — `npx svelte-check` clean; commit
|
||||
|
||||
### Step 6 scope (mechanical find-replace)
|
||||
|
||||
The `$events_loc.pres_mgmt` pattern appears across:
|
||||
- `ae_comp__event_session_obj_li.svelte`
|
||||
- `ae_comp__events_menu_opts.svelte`
|
||||
- `session/[session_id]/+page.svelte`
|
||||
- `session/[session_id]/session_view.svelte`
|
||||
- `session/[session_id]/session_page_menu.svelte`
|
||||
- `locations/locations_page_menu.svelte`
|
||||
- `reports/+page.svelte`
|
||||
- `pres_mgmt/+page.svelte`
|
||||
- (and likely others — run `grep -r 'events_loc.pres_mgmt' src/` to get full list)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The `lock_config: true` default means most events will always sync from remote.
|
||||
This is intentional — it prevents presenter laptops from drifting into different configs.
|
||||
- `file_purpose_option_kv` may need a structured editor (not raw JSON) to be usable.
|
||||
Consider a simple key-value form row per purpose type for Phase 2.
|
||||
- QR link keys (`hide__presenter_qr_link`, `hide__session_qr_link`) appeared in LCI config
|
||||
but are not in the canonical schema above. Evaluate whether they're actively used before
|
||||
adding them back.
|
||||
- `limit__navigation` and `limit__options` are in the DB but not currently read by
|
||||
`sync_config__event_pres_mgmt()`. Confirm where they're consumed before adding to sync.
|
||||
@@ -1,51 +0,0 @@
|
||||
# Project Plan: Aether AE Obj Field Editor v3 (Consolidated)
|
||||
|
||||
> **Status:** 🟡 Mostly Complete — Phase 3 items + GUIDE update remaining
|
||||
> **Date:** February 13, 2026 (last updated: 2026-03-20)
|
||||
> **Target Component:** `src/lib/elements/element_ae_obj_field_editor.svelte`
|
||||
> **Replaces:** `element_ae_crud.svelte` and `element_ae_crud_v2.svelte`
|
||||
|
||||
## 1. Overview
|
||||
Consolidate the legacy CRUD components into a single, high-performance "Aether Object Field Editor" (v3). This component will be the standard for single-property editing across the platform, fully aligned with the FastAPI V3 CRUD patterns and Svelte 5 Runes.
|
||||
|
||||
## 2. Strategic Objectives
|
||||
- **Consolidation:** Retire `v1` and `v2` components in favor of a single, unified codebase.
|
||||
- **API Alignment:** Native support for `PATCH /v3/crud/{obj_type}/{obj_id}`.
|
||||
- **Svelte 5 Runes:** Pure `$props`, `$state`, and `$derived` implementation. No legacy imports.
|
||||
- **Callback Pattern:** Replace `createEventDispatcher` with callback props (`on_patch`, `on_success`, `on_error`).
|
||||
- **Iconography:** Standardize on **Lucide-Svelte**.
|
||||
- **Mobile-First:** Improved "Tap to Edit" targets and mobile-responsive popovers.
|
||||
|
||||
## 3. Implementation Phases
|
||||
|
||||
### Phase 1: Foundation & Reactivity (COMPLETED)
|
||||
- [x] Create the new `v3` component shell.
|
||||
- [x] Implement strict TypeScript interface for Props.
|
||||
- [x] Use `$state` for local "draft" values to prevent reactivity loops with the global store.
|
||||
- [x] Implement the `handle_patch` logic using the central `api.patch` helper.
|
||||
|
||||
### Phase 2: UI & UX Refinement (COMPLETED)
|
||||
- [x] Standardize Tailwind classes (using Tailwind 4 patterns).
|
||||
- [x] Implement "Edit Mode" awareness (syncing with `$ae_loc.edit_mode`).
|
||||
- [x] Add a "Save" loading state with Lucide's `LoaderCircle` spinner.
|
||||
- [x] Implement a clear "Cancel" path that restores the original value.
|
||||
|
||||
### Phase 3: Field Type Parity (IN PROGRESS)
|
||||
- [x] Support `text`, `textarea`, `select`, `tiptap`, and `checkbox`.
|
||||
- [x] Add `datetime` support using native browser pickers — `date` and `datetime-local` inputs implemented.
|
||||
- [ ] Implement searchable dropdowns for the `select` type.
|
||||
|
||||
### Phase 4: Migration & Cleanup
|
||||
- [x] Create a playground route for V3 verification (`/testing/ae_obj_field_editor`).
|
||||
- [x] Deprecate and remove `v1` and `v2` files — `element_ae_crud.svelte` and `element_ae_crud_v2.svelte` removed 2026-03-20.
|
||||
- [ ] Update `GUIDE__Development.md` with the new usage patterns.
|
||||
|
||||
## ⚠️ Security & Reliability Stabilization (NEW)
|
||||
- [x] **Account Context:** Fixed 403 errors by unifying API helpers to the `/v3/crud/` standard.
|
||||
- [x] **Race Conditions:** Implemented `localStorage` scavenging for Account IDs to fix Svelte 5 hydration lags.
|
||||
- [x] **Protocol Hygiene:** Purged redundant/misplaced headers (`x-aether-api-token`, `Access-Control-Allow-Origin`).
|
||||
|
||||
## 4. Maintenance & Standards
|
||||
- Component must respect `$ae_loc.trusted_access` for visibility of edit triggers.
|
||||
- Always use `type="button"` for internal actions to prevent form collisions.
|
||||
- Maintain the `object_reload` pattern for SWR cache invalidation.
|
||||
@@ -1,408 +0,0 @@
|
||||
# PROJECT: Site Passcode Security — API-Verified Auth
|
||||
|
||||
**Last updated:** 2026-04-10
|
||||
**Status:** Backend work in progress — frontend pending backend completion
|
||||
**Priority:** High — passcodes for trusted/administrator access currently in localStorage plaintext
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
When a user loads the Aether frontend, the site bootstrap response includes `access_code_kv_json` — a JSON object containing all passcodes for all access levels (administrator, trusted, public, authenticated). The frontend stores this verbatim in `$ae_loc.site_access_code_kv`, which is persisted in localStorage.
|
||||
|
||||
**Result:** Anyone with DevTools → Application → Local Storage can see every passcode for every access level on any Aether site. For public/authenticated this is low risk, but for trusted and administrator this is a real exposure — these passcodes can grant control over event data, badge printing, edit mode, etc.
|
||||
|
||||
The passcode check (`handle_check_access_type_passcode` in `e_app_access_type.svelte`) is entirely local — it reads the cached values and compares directly. No API call is made. The backend already has a `/authenticate_passcode` endpoint that verifies server-side, but it needs the fixes described below before the frontend can rely on it.
|
||||
|
||||
### Source of Truth
|
||||
|
||||
`site.access_code_kv_json` is the single source of truth for all passcodes. The `v_site_domain` DB view joins this field from the site table — there is no separate copy. Both the bootstrap response and `/authenticate_passcode` read from the same data.
|
||||
|
||||
---
|
||||
|
||||
## Threat Model
|
||||
|
||||
| Threat | Current | After Fix |
|
||||
|---|---|---|
|
||||
| Attacker inspects localStorage | Sees all passcodes in plaintext | Sees a JWT (opaque, no passcode) |
|
||||
| Attacker uses stolen trusted passcode | Trivial if they have localStorage access | Still possible if they enter the passcode — unavoidable |
|
||||
| Attacker replays an old passcode after it changes | Works forever (cached value never refreshes) | Fails — API verifies against current DB value |
|
||||
| Attacker tampers with `access_type` in localStorage | Grants apparent permission but API calls still fail | Same — `access_type` is still persisted separately |
|
||||
| Passcode reuse across sessions | Works indefinitely | JWT TTL enforces session expiry per role |
|
||||
| Offline / API-unavailable entry | Works (local cache) | **Blocked** — requires API to verify |
|
||||
|
||||
### The fundamental constraint
|
||||
|
||||
Passcode-based access is inherently weaker than username/password login with a hashed credential. The system's security model layers passcode access below user login, and API calls themselves are still gated by `x-aether-api-key` + `x-account-id`. The passcode primarily controls **what the frontend shows** and some API-level permission gates for trusted routes.
|
||||
|
||||
---
|
||||
|
||||
## Proposed Solution: API-Verified Passcode + JWT Session
|
||||
|
||||
### Core idea
|
||||
|
||||
1. **Never send passcodes to the client.** The frontend stops reading/storing `access_code_kv_json` from the bootstrap response.
|
||||
2. **Passcode entry triggers an API call** to `/authenticate_passcode`. API verifies server-side against the DB.
|
||||
3. **On success, the API returns a JWT** — the JWT contains the role, account context, and expiry.
|
||||
4. **Store the JWT in `$ae_loc.jwt`** (already a field, already wired into `$ae_api`).
|
||||
5. **On page reload**, check the JWT's `eat` (expires-at) claim locally (base64 decode, no signature verification needed client-side). If expired, drop to anonymous. If valid, `access_type` is already persisted in `$ae_loc`.
|
||||
|
||||
### Session restore on reload
|
||||
|
||||
- `access_type` still persists in localStorage (no change here)
|
||||
- The JWT is the **proof** that the access was legitimately granted and is still valid
|
||||
- On page load: decode JWT payload (base64 the middle segment), check `eat` vs `Date.now()/1000`
|
||||
- If JWT expired → reset `access_type` to anonymous, clear JWT
|
||||
- If JWT valid → no action needed, `access_type` is already correct
|
||||
|
||||
This gives session expiry without a network call on every page load.
|
||||
|
||||
---
|
||||
|
||||
## TTL Per Role — Decided
|
||||
|
||||
| Access Level | JWT TTL | Notes |
|
||||
|---|---|---|
|
||||
| `super` | 8 hours | Highest privilege |
|
||||
| `manager` | 24 hours | |
|
||||
| `administrator` | 48 hours | |
|
||||
| `trusted` | 48 hours | Onsite staff — covers multi-day events |
|
||||
| `public` | 24 hours | |
|
||||
| `authenticated` | 12 hours | |
|
||||
| `anonymous` | N/A | No passcode |
|
||||
|
||||
---
|
||||
|
||||
## Caching Decision
|
||||
|
||||
**No passcode caching.** Every passcode entry makes one API call. The JWT handles session persistence — no passcode ever touches localStorage. Performance impact is only at the moment of entry (~50–150ms), which is acceptable for a once-per-session action.
|
||||
|
||||
---
|
||||
|
||||
## Backend Changes Required
|
||||
|
||||
**Note:** The backend fixes described below have been implemented and tested in the `aether_api_fastapi` repository (the `/authenticate_passcode` endpoint now uses explicit role priority, returns a full passcode JWT with `auth_type: 'passcode'`, applies per-role TTLs, and validates passcode length). Frontend changes can proceed once the backend deployment with these fixes is available.
|
||||
|
||||
### Backend Agent Follow-Up
|
||||
|
||||
If the backend team revisits this area, keep the next round focused on narrowing escape hatches rather than adding new ones:
|
||||
|
||||
1. Audit every `x-no-account-id` use and decide whether it is still required for bootstrap, public delivery, or a global-default fallback.
|
||||
2. Prefer JWT-backed auth once a session exists; do not add new transport-level bypass paths for authenticated UI flows.
|
||||
3. Mark any remaining bypass-only helper as temporary and add a removal target.
|
||||
4. Plan the eventual removal of `access_code_kv_json` from public bootstrap payloads once passcode auth is fully deployed.
|
||||
|
||||
### Frontend special-case endpoints to review
|
||||
|
||||
These are the current frontend-facing exceptions that the backend work should assume are special-cased. None require a frontend/client code change today, but some are intentionally temporary.
|
||||
|
||||
| Frontend path / helper | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| `src/routes/+layout.ts` | Keep | Bootstrap site-domain lookup before account context is known. |
|
||||
| `src/routes/manifest.webmanifest/+server.ts` | Keep | Public PWA branding lookup; bootstrap key only. |
|
||||
| `src/lib/ae_core/ae_core__site.ts` | Keep | Cache-first site-domain bootstrap path. Still a bootstrap-only special case. |
|
||||
| `src/lib/ae_api/api_get__data_store.ts` + `src/lib/ae_core/core__data_store.ts` + `src/lib/elements/element_data_store.svelte` | Temporary | Global-default fallback. Target state is JWT-backed account-scoped access only. |
|
||||
| `src/lib/ae_core/ae_core_functions.ts` | Remove candidate | Legacy site-domain helper with forced no-account scope. |
|
||||
| `src/routes/testing/+page.svelte` | Dev-only | Useful for trace testing; do not add to any production allowlist. |
|
||||
|
||||
**Phase 2 status:** Not started — removing `access_code_kv_json` from the public site model remains pending.
|
||||
|
||||
**File:** `aether_api_fastapi/app/routers/api.py`
|
||||
|
||||
The `/authenticate_passcode` endpoint exists and is structurally correct but has four issues that must be fixed before the frontend migrates to using it.
|
||||
|
||||
### Fix 1: Passcode matching must use explicit priority order
|
||||
|
||||
**Current (wrong):**
|
||||
```python
|
||||
for role, code in access_codes.items(): # dict insertion order — not guaranteed
|
||||
if str(code) == str(passcode):
|
||||
matched_role = role
|
||||
break
|
||||
```
|
||||
|
||||
**Required:**
|
||||
```python
|
||||
ROLE_PRIORITY = ['super', 'manager', 'administrator', 'trusted', 'public', 'authenticated']
|
||||
|
||||
matched_role = None
|
||||
for role in ROLE_PRIORITY:
|
||||
code = access_codes.get(role)
|
||||
if code and str(code) == str(passcode):
|
||||
matched_role = role
|
||||
break
|
||||
```
|
||||
|
||||
This ensures that if a config mistake causes two roles to share a passcode, the higher-privilege role always wins. It also makes the intent explicit and independent of JSON storage order.
|
||||
|
||||
### Fix 2: JWT payload must include all six role flags
|
||||
|
||||
**Current (incomplete):**
|
||||
```python
|
||||
payload = {
|
||||
'account_id': account_id_random,
|
||||
'administrator': (matched_role == 'administrator'),
|
||||
'manager': (matched_role == 'manager'),
|
||||
'super': (matched_role == 'super'),
|
||||
# trusted / public / authenticated missing
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Required:**
|
||||
```python
|
||||
payload = {
|
||||
'account_id': account_id_random,
|
||||
'super': (matched_role == 'super'),
|
||||
'manager': (matched_role == 'manager'),
|
||||
'administrator': (matched_role == 'administrator'),
|
||||
'trusted': (matched_role == 'trusted'),
|
||||
'public': (matched_role == 'public'),
|
||||
'authenticated': (matched_role == 'authenticated'),
|
||||
'json_str': json.dumps({
|
||||
'auth_type': 'passcode', # distinguishes from user login JWTs
|
||||
'site_id': site_id,
|
||||
'role': matched_role # canonical role string — frontend uses this
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `auth_type: 'passcode'` marker is critical — it allows the frontend and any future backend consumers to distinguish a passcode JWT from a user login JWT.
|
||||
|
||||
### Fix 3: Per-role TTL
|
||||
|
||||
**Current:**
|
||||
```python
|
||||
token = sign_jwt(
|
||||
secret_key=settings.JWT_KEY,
|
||||
ttl=3600 * 24, # hardcoded 24h for all roles
|
||||
**payload
|
||||
)
|
||||
```
|
||||
|
||||
**Required:**
|
||||
```python
|
||||
ROLE_TTL = {
|
||||
'super': 8 * 3600, # 8 hours
|
||||
'manager': 24 * 3600, # 24 hours
|
||||
'administrator': 48 * 3600, # 48 hours
|
||||
'trusted': 48 * 3600, # 48 hours
|
||||
'public': 24 * 3600, # 24 hours
|
||||
'authenticated': 12 * 3600, # 12 hours
|
||||
}
|
||||
|
||||
token = sign_jwt(
|
||||
secret_key=settings.JWT_KEY,
|
||||
ttl=ROLE_TTL[matched_role],
|
||||
**payload
|
||||
)
|
||||
```
|
||||
|
||||
### Fix 4: Add minimum length validation to `passcode` field
|
||||
|
||||
**Current:**
|
||||
```python
|
||||
passcode: str = Field(..., description="The passcode to verify")
|
||||
```
|
||||
|
||||
**Required:**
|
||||
```python
|
||||
passcode: str = Field(..., min_length=5, description="The passcode to verify")
|
||||
```
|
||||
|
||||
This matches the frontend's 5-character trigger and prevents empty/trivial submissions.
|
||||
|
||||
### Complete corrected endpoint (for reference)
|
||||
|
||||
```python
|
||||
ROLE_PRIORITY = ['super', 'manager', 'administrator', 'trusted', 'public', 'authenticated']
|
||||
|
||||
ROLE_TTL = {
|
||||
'super': 8 * 3600,
|
||||
'manager': 24 * 3600,
|
||||
'administrator': 48 * 3600,
|
||||
'trusted': 48 * 3600,
|
||||
'public': 24 * 3600,
|
||||
'authenticated': 12 * 3600,
|
||||
}
|
||||
|
||||
class PasscodeAuthRequest(BaseModel):
|
||||
"""Request model for site-based passcode authentication."""
|
||||
site_id: str = Field(..., description="Random string ID of the site")
|
||||
passcode: str = Field(..., min_length=5, description="The passcode to verify")
|
||||
|
||||
@router.post('/authenticate_passcode', response_model=Resp_Body_Base)
|
||||
async def authenticate_passcode(
|
||||
auth_req: PasscodeAuthRequest,
|
||||
response: Response = Response,
|
||||
):
|
||||
"""
|
||||
Passcode-to-JWT Endpoint.
|
||||
Verifies a passcode against site.access_code_kv_json (single source of truth —
|
||||
v_site_domain joins from the same site record).
|
||||
Returns a signed JWT with the site's account context, full role flags, and
|
||||
a per-role TTL. The jwt.json_str.auth_type='passcode' field distinguishes
|
||||
this token from a user login JWT.
|
||||
"""
|
||||
site_id = auth_req.site_id
|
||||
passcode = auth_req.passcode
|
||||
|
||||
# 1. Look up the site record
|
||||
search_data = {'id_random': site_id}
|
||||
if record := sql_select(table_name='site', data=search_data):
|
||||
# 2. Parse access codes
|
||||
access_codes_raw = record.get('access_code_kv_json')
|
||||
access_codes = {}
|
||||
if access_codes_raw:
|
||||
try:
|
||||
access_codes = json.loads(access_codes_raw) if isinstance(access_codes_raw, str) else access_codes_raw
|
||||
except Exception as e:
|
||||
log.error(f"Failed to parse access_code_kv_json for site {site_id}: {e}")
|
||||
|
||||
# 3. Verify passcode in explicit priority order (highest privilege wins)
|
||||
matched_role = None
|
||||
for role in ROLE_PRIORITY:
|
||||
code = access_codes.get(role)
|
||||
if code and str(code) == str(passcode):
|
||||
matched_role = role
|
||||
break
|
||||
|
||||
if matched_role:
|
||||
log.info(f"Auth Success: Verified '{matched_role}' passcode for site {site_id}")
|
||||
|
||||
# 4. Resolve account context
|
||||
account_id_random = record.get('account_id_random')
|
||||
if not account_id_random:
|
||||
if account_id_int := record.get('account_id'):
|
||||
account_id_random = get_id_random(record_id=account_id_int, table_name='account')
|
||||
|
||||
# 5. Mint JWT with complete role flags and per-role TTL
|
||||
payload = {
|
||||
'account_id': account_id_random,
|
||||
'super': (matched_role == 'super'),
|
||||
'manager': (matched_role == 'manager'),
|
||||
'administrator': (matched_role == 'administrator'),
|
||||
'trusted': (matched_role == 'trusted'),
|
||||
'public': (matched_role == 'public'),
|
||||
'authenticated': (matched_role == 'authenticated'),
|
||||
'json_str': json.dumps({
|
||||
'auth_type': 'passcode',
|
||||
'site_id': site_id,
|
||||
'role': matched_role
|
||||
})
|
||||
}
|
||||
|
||||
token = sign_jwt(
|
||||
secret_key=settings.JWT_KEY,
|
||||
ttl=ROLE_TTL[matched_role],
|
||||
**payload
|
||||
)
|
||||
|
||||
return mk_resp(
|
||||
data={'jwt': token, 'account_id': account_id_random, 'role': matched_role},
|
||||
response=response
|
||||
)
|
||||
else:
|
||||
log.warning(f"Auth Failed: Invalid passcode for site {site_id}")
|
||||
return mk_resp(data=False, status_code=401, response=response, status_message="Invalid passcode.")
|
||||
else:
|
||||
log.warning(f"Auth Failed: Site {site_id} not found.")
|
||||
return mk_resp(data=False, status_code=404, response=response, status_message="Site not found.")
|
||||
```
|
||||
|
||||
### Backend Phase 2 (follow-up — not blocking frontend)
|
||||
|
||||
**Remove `access_code_kv_json` from the `Site_Domain_Base` response model** (`site_domain_models.py`). This ensures passcodes are never sent to the client even if future code reads from the bootstrap. Requires confirming no other endpoint consumers rely on `access_code_kv_json` being in the base response before making this change.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Changes Required
|
||||
|
||||
**These depend on the backend fixes above being deployed first.**
|
||||
|
||||
### 1a. `src/lib/app_components/e_app_access_type.svelte`
|
||||
|
||||
Replace `handle_check_access_type_passcode` entirely. The new version:
|
||||
|
||||
- Is `async`
|
||||
- Adds `auth_pending: boolean = $state(false)` and `auth_error: string | null = $state(null)`
|
||||
- Uses a direct `fetch` call (NOT `post_object` — avoids triggering the session-expired banner on a 401)
|
||||
- On success: sets `$ae_loc.access_type = data.role`, stores `$ae_loc.jwt = data.jwt`, triggers `process_permission_check` as before
|
||||
- On 401: shows inline error, clears `entered_passcode`, resets `checked_passcode = null` to allow retry
|
||||
- On network error: shows inline connection error
|
||||
- Clears `auth_error` when `entered_passcode` changes
|
||||
|
||||
API call shape:
|
||||
```http
|
||||
POST /authenticate_passcode
|
||||
Content-Type: application/json
|
||||
x-aether-api-key: <from $ae_api.headers['x-aether-api-key']>
|
||||
Body: { site_id: $ae_loc.site_id, passcode: entered_passcode }
|
||||
```
|
||||
|
||||
Add to template (near the passcode input):
|
||||
```svelte
|
||||
{#if auth_pending}
|
||||
<Loader size="1em" class="animate-spin text-gray-400" />
|
||||
{/if}
|
||||
{#if auth_error}
|
||||
<span class="text-error-500 text-xs">{auth_error}</span>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### 1b. `src/routes/+layout.ts`
|
||||
|
||||
**Stop caching passcodes from bootstrap** — remove line ~394:
|
||||
```ts
|
||||
// ae_loc_init['site_access_code_kv'] = json_data.access_code_kv_json || {};
|
||||
```
|
||||
|
||||
**Add passcode JWT expiry check** — after the block around line 84 where `ae_loc_json.jwt` is read, add:
|
||||
```ts
|
||||
// Enforce passcode JWT TTL on page load.
|
||||
// Decodes the JWT payload (base64, no secret needed) and resets access to anonymous if expired.
|
||||
// User login JWTs (auth_type !== 'passcode') are left untouched.
|
||||
if (ae_loc_json?.jwt) {
|
||||
try {
|
||||
const parts = ae_loc_json.jwt.split('.');
|
||||
if (parts.length === 3) {
|
||||
const jwt_payload = JSON.parse(atob(parts[1]));
|
||||
const json_str = typeof jwt_payload.json_str === 'string'
|
||||
? JSON.parse(jwt_payload.json_str)
|
||||
: jwt_payload.json_str;
|
||||
if (json_str?.auth_type === 'passcode' && jwt_payload.eat < Date.now() / 1000) {
|
||||
// Passcode JWT has expired — revoke access
|
||||
ae_loc_json.jwt = null;
|
||||
ae_loc_json.access_type = 'anonymous';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Malformed JWT — leave untouched, let existing handling deal with it
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1c. `src/lib/stores/ae_stores__auth_loc_defaults.ts` (cleanup)
|
||||
|
||||
Remove `site_access_code_kv` from the `AuthLocState` interface and the `auth_loc_defaults` object. The field is unused after 1a. Confirm no other component reads from it first (current grep: only `e_app_access_type.svelte` uses it — confirmed).
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Users with existing localStorage will still have `site_access_code_kv` cached — this is harmless after the frontend stops reading it. No forced cache clear needed.
|
||||
- Existing persisted `access_type` is unaffected — users keep their current session level until their JWT expires or they manually clear storage.
|
||||
- The `$ae_loc.jwt` field is already used by the user login flow. The `auth_type: 'passcode'` marker in `json_str` ensures the expiry logic only targets passcode sessions, not user login sessions.
|
||||
|
||||
---
|
||||
|
||||
## Files Affected
|
||||
|
||||
| File | Repo | Change |
|
||||
| --- | --- | --- |
|
||||
| `app/routers/api.py` | `aether_api_fastapi` | **Backend — do first.** Priority ordering, full JWT payload, per-role TTL, min_length on passcode |
|
||||
| `app/models/site_domain_models.py` | `aether_api_fastapi` | Phase 2: remove `access_code_kv_json` from public model |
|
||||
| `src/lib/app_components/e_app_access_type.svelte` | `aether_app_sveltekit` | Replace local check with async API call; loading/error UI |
|
||||
| `src/routes/+layout.ts` | `aether_app_sveltekit` | Stop caching passcodes; add JWT expiry check |
|
||||
| `src/lib/stores/ae_stores__auth_loc_defaults.ts` | `aether_app_sveltekit` | Cleanup: remove `site_access_code_kv` |
|
||||
| `documentation/AE__Permissions_and_Security.md` | `aether_app_sveltekit` | Update passcode auth section to reflect new flow |
|
||||
@@ -1,411 +0,0 @@
|
||||
# PROJECT: Aether App — Comprehensive Style Review
|
||||
|
||||
**Status:** Phase 1 & 2 Complete — Phase 3 Deferred (post-April 2026 conference)
|
||||
**Priority:** Medium
|
||||
**Created:** 2026-03-13
|
||||
**Updated:** 2026-03-16
|
||||
**Related:** `src/app.css`, `src/routes/+layout.svelte`, `documentation/AE__UI_Component_Patterns.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Audit and unify the visual design system across all Aether modules. The goal is consistent:
|
||||
- Color token usage (Skeleton `preset-*` / `surface-*` / semantic tokens)
|
||||
- Button and interactive element styling
|
||||
- Dark mode handling
|
||||
- Typography hierarchy
|
||||
- Layout patterns (cards, lists, tables, modals, banners)
|
||||
- Icon system (Lucide only)
|
||||
|
||||
This review covers all modules and their distinct use cases. Changes must be sequenced to avoid breaking live-production systems (Events Launcher, IDAA).
|
||||
|
||||
### Scope of Modules
|
||||
|
||||
| Module | Routes | Primary Users | Notes |
|
||||
|---|---|---|---|
|
||||
| Core / App Shell | `+layout.svelte`, `e_app_sys_bar.svelte` | All users | Foundation — impacts everything |
|
||||
| Core Admin | `/core/` | OSIT staff / managers | Manager-only section |
|
||||
| Journals | `/journals/` | Authenticated users | Canonical/frontier model |
|
||||
| Events — General | `/events/`, `/events/[id]/` | Event staff, attendees | Hub page |
|
||||
| Events — Pres Mgmt | `/events/[id]/(pres_mgmt)/` | Event coordinators | Operations tool |
|
||||
| Events — Launcher | `/events/[id]/(launcher)/` | AV/tech staff (Electron kiosk) | ⚠️ Live production — April 2026 conference |
|
||||
| Events — Badges | `/events/[id]/(badges)/` | Registration desk staff | On-site kiosk use |
|
||||
| Events — Leads | `/events/[id]/(leads)/` | Exhibitor booth staff | On-site kiosk use |
|
||||
| IDAA | `/idaa/` | IDAA members (iframe) | ⚠️ Privacy-critical, iframe context |
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State: Dual-Generation Problem
|
||||
|
||||
The codebase has two parallel style generations that co-exist:
|
||||
|
||||
| Era | Pattern | Where Used |
|
||||
|---|---|---|
|
||||
| **Modern** | `preset-tonal-*`, `preset-filled-*`, Lucide icons, Svelte 5 runes | `sys_bar`, Journals, IDAA dashboard |
|
||||
| **Legacy** | `variant-soft-*`, `variant-filled-*`, FontAwesome `fas fa-*` icons | Events routes (most), some Core |
|
||||
|
||||
The Journals module is the canonical reference for modern patterns — when in doubt, match it.
|
||||
|
||||
---
|
||||
|
||||
## 3. Color Token Architecture
|
||||
|
||||
Three overlapping systems are in use. The goal is to consolidate to two:
|
||||
|
||||
### ✅ System 1: Skeleton Semantic Tokens (keep, expand usage)
|
||||
Used for colored/semantic elements.
|
||||
```
|
||||
primary, secondary, tertiary, success, warning, error, surface
|
||||
```
|
||||
Usage: `preset-tonal-primary`, `bg-primary-500/10`, `text-error-500`, `border-warning-500`
|
||||
|
||||
### ✅ System 2: Plain Tailwind Grayscale (keep for neutrals)
|
||||
Used for neutral backgrounds, borders, and text. Predictable across all Skeleton themes.
|
||||
```
|
||||
gray-50/100/200/300/400/500/600/700/800/900
|
||||
```
|
||||
Usage: `bg-gray-50 dark:bg-gray-900`, `border-gray-200 dark:border-gray-700`
|
||||
|
||||
### ❌ System 3: Hardcoded RGB/HSL (eliminate)
|
||||
```
|
||||
bg-orange-600/90 — root layout banner
|
||||
hsla(0, 100%, 50%, .5) — journal entry eye icon
|
||||
rgb(243 244 246) — form dark mode hacks in <style> blocks
|
||||
```
|
||||
These are brittle, not theme-aware, and create maintenance debt.
|
||||
|
||||
### Decision Rule
|
||||
> **Semantic color needed?** → Use Skeleton token (`preset-*`, `text-primary-*`, etc.)
|
||||
> **Neutral background/border/text?** → Use `gray-*` with `dark:` pair
|
||||
> **Hardcoded color?** → Replace with token. No exceptions.
|
||||
|
||||
---
|
||||
|
||||
## 4. Dark Mode Architecture
|
||||
|
||||
### Current Setup (already correct in app.css)
|
||||
```css
|
||||
@custom-variant dark (&:where(.dark, .dark *)); /* Tailwind v4 class-based dark mode */
|
||||
html.dark { color-scheme: dark; } /* Native controls follow app theme */
|
||||
html.light { color-scheme: light; }
|
||||
```
|
||||
|
||||
### Gap: Skeleton Form Classes Lack Dark Mode
|
||||
Skeleton's `.input`, `.select`, `.textarea` classes do not include dark mode styles. This causes white text on white backgrounds in dark mode. Currently patched with inline `<style>` blocks per-component (see `e_app_sys_bar.svelte` lines 693–707).
|
||||
|
||||
**Fix:** Global utility in `app.css` (see Phase 1, Step 1). Once added, remove per-component patches.
|
||||
|
||||
---
|
||||
|
||||
## 5. Button System
|
||||
|
||||
### Standard: `preset-*` (Skeleton v4 pattern) ✅
|
||||
```html
|
||||
<button class="btn preset-tonal-secondary">Secondary</button>
|
||||
<button class="btn btn-sm preset-filled-primary">Primary</button>
|
||||
<button class="btn preset-outlined-surface">Outlined</button>
|
||||
```
|
||||
|
||||
|
||||
> **Note:** All legacy `variant-*` classes have been fully removed from the codebase. Use only `preset-*` classes for all buttons and interactive elements.
|
||||
|
||||
### Custom `ae_btn_*` Classes (app.css)
|
||||
These exist in `app.css` and wrap the `preset-*` system. They are valid but underused. Consider adopting where button groups need reuse.
|
||||
|
||||
---
|
||||
|
||||
## 6. Icon System
|
||||
|
||||
**Standard:** Lucide (`@lucide/svelte`) — SVG, tree-shakeable, consistent stroke weight
|
||||
**Legacy:** FontAwesome (`fas fa-*` / `far fa-*`) — CSS class-based, heavier
|
||||
|
||||
FontAwesome is still imported (likely via global CSS). Goal: complete removal from all new work; migrate existing usage to Lucide progressively.
|
||||
|
||||
### FontAwesome → Lucide Reference Map (events module)
|
||||
|
||||
| FontAwesome | Lucide Component | Notes |
|
||||
|---|---|---|
|
||||
| `fa-cogs` | `Settings` | Settings/config |
|
||||
| `fa-chart-line` | `TrendingUp` | Reports/charts |
|
||||
| `fa-map-marked-alt` | `MapPinned` | Location with map |
|
||||
| `fa-map-marker-alt` | `MapPin` | Location marker |
|
||||
| `fa-tools` | `Wrench` | Tools/maintenance |
|
||||
| `fa-search` | `Search` | Search |
|
||||
| `fa-chalkboard-teacher` | `PresentationIcon` or `GraduationCap` | Presenter |
|
||||
| `fa-plane` | `Plane` | Travel/remote |
|
||||
| `fa-sync-alt fa-spin` | `RefreshCw` + CSS animation | Spinner |
|
||||
| `fa-arrow-up` | `ArrowUp` | Up arrow |
|
||||
| `fa-arrow-down` | `ArrowDown` | Down arrow |
|
||||
| `fa-list-ol` | `ListOrdered` | Ordered list |
|
||||
| `fa-file-csv` | `FileSpreadsheet` | CSV file |
|
||||
| `fa-toggle-on` | `ToggleRight` | Toggle |
|
||||
| `fa-calendar-alt` | `CalendarDays` | Calendar |
|
||||
| `fa-exclamation-triangle` | `TriangleAlert` | Warning |
|
||||
| `fa-lock` | `Lock` | Locked |
|
||||
| `fa-unlock` | `Unlock` | Unlocked |
|
||||
| `fa-eye` | `Eye` | Visible |
|
||||
| `fa-eye-slash` | `EyeOff` | Hidden |
|
||||
| `fa-trash` | `Trash2` | Delete |
|
||||
| `fa-edit` / `fa-pencil` | `Pencil` | Edit |
|
||||
| `fa-plus` | `Plus` | Add |
|
||||
| `fa-times` / `fa-close` | `X` | Close/remove |
|
||||
| `fa-check` | `Check` | Confirm |
|
||||
| `fa-ban` | `Ban` | Denied/blocked |
|
||||
| `fa-user` | `User` | Person |
|
||||
| `fa-users` | `Users` | Group |
|
||||
| `fa-tag` | `Tag` | Tag/label |
|
||||
| `fa-print` | `Printer` | Print |
|
||||
| `fa-download` | `Download` | Download |
|
||||
| `fa-upload` | `Upload` | Upload |
|
||||
| `fa-copy` | `Copy` | Copy |
|
||||
| `fa-qrcode` | `QrCode` | QR code |
|
||||
| `fa-id-card` | `IdCard` | Badge/ID |
|
||||
| `fa-file-alt` | `FileText` | File |
|
||||
| `fa-compress-arrows-alt` | `Minimize2` | Collapse |
|
||||
| `fa-expand` | `Maximize2` | Expand |
|
||||
| `fa-angle-right` | `ChevronRight` | Nav arrow |
|
||||
| `fa-angle-down` | `ChevronDown` | Accordion |
|
||||
| `fa-clock` | `Clock` | Time |
|
||||
|
||||
### Lucide Usage Pattern
|
||||
```svelte
|
||||
<script>
|
||||
import { Settings, Search, MapPin } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
<!-- Decorative icon with adjacent label -->
|
||||
<Settings size="1em" class="shrink-0" aria-hidden="true" />
|
||||
|
||||
<!-- Icon-only button — MUST have aria-label -->
|
||||
<button aria-label="Settings" class="btn btn-sm preset-tonal-surface">
|
||||
<Settings size="1.1em" />
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Typography
|
||||
|
||||
### Standard Hierarchy
|
||||
```html
|
||||
<!-- Page title -->
|
||||
<h1 class="text-3xl sm:text-4xl font-black tracking-tight">Title</h1>
|
||||
|
||||
<!-- Section heading -->
|
||||
<h2 class="text-xl font-bold">Section</h2>
|
||||
|
||||
<!-- Card/item heading -->
|
||||
<h3 class="text-lg font-semibold">Item</h3>
|
||||
|
||||
<!-- Label / eyebrow -->
|
||||
<span class="text-xs font-bold uppercase tracking-wide opacity-40">Label</span>
|
||||
|
||||
<!-- Muted secondary text -->
|
||||
<span class="text-sm opacity-60">Secondary info</span>
|
||||
|
||||
<!-- Hint / placeholder text -->
|
||||
<span class="text-xs opacity-40 italic">Hint</span>
|
||||
```
|
||||
|
||||
### Rule: Opacity Over Fixed Colors for Muted Text
|
||||
```html
|
||||
<!-- ✅ Theme-aware muted text -->
|
||||
<span class="text-sm opacity-60">Note</span>
|
||||
|
||||
<!-- ❌ Fixed muted text — breaks in dark mode -->
|
||||
<span class="text-sm text-gray-500">Note</span>
|
||||
```
|
||||
|
||||
> **Exception:** `text-gray-*` is acceptable in components that intentionally use plain Tailwind grayscale for neutrality (e.g., Journals list cards), as long as `dark:text-gray-*` counterpart is always included.
|
||||
|
||||
---
|
||||
|
||||
## 8. Card & Layout Patterns
|
||||
|
||||
See `documentation/AE__UI_Component_Patterns.md` for the full pattern reference.
|
||||
|
||||
Key standard patterns:
|
||||
- **List item card:** `border border-gray-200 dark:border-gray-700 border-l-4 border-l-primary-500/40` with hover intensification
|
||||
- **Content card:** `rounded-lg border border-surface-200-800 bg-surface-50-900 px-4 py-3`
|
||||
- **Glow accent:** `absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 rounded-2xl blur opacity-25 dark:opacity-40 group-hover:opacity-60 transition duration-1000 group-hover:duration-200 pointer-events-none`
|
||||
- **Empty state:** `preset-tonal-warning p-6 rounded-xl` centered, with icon + heading + description
|
||||
|
||||
---
|
||||
|
||||
## 9. Accessibility Rules
|
||||
|
||||
- **Never remove focus rings.** `focus:ring-0` on text inputs fails WCAG 2.1 AA. Use `focus:ring-2 focus:ring-primary-500` instead.
|
||||
- **Icon-only buttons must have `aria-label`.** No exceptions.
|
||||
- **Decorative icons must have `aria-hidden="true"`.** This applies to ALL FontAwesome `<span class="fas ...">` elements too.
|
||||
- **Color is not the only status indicator.** Pair color with text or icon shape.
|
||||
- **Form labels must be explicit.** Use `<label for="...">` or `aria-label`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Module-by-Module Status
|
||||
|
||||
### Core / App Shell
|
||||
|
||||
| Item | Status | Notes |
|
||||
|---|---|---|
|
||||
| Root layout banners (offline, expired) | ✅ Done | `bg-orange-600/90` → `preset-tonal-warning` (2026-03-16) |
|
||||
| `e_app_sys_bar.svelte` | ✅ Modern | Best-practice reference component |
|
||||
| `e_app_theme.svelte` | 🟡 Legacy | Redundant with sys_bar theme section; keep but no new work |
|
||||
| `core/+layout.svelte` | ✅ Done | `variant-*` → `preset-*` (2026-03-16) |
|
||||
| `core/+page.svelte` | ✅ Good | Excellent card grid template |
|
||||
| All `/core/` files (21 files) | ✅ Done | `variant-*` → `preset-*`, FA → Lucide (2026-03-16) |
|
||||
|
||||
### Journals
|
||||
|
||||
| Item | Status | Notes |
|
||||
|---|---|---|
|
||||
| Overall | ✅ Canonical reference | Use as template for all new work |
|
||||
| `ae_comp__journal_obj_li.svelte` | ✅ Excellent | Card pattern, icon sizing, hover states |
|
||||
| `ae_comp__journal_entry_obj_li.svelte` | ✅ Done | `bg-slate-*` → `bg-gray-*`; hardcoded HSL → Tailwind tokens (2026-03-16) |
|
||||
| `ae_comp__journal_entry_header.svelte` | ✅ Done | `focus:ring-0` restored to `focus:ring-2` (2026-03-16) |
|
||||
|
||||
### Events — General
|
||||
|
||||
| Item | Status | Notes |
|
||||
|---|---|---|
|
||||
| `events/+page.svelte` | ✅ Done | FA → Lucide; `variant-ghost-surface` → `preset-outlined-surface` (2026-03-16) |
|
||||
| `events/+layout.svelte` | ✅ Done | FA → Lucide (spinners, arrows) (2026-03-16) |
|
||||
| `events/ae_comp__events_menu_nav.svelte` | ✅ Done | FA → Lucide (2026-03-16) |
|
||||
| `events/[id]/+page.svelte` | ✅ Done | FA → Lucide; `variant-*` → `preset-*` (2026-03-16) |
|
||||
| `events/ae_comp__event_file_obj_tbl.svelte` | ✅ Done | FA → Lucide (2026-03-16) |
|
||||
| `events/ae_comp__event_presentation_obj_li.svelte` | ✅ Done | FA → Lucide (2026-03-16) |
|
||||
| Lucide inline flow | ✅ Done | Global `svg.lucide { display: inline }` rule in `app.css` (2026-03-16) |
|
||||
|
||||
### Events — Pres Mgmt
|
||||
|
||||
| Item | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| All 24 pres_mgmt files | ✅ Done | FA → Lucide; `variant-*` → `preset-*` (2026-03-16) |
|
||||
| Card styling for session/presenter lists | 🔒 Phase 3 | Deferred to post-April 2026 |
|
||||
|
||||
### Events — Launcher ⚠️ Live Production
|
||||
|
||||
| Item | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| FA → Lucide | ✅ Done | All FA spans were already in HTML comments — launcher is clean (verified 2026-03-16) |
|
||||
| `variant-*` | ✅ Done | The `variant-soft-secondary` at line 329 is inside a comment block — no live variants remain |
|
||||
| Card styling / UX polish | 🔒 Phase 3 | Deferred to post-April 2026 conference |
|
||||
|
||||
### Events — Badges
|
||||
|
||||
| Item | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| FA → Lucide | ✅ Done | All badge files migrated (2026-03-16) |
|
||||
| `badge_upload_form.svelte` | ✅ Done | `variant-*` → `preset-*` (2026-03-16) |
|
||||
| `badge_template_form.svelte` | ✅ Done | `variant-*` → `preset-*` (2026-03-16) |
|
||||
| `code_to_html` | ✅ Refactored | FA HTML string dict → `code_to_icon` Lucide component map (2026-03-16) |
|
||||
|
||||
### Events — Leads
|
||||
|
||||
| Item | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| FA → Lucide | ✅ Done | All leads files migrated (2026-03-16) |
|
||||
| `ae_comp__exhibit_signin.svelte` | ✅ Done | `variant-*` → `preset-*` (2026-03-16) |
|
||||
| `ae_comp__lead_qr_scanner.svelte` | ✅ Done | `variant-*` → `preset-*` (2026-03-16) |
|
||||
| `ae_tab__add.svelte` | ✅ Done | `variant-*` → `preset-*` (2026-03-16) |
|
||||
|
||||
### IDAA ⚠️ Privacy-Critical
|
||||
|
||||
| Item | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| `(idaa)/+page.svelte` | ✅ Modern | Semantic tokens, good access gate |
|
||||
| FA CDN | ✅ Scoped | Moved from `app.html` → `idaa/+layout.svelte` `<svelte:head>` (2026-03-16) |
|
||||
| Archives, BB, Recovery Meetings | 🔒 Deferred | Full style review deferred to Phase 3 |
|
||||
| **All IDAA FA → Lucide** | 🔒 Last priority | Review only after non-IDAA modules are complete |
|
||||
|
||||
---
|
||||
|
||||
## 11. Issues Ranked by Priority
|
||||
|
||||
| # | Severity | Issue | Location | Phase |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 🔴 A11y | `focus:ring-0` removes focus indicator on journal name input | `ae_comp__journal_entry_header.svelte:108` | 1 |
|
||||
| 2 | 🔴 Maintenance | No global dark mode form fix — per-component patches scattered | `e_app_sys_bar.svelte` lines 693–707; others | 1 |
|
||||
| 3 | 🟡 Consistency | FontAwesome icons throughout events module | 61 event files | 1–2 |
|
||||
| 4 | 🟡 Consistency | `variant-*` buttons used instead of `preset-*` | Events, Badges, Leads routes | 1–2 |
|
||||
| 5 | 🟡 Theme | Hardcoded `bg-orange-600/90` on root layout offline banner | `+layout.svelte` | 2 |
|
||||
| 6 | 🟡 Theme | Hardcoded HSL colors on journal entry eye icon | `ae_comp__journal_entry_obj_li.svelte` | 2 |
|
||||
| 7 | 🟢 Consistency | `bg-slate-*` used in journal entry instead of `bg-gray-*` | `ae_comp__journal_entry_obj_li.svelte` | 2 |
|
||||
| 8 | 🟢 Polish | Pres Mgmt pages lack card styling (bare `<ul>` lists) | `pres_mgmt/+page.svelte` and sub-pages | 3 |
|
||||
| 9 | 🟢 Polish | Root layout banner uses direct `font-semibold text-white` instead of preset | `+layout.svelte` | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 12. Implementation Plan
|
||||
|
||||
### Phase 1: Quick Wins (current)
|
||||
|
||||
**Step 1 — Global form dark mode utility** ✅ Target: `src/app.css`
|
||||
|
||||
Add a global utility so Skeleton `.input`, `.select`, `.textarea` classes render correctly in dark mode. This eliminates all per-component `<style>` patches.
|
||||
|
||||
**Step 2 — FontAwesome → Lucide in events nav/layout files**
|
||||
|
||||
Scope: Non-Launcher, non-IDAA files only. Priority order:
|
||||
1. `src/routes/events/ae_comp__events_menu_nav.svelte` — top-level navigation
|
||||
2. `src/routes/events/+layout.svelte` — spinner and sort icons
|
||||
|
||||
**Step 3 — Standardize `variant-*` → `preset-*` in events**
|
||||
|
||||
Scope: `+page.svelte`, `settings/+page.svelte`, `sign_in_out.svelte`
|
||||
Skip: Launcher files (frozen), IDAA files (deferred)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Consolidation
|
||||
|
||||
- Replace hardcoded banner color in root layout (`bg-orange-600/90` → `preset-tonal-warning`)
|
||||
- Fix journal entry eye icon hardcoded HSL colors
|
||||
- Fix `bg-slate-*` inconsistency in journal entry
|
||||
- Migrate `variant-*` in remaining events files: Pres Mgmt, Badges, Leads
|
||||
- Remove per-component `<style>` dark mode patches (now covered by global utility)
|
||||
- Add responsive typography to events hub and pres mgmt pages
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Module Refactors (post-April 2026 conference)
|
||||
|
||||
- Events Launcher: FontAwesome → Lucide, `variant-*` → `preset-*`
|
||||
- Events Pres Mgmt: Card styling for session/presenter lists
|
||||
- IDAA: Full style review (Archives, BB, Recovery Meetings)
|
||||
- Create `.prose-journal` utility in app.css to centralize markdown prose overrides
|
||||
|
||||
---
|
||||
|
||||
## 13. Files to Modify (Phase 1)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/app.css` | Add global `.dark` form element utility |
|
||||
| `src/routes/events/ae_comp__events_menu_nav.svelte` | Replace `fas fa-*` with Lucide imports |
|
||||
| `src/routes/events/+layout.svelte` | Replace `fas fa-sync-alt fa-spin` and arrow icons with Lucide |
|
||||
| `src/routes/events/+page.svelte` | Replace `variant-ghost-surface` → `preset-outlined-surface` |
|
||||
| `src/routes/events/[event_id]/settings/+page.svelte` | Replace `variant-filled-secondary/primary` → `preset-*` |
|
||||
| `src/routes/events/[event_id]/sign_in_out.svelte` | Replace `variant-soft-warning` → `preset-tonal-warning` |
|
||||
|
||||
---
|
||||
|
||||
## 14. Testing Notes
|
||||
|
||||
- Run `npx svelte-check` after every file change
|
||||
- Test dark mode toggle after form utility added — confirm inputs render correctly in dark
|
||||
- Test light mode — confirm no regressions
|
||||
- Events nav: verify all Lucide icons render at correct size and with correct meaning
|
||||
- Launcher: do not touch — verify no unintended changes via `git diff`
|
||||
- IDAA: do not touch — verify no unintended changes via `git diff`
|
||||
|
||||
---
|
||||
|
||||
## 15. What We Are NOT Changing (Phase 1)
|
||||
|
||||
- Events Launcher files — frozen until post-April 2026 conference
|
||||
- All IDAA files — deferred to last phase
|
||||
- Root layout banner hardcoded color — Phase 2
|
||||
- Journal entry HSL eye icon colors — Phase 2
|
||||
- Pres Mgmt card styling — Phase 3
|
||||
@@ -1,189 +0,0 @@
|
||||
# Aether Journals UI Update (2026)
|
||||
|
||||
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Journal Entry config rework in progress)
|
||||
> **Last Updated:** 2026-05-05
|
||||
> **Primary Agent:** Frontend SvelteKit Agent
|
||||
|
||||
## 1. Project Overview
|
||||
This document outlines the modernization of the Journals module UI in the SvelteKit frontend (`aether_app_sveltekit`). The primary goals are to fully leverage the generic V3 API architecture and introduce high-velocity productivity features for journal management.
|
||||
|
||||
**Context:** The backend transition to the generic `api_crud` router is complete. Custom legacy routers have been removed. The frontend must now fully align with this pattern and provide a frictionless user experience.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Objectives
|
||||
|
||||
### 🎯 Primary Goals
|
||||
1. **V3 API Verification:** Ensure all CRUD operations utilize the generic `api_crud` endpoints (Verified).
|
||||
2. **Quick Add UI:** Implement a specialized interface for rapid, friction-free entry creation.
|
||||
3. **Append/Prepend UI:** Allow users to quickly add text to the beginning or end of existing entries without full edit mode.
|
||||
4. **Interop & Portability:** Robust import/export logic for Markdown/HTML (Nextcloud Notes compatibility).
|
||||
5. **Security Hardening:** Review and harden client-side encryption logic (BLOCKED).
|
||||
|
||||
---
|
||||
|
||||
## 3. Technical Architecture
|
||||
|
||||
### Backend (Completed)
|
||||
* **Router:** `api_crud` (Generic)
|
||||
* **Definitions:** `app/ae_obj_types_def.py` -> `app/object_definitions/journals.py`
|
||||
* **Endpoints:** `/v3/crud/journal/...` and `/v3/crud/journal_entry/...`
|
||||
|
||||
### Frontend (In Progress)
|
||||
* **State Management:** `src/lib/ae_journals/ae_journals_stores.ts`
|
||||
* **Local Storage:** Dexie.js (`db_journals`)
|
||||
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj`
|
||||
* **Export Engine:** Centralized templates in `src/lib/ae_journals/ae_journals_export_templates.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Feature Specifications
|
||||
|
||||
### ⚡ Quick Add (Complete)
|
||||
* **Component:** `src/routes/journals/ae_comp__journal_entry_quick_add.svelte`
|
||||
* **Behavior:** Creates a new `journal_entry` attached to the active journal without leaving the list view.
|
||||
|
||||
### 📝 Append / Prepend (Complete)
|
||||
* **Interaction:** Fast text injection via `AeCompModalJournalEntryAppend`.
|
||||
* **Logic:** Updates entry content without full editor state overhead.
|
||||
|
||||
### 🔄 Interop (Markdown/HTML) (Complete)
|
||||
* **Goal:** Bulk export/import for data portability.
|
||||
* **Templates:** Standard, Personal Log, Amazon Vine.
|
||||
|
||||
---
|
||||
|
||||
## 5. Implementation Plan
|
||||
|
||||
### Phase 1: Foundation (Done)
|
||||
- [x] Backend cleanup (remove legacy routers).
|
||||
- [x] Verify frontend uses V3 API (`ae_journals__journal.ts`).
|
||||
|
||||
### Phase 2: Rapid Entry (Complete)
|
||||
- [x] Create `ae_comp__journal_entry_quick_add.svelte`.
|
||||
- [x] Integrate Quick Add into `+page.svelte`.
|
||||
|
||||
### Phase 3: Content Manipulation & Portability (Complete)
|
||||
- [x] Implement Append/Prepend logic.
|
||||
- [x] Implement Bulk Export/Import system.
|
||||
- [x] Establish centralized Export Template engine.
|
||||
|
||||
### Phase 4: Polish & Security (ACTIVE)
|
||||
- [x] Implement Auto-Save toggle and visual status indicators.
|
||||
- [x] Extract decryption workflow to non-reactive helper.
|
||||
- [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
|
||||
- [x] **Journal Entry Config cleanup:** Summary now lives in Metadata; Alert lives in its own Alerts & Messaging section; Privacy Flags is visibility-only; Admin controls are split out and gated to trusted-access and above.
|
||||
- [x] **Shared Flags widget:** `AE_Object_Flags` now shows visible button text and hover titles instead of icon-only controls.
|
||||
- [x] **Modal sizing:** Entry config modal now expands to viewport height instead of stopping at a fixed 60vh body cap.
|
||||
- [x] **Delete/Remove behavior:** Entry config Admin section now uses the real delete helper. Managers/admins see Delete (hard delete); trusted access sees Remove (disable semantics).
|
||||
- [x] **RESOLVED:** Decryption workflow stability (Fixed via dependency isolation).
|
||||
- [x] **Style Standardization (2026-03-06):** Full Skeleton v4 `preset-*` class pass across all 17 journal components. See style token table in Lessons Learned below.
|
||||
- [x] **Dark mode fixes:** Entry content hover, journal view section/description background and text colors.
|
||||
- [x] **Modal close button:** All 3 config modals use `dismissable={false}` + explicit `<X>` button in header snippet for correct right-aligned placement.
|
||||
- [x] **Global select padding:** Added `padding-inline: 0.5rem` to `@layer base` in `app.css` (safe — utility `px-*` classes override it where intentional).
|
||||
- [ ] Solidify E2EE passcode system for Journals and Entries.
|
||||
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
||||
- [ ] Integrate Outbound Email sharing.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Lessons Learned: Solving the Svelte 5 Reactivity Hang
|
||||
|
||||
During the implementation of the Privacy/Decryption toggle and the new Configuration Modals, we encountered critical browser hangs caused by infinite reactivity loops. Here is how we resolved them:
|
||||
|
||||
### 1. Rigorous Dependency Isolation (`untrack`)
|
||||
Svelte 5 runes (`$effect`, `$derived`) automatically track **every** reactive variable read inside them.
|
||||
* **The Problem:** An effect would read `save_status` or `tmp_entry_obj.content` to decide if it should sync, but the act of syncing would update those same variables, re-triggering the effect.
|
||||
* **The Fix:** Wrap any "check-only" state or store reads in `untrack(() => ... )`. This allows the effect to use the value without becoming a dependency of it. This is **CRITICAL** when initializing local state from props inside an effect.
|
||||
|
||||
### 2. Standardized Modal UI ("Aether Orange") & Style Token Conventions
|
||||
We have established a unified design language for configuration interfaces and all Journals UI. **Use these as the module template.**
|
||||
|
||||
#### Modal Chrome
|
||||
* **Header/Footer:** `bg-orange-100 dark:bg-orange-900` with consistent orange borders.
|
||||
* **Close button:** Always use `dismissable={false}` on the `<Modal>` and add an explicit `<button>` with `<X>` inside the `{#snippet header()}` so placement is fully in our control. The `flex-1` class on the `<h3>` pushes it right.
|
||||
* **Tabs:** Center-aligned `btn btn-sm` with `preset-filled-primary` (active) / `preset-tonal-surface` (inactive).
|
||||
* **Icons:** Every tab and primary action should have a Lucide icon for better scannability.
|
||||
* **Button titles:** Any button that uses icon+text or icon-only must include a descriptive `title` for hover clarity.
|
||||
* **Explicit Persistence:** Follow "Edit working copy → Save Changes" pattern to prevent accidental store/API churn.
|
||||
|
||||
#### Skeleton v4 Style Token Reference (Journals = canonical example)
|
||||
| Intent | Class |
|
||||
|---|---|
|
||||
| Primary CTA (save, create, open) | `btn preset-filled-primary` |
|
||||
| Neutral / cancel / close | `btn preset-tonal-surface` |
|
||||
| Secondary action | `btn preset-tonal-secondary` |
|
||||
| Success (confirmed save) | `btn preset-filled-success` |
|
||||
| Warning (caution action) | `btn preset-tonal-warning hover:preset-filled-warning-500` |
|
||||
| Error / danger (delete, force reset) | `btn preset-tonal-error hover:preset-filled-error-500` |
|
||||
| Warning action (remove/disable) | `btn preset-tonal-warning hover:preset-filled-warning-500` |
|
||||
| Active tab | `preset-filled-primary` |
|
||||
| Inactive tab | `preset-tonal-surface` |
|
||||
| Icon button | `btn-icon btn-icon-sm preset-tonal-surface` |
|
||||
| Input (base) | `input` |
|
||||
| Input (small) | `input input-sm` |
|
||||
| Select (base) | `select` |
|
||||
| Select (small) | `select select-sm` |
|
||||
| Textarea | `textarea` |
|
||||
| Badge (info/neutral) | `badge preset-tonal-surface` |
|
||||
| Badge (success) | `badge preset-tonal-success` |
|
||||
| Badge (error) | `badge preset-tonal-error` |
|
||||
|
||||
#### Removed Patterns (never use)
|
||||
- All `variant-*` classes are now fully removed from the codebase. Use only `preset-*` classes for all buttons and interactive elements.
|
||||
- `variant-form-material` — Skeleton v2, removed from all inputs/selects/textareas
|
||||
- `input-bordered` — non-standard, removed
|
||||
- DaisyUI `modal` / `modal-box` / `modal-action` wrapper divs inside Flowbite `<Modal>` — removed
|
||||
|
||||
#### Dark Mode Rules
|
||||
- Any `bg-{color}-100` dynamic background **must** have a `dark:bg-gray-800` (or similar) override — light shades are unreadable in dark mode.
|
||||
- Hover states on content areas need both light and dark variants: `hover:bg-blue-100 dark:hover:bg-blue-950`.
|
||||
- Text locked to `dark:text-gray-900` is almost always wrong — use `dark:text-gray-100`.
|
||||
|
||||
### 3. Dexie LiveQuery Subscriptions
|
||||
* **The Problem:** Accessing `liveQuery` observables directly in templates results in `[object Object]` or `undefined` property errors.
|
||||
* **The Mandate:** ALWAYS use the `$` prefix (e.g. `$lq__obj`) when passing or using data from a Dexie `liveQuery`.
|
||||
|
||||
### 4. Manual Deep Copying vs. Proxies
|
||||
Svelte 5 state is backed by Proxies.
|
||||
* **The Problem:** Using `JSON.parse(JSON.stringify(proxy))` can sometimes trigger unexpected behavior or loops when used inside a reactive context.
|
||||
* **The Fix:** Implement a manual `deep_copy` helper or selective property assignment when syncing "Original" vs "Temporary" state. This ensures `orig_entry_obj` is a plain JS object, making the `has_unsaved_changes` check stable.
|
||||
|
||||
### 5. Journal Entry Config Layout Notes
|
||||
The Entry Config modal now follows a stricter section grammar:
|
||||
* `Metadata` contains category, tags, summary, archive date, and template.
|
||||
* `Status & Security` contains enabled/hidden/priority/sort.
|
||||
* `Visibility & Audience` contains only visibility/audience toggles.
|
||||
* `Alerts & Messaging` contains alert flag + alert message.
|
||||
* `Admin` is gated to trusted access and above, and is the only place for notes plus delete/remove actions.
|
||||
|
||||
### 3. Concurrency Locking (`is_processing`)
|
||||
* **The Problem:** Decryption (Async) and Auto-Save (Debounced Async) can fire nearly simultaneously.
|
||||
* **The Fix:** Use a simple `is_processing` boolean flag. If any async workflow is active, block others from starting and prevent the `has_unsaved_changes` derived rune from reporting `true`.
|
||||
|
||||
### 4. Comparison Normalization
|
||||
* **The Problem:** Trivial differences (e.g., `null` vs `""` or trailing whitespace) would trigger "unsaved changes" and fire the save loop.
|
||||
* **The Fix:** Use a `normalize()` function in the `has_unsaved_changes` derived rune to trim strings and treat `null/undefined` as empty strings during comparison.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Technical Blocker: The "Decryption-Sync" Loop (Resolved)
|
||||
|
||||
### The Issue
|
||||
The component is suffering from a **Reactive Feedback Loop** between decryption, auto-save, and background IDB refreshes.
|
||||
1. **Decrypting content** triggers a change in `tmp_entry_obj.content`.
|
||||
2. The **Auto-Save effect** sees this as a manual user edit and saves to the database.
|
||||
3. **Dexie LiveQuery** detects the DB update and refreshes the object.
|
||||
4. The **Sync effect** resets the entry to its encrypted state (from DB).
|
||||
5. The **Auto-Decryption effect** fires again, starting the loop over.
|
||||
|
||||
### What we tried:
|
||||
* **`is_processing` flags:** Attempted to block reactivity during decryption.
|
||||
* **`untrack()`:** Attempted to isolate store updates.
|
||||
* **Reference Sync:** Attempted to update `orig_entry_obj` simultaneously with decryption to fool the change detector.
|
||||
* **Direct Store Updates:** Switching from property assignment to `journals_sess.update()` to fix Svelte 5 notification failures.
|
||||
|
||||
### Future Fix Ideas:
|
||||
1. **Native Svelte 5 State:** Refactor `journals_sess` from a Svelte 4 `Writable` to a class using `$state`.
|
||||
2. **Logic Extraction:** Move decryption logic into a non-reactive class/helper to isolate side effects from the UI render cycle.
|
||||
3. **Hash Comparison:** Use content hashes for change detection instead of string comparisons to avoid whitespace/normalization loops.
|
||||
@@ -1,97 +0,0 @@
|
||||
# Project: Svelte 4 Store → Svelte 5 State Migration
|
||||
|
||||
**Status:** Execution / Phase B (In Progress)
|
||||
**Priority:** High (post-April 2026 conference)
|
||||
**Created:** 2026-03-30
|
||||
**Related:** `TODO__Agents.md` — [Stores] Svelte 5 State Migration entry
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
All core Aether stores (`ae_loc`, `idaa_loc`, `ae_events_loc`, etc.) are being migrated from
|
||||
Svelte 4 stores to Svelte 5 `$state` using the `runed` library's `PersistedState`. This provides
|
||||
fine-grained reactivity, ensuring that effects only re-run when specific fields they access are
|
||||
updated, rather than on every write to the store object.
|
||||
|
||||
### Phase B Progress & Learnings (Updated 2026-03-30)
|
||||
|
||||
1. **Dependency Installed**: `runed` is now a project dependency.
|
||||
2. **Module Resolution Strategy**:
|
||||
- Core store files renamed: `ae_stores.ts` → `ae_stores.svelte.ts`, etc.
|
||||
- **Critical Discovery**: SvelteKit and CLI tools (`svelte-check`) struggled to resolve
|
||||
extension-less imports (like `$lib/stores/ae_stores`) when only `.svelte.ts` existed.
|
||||
- **Solution**: Created `.ts` wrapper files (e.g., `src/lib/stores/ae_stores.ts`) that
|
||||
simply re-export everything via `export * from './ae_stores.svelte'`. This maintains
|
||||
backward compatibility for all existing import paths without manual updates.
|
||||
3. **API Confirmation**: Confirmed that `runed`'s `PersistedState` uses `.current` to access
|
||||
the state object, matching the intended migration syntax.
|
||||
4. **Mass Replacement**:
|
||||
- `$ae_loc` → `ae_loc_v5.current`
|
||||
- `$idaa_loc` → `idaa_loc_v5.current`
|
||||
- `$events_loc` → `events_loc_v5.current`
|
||||
- These replacements have been applied across the entire `src/` directory (~2000+ sites).
|
||||
5. **Import Updates**: A robust Python script was used to surgically add `ae_loc_v5`,
|
||||
`idaa_loc_v5`, and `events_loc_v5` to existing import blocks, avoiding `import type`
|
||||
lines and duplicates.
|
||||
|
||||
---
|
||||
|
||||
## Syntax Changes: Before / After
|
||||
|
||||
### Store declaration
|
||||
|
||||
```typescript
|
||||
// BEFORE (ae_stores.svelte.ts)
|
||||
export const ae_loc: Writable<key_val> = persisted('ae_loc', defaults);
|
||||
|
||||
// AFTER (ae_stores.svelte.ts)
|
||||
export const ae_loc_v5 = new PersistedState('ae_loc', defaults);
|
||||
```
|
||||
|
||||
### Reading/Writing (Consumers)
|
||||
|
||||
```svelte
|
||||
<!-- BEFORE -->
|
||||
{$ae_loc.theme_mode}
|
||||
{#if $ae_loc.trusted_access}
|
||||
|
||||
<!-- AFTER -->
|
||||
{ae_loc_v5.current.theme_mode}
|
||||
{#if ae_loc_v5.current.trusted_access}
|
||||
```
|
||||
|
||||
### Batch Update Pattern
|
||||
|
||||
```typescript
|
||||
// Use Object.assign for multi-field updates to maintain fine-grained reactivity
|
||||
Object.assign(ae_loc_v5.current, { theme_mode: 'dark', edit_mode: true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Prettier & Formatting
|
||||
Because the mass migration touches ~250 files, formatting may be inconsistent (especially in
|
||||
import blocks). It is recommended to run `npm run format` (if available) or rely on standard
|
||||
linting after the imports are stabilized.
|
||||
|
||||
### Batching vs. "Big Sweep"
|
||||
While the original plan suggested smaller batches, the global nature of `ae_loc` and the
|
||||
hundreds of interdependent files made a "Big Sweep" more efficient to ensure the app remains
|
||||
in a consistent state. However, verification should still be done module-by-module.
|
||||
|
||||
---
|
||||
|
||||
## Current Status (Pause Point)
|
||||
|
||||
- [x] Phase A: Plan written.
|
||||
- [x] Phase B: Core infrastructure setup (runed, renames, wrappers).
|
||||
- [x] Phase B: Mass variable replacement ($ae_loc -> ae_loc_v5.current).
|
||||
- [/] Phase B: Mass import update (In Progress/Verifying).
|
||||
- [ ] Phase B: Final validation (`svelte-check` clean).
|
||||
|
||||
**Do NOT stage or commit until `npx svelte-check` is fully verified.**
|
||||
The app currently has a high error count due to the transition period where imports are being
|
||||
re-aligned. Final verification is the next step after the pause.
|
||||
@@ -1,121 +0,0 @@
|
||||
# Project: CRUD V3 Final Migration
|
||||
|
||||
> **Status:** 🟡 Surgical Cleanup (90% Complete — Events Module Fully Migrated)
|
||||
> **Last Updated:** 2026-05-21
|
||||
> **Goal:** Eliminate all dependency on legacy API wrappers (`create_ae_obj_crud`, `get_ae_obj_id_crud`, etc.) and ensure 100% adoption of the V3 Standard (`/v3/crud/...`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
While the **Journals** and **Identity (User/Account)** modules have been successfully migrated to the V3 architecture, a significant portion of the **Events**, **Sponsorships**, and **IDAA** modules still rely on legacy V1/V2 wrappers. This document serves as the master checklist to reach 100% V3 compliance.
|
||||
|
||||
**Why this matters:**
|
||||
* **Security:** V3 enforces strict multi-tenant isolation via JWT.
|
||||
* **Maintenance:** Legacy wrappers in `api.ts` contribute to technical debt and "God Object" anti-patterns.
|
||||
* **Performance:** V3 offers optimized search and partial updates (PATCH) that legacy endpoints lack.
|
||||
|
||||
---
|
||||
|
||||
## 2. Migration Audit (Findings)
|
||||
|
||||
The following files have been identified as using legacy CRUD wrappers.
|
||||
|
||||
### 🔴 High Priority: Events Module
|
||||
- [x] `src/lib/ae_events/ae_events__event_session.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_presenter.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_presentation.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_location.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_badge_template.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_device.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28)
|
||||
- [x] `src/lib/ae_events/ae_events__event_file.ts` (Migrated 2026-01-30)
|
||||
|
||||
### 🟠 Medium Priority: Core & Sponsorships
|
||||
Legacy patterns persisting in core logic and config modules.
|
||||
|
||||
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
||||
- [x] `src/lib/ae_core/core__hosted_files.ts` (Migrated 2026-01-20)
|
||||
- [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26; bootstrap path uses V3 `search_ae_obj` by `fqdn`)
|
||||
- [x] `src/lib/ae_core/core__site_domain.ts` (Retired 2026-06-02; helper removed after bootstrap migration to `core__site.ts`)
|
||||
- [ ] `src/lib/ae_core/ae_core_functions.ts` (STILL USES `get_ae_obj_id_crud` / `update_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__country_subdivisions.ts`
|
||||
- [ ] `src/lib/ae_core/core__time_zones.ts`
|
||||
- [ ] `src/lib/ae_core/core__countries.ts`
|
||||
|
||||
### 🟡 Low Priority: UI Components & Routes
|
||||
Specific UI components that make direct API calls instead of using store functions.
|
||||
|
||||
- [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`)
|
||||
- [x] `src/lib/elements/element_data_store_v2.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/event_page_menu.svelte`
|
||||
- [x] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.svelte` (Migrated to `update_ae_obj`)
|
||||
- [ ] `src/routes/events/ae_comp__event_session_obj_li.svelte`
|
||||
- [ ] `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/presenter/[presenter_id]/ae_comp__event_presenter_form_agree.svelte` (STILL USES `update_ae_obj_id_crud`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Migration Procedure
|
||||
|
||||
For each file listed above, follow this standard refactoring pattern:
|
||||
|
||||
1. **Imports:**
|
||||
* Remove imports of `create_ae_obj_crud`, `update_ae_obj_id_crud`, etc.
|
||||
* Import V3 helpers: `get_ae_obj`, `create_ae_obj`, `update_ae_obj`, `delete_ae_obj`, `search_ae_obj`.
|
||||
|
||||
2. **Pattern Replacement:**
|
||||
|
||||
* **Get (Single):**
|
||||
* *Old:* `get_ae_obj_id_crud({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
* *New:* `get_ae_obj({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
|
||||
* **Get (List):**
|
||||
* *Old:* `get_ae_obj_li_for_obj_id_crud_v2(...)`
|
||||
* *New:* `get_ae_obj_li(...)` or `search_ae_obj(...)` if complex filtering is needed.
|
||||
|
||||
* **Update:**
|
||||
* *Old:* `update_ae_obj_id_crud({ ..., fields: { name: 'New Name' } })`
|
||||
* *New:* `update_ae_obj({ ..., data: { name: 'New Name' } })`
|
||||
* *Note:* Ensure payload whitelisting is applied! V3 will 400 Error on unknown columns.
|
||||
|
||||
* **Create:**
|
||||
* *Old:* `create_ae_obj_crud({ ..., fields: { ... } })`
|
||||
* *New:* `create_ae_obj({ ..., data: { ... } })`
|
||||
|
||||
3. **Verification:**
|
||||
* Verify the module still loads data (check Network tab for `/v3/` requests).
|
||||
* Verify saving works (check for 400 Bad Request errors).
|
||||
|
||||
## 4. Standard Practices (V3)
|
||||
|
||||
### A. Permissive Update Mode
|
||||
To simplify frontend state management, V3 supports ignoring unknown fields in update payloads.
|
||||
- **Header:** `x-ae-ignore-extra-fields: true` (Enabled by default in `api_patch_object`).
|
||||
- **Use Case:** Allows syncing objects that contain read-only metadata (e.g. `created_on`, `_lq_id`) without manual scrubbing.
|
||||
|
||||
### B. Structured Error Handling
|
||||
V3 returns detailed error metadata in the `meta.details` object.
|
||||
- **Implementation:** Core helpers automatically extract this metadata.
|
||||
- **FastAPI Fallback:** Standard `{"detail": "..."}` responses are automatically wrapped into the `meta.details` format by the frontend helpers.
|
||||
|
||||
---
|
||||
|
||||
## 5. Known Pitfalls
|
||||
|
||||
### A. The "Integer Trap" (Search Mapping)
|
||||
**Issue:** The backend automatically maps certain fields (like `account_id`) from string IDs to internal integers.
|
||||
**Symptom:** Providing a string ID in a search body that the backend maps to an integer can result in **Zero Results** if the underlying view expects a string.
|
||||
|
||||
**Final Solution (Body + Header Injection):**
|
||||
1. **Body:** Inject the raw field name (e.g. `account_id`) into the `search_query.and` array to bypass automatic backend mapping.
|
||||
2. **Headers:** Pass `headers: { 'x-account-id': ... }` manually to provide context for Auth validation.
|
||||
3. **Isolation (IDAA):** Due to specific bugs in the IDAA module, it has been temporarily isolated to a legacy V2 search function (`qry_ae_obj_li__event_v2`) using `default_qry_str` for text searching, while the main module continues to use the V3 implementation.
|
||||
|
||||
---
|
||||
|
||||
## 6. Final Cleanup
|
||||
Once all checkboxes above are completed:
|
||||
1. [x] Remove legacy exports from `src/lib/api/api.ts`.
|
||||
2. [x] Delete `src/lib/ae_api/api_get__crud_obj_li_v1.ts`.
|
||||
3. [x] Delete `src/lib/ae_api/api_get__crud_obj_li_v2.ts`.
|
||||
4. [x] Delete `src/lib/ae_api/api_get__crud_obj_id.ts` (Legacy version).
|
||||
@@ -1,58 +0,0 @@
|
||||
# IDAA Recovery Meetings: UI/UX Improvement Roadmap
|
||||
|
||||
This document outlines proposed enhancements for the IDAA Recovery Meeting module. The goal is to make it easier for members to find and attend meetings, especially on mobile devices, while providing IDAA staff with better tools to manage meeting data quality.
|
||||
|
||||
## 🏆 The "Big Wins" (Highest Member Impact)
|
||||
|
||||
### 1. Automatic Timezone Conversion
|
||||
* **The Problem:** Meetings currently show their "native" time (e.g., 7:00 PM Central). Members must manually calculate the time for their own location.
|
||||
* **The Fix:** The app will automatically detect the member's local timezone and show a converted time side-by-side (e.g., *"7:00 PM Central — 8:00 PM your time"*).
|
||||
|
||||
### 2. "Live Now" & "Today’s Meetings"
|
||||
* **The Fix:**
|
||||
* **Live Now:** A high-visibility green "LIVE" badge will pulse next to meetings currently in progress.
|
||||
* **Today’s Section:** A dedicated section at the very top of the list will show only meetings happening today, sorted by time, so members don't have to scroll through the full 140+ meeting list.
|
||||
|
||||
### 3. Clearer Meeting Schedules
|
||||
* **The Problem:** Days of the week are currently listed as a flat string (Sunday Monday Wednesday).
|
||||
* **The Fix:** Convert schedules into natural language one-liners: *"Mondays, Wednesdays, and Fridays at 7:00 PM."* This is much faster for the human eye to scan.
|
||||
|
||||
### 4. Favorites ("My Meetings")
|
||||
* **The Fix:** Members can "Star" their regular meetings. These favorites will be pinned to the top of their list for one-tap access every week.
|
||||
|
||||
### 5. "Add to Calendar"
|
||||
* **The Fix:** A button to automatically add a recurring meeting to a member’s Google, Apple, or Outlook calendar, including the Zoom/Jitsi link in the calendar event description.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Staff Tools & Data Quality
|
||||
|
||||
### 6. "Confirmed Only" Default View
|
||||
* **The Strategy:** To encourage meeting chairs to keep their information current, we propose defaulting the list to show **only** meetings confirmed by the Central Office.
|
||||
* **Member Benefit:** Higher confidence that the meeting they are about to join is active and the link is correct.
|
||||
* **Staff Benefit:** Creates a natural incentive for chairs to contact IDAA to get "Verified," as unverified meetings would require an extra click to see.
|
||||
|
||||
### 7. Mobile-Friendly "Not Confirmed" Explanations
|
||||
* **The Problem:** On mobile, the warning badge for unconfirmed meetings doesn't explain *why* it's there or *how* to fix it.
|
||||
* **The Fix:** Tapping the badge will show a simple popup: *"This meeting hasn't been verified recently. If you are the chair, please email info@idaa.org to confirm."*
|
||||
|
||||
---
|
||||
|
||||
## 📱 Ease-of-Use & Mobile Polishing
|
||||
|
||||
### 8. Prominent "Join" Buttons & Easy Sharing
|
||||
* **The Fix:** For virtual meetings, we will move the "Join Zoom" button to a prominent, full-width position at the top of the card. We will also add a "Share" button so members can easily text a meeting link to a sponsee.
|
||||
|
||||
### 9. Simplified "Quick-Filter" Chips
|
||||
* **The Fix:** Instead of small checkboxes, we will add large "Chips" (buttons) for common filters: `[🖥 Virtual]` `[🏠 In-Person]` `[🩺 IDAA]` `[Caduceus]`. These are much easier to tap on a phone screen.
|
||||
|
||||
### 10. Intelligent "No Results" Guidance
|
||||
* **The Problem:** If a member filters too narrowly (e.g., "Caduceus meetings in Hawaii on Tuesdays"), they just see a blank screen.
|
||||
* **The Fix:** A helpful prompt will appear: *"No meetings found for these filters. [Clear all filters →]"* to prevent members from thinking the app is broken.
|
||||
|
||||
---
|
||||
|
||||
### Next Steps
|
||||
1. **Feedback:** Staff identifies which 3–4 items are the highest priority for the next update.
|
||||
2. **Prototype:** We implement the high-priority items in the testing environment for staff review.
|
||||
3. **Deployment:** Changes are pushed live to the IDAA website.
|
||||
@@ -1,101 +0,0 @@
|
||||
# Frontend Agent Task List
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** Stable — ongoing development.
|
||||
> **Scope:** Active/open work only. Completed detail lives in archive files.
|
||||
|
||||
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
|
||||
**Post-show hardening only**
|
||||
|
||||
- [ ] **[Launcher/Electron] Wallpaper reliability (post-CMSC)**
|
||||
- [ ] Use timestamp/randomized temp filename so macOS always sees a new path.
|
||||
- [ ] Add resilient reconciliation loop or event-driven reapply on display topology changes.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||
**Setup/Registration:** June 8 | **Show:** June 9
|
||||
|
||||
- [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
|
||||
wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
|
||||
|
||||
---
|
||||
|
||||
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||
|
||||
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
|
||||
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
|
||||
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 High Priority Workstreams
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration
|
||||
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
|
||||
`$state`-based persistence for fine-grained updates.
|
||||
|
||||
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
|
||||
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
||||
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
||||
|
||||
### [Data Layer] IDB sorting + content version rollout
|
||||
Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
|
||||
|
||||
**⚠️ Exception:** `ae_events__event.ts` and `ae_events__event_session.ts` use **legacy encoding**
|
||||
(`priority ? 1 : 0`, priority=true→`'1'`). Their sort comparators must remain **descending**
|
||||
until the modules are migrated to `build_tmp_sort`. `ae_events__event_presentation.ts` already
|
||||
uses `build_tmp_sort` (overrides generic encoding in its `specific_processor`). See
|
||||
`CLIENT__IDAA_and_customized_mods.md` → "Sort Encoding" for full table.
|
||||
|
||||
- [ ] **[IDB Sort] Migrate `ae_events__event.ts` to `build_tmp_sort`** — requires bumping
|
||||
`IDB_CONTENT_VERSIONS.events.event` (currently v3) and switching all event sort comparators
|
||||
to ascending. Check all pages that sort events before doing this.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_session`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_presenter`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_location`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_core__person` + `ae_core__account`** after sort behavior review.
|
||||
- [ ] **[IDB Version] Roll out to `db_events.ts`** (session, presenter, badge, etc.).
|
||||
- [ ] **[IDB Version] Roll out to `db_core.ts`** (site_domain, person, user).
|
||||
|
||||
### [Journals] Journal Entry Config follow-ups
|
||||
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing & Optimization
|
||||
|
||||
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
|
||||
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
|
||||
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
|
||||
- [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ DevOps & Backend
|
||||
|
||||
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
|
||||
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
|
||||
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
|
||||
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
|
||||
backend field (restoring global/cross-device persistence). Frontend code is in
|
||||
`launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||
- [x] **[DevOps] Service worker `skipWaiting` + `clients.claim`** — Root cause of "users see
|
||||
old code / can't reproduce in dev testing": the SW sat in waiting state until all tabs closed.
|
||||
IDAA members leave idaa.org open all day. Fixed 2026-06-03: both calls added to
|
||||
`src/service-worker.js`. See mistake #16 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
- [ ] **[DevOps] Nginx proxy buffer tuning** — Buffer settings copied from PHP guide; not
|
||||
optimal for Node.js. `proxy_busy_buffers_size` technically exceeds safe limit. Re-examine
|
||||
when enabling compression (now re-enabled) stabilizes.
|
||||
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed (archived)
|
||||
See the full completed history in:
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-06.md](documentation/archive/TODO__Agents__ARCHIVE_2026-06.md)
|
||||
@@ -1,289 +0,0 @@
|
||||
# PROJECT: Access Control UX — Session Expired & Access Denied
|
||||
|
||||
**Status:** Complete
|
||||
**Priority:** Medium-High
|
||||
**Created:** 2026-02
|
||||
**Updated:** 2026-03-11
|
||||
**Related:** `src/routes/+layout.svelte`, `src/lib/ae_api/`, `src/lib/stores/ae_stores.ts`
|
||||
|
||||
---
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Clean up inconsistent access-denied and session-expired UX across the app:
|
||||
|
||||
1. **Session Expired banner** — When the API returns 401/403, show a non-blocking dismissible banner in the root layout rather than silently failing. The `flag_expired` placeholder in the root layout is already wired for this but nothing sets it.
|
||||
2. **Standardize Access Denied display** — Replace the one-off browser `alert()` in event settings with a proper in-page gate. Create a small reusable component for inline denial cards.
|
||||
3. **Maintain intentional special cases** — IDAA Novi UUID gate and the Root Site Access Key gate are correct and must not be touched.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State Inventory
|
||||
|
||||
### Pattern A — Root Layout: Site Access Key Gate ✅ Working, keep as-is
|
||||
|
||||
**File:** `src/routes/+layout.svelte` lines 130–135, 299–305
|
||||
**Type:** Full-screen blocker
|
||||
**Logic:** If `site_access_key` is set and `allow_access` key doesn't match + user is not `trusted_access`, `flag_denied = true`.
|
||||
**Current UI:** Minimal full-screen `<h1>Access Denied</h1>` + Reload button.
|
||||
**Verdict:** Works correctly. The minimal styling is intentional (it's a hard site gate). Keep but no code change needed unless you want to polish the styling later.
|
||||
|
||||
---
|
||||
|
||||
### Pattern B — Root Layout: Session Expired Banner 🔴 Declared, never set
|
||||
|
||||
**File:** `src/routes/+layout.svelte` line 63
|
||||
**Variable:** `let flag_expired: boolean = $state(false)` — **never set anywhere.**
|
||||
**Intent:** This should show a non-blocking dismissible banner whenever the API returns 401/403, signaling a stale JWT/session.
|
||||
**Gap:** API helpers detect 401/403 and log diagnostic info but never fire any store event.
|
||||
**Fix:** See Implementation Plan step 1.
|
||||
|
||||
---
|
||||
|
||||
### Pattern C — Core Layout: Manager Access Gate ✅ Already correct
|
||||
|
||||
**File:** `src/routes/core/+layout.svelte` lines 13–20, 62–75
|
||||
**Logic:**
|
||||
- `onMount`: 500ms delay then `goto('/')` if still not `manager_access` (handles slow hydration)
|
||||
- `{:else}` block: Shows a styled "Access Restricted" card with Lock icon, description, "Return Home" button while waiting/denied
|
||||
|
||||
**Verdict:** This pattern is correct and consistent. The `{:else}` visual gate prevents flashing. The delayed redirect is a graceful fallback. **No changes needed.**
|
||||
|
||||
---
|
||||
|
||||
### Pattern D — IDAA Layout: Novi UUID Gate ✅ Intentionally custom, keep as-is
|
||||
|
||||
**File:** `src/routes/idaa/(idaa)/+layout.svelte`
|
||||
**Logic:** Async POST to `https://www.idaa.org/api` to verify Novi UUID. `novi_verifying` flag prevents Access Denied flash during network round-trip.
|
||||
**Verdict:** Intentionally custom to IDAA's member verification flow. **Do not standardize or touch this.**
|
||||
|
||||
---
|
||||
|
||||
### Pattern E — Event Settings: browser `alert()` 🟡 Needs fix
|
||||
|
||||
**File:** `src/routes/events/[event_id]/settings/+page.svelte` lines 44–47
|
||||
**Current code:**
|
||||
```ts
|
||||
if (!$ae_loc.administrator_access) {
|
||||
if (browser) {
|
||||
alert('Access Denied: Administrative privileges and Edit Mode required.');
|
||||
goto(`/events/${event_id}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
**Problems:**
|
||||
1. `alert()` is a blocking browser dialog — ugly, inconsistent with app UX
|
||||
2. Runs in module-level `if` block (not `onMount`) — can fire before hydration is fully complete
|
||||
3. No visual component shown; just redirects
|
||||
|
||||
**Fix:** Remove `alert()`, move check into `onMount` with a small delay (like `/core` pattern), or add an inline gate using a reusable component.
|
||||
|
||||
---
|
||||
|
||||
### Pattern F — Badge Review: Inline "Access Denied" Card 🟡 Acceptable, minor polish
|
||||
|
||||
**File:** `src/routes/events/[event_id]/(badges)/badges/[badge_id]/review/+page.svelte` lines 315–330
|
||||
**Context:** Passcode check failure — attendee entered wrong passcode
|
||||
**Current UI:**
|
||||
```html
|
||||
<div class="card p-6 space-y-4 max-w-sm">
|
||||
<div class="flex items-center gap-2 text-error-500">
|
||||
<h3 class="text-lg font-semibold">Access Denied</h3>
|
||||
</div>
|
||||
<p class="text-sm text-gray-700">{passcode_error}</p>
|
||||
<button ... >Try Again</button>
|
||||
</div>
|
||||
```
|
||||
**Verdict:** Contextually appropriate and functional. The "Try Again" button is good UX. This is a prime candidate to be replaced with a reusable component once one exists, but it is not broken.
|
||||
|
||||
---
|
||||
|
||||
### Pattern G — API Helpers: 401/403 Detection Without UI Feedback 🔴 Gap
|
||||
|
||||
**Files:** `src/lib/ae_api/api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts` (all ~line 237)
|
||||
**Current behavior:** Logs auth diagnostics to console, returns `false` or `null`. No store event fired.
|
||||
**Gap:** When a JWT expires mid-session, the user sees requests silently fail (data doesn't load/save) with no explanation. They may think the app broke.
|
||||
**Fix:** On 401/403, set `ae_auth_error` store → root layout watches it and sets `flag_expired = true`.
|
||||
|
||||
---
|
||||
|
||||
### Pattern H — Presenter Auth (`auth__person`) — Existing system, no UX issues to fix now
|
||||
|
||||
**Store:** `$events_loc.auth__person` — stores authenticated presenter identity
|
||||
**URL params:** `?person_id=...&person_pass=...&presentation_id=...&presenter_id=...`
|
||||
**Anonymous toggle:** Per-event config allows presenters to upload files without signing in
|
||||
**Verdict:** Auth system is working. The gating UI in the presenter pages is contextually managed. Not in scope for this cleanup. Revisit when building out the Leads feature or a future auth refactor.
|
||||
|
||||
---
|
||||
|
||||
## 3. Issues Ranked by Priority
|
||||
|
||||
| # | Severity | Issue | File | Fix |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 🔴 High | API 401/403 silently fails — users have no feedback | `api_*.ts` | Wire to `ae_auth_error` store |
|
||||
| 2 | 🔴 High | `flag_expired` never set — session expired banner never shows | `+layout.svelte` | Watch `ae_auth_error`, render banner |
|
||||
| 3 | 🟡 Medium | `alert()` in event settings — ugly, blocking, not idiomatic | `settings/+page.svelte` | Replace with `onMount` gate + reusable component |
|
||||
| 4 | 🟢 Low | Badge review inline card — not reusing a component | `badges/.../review/+page.svelte` | Replace when `element_access_denied.svelte` is ready |
|
||||
|
||||
---
|
||||
|
||||
## 4. Design Decisions
|
||||
|
||||
### 4a. Session Expired Banner Design
|
||||
|
||||
- **Non-blocking top bar** — similar to the existing `is_offline` and `api_unreachable` banners in the root layout
|
||||
- **Dismissible** — user clicks X to clear; or auto-hides after signing back in
|
||||
- **Message:** "Your session has expired. Please reload or sign in again." with a Reload button
|
||||
- **Trigger:** Any 401 or 403 from any of the three API helpers
|
||||
|
||||
### 4b. `ae_auth_error` Store
|
||||
|
||||
Simple writable in `ae_stores.ts`:
|
||||
```ts
|
||||
export const ae_auth_error = writable<{ type: 'expired' | 'denied' | null, ts: number | null }>({ type: null, ts: null });
|
||||
```
|
||||
This is intentionally minimal — just enough to signal the root layout.
|
||||
|
||||
### 4c. Reusable `element_access_denied.svelte`
|
||||
|
||||
A small card component for inline access denial within a page:
|
||||
```
|
||||
Props:
|
||||
- title?: string (default: "Access Denied")
|
||||
- message?: string (default: "You do not have permission to view this content.")
|
||||
- show_reload?: boolean
|
||||
- show_return_home?: boolean
|
||||
- action_label?: string (optional extra button)
|
||||
- on_action?: () => void
|
||||
```
|
||||
Location: `src/lib/elements/element_access_denied.svelte`
|
||||
|
||||
### 4d. Event Settings Fix
|
||||
|
||||
The settings page check should mirror the `/core` pattern:
|
||||
- Move to `onMount` with 500ms grace delay
|
||||
- No `alert()` — if not authorized, the redirect fires silently after the delay
|
||||
- Add inline gate (`{:else}` block with "Access Restricted" message) if the user somehow lands here
|
||||
|
||||
### 4e. What We Are NOT Changing
|
||||
|
||||
- Root Layout site access key gate — working correctly
|
||||
- `/core` layout — already correct
|
||||
- IDAA Novi UUID gate — intentionally custom
|
||||
- Presenter auth system (`auth__person`) — not in scope
|
||||
|
||||
---
|
||||
|
||||
## 5. Implementation Plan
|
||||
|
||||
### Step 1: Add `ae_auth_error` store ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/lib/stores/ae_stores.ts`
|
||||
|
||||
Add after the existing store declarations:
|
||||
```ts
|
||||
// Auth error signal — set by API helpers on 401/403 to trigger root layout session-expired banner
|
||||
export const ae_auth_error = writable<{ type: 'expired' | null, ts: number | null }>({ type: null, ts: null });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Wire API helpers to `ae_auth_error` ✅ DONE (2026-03-11)
|
||||
|
||||
**Files:** `src/lib/ae_api/api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts` (same pattern in all three)
|
||||
|
||||
In the existing `if (response.status === 401 || response.status === 403)` block, add one line after the existing `console.warn(...)`:
|
||||
```ts
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
// ...
|
||||
ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
```
|
||||
|
||||
**Note:** Only import `ae_auth_error` — no other store changes. Do NOT import `ae_auth_error` at module level if the API helpers are used SSR-side. Use a dynamic import or guard with `browser` check if needed.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Wire `flag_expired` in root layout ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/routes/+layout.svelte`
|
||||
|
||||
Add an `$effect` that watches `$ae_auth_error` and sets `flag_expired`:
|
||||
```ts
|
||||
$effect(() => {
|
||||
if ($ae_auth_error?.type === 'expired' && $ae_auth_error?.ts) {
|
||||
untrack(() => { flag_expired = true; });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Add the dismissible banner to the template (after/near the existing `is_offline` banner, in the `{#if browser && $ae_loc?.allow_access}` block):
|
||||
```html
|
||||
{#if flag_expired}
|
||||
<div class="fixed top-0 left-0 right-0 z-50 bg-warning-500 text-white px-4 py-2 flex items-center justify-between">
|
||||
<p class="text-sm font-semibold">Your session has expired. Please reload or sign in again.</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm preset-filled-surface" onclick={() => window.location.reload()}>Reload</button>
|
||||
<button class="btn btn-sm" onclick={() => { flag_expired = false; ae_auth_error.set({ type: null, ts: null }); }}>Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Create `element_access_denied.svelte` ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/lib/elements/element_access_denied.svelte`
|
||||
|
||||
Reusable card for inline access denial. Props per design decision 4c.
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Fix Event Settings `alert()` ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/routes/events/[event_id]/settings/+page.svelte`
|
||||
|
||||
Replace the module-level `if (!$ae_loc.administrator_access)` + `alert()` block with:
|
||||
1. Move check into `onMount` with the same 500ms grace-delay pattern as `/core`
|
||||
2. Add `{:else}` gate in the template using `element_access_denied.svelte`
|
||||
3. Remove the `browser` guard (not needed inside `onMount`)
|
||||
|
||||
---
|
||||
|
||||
### Step 6 (Optional / Low Priority): Swap badge review inline card ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/routes/events/[event_id]/(badges)/badges/[badge_id]/review/+page.svelte`
|
||||
|
||||
Replace inline access denied card with `element_access_denied.svelte` once the component exists. Keep "Try Again" action via `on_action` prop.
|
||||
|
||||
---
|
||||
|
||||
## 6. Files to Modify Summary
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/lib/stores/ae_stores.ts` | Add `ae_auth_error` writable store |
|
||||
| `src/lib/ae_api/api_get_object.ts` | Set `ae_auth_error` on 401/403 |
|
||||
| `src/lib/ae_api/api_post_object.ts` | Set `ae_auth_error` on 401/403 |
|
||||
| `src/lib/ae_api/api_patch_object.ts` | Set `ae_auth_error` on 401/403 |
|
||||
| `src/routes/+layout.svelte` | Watch `ae_auth_error`, render session-expired banner |
|
||||
| `src/routes/events/[event_id]/settings/+page.svelte` | Remove `alert()`, fix auth gate pattern |
|
||||
| `src/lib/elements/element_access_denied.svelte` | **NEW** — reusable inline denial card |
|
||||
| `src/routes/events/[event_id]/(badges)/badges/[badge_id]/review/+page.svelte` | Swap inline card with component (low priority) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Notes
|
||||
|
||||
- **Session expired banner:** Force a 401 by testing with an expired JWT or by calling an API with wrong credentials. Banner should appear. Dismiss should clear it. Reload should reload browser.
|
||||
- **Event settings gate:** Navigate to `/events/{id}/settings` without `administrator_access`. Should redirect without any `alert()` dialog.
|
||||
- **Badge review:** Enter a bad passcode — Access Denied card should appear with "Try Again".
|
||||
- **IDAA, /core, root site key gate:** Verify no regressions.
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks & Notes
|
||||
|
||||
- The API helpers (`api_get_object.ts` etc.) run in a module context that is imported across many components. The `ae_auth_error` store must only be imported/used inside a `browser` guard or dynamically if the helpers are ever called SSR-side. Check `src/lib/ae_core/ae_core__site.ts` (which uses these helpers during SSR hydration) — **do not set the store from SSR context.** Add `if (browser)` before the `ae_auth_error.set(...)` call.
|
||||
- The root layout sync effect runs `untrack()` to prevent circular store updates. The new `$effect` for `ae_auth_error` must also use `untrack()` to be safe.
|
||||
- `flag_expired` should NOT permanently gate the UI — it should only show a top banner. The user may have been mid-editing and should not lose their work. The current `flag_denied` full-screen block is for site key access only.
|
||||
@@ -1,53 +0,0 @@
|
||||
# Project: AE Docker + CI BuildKit Implementation
|
||||
|
||||
**Status:** Proposed
|
||||
|
||||
**Goal:** Make Docker image builds for Aether cache-friendly using BuildKit/buildx and CI registry caching, while keeping local developer caches small and manageable.
|
||||
|
||||
Summary
|
||||
- Implement a BuildKit-friendly multi-stage `Dockerfile` pattern for frontend and API images.
|
||||
- Add CI `buildx` examples that push/read registry-based cache to avoid local disk bloat.
|
||||
- Provide cache retention/rotation guidance and developer commands for safe pruning.
|
||||
|
||||
Scope
|
||||
- Repository areas: `aether_container_env/`, root `Dockerfile` (if present), and CI pipeline definitions (Gitea/Drone or other).
|
||||
- Non-goal: full CI pipeline migration to a new provider. This work provides CI snippets and a PR-ready set of files for your CI team.
|
||||
|
||||
Deliverables (this PR)
|
||||
- `documentation/PROJECT__AE_Docker_CI_BuildKit_implement.md` (this file)
|
||||
- `aether_container_env/Dockerfile.buildkit.example` — BuildKit-friendly multi-stage Dockerfile example.
|
||||
- `aether_container_env/ci_buildx_example.sh` — standalone CI script examples (registry cache + local cache usage).
|
||||
- `documentation/AE_Docker_CI_cache_policy.md` — cache rotation and prune guidance.
|
||||
|
||||
Tasks (implementation checklist)
|
||||
- [ ] Review existing `Dockerfile`(s) under `aether_container_env/` and repository root.
|
||||
- [ ] Replace/extend Dockerfile with multi-stage BuildKit-friendly layout (use example as guide).
|
||||
- [ ] Ensure `.dockerignore` (already added) excludes large build artifacts.
|
||||
- [ ] Add CI step using `docker buildx build` with `--cache-from` and `--cache-to` pointed at a registry cache.
|
||||
- [ ] Add a scheduled job or registry lifecycle rule to delete old cache images (30 days default).
|
||||
- [ ] Document required CI secrets and permissions (registry write/read) for the operations team.
|
||||
- [ ] Run verification builds (dev local with BuildKit; CI runs with cache) and record timings.
|
||||
|
||||
Verification
|
||||
- Local dev: `DOCKER_BUILDKIT=1` build with `--cache-to`/`--cache-from` shows cache hits on second run and faster build time.
|
||||
- CI: subsequent CI runs log `cache hit` from `buildx` and total build time reduced vs baseline.
|
||||
- Confirm registry contains `cache` image tags and that rotation job/prune removes old entries.
|
||||
|
||||
Notes about Gitea/CI
|
||||
- Gitea does not include native Actions like GitHub; teams typically use Drone CI, Tekton, or a self-hosted runner that can execute the `docker`/`buildx` CLI.
|
||||
- The provided `ci_buildx_example.sh` is intentionally provider-agnostic — pasteable into Drone, Jenkins, GitLab CI, or any shell-capable runner.
|
||||
|
||||
Risks & Mitigations
|
||||
- Risk: Unbounded registry cache growth. Mitigation: enforce retention policy and rotation job; prefer a single `cache` tag reused by CI.
|
||||
- Risk: Developers unfamiliar with BuildKit. Mitigation: examples show simple `DOCKER_BUILDKIT=1` usage and local cache prune commands.
|
||||
|
||||
Next steps for the container team
|
||||
1. Review examples in `aether_container_env/` and adapt the Dockerfile to your runtime constraints (ssl certs, env injection, secrets).
|
||||
2. Add a CI job using the `ci_buildx_example.sh` snippet; configure registry credentials as secrets.
|
||||
3. Add a scheduled job to rotate/delete old cache images or configure registry lifecycle rules.
|
||||
4. Run a before/after benchmark of `time npm run build:prod` inside the build stage to quantify improvement.
|
||||
|
||||
Files included in this PR for reference:
|
||||
- `aether_container_env/Dockerfile.buildkit.example`
|
||||
- `aether_container_env/ci_buildx_example.sh`
|
||||
- `documentation/AE_Docker_CI_cache_policy.md`
|
||||
@@ -1,258 +0,0 @@
|
||||
# Project: Badges Config Cleanup & Config UI
|
||||
|
||||
**Status:** Executed — Complete
|
||||
**Priority:** Medium-High (post-April 2026 BGH conference; same pattern as pres_mgmt cleanup)
|
||||
**Created:** 2026-04-02
|
||||
**Related:** `TODO__Agents.md`, `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`, `PROJECT__Stores_Svelte5_Migration.md`, `MODULE__AE_Events_Badges.md`
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The badges module has accumulated the same class of problems as pres_mgmt before its cleanup:
|
||||
|
||||
- `mod_badges_json` is typed as `any` in `ae_types.ts` and `key_val | null` in `db_events.ts` — no canonical TypeScript interface exists.
|
||||
- Badge search and UI state still lives in `events_loc.badges` (Svelte 4 nested store), with manual `typeof x === 'undefined'` guards in `+page.svelte` and `ae_comp__badge_search.svelte`.
|
||||
- `ae_events_stores__badges_defaults.ts` already has typed `BadgesLocState` and `BadgesSessState` interfaces wired into `events_loc` — but these have not yet been promoted to a standalone `PersistedState` like pres_mgmt's `pres_mgmt_loc`.
|
||||
- The `edit_permissions` sub-object (which controls which badge fields each access level may edit) is documented and wired up in `ae_comp__event_settings_badges_form.svelte`, but the review page (`[badge_id]/review/+page.svelte`) still uses hardcoded defaults with `TODO` markers instead of reading from `mod_badges_json`.
|
||||
- `trusted_passcode` and `administrator_passcode` are stored in `mod_badges_json` and managed via the legacy settings form — no dedicated config UI exists.
|
||||
- Admin must edit the settings form (or DB directly) to change badge config — no standalone, grouped config page exists (unlike pres_mgmt, which now has `/pres_mgmt/config`).
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Canonical config schema** — define `BadgesRemoteCfg` TypeScript interface for `mod_badges_json`
|
||||
2. **New Svelte 5 store** — promote `events_loc.badges` to a standalone `PersistedState` (`badges_loc`) with its own localStorage key
|
||||
3. **Wire `edit_permissions`** — connect the review page to `mod_badges_json.edit_permissions` (remove hardcoded defaults)
|
||||
4. **Config UI** — dedicated admin page at `(badges)/badges/config/` for managing `mod_badges_json`
|
||||
5. **Security review** — ensure passcode fields are never exposed to non-administrator access
|
||||
|
||||
---
|
||||
|
||||
## Canonical Remote Config Schema
|
||||
|
||||
`BadgesRemoteCfg` — the authoritative TypeScript interface for `event.mod_badges_json`:
|
||||
|
||||
```typescript
|
||||
interface BadgesRemoteCfg {
|
||||
// Search & UI behaviour
|
||||
enable_mass_print: boolean; // show the mass-print controls
|
||||
enable_add_badge_btn: boolean; // show the "Add Badge" button
|
||||
enable_upload_badge_li_btn: boolean; // show the "Upload Badge List" button
|
||||
enable_search_qr: boolean; // enable QR scan search
|
||||
|
||||
// QR code configuration
|
||||
qr_type: string | null; // QR payload format (e.g. 'badge_id', 'url')
|
||||
|
||||
// Access control — passcodes for attendee / staff tiered access
|
||||
// WARNING: Only expose to administrator_access. Never render client-side for lower levels.
|
||||
trusted_passcode: string | null;
|
||||
administrator_passcode: string | null;
|
||||
|
||||
// Field-level edit permissions per access tier
|
||||
// key = access level ('authenticated' | 'trusted' | 'administrator')
|
||||
// value.can_edit = string[] of field keys, or '*' for all fields
|
||||
edit_permissions: {
|
||||
authenticated?: { can_edit: string[] | '*' };
|
||||
trusted?: { can_edit: string[] | '*' };
|
||||
administrator?: { can_edit: string[] | '*' };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Default field permissions (encoded in defaults, not hardcoded in review page)
|
||||
|
||||
```typescript
|
||||
// Attendee (passcode-authenticated)
|
||||
authenticated.can_edit = [
|
||||
'pronouns_override',
|
||||
'full_name_override',
|
||||
'professional_title_override',
|
||||
'affiliations_override',
|
||||
'phone_override',
|
||||
'location_override',
|
||||
'allow_tracking',
|
||||
'agree_to_tc',
|
||||
]
|
||||
|
||||
// Trusted staff
|
||||
trusted.can_edit = [
|
||||
'pronouns_override',
|
||||
'full_name_override',
|
||||
'professional_title_override',
|
||||
'affiliations_override',
|
||||
'phone_override',
|
||||
'location_override',
|
||||
'email_override',
|
||||
'badge_type_code_override',
|
||||
'registration_type_code_override',
|
||||
'allow_tracking',
|
||||
'agree_to_tc',
|
||||
'hide',
|
||||
'priority',
|
||||
'notes',
|
||||
// other_1_code ... other_8_code
|
||||
// ticket_1_code ... ticket_8_code
|
||||
]
|
||||
|
||||
// Administrator
|
||||
administrator.can_edit = '*'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Svelte 5 Local Store
|
||||
|
||||
**Do NOT touch `events_loc` or the paused Svelte 5 migration.**
|
||||
Instead, promote the existing `BadgesLocState` to a standalone store.
|
||||
|
||||
**Files to create/modify:**
|
||||
- **New store:** `src/lib/stores/ae_events_stores__badges.svelte.ts`
|
||||
- **Defaults file:** `src/lib/stores/ae_events_stores__badges_defaults.ts` (already exists — no change needed to the types)
|
||||
- **Version gate:** add `AE_BADGES_LOC_VERSION` to `store_versions.ts`
|
||||
|
||||
```typescript
|
||||
// ae_events_stores__badges.svelte.ts
|
||||
import { PersistedState } from 'runed';
|
||||
import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
|
||||
|
||||
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults);
|
||||
// Usage: badges_loc.current.fulltext_search_qry_str
|
||||
```
|
||||
|
||||
New localStorage key: `ae_badges_loc` (separate from `ae_events_loc`)
|
||||
|
||||
Consumer syntax change:
|
||||
```
|
||||
BEFORE: $events_loc.badges.fulltext_search_qry_str
|
||||
AFTER: badges_loc.current.fulltext_search_qry_str
|
||||
```
|
||||
|
||||
### Store migration scope
|
||||
|
||||
`$events_loc.badges` is used in two files (~48 references total):
|
||||
- `(badges)/badges/+page.svelte` — all search params, inline guards (lines 59-73, 116-148, 423-424)
|
||||
- `(badges)/badges/ae_comp__badge_search.svelte` — all filter bindings (lines 40-228)
|
||||
|
||||
The manual `typeof x === 'undefined'` guards in `+page.svelte` are eliminated entirely —
|
||||
`PersistedState` with typed defaults guarantees fields always exist.
|
||||
|
||||
---
|
||||
|
||||
## Review Page — Wire `edit_permissions`
|
||||
|
||||
**File:** `(badges)/badges/[badge_id]/review/+page.svelte`
|
||||
|
||||
Currently has two `TODO` markers at lines ~60 and ~197 where `can_edit_fields` is built
|
||||
from hardcoded arrays instead of `mod_badges_json.edit_permissions`.
|
||||
|
||||
**After this change:**
|
||||
1. Load `lq__event_obj` (already available via Dexie liveQuery in that page)
|
||||
2. Derive `can_edit_fields` from `$lq__event_obj?.mod_badges_json?.edit_permissions`
|
||||
3. Fall back to the defaults from `BadgesRemoteCfg` defaults if `edit_permissions` is not set
|
||||
4. The `ae_comp__badge_review_form.svelte` component interface is already correct — it accepts `can_edit_fields: string[]` prop
|
||||
|
||||
---
|
||||
|
||||
## Config UI Page
|
||||
|
||||
**Route:** `/events/[event_id]/(badges)/badges/config/`
|
||||
**Access:** `$ae_loc.administrator_access` only (passcodes present — stricter than pres_mgmt's manager_access)
|
||||
**Button visibility:** Edit mode only (or always visible in the section header, admin-gated)
|
||||
|
||||
### Page behaviour
|
||||
- Loads `event.mod_badges_json` fresh from API (or Dexie) on page open
|
||||
- Displays grouped form sections (see below)
|
||||
- Save = load → merge draft → PATCH `/v3/crud/event/{event_id}` with `{ mod_badges_json: updated }`
|
||||
- Settings page `Badges (mod_badges_json)` section gets a link to this page + raw JSON fallback (same pattern as pres_mgmt)
|
||||
|
||||
### Form sections
|
||||
|
||||
1. **Search & UI** — `badge_id_only_search`, `enable_mass_print`, `enable_add_badge_btn`, `enable_upload_badge_li_btn`, `enable_search_qr`
|
||||
2. **QR Config** — `qr_type` (text input)
|
||||
3. **Access Passcodes** — `trusted_passcode`, `administrator_passcode` (masked inputs; only visible to administrator_access)
|
||||
4. **Attendee Editable Fields** — `edit_permissions.authenticated.can_edit` (checkbox list per known field)
|
||||
5. **Staff Editable Fields** — `edit_permissions.trusted.can_edit` (checkbox list per known field)
|
||||
|
||||
> Administrator is always `*` (all fields) — no UI control needed, show as read-only note.
|
||||
|
||||
---
|
||||
|
||||
## Settings Page Changes
|
||||
|
||||
`settings/+page.svelte` → `Badges (mod_badges_json)` section:
|
||||
|
||||
```svelte
|
||||
<!-- Replace the form+toggle with: -->
|
||||
<p class="text-sm text-surface-500">
|
||||
Manage badge search, print controls, QR config, passcodes, and field permissions.
|
||||
</p>
|
||||
<a href="/events/{event_id}/badges/config" class="btn btn-sm preset-tonal-primary">
|
||||
Open Badges Config
|
||||
</a>
|
||||
<!-- Raw JSON fallback for debugging / emergency edits -->
|
||||
<details class="mt-2">
|
||||
<summary class="text-xs text-surface-400 cursor-pointer">Raw JSON (advanced)</summary>
|
||||
<!-- existing CodeMirror editor remains here -->
|
||||
</details>
|
||||
```
|
||||
|
||||
The old `ae_comp__event_settings_badges_form.svelte` can be retired after the config page is live —
|
||||
keep the file for now but stop importing it from the settings page.
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- `trusted_passcode` and `administrator_passcode` are sensitive credentials.
|
||||
- The config page must be gated at `administrator_access` (not just `manager_access`).
|
||||
- Input fields should use `type="password"` with a show/hide toggle — do not render as plain text.
|
||||
- Never include passcode values in client-side logs or error messages.
|
||||
- `edit_permissions` affects what data attendees can self-modify — changes take effect on the next page load (no caching concern since it's read from `mod_badges_json` on load).
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
Safe and backward compatible — the review page already falls back to hardcoded defaults.
|
||||
|
||||
1. New `BadgesRemoteCfg` interface — no DB changes needed
|
||||
2. `ae_events_stores__badges.svelte.ts` — new file, new localStorage key (`ae_badges_loc`)
|
||||
3. Migrate `$events_loc.badges.*` → `badges_loc.current.*` in two files (~48 refs)
|
||||
4. Wire review page `can_edit_fields` to `mod_badges_json.edit_permissions`
|
||||
5. Build config UI page and update settings page
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
- [x] **Step 1** — Define `BadgesRemoteCfg` TypeScript interface (added to `ae_events_stores__badges_defaults.ts`; also extracted `default_authenticated_can_edit` and `default_trusted_can_edit` constants)
|
||||
- [x] **Step 2** — Created `ae_events_stores__badges.svelte.ts` with `PersistedState`; added `AE_BADGES_LOC_VERSION` to `store_versions.ts`
|
||||
- [x] **Step 3** — Migrated `$events_loc.badges.*` → `badges_loc.current.*` in `+page.svelte` and `ae_comp__badge_search.svelte`; removed all manual `typeof` guards
|
||||
- [x] **Step 4** — Wired `edit_permissions` into review page `can_edit_fields`; the two TODO blocks resolved
|
||||
- [x] **Step 5** — Built config UI at `(badges)/badges/config/+page.svelte` (administrator_access gated)
|
||||
- [x] **Step 6** — Updated settings page `Badges` section with link to config page; retired the old form component import
|
||||
- [ ] **Step 7** — Update active event(s) via new UI; verify passcode fields function correctly
|
||||
- [x] **Step 8** — `npx svelte-check` clean; commit
|
||||
|
||||
> **Implementation note (2026-04-02):** Passcode fields use plain `type="text"` inputs, not `type="password"`. This matches the admin UI convention for this codebase.
|
||||
|
||||
### Step 3 scope (find-replace)
|
||||
|
||||
```
|
||||
grep -rn 'events_loc\.badges' src/
|
||||
```
|
||||
Affected files:
|
||||
- `src/routes/events/[event_id]/(badges)/badges/+page.svelte` (~35 refs)
|
||||
- `src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte` (~13 refs)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `BadgesLocState` already has typed interfaces in `ae_events_stores__badges_defaults.ts` — this is ahead of where pres_mgmt was. Steps 1-3 are therefore lower risk.
|
||||
- The `BadgesSessState` (in-memory, resets on page load) does **not** need to move — it can stay in `events_sess.badges` inside the main store for now; it contains no persisted user prefs.
|
||||
- `enable_search_qr` and `qr_type` need validation: verify what QR type values are actually consumed by the scan component before exposing them as free-text inputs. A select with known options is safer.
|
||||
- Badge type code options (`member`, `non-member`, `guest`, etc.) are defined per Event Badge Template — the config page should not hardcode them. If badge type selects are needed in config, pull from `db_events.badge_template` liveQuery.
|
||||
- The `agree_to_tc` field in `can_edit_fields` is a placeholder — no Terms & Conditions flow exists yet. Gate it with a note in the UI.
|
||||
@@ -1,175 +0,0 @@
|
||||
Aether Events Exhibitor Leads v3
|
||||
=======
|
||||
|
||||
|
||||
## Overview:
|
||||
* Mobile first!
|
||||
* Offline caching for spotty network connections
|
||||
* Clean and simple
|
||||
* Exhibitors have between 0 and X license
|
||||
* Sign in and "claim" an assigned license linked to an Exhibitor
|
||||
* Collect and manage Leads per Exhibitor
|
||||
* Export Leads data to CSV or XLSX
|
||||
* Exhibitors will have the link to their Exhibit sent to them or they can use the Exhibit Search to find their Exhibit and sign in with the shared passcode or their assigned licensed user credentials.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Primary Leads Pages/Tabs
|
||||
1. start - Sign In / Licenses
|
||||
* Exhibit passcode sign in
|
||||
* Exhibitor Leads user sign in
|
||||
* Payment - Leads Payment
|
||||
2. add - Add (Search/QR)
|
||||
* Text search
|
||||
* QR scan
|
||||
3. leads - Leads List
|
||||
* List of Attendees (Event Badge ID) linked to Exhibit
|
||||
* Click to view/edit Exhibit Tracking entry
|
||||
4. manage - Leads (app and exhibit) Manage
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Exhibitor Sign In and Licenses:
|
||||
* An Exhibit needs to have a "Shared exhibit passcode" for primary sign in.
|
||||
* A Licensed Exhibit staff person needs to have a License assigned to them. It is important that the email address not be changed per license. Or if they do, be aware of the affects on tracked attendees. They can then sign in with that email address and passcode assigned. And/Or we need to make the email sign in link work as well.
|
||||
* Once signed in with the Exhibit passcode, they can change it (passcode) and assign Exhibit Leads licenses once they have paid.
|
||||
* An exhibitor marked as "priority" means they have paid. Only OSIT admins can mark them as paid.
|
||||
* Should at least claim/assign one initial user license so an Exhibit related staff can sign in (without the shared Exhibit passcode). Then add, update, or remove licenses based on max allowed per Exhitibor.
|
||||
|
||||
Payment:
|
||||
* Not Paid: <span class="fas fa-question text-red-500 m-1"></span> <span class="fas fa-credit-card mx-1"></span> Waiting for payment
|
||||
* Paid: <span class="fas fa-check text-green-500 m-1"></span> <span class="fas fa-credit-card mx-1"></span> Marked as paid
|
||||
|
||||
### Licenses:
|
||||
A client staff (Trusted Access or above) or someone signed in with an Exhibit passcode can add/edit/remove icenses.
|
||||
|
||||
License:
|
||||
* full_name
|
||||
* email
|
||||
* passcode
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Structure Overview
|
||||
* There are 4 primary tabs for the Event Exhibit Leads module.
|
||||
* The overall header/footer should hide by default once signed in.
|
||||
* If not signed in, only the first tab, to start and sign in is shown.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [tab 1] Start / Sign In / Payment
|
||||
* Only shows when not signed in as Exhibit Licensed Leads User
|
||||
* Sign in with Exhibit passcode
|
||||
* This will then allow them to Manage the Exhibit License
|
||||
* Sign in with Exhibit Licensed user
|
||||
|
||||
### [tab section] Payment
|
||||
* Shows if signed in with Exhibit Passcode and not marked as paid / priority
|
||||
* Field for license info - event_exhibit.license_li_json
|
||||
* Use Exhibit passcode first?
|
||||
* Select number of licenses
|
||||
* Select number of small/large devices
|
||||
* Make payment (Stripe)
|
||||
|
||||
### [tab section] Licensed Users
|
||||
* Shows if signed in with Exhibit Passcode and are marked as paid / priority
|
||||
* Fill in Exhibit staff users per max licenses
|
||||
|
||||
|
||||
## [tab 2] Add - Search / QR Scan
|
||||
* One button that toggles between showing and hiding the QR add mode? The text search is always shown above or below the QR scan camera image area.
|
||||
* Search - Essentially the same as the Badge search. Full name, email, affiliations, ID, etc
|
||||
* QR Scan - Allow for auto add toggle instead of confirming per scan. Allow for manual entire of Badge ID
|
||||
* The QR scan is basically just using the Event Badge ID encoded in the persons QR code on their badge. In fact it can probably just populate the text search field?
|
||||
* Must include the Leads licensed user's email address when adding to the Exhibitor Tracking list. Linked using event_exhibit_tracking with the Licensed user's email address.
|
||||
|
||||
|
||||
## [tab 3] Leads - List of Attendee Leads for Exhibitor
|
||||
* Allow for toggle between showing all per Exhibit and per licensed user based on their email address. Not perfect, but works well enough.
|
||||
* Allow for easy edit or remove
|
||||
* Button to Export Data - CSV or XLSX
|
||||
|
||||
* Toggle for show/hide Hidden records
|
||||
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
|
||||
|
||||
## [tab 4] Manage / Config
|
||||
### Exhibit Specific
|
||||
* Priority/payment toggle - Administrator Access or above
|
||||
* Max licenses (number) - readonly or edit for Administrator Access or above
|
||||
* Small devices (number) - readonly or edit for Administrator Access or above
|
||||
* Large devices (number) - readonly or edit for Administrator Access or above
|
||||
* Exhibit (shared) Passcode
|
||||
* Same Exhibit Leads License list component as the Start tab's Licensed Users section
|
||||
|
||||
### App Specific
|
||||
|
||||
* Show/Hide Payment Tab
|
||||
* Additional Settings:
|
||||
* List refresh interval in seconds - default 25 seconds; 1 second to 2 minutes (120000)
|
||||
* Basic reload/refresh
|
||||
* Clear Indexed DB
|
||||
* Clear localStorage
|
||||
* Auto hide header/footer on sign in - default true
|
||||
* (?) Turn on iframe mode
|
||||
* (?) Show or hide additional details - Use "$events_loc.show_details"?
|
||||
|
||||
|
||||
## [page] View / Edit Tracked Attendee (event_exhbit_id, event_badge_id
|
||||
* Able to edit the obvious things
|
||||
* Can add custom exhibitor_notes and custom responses_json
|
||||
* Can set Priority (Star/Flag)
|
||||
* Can set Sort (number)
|
||||
* Can set Hide (toggle)
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## App Specific - Leads Module Config Options and Stored Data
|
||||
* Signed in using Shared Exhibit Passcode
|
||||
* Can make payment and manage user licenses
|
||||
* Need to be able to sign in with Shared Passcode and Leads License passcode.
|
||||
* Signed in using one Leads License slot they were assigned to
|
||||
* Things will be tied to their email address
|
||||
* Can add, edit, remove, etc the Leads specific to an Exhibit.
|
||||
* This list is shared among all Licensed staff for a specific Exhibit. It is recommended they stay toggled to only showing their additions by default.
|
||||
* Need to be able to sign in with Shared Passcode and Leads License passcode.
|
||||
* Show and Hide payment tab (override sort of)
|
||||
* Show and Hide header/footer (override sort of)
|
||||
* Use iframe mode??
|
||||
* Light and Dark mode??
|
||||
* List refresh interval
|
||||
* Tracking list sorting
|
||||
* Show and hide hidden records
|
||||
* Show and hide enabled records (Administrator Access or above)
|
||||
|
||||
There are essentially 3 ways to "Sign In":
|
||||
1. Aether Platform in general using a defined Core User or using a Client Site specific passcode.
|
||||
2. Exhibit shared passcode for general Exhibitor management. Should only be briefly needed to pay and assign license slots
|
||||
3. Licensed Leads User using the email address and passcode assigned by the Exhibitor manager or OSIT/client staff person.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## First Steps for Leads v3
|
||||
Create stub directories, pages, and other supporting files for each primary tab, and each primary section within each tab, and for viewing a specific Lead (Exhibit Tracking).
|
||||
|
||||
Once we have that looking good, then we can add the functionality. So we are kind of laying out the framework and pre-documenting (commenting) the files as we go.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## For Reference if Needed
|
||||
Known goodish reference version of Leads v2ish:
|
||||
d1021e28227db6d49b16cc48e324872b1a322da3 November 19, 2025 at 10:45 PM
|
||||
Hopefully that is not needed...
|
||||
@@ -1,153 +0,0 @@
|
||||
Aether Events Exhibitor Leads v3 - Details
|
||||
=======
|
||||
|
||||
## [root page] Leads for Exhibitors Main Landing
|
||||
* Has a search for Exhibits section that allows for searching Exhibits by name or code.
|
||||
* Results can be sortable. They can be sorted by name, booth number, or last created/updated.
|
||||
* This is for exhibitors to find their Exhibit and sign in with the shared passcode or their assigned licensed user credentials.
|
||||
* If not signed in with Administrator Access or above, then only show results for Exhibitors that have been marked as "priority" (paid). Otherwise show all Exhibitors (to admins).
|
||||
* Has a list of Exhibits with basic info and link to Exhibit Leads management page for each Exhibit. Do not show results by default or if less than 3 characters in search and they are not signed in with Trusted Access or above.
|
||||
|
||||
|
||||
## [exhibit_id page] Exhibit Leads Module
|
||||
* This is minimalistic. Only the tabs and content needed.
|
||||
|
||||
### Header/Footer
|
||||
* Overall header/footer should hide by default once signed in. Use Aether iframe mode?
|
||||
* Leads module header should show the Exhibit name and booth number. It should also have a button to toggle the Add Lead tab and Lead List tab. There should also be a button to show the Manage/Config tab for the Exhibit.
|
||||
Header: [Name and Booth # text] [Add Lead / Lead List toggle] [Manage/Config button]
|
||||
|
||||
|
||||
### Tabs:
|
||||
I am probably using the term "tab" loosely here. It may just be sections that show/hide based on buttons or whatever. The main thing is to keep the UI simple and not overwhelm the user with too many options at once. The main flow should be to easily add leads and then view/manage those leads. The Manage/Config tab is more for the person managing the Exhibit and should be separate from the lead capture and management interface.
|
||||
|
||||
1. Start - Sign In / Licenses / Payment
|
||||
2. Add - Search/QR
|
||||
3. Leads - List of Attendee Leads for Exhibitor
|
||||
4. Manage - Leads (app and exhibit) Manage
|
||||
|
||||
|
||||
### [tab 1] Start / Sign In / Payment
|
||||
* Only shows when not signed in as Exhibit Licensed Leads User
|
||||
* Show notification and or message that this is a PWA and can be installed on their device for easier access.
|
||||
|
||||
* Sign in with Exhibit passcode
|
||||
* This will then allow them to Manage the Exhibit License
|
||||
* Sign in with Exhibit Licensed user
|
||||
* This will then allow them to Manage the Exhibit License
|
||||
* Payment - Leads Payment
|
||||
* Not Paid: <span class="fas fa-question text-red-500 m-1"></span> <span class="fas fa-credit-card mx-1"></span> Waiting for payment
|
||||
* Paid: <span class="fas fa-check text-green-500 m-1"></span> <span class="fas fa-credit-card mx-1"></span> Marked as paid
|
||||
* Need to switch to Lucide icons
|
||||
* Sections:
|
||||
* Sign in with Exhibit "shared" passcode
|
||||
* This will then allow them to Manage the Exhibit License
|
||||
* Change to Sign Out once signed in
|
||||
* Once signed in, show message to the Exhibit person. Allow them to select number of Licenses, make a payment, and manage the Licenses if paid.
|
||||
* Sign in with Exhibit Licensed user
|
||||
* This will then allow them to Manage the Exhibit License
|
||||
* Change to Sign Out once signed in
|
||||
* Once signed in, show message to the Leads user. A big button should allow them to then start adding leads.
|
||||
* Payment (Stripe) - Leads Payment
|
||||
* Only show if signed in with Exhibit "shared" passcode and not marked as paid.
|
||||
* Licenses (table):
|
||||
* A client staff (Trusted Access or above) or someone signed in with an Exhibit passcode can add/edit/remove licenses.
|
||||
* License:
|
||||
* full_name
|
||||
* email
|
||||
* passcode
|
||||
* Buttons and Inputs:
|
||||
* Sign in with Exhibit passcode
|
||||
* Sign in with Exhibit Licensed user
|
||||
* Payment - Leads Payment
|
||||
* Add/Edit/Remove Licenses (if signed in with Exhibit passcode or Trusted Access or above)
|
||||
|
||||
### [tab 2] Add - Search/QR
|
||||
* Show only when signed in as Exhibit Licensed Leads User or Trusted Access or above.
|
||||
* Allow for text search of Attendee Badge ID, QR code, name, email, or affiliations.
|
||||
* Allow for QR code scan to add Attendee Badge as Lead.
|
||||
* Once found, show basic Attendee Badge info and button to "Add as Lead".
|
||||
* If already added as Lead, show message and button to "View Lead".
|
||||
* Sections:
|
||||
* Text search
|
||||
* QR scan
|
||||
* Results with "Add as Lead" or "View Lead" button
|
||||
* Buttons and Inputs:
|
||||
* Text input for search
|
||||
* Button to trigger search
|
||||
* Button to trigger QR scan (opens camera and scans QR code on badge)
|
||||
* Button to "Add as Lead" if Attendee Badge found and not already a Lead
|
||||
* Button to "View Lead" if Attendee Badge found and already a Lead
|
||||
Functions needed:
|
||||
* Search function to find Attendee Badge by Badge ID, QR code, name, email, or affiliations.
|
||||
* QR code scan function to read QR code and find Attendee Badge.
|
||||
* Add Lead function to create Exhibit_tracking entry linking Exhibit and Attendee Badge.
|
||||
|
||||
### [tab 3] Leads - List of Attendee Leads for Exhibitor
|
||||
* Allow for toggle between showing all per Exhibit and per licensed user based on their email address. Not perfect, but works well enough.
|
||||
* Allow for easy edit or remove
|
||||
* Sections:
|
||||
* List of Leads with basic info and buttons to Edit or Remove
|
||||
* Options:
|
||||
* Filter by Licensed user email address (dropdown of emails that have added leads for this Exhibit)
|
||||
* Toggle for show/hide Hidden records
|
||||
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
* Buttons and Inputs:
|
||||
* Button to Export Data - CSV or XLSX
|
||||
* Toggle for show/hide Hidden records
|
||||
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
* Should it have a text search?
|
||||
* NOTE: It is probably easiest for them to us the search tab to find a lead that has already been added. It will show "View Lead" button if already added.
|
||||
Functions needed:
|
||||
* Load Leads function to get Exhibit_tracking entries for the Exhibit.
|
||||
* Filter function to filter by Licensed user email address.
|
||||
* Sort function to sort by selected option.
|
||||
* Export function to export displayed Leads to CSV or XLSX.
|
||||
|
||||
### [tab 4] Manage - Leads (app and exhibit) Manage / Config
|
||||
#### Exhibit Specific
|
||||
* Priority/payment toggle - Administrator Access or above
|
||||
* Max licenses (number) - readonly or edit for Administrator Access or above
|
||||
* Small devices (number) - readonly or edit for Administrator Access or above
|
||||
* Large devices (number) - readonly or edit for Administrator Access or above
|
||||
* Exhibit (shared) Passcode
|
||||
* Same Exhibit Leads License list component as the Start tab's Licensed Users section
|
||||
|
||||
#### App Specific
|
||||
|
||||
* Show/Hide Payment Tab
|
||||
* Additional Settings:
|
||||
* List refresh interval in seconds - default 25 seconds; 1 second to 2 minutes (120000)
|
||||
* Basic reload/refresh
|
||||
* Clear Indexed DB
|
||||
* Clear localStorage
|
||||
* Auto hide header/footer on sign in - default true
|
||||
* (?) Turn on iframe mode
|
||||
* (?) Show or hide additional details - Use "$events_loc.show_details"?
|
||||
|
||||
* Sections:
|
||||
* Exhibit Specific Manage/Config
|
||||
* App Specific Manage/Config
|
||||
* Buttons and Inputs:
|
||||
* Exhibit Specific:
|
||||
* Priority/payment toggle - Administrator Access or above
|
||||
* Max licenses (number) - readonly or edit for Administrator Access or above
|
||||
* Small devices (number) - readonly or edit for Administrator Access or above
|
||||
* Large devices (number) - readonly or edit for Administrator Access or above
|
||||
* Exhibit (shared) Passcode
|
||||
* Same Exhibit Leads License list component as the Start tab's Licensed Users section
|
||||
* App Specific:
|
||||
* Show/Hide Payment Tab
|
||||
* Show last refresh time and counter for next refresh based on the List refresh interval setting.
|
||||
* Additional Settings:
|
||||
* List refresh interval in seconds - default 25 seconds; 1 second to 2 minutes (120000)
|
||||
* Basic reload/refresh (F5)
|
||||
* Clear Indexed DB
|
||||
* Clear localStorage
|
||||
* Auto hide header/footer on sign in - default true
|
||||
* (?) Turn on iframe mode
|
||||
* (?) Show or hide additional details - Use "$events_loc.show_details"?
|
||||
* Functions:
|
||||
* Update Exhibit configuration function to update the Exhibit with the new settings.
|
||||
* Update App configuration function to update the app-wide settings for the Leads module.
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# PROJECT: Zebra ZC10L Hardware Test Day
|
||||
|
||||
**Created:** 2026-03-12
|
||||
**Planned date:** ~week of 2026-03-16 (printer rented for one day)
|
||||
**Hardware:** Zebra ZC10L, 3.5" × 5.5" PVC card stock
|
||||
**Goal:** Validate real-world badge printing before Axonius (NYC, mid-April 2026)
|
||||
**Owner:** Scott Idem / One Sky IT
|
||||
|
||||
---
|
||||
|
||||
## Before the Printer Arrives — Pre-Test Checklist
|
||||
|
||||
These must be done before the printer is on-site so you're not burning rental time on setup.
|
||||
|
||||
- [ ] **Remove debug outlines** from `print/+page.svelte` print CSS.
|
||||
The lime/blue/red/orange/purple/cyan debug outlines are still in the file. Remove the
|
||||
entire `TEMPORARY DEBUG OUTLINES` block. Commit before the test day.
|
||||
|
||||
- [ ] **Zebra ZC10L Linux driver** — install CUPS driver ahead of time.
|
||||
- Check Zebra's site for the Linux CUPS driver for ZC10L.
|
||||
- Install, configure in CUPS (`http://localhost:631`), do a test page print with a spare card.
|
||||
- Confirm the card feeds without jam and ink/dye-sub layer applies cleanly.
|
||||
- Driver may need the printer set to the correct card stock size (3.5" × 5.5").
|
||||
|
||||
- [ ] **Wire `style_href`** — add `<link rel="stylesheet" href={...}>` to `<svelte:head>` in
|
||||
`print/+page.svelte` when `$lq__event_badge_template_obj?.style_href` is set.
|
||||
Without this, any client-specific external CSS won't load.
|
||||
See `documentation/MODULE__AE_Events_Badge_Templates.md` → "External CSS Approach".
|
||||
|
||||
- [ ] **Confirm single-sided print (duplex=0)** — `duplex` backend field doesn't exist yet.
|
||||
For PVC cards, the badge back must NOT print. Verify this works by checking that
|
||||
`.badge_back` is hidden in `@media print` when the layout is `badge_3.5x5.5_pvc`.
|
||||
The PVC CSS (`badge_layout_zebra_zc10l_pvc.css`) may already handle this — confirm.
|
||||
If not, add a print rule: `[data-layout="badge_3.5x5.5_pvc"] .badge_back { display: none !important; }`
|
||||
|
||||
- [ ] **Test event + template in dev DB** — create/confirm:
|
||||
- Event with `mod_badges_json` configured
|
||||
- PVC template: `layout: badge_3.5x5.5_pvc`, `duplex: 0` (once backend supports it),
|
||||
`header_path` set to a real image URL, `badge_type_list` JSON populated
|
||||
- Test badge records (see "Test Data Set" below)
|
||||
|
||||
- [ ] **Test data set** — create badge records covering:
|
||||
- Short name: "Kim Lee"
|
||||
- Long name: "Bartholomew Vandenberghe-Christopoulos"
|
||||
- HTML in name: `<b>Dr.</b> Patricia Adams`
|
||||
- HTML in affiliations: `University of Minnesota<br>Dept. of Surgery`
|
||||
- Badge with no affiliations, no location
|
||||
- Badge with all 8 ticket codes set
|
||||
- Three different badge_type_codes (e.g. member, staff, guest) — to verify footer
|
||||
stripe color for each
|
||||
|
||||
- [ ] **Browser setup on kiosk machine** — confirm Chrome and Firefox both installed.
|
||||
Test print dialog settings once driver is working:
|
||||
- Chrome: Margins → None (required), paper size set to 3.5×5.5 if available
|
||||
- Firefox: should just work — `@page { size: 3.5in 5.5in }` is honored
|
||||
|
||||
---
|
||||
|
||||
## Test Day Checklist
|
||||
|
||||
### 1. Basic Print Path
|
||||
|
||||
- [ ] Card feeds and prints without jam
|
||||
- [ ] Badge fills the card edge-to-edge (no white border, no clipping)
|
||||
- [ ] Content horizontally centered on the card
|
||||
- [ ] Content vertically centered on the card
|
||||
- [ ] No debug outlines visible (confirm cleanup commit applied)
|
||||
|
||||
**Chrome:**
|
||||
- [ ] Print → Margins: **None** → correct output
|
||||
- [ ] Print → Margins: **Default** → bad (expected — documents the known issue)
|
||||
- [ ] Print → Margins: **Minimum** → correct output
|
||||
- [ ] "More settings" → paper size: does selecting 3.5×5.5 matter, or does Zebra driver override?
|
||||
|
||||
**Firefox:**
|
||||
- [ ] Print → just works out of the box
|
||||
|
||||
### 2. Single-Sided PVC
|
||||
|
||||
- [ ] Only the front face prints — back does NOT print
|
||||
- [ ] No second card ejected or blank card printed for the back
|
||||
|
||||
### 3. Visual Quality
|
||||
|
||||
- [ ] Font sizes readable at 3.5×5.5 physical scale (name, title, affiliations, location)
|
||||
- [ ] Auto-scaling text (v2) — does it look natural, not crunched?
|
||||
- [ ] Header image renders correctly (colors, resolution, no pixelation)
|
||||
- [ ] Footer stripe color correct for each badge_type_code tested
|
||||
- [ ] HTML in name/affiliations renders correctly (bold, line break, etc.)
|
||||
- [ ] Badge with no affiliations — no awkward blank space
|
||||
|
||||
### 4. Edge Cases — Badge Content
|
||||
|
||||
- [ ] Long name auto-scales without overflow or clipping
|
||||
- [ ] HTML markup in name field: `<b>Dr.</b>` renders bold on the physical card
|
||||
- [ ] HTML line break in affiliations: two-line org renders cleanly
|
||||
- [ ] Badge with no location — layout doesn't break
|
||||
- [ ] Badge with all 8 ticket/option codes — back of badge (if applicable) lays out cleanly
|
||||
|
||||
### 5. Print Tracking
|
||||
|
||||
- [ ] `print_count` increments after printing via the **Print Badge** button
|
||||
- [ ] `print_first_datetime` set on first print
|
||||
- [ ] "Printed N×" amber chip appears in print page header after first print
|
||||
- [ ] Reprint via Re-print shortcut (trusted + edit mode) does NOT increment count
|
||||
- [ ] Second print via Print Badge button DOES increment count (to 2)
|
||||
|
||||
### 6. QR Code
|
||||
|
||||
- [ ] QR on printed card is scannable with a phone camera
|
||||
- [ ] QR scans to the correct badge ID (test with `/events/[id]/badges` search by QR scan)
|
||||
- [ ] QR on back (if `show_qr_back=1`) also scans correctly
|
||||
- [ ] If `show_qr_back=0` — no QR code visible on back
|
||||
|
||||
### 7. Font Size Controls
|
||||
|
||||
- [ ] Manual font size override (+ / − in controls panel) changes the badge render live
|
||||
- [ ] Change is visible on the physical printed card
|
||||
- [ ] Reset (↺) returns to auto-sizing
|
||||
- [ ] Auto-sizing produces a reasonable default for all test names without manual adjustment
|
||||
|
||||
### 8. Edit Fields at Kiosk
|
||||
|
||||
- [ ] Badge info editable before printing: change full_name_override, verify change appears on card
|
||||
- [ ] Save change → re-print → new value printed
|
||||
- [ ] Cancel reverts to saved value
|
||||
- [ ] Badge type dropdown changes footer stripe color on the rendered badge
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations on Test Day
|
||||
|
||||
These are not bugs — just gaps that won't be addressed during the test day:
|
||||
|
||||
- **`style_href` external CSS**: Must be wired before test day (see pre-checklist). If not
|
||||
done, client-specific CSS from the template won't load — fall back to default styles.
|
||||
- **`duplex` backend field**: Not yet in the backend schema. Single-sided behavior depends
|
||||
on the PVC CSS hiding `.badge_back` in print. Verify manually.
|
||||
- **Per-template print margins (`print_margin_cfg`)**: UI doesn't exist yet. If the card
|
||||
needs a physical offset for the Zebra's feed, apply a manual CSS tweak to the PVC layout
|
||||
CSS file and revert after testing.
|
||||
- **Kiosk attendee editing (TASK 4.0)**: Edit panel is currently trusted_access only.
|
||||
Attendee self-edit at kiosk isn't finished — staff will need to do all edits on test day.
|
||||
- **`@page { size }` in Chrome**: Chrome ignores the CSS page size for Save as PDF.
|
||||
For physical Zebra printing the driver controls paper size — this is fine.
|
||||
|
||||
---
|
||||
|
||||
## Known Print Dialog Behavior (for reference)
|
||||
|
||||
| Browser | Save to PDF | Physical Printer |
|
||||
|---|---|---|
|
||||
| Firefox | Paper size locked to CSS `@page { size }` ✅ | Can select paper size in dialog |
|
||||
| Chrome | Paper size = system default (letter/A4) ❌ | Can select paper size under "More settings" |
|
||||
|
||||
| Chrome Margin Setting | Result |
|
||||
|---|---|
|
||||
| Default | ❌ Inserts URL/date/page chrome, offsets badge centering |
|
||||
| None | ✅ Badge centered correctly |
|
||||
| Minimum | ✅ Badge centered correctly |
|
||||
|
||||
---
|
||||
|
||||
## Things to Note / Capture During Testing
|
||||
|
||||
Use this section to log observations on the day:
|
||||
|
||||
```
|
||||
DATE: ___________
|
||||
|
||||
Driver version: ___________
|
||||
Card stock type: ___________
|
||||
CUPS printer name: ___________
|
||||
|
||||
Observations:
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
Font size adjustments needed:
|
||||
Name default: was __px, adjusted to __px
|
||||
Title default: was __px, adjusted to __px
|
||||
Affiliations: was __px, adjusted to __px
|
||||
Location: was __px, adjusted to __px
|
||||
|
||||
Physical offset needed (crop/margin): ___________
|
||||
|
||||
Bugs found:
|
||||
-
|
||||
-
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up After Test Day
|
||||
|
||||
- [ ] Update font size defaults in `ae_comp__badge_print_controls.svelte` based on observations
|
||||
- [ ] Note any physical margin/offset needed into `cfg_json: { print_margin: {...} }` once that UI exists
|
||||
- [ ] Document driver version and CUPS config that worked in this file
|
||||
- [ ] Commit any fixes, re-run `npx svelte-check`, commit clean
|
||||
@@ -1,45 +0,0 @@
|
||||
**AE Firefly Theme Repair — Summary (Recovery & Integration Complete)**
|
||||
|
||||
- **Summary**: Investigation, targeted repair, and successful integration of the AE Firefly theme family and key UI components. This document records the final resolution, the migration of the Svelte 5 Modal component, and the validation of the `ae_app_3x_llm` branch as the project's stable baseline.
|
||||
|
||||
### 1. Root Cause Resolution (Themes)
|
||||
- **Variables outside selectors**: Fixed. Custom-property declarations in `src/ae-firefly*.css` were moved into proper `[data-theme='AE_Firefly*']` selector blocks.
|
||||
- **Variable precedence**: Resolved by enforcing `--background: ... !important` within the theme-specific selectors to ensure they override global `:root` defaults.
|
||||
- **Files Repaired**:
|
||||
- `src/ae-firefly.css`
|
||||
- `src/ae-firefly-steelblue.css`
|
||||
- `src/ae-firefly-indigo.css`
|
||||
- `src/ae-firefly-rainbow.css`
|
||||
|
||||
### 2. Actions Taken (Recovery Phase)
|
||||
- **Surgical Integration**: Selective harvesting of "90% done" work from WIP branches while preserving the integrity of the Lucide migration and Svelte 5 baseline.
|
||||
- **`element_modal_v1.svelte`**:
|
||||
- Successfully imported from `wip-modal-fix-attempt`.
|
||||
- **Full Refactor**: Migrated from deprecated `svelte/legacy` and `<slot>` patterns to **Svelte 5 Snippets** and `{@render}` tags.
|
||||
- Verified and tested as the new standard modal component.
|
||||
- **Selective Vetting**:
|
||||
- **Abandoned**: `element_input_v2.svelte` and legacy Badge View v1 (rejected due to legacy FontAwesome regressions and high error counts).
|
||||
- **Removed**: Redundant `element_data_store_v2.svelte` (superseded by `v3`).
|
||||
- **Kept**: Clean, Lucide-based versions of all core components already present on `ae_app_3x_llm`.
|
||||
|
||||
### 3. Repository State (Final Validation)
|
||||
- **Baseline**: `ae_app_3x_llm` is now the unified, verified "known-good" state.
|
||||
- **Validation**: `npx svelte-check` performed on the merged state returned **0 errors and 0 warnings**.
|
||||
- **Cleanup**: Temporary integration branches have been deleted.
|
||||
- **Backups**: `wip-modal-fix-attempt` and `wip/theme-fix` remain as reference points but are no longer active.
|
||||
|
||||
### 4. Merged Files (Key Updates)
|
||||
- `src/ae-firefly*.css` (Repaired themes)
|
||||
- `src/lib/elements/element_modal_v1.svelte` (New Svelte 5 Modal)
|
||||
- `documentation/PROJECT__AE_Firefly_Theme_Repair_SUMMARY.md` (This document)
|
||||
|
||||
### 5. Final Status
|
||||
- **Status**: **COMPLETE / STABLE**
|
||||
- **Branch**: `ae_app_3x_llm`
|
||||
- **Verification**: Verified via `svelte-check` and theme inspections.
|
||||
|
||||
---
|
||||
*Prepared by: Gemini CLI (March 17, 2026)*
|
||||
|
||||
---
|
||||
*Archival note (2026-03-20): `element_modal_v1.svelte` (referenced in §2 as "new standard modal") was subsequently retired — it had zero active importers. Modal usage in the codebase relies on Flowbite `<Modal>` component. See `AE__UI_Component_Patterns.md` §11.*
|
||||
@@ -1,163 +0,0 @@
|
||||
PRES-MGMT Session View — Refactor Plan
|
||||
|
||||
**STATUS: ✅ RESOLVED (2026-02-26)**
|
||||
|
||||
## Resolution Summary
|
||||
|
||||
The "refresh twice" bug was fixed by addressing **two root causes** in the nested data loader chain:
|
||||
|
||||
1. **Disabled caching in nested loads**: Changed `try_cache: false` to `try_cache` (preserve parent value) in:
|
||||
- `ae_events__event_session.ts` → `_refresh_session_id_background()`
|
||||
- `ae_events__event_presentation.ts` → `_refresh_presentation_li_background()`
|
||||
|
||||
2. **Missing microtask yields**: Added `await Promise.resolve()` after each `db_save_ae_obj_li__ae_obj()` call to ensure Dexie liveQuery observers fire before functions return.
|
||||
|
||||
3. **Fire-and-forget nested loads**: Changed `forEach()` to `await Promise.all()` in presentation loader to block until all presenter loads complete.
|
||||
|
||||
**Result:** Session view now renders presentations AND presenters on first load without manual refresh.
|
||||
|
||||
**Performance Impact:** Adds ~100-200ms to initial navigation (time to write 1-5 presenter records + microtask yields), but guarantees correct first-render.
|
||||
|
||||
**Files Modified:**
|
||||
- `src/lib/ae_events/ae_events__event_session.ts` (lines 100-107)
|
||||
- `src/lib/ae_events/ae_events__event_presentation.ts` (lines 187-198)
|
||||
- `src/lib/ae_events/ae_events__event_presenter.ts` (line 157-161)
|
||||
|
||||
**Documentation Updated:**
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` - Added "try_cache: false" bug section with detailed explanation
|
||||
- Code comments added explaining the critical fixes in all modified loader functions
|
||||
- Journals module updated with microtask yields for consistency (already preserved try_cache correctly)
|
||||
|
||||
**Test Status:**
|
||||
- Manual testing: ✅ Confirmed working (session + presentations + presenters render on first load)
|
||||
- Playwright test: Created at `tests/coldstart_event_session.test.ts` (requires valid session ID to run)
|
||||
|
||||
**Lessons Learned:**
|
||||
1. **Always preserve `try_cache` through nested data loads** - Disabling caching at any level breaks the entire chain
|
||||
2. **Add microtask yields after IndexedDB writes** - Ensures liveQuery observers fire before functions return
|
||||
3. **Block on nested loads with `await Promise.all()`** - Fire-and-forget forEach() causes race conditions
|
||||
4. **Journals module was already correct** - It preserved `try_cache` for nested entry loads; only added yields for consistency
|
||||
5. **The existing blocking pattern was sufficient** - No special helper function needed; the bug was in the loader implementation, not the architecture
|
||||
|
||||
**What We Implemented:**
|
||||
We effectively implemented **Option A (Blocking Hydration)** but at a lower level than originally planned. Instead of creating a new `load_session_with_relations()` helper, we fixed the existing loader chain to properly block and cache nested data. The `+page.ts` already used `await` for the session load - it just wasn't working because the underlying loaders weren't caching nested data.
|
||||
|
||||
---
|
||||
|
||||
## Original Plan (For Reference)
|
||||
|
||||
Goal
|
||||
|
||||
Make the Presentation Management Session view deterministic on cold-start (empty IndexedDB). The page must render Presentations, Presenters, and Hosted Files without requiring manual refreshes.
|
||||
|
||||
Constraints
|
||||
|
||||
- Svelte 5 runes and Dexie `liveQuery` behavior (observable recreation, subscription timing).
|
||||
- Minimize user-perceived latency — keep navigation snappy where possible.
|
||||
- Avoid large architectural changes unless necessary.
|
||||
|
||||
Options (high level)
|
||||
|
||||
A) Blocking Hydration (recommended for correctness)
|
||||
- Block the route `+page.ts` load until the session and all directly required related objects are fetched from the backend and written into IndexedDB. Return `initial_session_obj` in the load data for immediate rendering.
|
||||
- Pros: simplest to guarantee first-draw correctness; minimal component changes.
|
||||
- Cons: adds latency to navigation (can be mitigated with optimistic UI or progress indicator).
|
||||
|
||||
B) Prefetch Related Records + Hydrate Fallback (hybrid)
|
||||
- Non-blocking load but `+page.ts` returns `initial_session_obj` and small related-objects payloads (presentations, presenter IDs, hosted_file metadata). Components use these fallbacks while `liveQuery` takes over.
|
||||
- Pros: keeps navigation responsive; often sufficient.
|
||||
- Cons: requires careful payload shaping and DB write ordering.
|
||||
|
||||
C) Explicit Dependency Chaining in UI (advanced)
|
||||
- Keep non-blocking loads and use explicit dependency chaining: write session -> await write completion -> then write presentations -> await -> then presenters, ensuring microtask queue flushes between writes. Use targeted `liveQuery` re-creation only when upstream dependency fully resolved.
|
||||
- Pros: minimal route latency; deterministic ordering.
|
||||
- Cons: more complex to implement and test.
|
||||
|
||||
Recommendation
|
||||
|
||||
Start with Option A (Blocking Hydration) for the session page to restore deterministic behavior quickly. After correctness is achieved, consider converting to Option B or C for improved perceived performance if needed.
|
||||
|
||||
Detailed Steps (Option A - Blocking Hydration)
|
||||
|
||||
1) Add a small helper in `events_func` (e.g., `load_session_with_relations`) that:
|
||||
- fetches session by ID from API
|
||||
- fetches related presentations (limit/filters as needed)
|
||||
- fetches presenters referenced by those presentations (deduplicate IDs)
|
||||
- fetches hosted_file metadata for presentation files (if required for the view)
|
||||
- writes all results to IndexedDB in a controlled order (session -> presentations -> presenters -> hosted_files)
|
||||
- returns a compact `initial_session_obj` payload containing fields needed for first-draw (session, presentation list, presenter summary)
|
||||
|
||||
Implementation note: Use `await db.transaction('rw', db_events.session, db_events.presentation, db_events.presenter, async () => {...})` if atomicity helps. Alternatively write in sequential awaits and call `await Promise.resolve()` after each write to let the microtask queue settle.
|
||||
|
||||
2) Update route loader: `src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.ts` (create if missing) to call and `await` the helper, then return `initial_session_obj` on `data`.
|
||||
|
||||
Example pseudo-code:
|
||||
|
||||
export async function load({ params, parent }) {
|
||||
const data = await parent();
|
||||
if (browser) {
|
||||
const init = await events_func.load_session_with_relations({ api_cfg: data[data.account_id].api, session_id: params.session_id, log_lvl: 0 });
|
||||
data.initial_session_obj = init;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
3) Ensure the page component `+page.svelte` uses the `initial_session_obj` as immediate fallback (it already does in Aether).
|
||||
|
||||
4) Add instrumentation logs inside `liveQuery` closures and the helper to verify ordering during QA.
|
||||
|
||||
5) Add tests (see below) and manual verification steps.
|
||||
|
||||
Alternative (Option B - Hybrid) Implementation Notes
|
||||
|
||||
- If you cannot block the route, return an `initial_session_obj` that includes minimal related object arrays (IDs + small metadata) and have `+page.svelte` write those into IDB before mounting heavy child components.
|
||||
- Use `untrack()` to set selection IDs so stores are updated without causing premature reactivity loops.
|
||||
|
||||
Explicit Dependency Chaining (Option C) Notes
|
||||
|
||||
- Implement a single `prefetch` function that sequentially performs writes and `await Promise.resolve()` between stages.
|
||||
- For debugging, add microtask delays (e.g., `await 0`) between writes to observe behaviour.
|
||||
|
||||
Testing and Verification
|
||||
|
||||
1) Integration test (Playwright recommended)
|
||||
- Clear IndexedDB for the app origin.
|
||||
- Navigate to `/events/<event_id>/.../session/<session_id>` and assert that the presentation list and presenters are visible within N ms without manual refresh.
|
||||
- Repeat on subsequent navigations to ensure no regressions.
|
||||
|
||||
2) Unit tests
|
||||
- For `events_func.load_session_with_relations`, stub API responses and assert DB writes are made in expected order.
|
||||
|
||||
3) Manual QA
|
||||
- With a cold profile or after clearing Site storage, navigate to the session page and confirm content is present after the initial navigation and that no manual refreshes are required.
|
||||
|
||||
Migration and Rollout
|
||||
|
||||
- Implement Option A behind a feature flag if you want to control rollout.
|
||||
- Short-term: apply Option A to the single problematic route to reduce blast radius.
|
||||
- Long-term: consider a library-level helper to standardize "blocking prefetch for nested related records" across other pages.
|
||||
|
||||
Rollback Plan
|
||||
|
||||
- Because changes are additive and limited to one route and helper, revert the `+page.ts` modification and helper call to restore prior behavior.
|
||||
|
||||
Deliverables for tomorrow
|
||||
|
||||
- `events_func.load_session_with_relations` helper (TS) + unit tests
|
||||
- Updated `+page.ts` loader for session route to `await` helper and return `initial_session_obj`
|
||||
- Small test harness / Playwright test that reproduces the cold-start issue and verifies the fix
|
||||
- Instrumentation logs temporarily enabled for QA
|
||||
|
||||
Estimated effort
|
||||
|
||||
- Blocking hydration implementation + tests: 2-4 hours
|
||||
- Hybrid or chaining implementations: additional 2-6 hours depending on thoroughness
|
||||
|
||||
Notes about Svelte 5 + Dexie specifics
|
||||
|
||||
- Keep `liveQuery` closures stable; capture primitive IDs rather than reactive objects.
|
||||
- Use `$derived` and `$derived.by` to keep observable instances stable across renders.
|
||||
- Use `untrack()` when setting selection values to avoid premature subscriptions.
|
||||
- After DB writes, allowing the microtask queue to settle (`await Promise.resolve()`) helps ensure observers are notified in the expected order during development and debugging.
|
||||
|
||||
If you want I can implement Option A for the session route tomorrow (create helper, update loader, add test).
|
||||
@@ -1,106 +0,0 @@
|
||||
# Project: Unified Aether Platform Orchestration (V3)
|
||||
> **Status: ✅ COMPLETE (2026-03-10)**
|
||||
> `ae_app` is live in `aether_container_env/docker-compose.yml`. Both frontend and backend deploy together via `npm run deploy:staging` / `npm run deploy:prod`. Internal `ae_api` networking active. Healthcheck wired. Old `ae_env_node_app` is superseded (archive when ready).
|
||||
|
||||
> **Goal:** Consolidate the SvelteKit Frontend and FastAPI Backend into a single Docker Compose environment within `aether_container_env`.
|
||||
|
||||
## 1. Overview & Benefits
|
||||
Currently, the platform runs in two separate Docker environments (`ae_env_node_app` and `aether_container_env`). Combining them into one allows for:
|
||||
- **Unified Lifecycle:** Start/Stop/Update the entire stack with one command.
|
||||
- **Internal Networking:** The Frontend (SvelteKit) can communicate with the Backend (FastAPI) via the high-speed internal Docker network (`ae_api:5005`) instead of routing through external IPs.
|
||||
- **Shared Infrastructure:** Single instances of Redis, Dozzle, and Nginx serving both tiers.
|
||||
- **Simplified Deployment:** Reduces the need for sibling directory dependencies on production servers.
|
||||
|
||||
---
|
||||
|
||||
## 2. Target Architecture (Workstation & Prod)
|
||||
The unified environment will reside in `~/OSIT_dev/aether_container_env/`.
|
||||
|
||||
### Directory Mapping
|
||||
- **API Source:** `~/OSIT_dev/aether_api_fastapi` -> Mounted to `ae_api`
|
||||
- **App Source:** `~/OSIT_dev/aether_app_sveltekit` -> Mounted to `ae_app`
|
||||
- **Nginx Config:** `~/OSIT_dev/aether_container_env/conf/nginx/`
|
||||
- **Logs:** `~/OSIT_dev/aether_container_env/logs/`
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Plan
|
||||
|
||||
### Step 1: Update `aether_container_env/.env`
|
||||
Add these new variables to the master `.env` file:
|
||||
```bash
|
||||
# --- SvelteKit Frontend settings ---
|
||||
AE_APP_SRC=/home/scott/OSIT_dev/aether_app_sveltekit
|
||||
CONTAINER_AE_APP=ae_app_dev
|
||||
AE_APP_REPLICAS=4
|
||||
AE_APP_NODE_PORT=3001
|
||||
# Note: Use internal DNS 'ae_api' for PUBLIC_AE_API_SERVER_INTERNAL
|
||||
```
|
||||
|
||||
### Step 2: Integrate `ae_app` into `docker-compose.yml`
|
||||
Add the consolidated SvelteKit service. This replaces the legacy Flask service:
|
||||
```yaml
|
||||
ae_app:
|
||||
restart: always
|
||||
build:
|
||||
context: ${AE_APP_SRC}
|
||||
dockerfile: Dockerfile
|
||||
target: deploy-node
|
||||
scale: ${AE_APP_REPLICAS}
|
||||
env_file:
|
||||
- ./.env
|
||||
ports:
|
||||
- "${AE_APP_NODE_PORT}:3000"
|
||||
extra_hosts:
|
||||
- "${DOCKER_AE_SERVER_EXTRA_HOST}"
|
||||
- "${DOCKER_AE_APP_SERVER_EXTRA_HOST}"
|
||||
- "${DOCKER_AE_API_SERVER_EXTRA_HOST}"
|
||||
volumes:
|
||||
# Mount source for real-time dev (Optional for production)
|
||||
- ${AE_APP_SRC}:/srv/aether_app
|
||||
- ${HOSTED_FILES_SRC}:/srv/hosted_files
|
||||
- ${HOSTED_TMP_SRC}:/srv/hosted_tmp
|
||||
depends_on:
|
||||
- ae_api
|
||||
- redis
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
```
|
||||
|
||||
### Step 3: Nginx Gateway Configuration
|
||||
Update (or create) the Nginx template in `conf/nginx/` to route traffic to the internal `ae_app` service.
|
||||
|
||||
**Example Upstream Block:**
|
||||
```nginx
|
||||
upstream svelte_backend {
|
||||
# Internal Docker DNS handles load balancing across replicas
|
||||
server ae_app:3000;
|
||||
# ip_hash; # Enable if session persistence is needed
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Verification & Migration
|
||||
1. **Healthcheck:** Ensure `src/routes/health/+server.ts` is active.
|
||||
2. **Internal Proxy Test:** Update SvelteKit's `PUBLIC_AE_API_SERVER` to `ae_api` (internal) and verify connectivity.
|
||||
3. **Clean Up:** Once verified, the old `ae_env_node_app` directory can be archived.
|
||||
|
||||
---
|
||||
|
||||
## 4. Scaling & Performance
|
||||
- **API Scaling:** Controlled by `AE_API_REPLICAS`.
|
||||
- **App Scaling:** Controlled by `AE_APP_REPLICAS`.
|
||||
- **Memory Management:** Each replica is isolated. Shared state (caching) is handled via the internal **Redis** service.
|
||||
- **Healthchecks:** Docker will automatically restart any `ae_app` replica that fails the `/health` check.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deployment Commands (Unified)
|
||||
```bash
|
||||
# From aether_container_env/
|
||||
docker compose up -d --build --remove-orphans
|
||||
docker compose ps
|
||||
docker compose logs -f ae_app
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
## ✅ Completed (2026-03)
|
||||
- [x] **[Stores] Phase 1 — Dead code cleanup** (`ae_stores.ts`, `ae_events_stores.ts`, `ae_idaa_stores.ts`): removed `ver_idb`, stale comments, `console.log` lines, Stripe button block (zero consumers), personal Novi UUIDs, dead alternatives. Net: −202 lines across 3 files. svelte-check: 0 errors. (2026-03-16)
|
||||
- [x] **[Stores] Phase 2a — Split defaults into domain sub-files**: `ae_stores__auth_loc_defaults.ts`; `ae_events_stores__badges/launcher/leads/pres_mgmt_defaults.ts`. Spread-merged back into store structs — zero consumer changes. (2026-03-16)
|
||||
- [x] **[Stores] Phase 2b — TypeScript interfaces for defaults sub-files**: `SiteCfgJson`, `AePerson`, `AeUser`, `AccessType`, `AuthLocState`; `BadgesLocState/SessState`; `SectionState`, `LauncherLocState/SessState`; `LeadsLocState/SessState`, `TmpLicense`; `PresMgmtLocState/SessState`. svelte-check: 0 errors. (2026-03-16)
|
||||
- [x] **[UI]** Style Review Phase 1 & 2 complete — all non-frozen, non-IDAA routes migrated: FA→Lucide (events, pres_mgmt, core, badges, leads, hosted_files), `variant-*`→`preset-*` (all modules), `code_to_html` badge dict refactored to Lucide component map, FA CDN scoped to IDAA layout, global `svg.lucide { display: inline }` CSS rule added to fix icon inline flow. See `documentation/PROJECT__AE_Style_Review.md`. (2026-03-16)
|
||||
- [x] **[UI]** Pres Mgmt Phase 3 — FA→Lucide icon migration across all 24 pres_mgmt files. (2026-03-16)
|
||||
- [x] **[IDAA]** `ae_idaa_comp__event_obj_id_edit.svelte` — inlined Tailwind utilities, removed `<style>` block; eliminated all 23 `@apply`/`@reference` svelte-check warnings. (2026-03-16)
|
||||
- [x] **[Badges]** Badge print page svelte-check fix: extracted print CSS to `static/ae-print-badge.css`; fixed unclosed `<script>` tag in `print/+page.svelte`. (2026-03-16)
|
||||
- [x] **[Svelte/Tests]** svelte-check cleanup: fixed `select_ref_badge_type` `$state()` declaration; two `<svelte:component>` deprecations in launcher components; `page.evaluate()` two-arg pattern in `badge_print_layout.test.ts`. (2026-03-16)
|
||||
- [x] **[Launcher]** Hosted file download button `require_auth` prop — added `require_auth?: boolean` (default `true`) to `ae_comp__hosted_files_download_button.svelte`; all existing consumers unchanged. Launcher `launcher_file_cont.svelte` passes `require_auth={false}` so unauthenticated kiosk users can open/download files without being blocked. (2026-03-16)
|
||||
- [x] **[Security]** `PUBLIC_AE_API_SECRET_KEY` audit complete. Key is `PUBLIC_*` by design (always in client bundle). Highest-risk anonymous path uses limited-permission `PUBLIC_AE_BOOTSTRAP_KEY`. Full server-side migration not justified given JWT + account_id auth layers. Current state acceptable. (2026-03-11)
|
||||
- [x] **[UX]** Session Expired banner — `ae_auth_error` store wired to API helpers; root layout sets `flag_expired` on 401/403; non-blocking dismissible banner rendered. (2026-03-12)
|
||||
- [x] **[UX]** Access Denied UI standardized — `element_access_denied.svelte` created; `/core` layout, `/events/settings`, and `/events/badges/review` updated to use it. (2026-03-12)
|
||||
- [x] **[Build]** Rollup/Vite circular dependency warnings eliminated — `manualChunks` in `vite.config.ts` colocates all `svelte/*` internals into a single `svelte-vendor` chunk, preventing `runtime.js` / `index-client.js` split (~35 warnings gone). (2026-03-11)
|
||||
- [x] **[Refactor]** `try_cache` audit + sponsorship/event_file/hosted_file SWR alignment — removed vestigial `try_cache` params from `generate_qr_code`, `ae_core_functions` wrappers; added SWR fast/slow path to sponsorship loaders; changed `event_file` and `hosted_file` single-object loader defaults from `false` → `true` for consistency. (2026-03-11)
|
||||
- [x] **[DevOps]** Frontend + Backend unified into single `aether_container_env` Docker Compose. `ae_app` service live with healthcheck, single exposed port (`AE_APP_NODE_PORT`), internal `ae_api` networking. Deploy scripts in `package.json` both target `../aether_container_env/docker-compose.yml`. (2026-03-10)
|
||||
- [x] **[DevOps]** `/health` endpoint live at `src/routes/health/+server.ts`. Docker `HEALTHCHECK` uses it. (2026-03-10)
|
||||
- [x] **[UI]** Dark mode `color-scheme` fix — `html.dark/light { color-scheme }` in `app.css`; all native browser controls now sync to app dark mode. (2026-03-10)
|
||||
- [x] **[Launcher]** Location select → session auto-load bug fixed via `$derived.by()` liveQuery pattern. (2026-03-10)
|
||||
- [x] **[Svelte]** `state_referenced_locally` warning fixes — 10 warnings resolved in IDAA archives/BB. (2026-03-09)
|
||||
- [x] **[TypeScript]** Sign In/Out TS errors fixed — `user_id` / `person_id` typed as `string | null`. (2026-03-09)
|
||||
- [x] **[Tests]** All badge data integrity and attendee workflow Playwright tests passing. Root causes documented in `tests/README.md`. (2026-03)
|
||||
- [x] **[Badges]** Badge print controls panel, QR code, duplex wiring, review form, print button, multi-word fulltext search, `data-testid` attributes. (2026-03)
|
||||
- [x] **[UI]** Firefly Theme + Pres Mgmt Visual Redesign (5 files). (2026-03-06)
|
||||
- [x] **[Docs]** UI Style Guidelines + Component Patterns docs created. (2026-03-06)
|
||||
- [x] **[API]** V3 Lookup system integration; Event File V3 mapping; `event_session` search 400-error fix. (2026-02/03)
|
||||
- [x] **[API]** All CRUD helpers on V3 `/v3/crud/...` paths. (2026-02)
|
||||
- [x] **[Security]** Purged `x-aether-api-token`; fixed misplaced CORS headers; Account ID Scavenging. (2026-02)
|
||||
- [x] **[Security]** Playwright integration tests replace `verify_jwt_logic.js` simulation tests. (2026-03)
|
||||
- [x] **[Framework]** `AE_Obj_Field_Editor_V3` with Svelte 5 Runes. CRUD v2 fully retired. (2026-03-05)
|
||||
- [x] **[IDAA]** Bulletin Board and Recovery Meetings functionality verified. (2026-02)
|
||||
@@ -1,227 +0,0 @@
|
||||
# Frontend Agent Task List
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** Stable — ongoing development.
|
||||
|
||||
|
||||
## 🚧 Upcoming High Priority
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
|
||||
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state
|
||||
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field**
|
||||
of a Svelte 4 store subscribes to the **entire store** — coarse-grained reactivity. This is the
|
||||
root cause of the IDAA Novi re-auth bug (2026-03-30): unrelated `$ae_loc` writes (e.g. iframe
|
||||
height, SWR cfg reload) triggered the Novi verification effect repeatedly.
|
||||
|
||||
Migration target: replace `svelte-persisted-store` with Svelte 5 `$state`-based persistence
|
||||
(e.g. `runed` `PersistedState`, or a lightweight custom wrapper). This gives fine-grained
|
||||
reactivity — only effects that actually read a changed field re-run.
|
||||
|
||||
**Phased approach (do NOT do all at once):**
|
||||
|
||||
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
Decide: `runed` library vs. custom `$state` + localStorage wrapper. Audit all store consumers.
|
||||
Identify stores in priority order. Estimate blast radius per store.
|
||||
|
||||
- [ ] **Phase B — Core auth stores (highest impact, start here):**
|
||||
- `ae_loc` (persisted) — auth flags, site cfg, UI state; ~471 consumer sites across 150+ files
|
||||
- `idaa_loc` (persisted) — Novi auth, IDAA query prefs
|
||||
These two cause the most reactive noise. Migrating them also unlocks Phase 2c (separate `ae_auth`
|
||||
store) since the callsite sweep is now required anyway.
|
||||
|
||||
- [ ] **Phase C — Remaining persisted stores:**
|
||||
- `ae_api` (persisted) — API config / JWT
|
||||
- `ae_events_stores` persisted entries (badges, launcher, leads, pres_mgmt loc stores)
|
||||
|
||||
- [ ] **Phase D — Non-persisted writable stores:**
|
||||
- `ae_sess`, `idaa_sess`, `slct`, `slct_trigger`, `ae_auth_error`, `ae_trig`, `ae_snip`, etc.
|
||||
- Lower urgency (no localStorage churn), but fine-grained reactivity still beneficial.
|
||||
|
||||
- [ ] **Phase E — Phase 2c (unblocked after B):** Split `ae_loc` into `ae_auth` + `ae_app`
|
||||
(see entry below — ~471 callsites, but sweep is cheap once already touching every consumer).
|
||||
|
||||
**Project plan doc needed:** Yes — scope is app-wide. Do NOT start Phase B without Phase A.
|
||||
|
||||
---
|
||||
|
||||
### [Stores] Refactor — Phase 2c (deferred)
|
||||
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
|
||||
|
||||
- [ ] **Phase 2c — Actual separate stores (`ae_auth`, `ae_app`):** Requires touching ~471
|
||||
`$ae_loc.*` auth-field read sites across 150+ files. Deferred until a Svelte runes migration
|
||||
of the store layer itself (touching every component anyway makes the callsite sweep cheap).
|
||||
|
||||
### [Backend] Join event_location_id onto event_presenter API view
|
||||
The `event_presenter` object currently has `event_session_id` but not `event_location_id`.
|
||||
When navigating from the Presenter View to the Launcher, the frontend has to do a secondary
|
||||
session lookup to discover the location (magic redirect in launcher base `+page.svelte`).
|
||||
Joining `event_session.event_location_id` into the presenter view/response would let the
|
||||
frontend pass the location directly in the Launcher URL without the extra lookup.
|
||||
- [x] Backend: added `event_location_id` (and `event_location_id_random`) to the `event_presenter` view or API response (2026-04-09)
|
||||
- [x] Frontend: updated `ae_EventPresenter` type and `properties_to_save`; now pass as `events__launcher_id` in `presenter_page_menu.svelte` (2026-04-09)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### [TypeScript] svelte-check hidden errors — discovered 2026-03-27
|
||||
**HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from
|
||||
`IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That
|
||||
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
|
||||
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
|
||||
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
|
||||
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
|
||||
lifted and the real pre-existing errors became visible.
|
||||
|
||||
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
|
||||
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
|
||||
|
||||
**Current state (2026-03-31):** 32 errors, 0 warnings — all `ModalProps.children`.
|
||||
|
||||
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
|
||||
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
|
||||
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
|
||||
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
|
||||
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
|
||||
|
||||
- [ ] **[IDAA] Make `contact_li_json_ext` searchable — Recovery Meeting contact search (2026-04-08)**
|
||||
Members cannot search for meetings by contact name or email. `contact_li_json` data is not
|
||||
included in `default_qry_str` and MariaDB cannot substring-search a JSON longtext directly.
|
||||
The `event` table already has `contact_li_json_ext` (STORED GENERATED, indexed) to work around this.
|
||||
|
||||
**Backend (blocked on this first):** Add `contact_li_json_ext` to the searchable fields
|
||||
whitelist for the `event` object type — likely a one-line change in `ae_obj_types_def.py`
|
||||
or the event object definition. Message sent to backend agent 2026-04-08.
|
||||
|
||||
**Frontend (after backend ships):**
|
||||
- `src/lib/ae_events/ae_events__event.ts` → `search__event()`: add `contact_li_json_ext`
|
||||
as an OR condition alongside `default_qry_str` when `qry_str` is present.
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
|
||||
`contact_li_json` and include contact names/emails in the local text match check.
|
||||
|
||||
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.**
|
||||
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
|
||||
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
|
||||
Check all other event search pages that use `db_events.event.filter()` or a secondary
|
||||
post-API text filter — they may have the same mismatch (local searches `name`/`description`
|
||||
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
|
||||
that has a full-text search input.
|
||||
|
||||
- [x] **[package.json] Remove orphaned ShadCN/bits-ui packages.** `shadcn-svelte` and `bits-ui`
|
||||
remain in `package.json` but have no usages — `src/lib/components/ui/` was removed 2026-03-27
|
||||
(trashed to `~/tmp/agents_trash/shadcn_components_ui_2026-03-27`). Removed from `package.json` and
|
||||
`package-lock.json` on 2026-04-02.
|
||||
|
||||
### [IDAA] Jitsi config editor + live site fix
|
||||
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
|
||||
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
|
||||
Need to update site 17 in **production** to `https://api.oneskyit.com/api/jitsi_token`.
|
||||
SQL: `UPDATE site SET cfg_json = JSON_SET(cfg_json, '$.jitsi_token_endpoint', 'https://api.oneskyit.com/api/jitsi_token') WHERE id = 17;`
|
||||
|
||||
- [ ] **Add IDAA Jitsi config editor UI** to the jitsi_reports page (administrator_access only),
|
||||
alongside the existing Jitsi URL Builder section. Should allow editing key fields in
|
||||
`site_cfg_json` without needing phpMyAdmin:
|
||||
- `jitsi_token_endpoint` — the JWT signing endpoint (needs to point to prod)
|
||||
- Jitsi domain default (currently hardcoded as `jitsi.dgrzone.com` fallback in the page)
|
||||
- `novi_jitsi_mod_li` — list of Novi UUIDs who get moderator privileges
|
||||
Read from `$ae_loc.site_cfg_json`, PATCH the site record via V3 CRUD
|
||||
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes
|
||||
effect without re-login.
|
||||
|
||||
### [PWA] Service worker ignoring `chrome-extension://` requests
|
||||
Browser console shows repeated errors:
|
||||
```text
|
||||
TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
|
||||
```
|
||||
The service worker's fetch/install handler is trying to cache requests with `chrome-extension://`
|
||||
URLs (injected by browser extensions), which the Cache API rejects. Fix: filter out non-`http`/`https`
|
||||
requests before attempting to cache. In the service worker fetch handler, add a guard:
|
||||
```js
|
||||
if (!event.request.url.startsWith('http')) return; // skip chrome-extension:// etc.
|
||||
```
|
||||
Locate in `static/service-worker.js` or the Vite PWA plugin config. Low severity — doesn't break
|
||||
functionality, but pollutes the console and may cause unhandled promise rejections.
|
||||
|
||||
### [Badges] Remaining badge work before first live event
|
||||
- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
|
||||
File: `ae_comp__badge_print_controls.svelte`.
|
||||
|
||||
### [CSS] Global placeholder text color — too dark in light mode
|
||||
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making
|
||||
placeholders indistinguishable from filled-in values. Most visible in badge print controls
|
||||
where placeholders show the actual badge value (e.g. "John Smith").
|
||||
|
||||
Workaround: scoped `::placeholder` rule added to `ae_comp__badge_print_controls.svelte`
|
||||
(gray-400 light / gray-500 dark) — `commit 7733ef8`.
|
||||
|
||||
**Long-term fix:** Add a global rule to the main CSS (e.g. `src/app.css` or a theme file):
|
||||
```css
|
||||
::placeholder {
|
||||
color: #9ca3af; /* gray-400 */
|
||||
opacity: 1; /* overrides Firefox's 0.54 default */
|
||||
}
|
||||
.dark ::placeholder {
|
||||
color: #6b7280; /* gray-500 */
|
||||
}
|
||||
```
|
||||
Once the global rule is in place, remove the scoped workaround from the badge controls.
|
||||
|
||||
|
||||
|
||||
### [Leads] Exhibitor Lead Scanning — IN PROGRESS (demo-ready prep)
|
||||
Module is substantially built as a PWA (no Electron). Core flow works end-to-end.
|
||||
Spec: `documentation/PROJECT__AE_Events_Exhibitor_Leads_v3.md` and `_detail.md`.
|
||||
Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_events__exhibit*.ts`.
|
||||
|
||||
**What's working:**
|
||||
- Exhibit search/landing (`/leads/`) — SWR, local + API search, sort
|
||||
- Exhibit detail page — 4-tab layout, sticky header with Add/List toggle, auto-refresh timer
|
||||
- Tab 1 (Start): sign-in via shared passcode OR licensed user (email + passcode)
|
||||
- Tab 2 (Add): QR scan (confirm mode — replaced rapid/qualify) + manual badge search; duplicate/re-enable detection on both
|
||||
- Tab 3 (List): SWR lead list, licensee filter (All / My Leads), sort options, export button
|
||||
- Tab 4 (Manage): admin tools, booth profile edit, passcode, license mgmt, custom questions config, app settings (refresh interval, clear IDB/localStorage, reload)
|
||||
- Lead detail page: view/edit custom question responses, exhibitor notes (TipTap), priority/enable flags
|
||||
- Export wired to V3 action endpoint `/v3/action/event_exhibit/{id}/tracking_export` (CSV/XLSX)
|
||||
|
||||
**Remaining before demo:**
|
||||
- [x] **Export endpoint** — V3 action endpoint confirmed live on backend (2026-03-16). Returns 403 if
|
||||
`leads_api_access` is not enabled on the exhibit — expected behavior. Export button now gated in
|
||||
UI: only renders when `$lq__exhibit_obj?.leads_api_access === true`. Enable via:
|
||||
`PATCH /v3/crud/event_exhibit/{id}` with `{ "leads_api_access": true }`.
|
||||
- [x] **`allow_tracking` gate** — implemented (2026-03-16). QR scanner shows a warning card and
|
||||
blocks the add. Manual search shows a ShieldOff "Opt-Out" badge per row and guards `add_as_lead`.
|
||||
Opt-in model: `allow_tracking` must be explicitly `true` on the badge. Also added `allow_tracking`
|
||||
and `agree_to_tc` to `ae_EventBadge` in `ae_types.ts`.
|
||||
**Demo note:** ensure test badges have `allow_tracking = true` or no one can be added.
|
||||
- [x] **Payment component** — `ae_comp__exhibit_payment.svelte` fully implemented (2026-03-27).
|
||||
Reads Stripe config from `$ae_loc.site_cfg_json` (`stripe_publishable_key`, `stripe_btn_1/3/6/10_license`).
|
||||
License tier selector (1/3/6/10 users) with `{#key}` remount pattern for Stripe web component.
|
||||
3 states: paid confirmation (priority=true), admin setup hint / "contact organizer" (no Stripe config),
|
||||
payment form. `client_reference_id=exhibit_id`. TypeScript declaration in `app.d.ts`.
|
||||
Stripe keys verified visible in `$ae_loc.site_cfg_json` on dev/demo site. Keys need validity check in Stripe dashboard.
|
||||
- [x] **End-to-end smoke test (canceled by client)** — sign in with shared passcode, scan/search a badge, add a lead, view detail, add notes/responses, export CSV; canceled 2026-04-09.
|
||||
- [x] **Install prompt** — PWA install nudge implemented (2026-03-16). `pwa_install.svelte.ts`
|
||||
singleton captures `beforeinstallprompt` (Chrome/Android/desktop) and detects iOS Safari
|
||||
for manual "Share → Add to Home Screen" instructions. Reusable `element_pwa_install_prompt.svelte`
|
||||
placed on the Leads Start tab between the feature grid and sign-in. `pwa_install.init()` wired
|
||||
into root `+layout.svelte`; dismiss persists 7 days via localStorage. svelte-check: 0 errors.
|
||||
|
||||
### [DevOps] Remaining deployment items
|
||||
- [x] **Wire AE_APP_REPLICAS:** `docker-compose.yml` line 147 already has `scale: ${AE_APP_REPLICAS:-1}`. (verified 2026-03-11)
|
||||
- [x] **Archive ae_env_node_app:** Archived as tar.gz under `~/OSIT_dev/backups/`; old history/docs moved to `~/OSIT_dev/for_reference_only/`. (2026-03-11)
|
||||
- [x] **Build Optimization:** Current state finalized. Local Gitea instance stood up at `git.dgrzone.com` (Docker, home server) — future: migrate repos from Bitbucket, verify Backblaze/restic backups cover Gitea data. (2026-03-11)
|
||||
- [x] **Remote deploy script:** `aether_container_env/deploy.sh` — SSH-triggered from workstation via `npm run deploy:remote:test/prod`. Handles git pull (ff-only) + docker build + restart. Tested and working on test env. (2026-03-25)
|
||||
- [x] **`.env.default` cleanup:** Removed 16 dead variables, added missing `AE_NETWORK_NAME`/`CONTAINER_DOZZLE`/`AE_DOZZLE_PORT`, parameterized all container names (`CONTAINER_MARIADB`, `CONTAINER_PMA`, `CONTAINER_AE_OPS`) with `:-default` fallbacks in compose. ("Dozzle" = log viewer container.) (2026-03-26)
|
||||
- [x] **Prod deploy:** Run `npm run deploy:remote:prod` (off-peak). Prerequisites: both repos pushed to Bitbucket ✓; verify `.env.prod` exists in `/srv/apps/prod_aether_app_sveltekit/` on Linode before running. (2026-03-30)
|
||||
- [x] **Bitbucket → SSH migration:** Switched all three repos (`aether_app_sveltekit`, `aether_container_env`, `aether_api_fastapi`) to SSH remotes (`git@bitbucket.org`) on workstation. App passwords deprecated — SSH unaffected. (2026-03-27)
|
||||
- [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from same branches. `deploy.sh` defaults are `ae_app_3x_llm` / `development` — acceptable for now but should establish proper branch separation (e.g. `main`/`master` for prod).
|
||||
- [ ] **Tier 2 deploy (Gitea webhook):** Push-triggered deploys via Gitea webhook → listener on Linode → `deploy.sh`. Deferred until Gitea usage is more established.
|
||||
|
||||
|
||||
### [General]
|
||||
- [x] **Temp Cleanup:** `cleanup_tmp_files` wired in `launcher_background_sync.svelte`; called at launcher startup. Confirmed working. (2026-03-11)
|
||||
- [x] **`window.print()` for badge print button:** Wired in `ae_comp__badge_print_controls.svelte` — increments count, fires `window.print()`, redirects to badge search. (done)
|
||||
- **Input Field Audit:** Several input fields are missing `name`/`id` attributes or `data-testid`. Known examples: badge override fields in `ae_comp__badge_obj_view.svelte`; template name input in `ae_comp__badge_template_form.svelte`. Matters for: accessibility, autofill, label associations, and test targeting. (For tests, use `getByLabel()` rather than `input[value*=...]` which only checks the HTML attribute, not the Svelte-bound DOM property.)
|
||||
|
||||
## ✅ Completed (2026-03)
|
||||
## ✅ Completed (archived)
|
||||
See the full completed history in [documentation/TODO__Agents__ARCHIVE_2026-03.md](documentation/TODO__Agents__ARCHIVE_2026-03.md).
|
||||
@@ -1,54 +0,0 @@
|
||||
# Frontend Agent Task List (Archived May 2026)
|
||||
|
||||
## ✅ Completed (2026-05)
|
||||
|
||||
### [API] GET/POST retry hardening — differentiate timeout aborts vs intentional aborts
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- GET/POST now explicitly distinguish abort class in helper code.
|
||||
- Timeout-triggered aborts are retryable via existing retry loop; intentional aborts fail fast.
|
||||
- Backoff behavior retained (`2s -> 4s -> 6s -> 8s`).
|
||||
- Validation done via Playwright tests.
|
||||
|
||||
### [API] PATCH/DELETE retry hardening — parity with GET/POST
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- PATCH and DELETE now implement the same retry-classification model used in GET/POST.
|
||||
- Added explicit fail-fast for 400/401/403/422.
|
||||
- DELETE now triggers the session-expired banner on 401/403.
|
||||
|
||||
### [Testing] V3 API performance probe (basic stress rounds)
|
||||
**Status:** ✅ Completed baseline harness (2026-05-21)
|
||||
- Implemented a gated Playwright probe for quick repeated list-query timing against live V3 endpoints.
|
||||
- Writes reports to `tests/results/`.
|
||||
|
||||
### [IDAA] Random "Access Denied" — Root Cause Review & Fixes
|
||||
**Status:** ✅ Resolved (2026-05-19)
|
||||
- Server-side Novi verification migrated to V3 action endpoint.
|
||||
- Extended Novi TTL to 12 hours.
|
||||
- Hardened retry and timeout logic in `+layout.svelte`.
|
||||
|
||||
### [IDAA] Server-side Novi verification — 503 not auto-retried
|
||||
**Status:** ✅ Fixed (2026-05-20)
|
||||
|
||||
### [IDAA] Jitsi Reports filters
|
||||
**Status:** ✅ Finished (2026-05-06)
|
||||
- Added Novi UUID exclusion plus meeting-name whitelist filtering.
|
||||
|
||||
### [PWA] Service worker ignoring `chrome-extension://` requests
|
||||
**Status:** ✅ Fixed (2026-05-14)
|
||||
- Added guard to filter out non-http/https requests before Attempting to cache.
|
||||
|
||||
### [Electron/Launcher] Display mirroring auto-detection
|
||||
**Status:** ✅ Completed (2026-05-20)
|
||||
- `native:set-display-layout` now auto-detects displays via `displayplacer list`.
|
||||
|
||||
### [Launcher] Force Sync Location
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- Implemented manual trigger and background engine logic to pre-cache all location files.
|
||||
|
||||
### [Launcher] Chronological Download Priority
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- Refactored download queue to prioritize Event Assets > Early Sessions > Presentation Order > Created Date.
|
||||
|
||||
### [Launcher] Error handling + fallback
|
||||
**Status:** ✅ Completed (2026-05-14)
|
||||
- Post-script failure surfaces 'fallback' status; `open_cmd` failure falls back to OS default.
|
||||
@@ -1,139 +0,0 @@
|
||||
# Frontend Agent Task List
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** Stable — ongoing development.
|
||||
|
||||
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
|
||||
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
|
||||
|
||||
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
|
||||
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
|
||||
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
|
||||
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
|
||||
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
|
||||
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
|
||||
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
|
||||
open (deferred — manual button sufficient for CMSC).
|
||||
- [x] **[Launcher] Hidden/deleted files still visible in Presenter file list** — Fixed by
|
||||
API-to-Dexie stale-record pruning plus Launcher background refresh loops for file lists.
|
||||
`ae_events__event_file.ts` now prunes stale records after refresh, and
|
||||
`launcher_background_sync.svelte` refreshes/prunes selected session and presenter file lists.
|
||||
(`fix(launcher): refresh file lists periodically to prune deleted/hidden files`, 2026-05)
|
||||
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
|
||||
Append timestamp/random suffix to temp filename so macOS always sees a new path.
|
||||
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
|
||||
Add resilient reconciliation loop or event-driven reapply on topology change.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||
**Setup/Registration:** June 8 | **Show:** June 9
|
||||
|
||||
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
|
||||
compatible with the Epson C3500 continuous stock format.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||
|
||||
- [x] **[Badges] Presenter Agreement Form** — migrated to `update_ae_obj` (2026-05-21)
|
||||
- [x] **[Core] Site Domain Bootstrap Refactor** — Bootstrap path is already on V3 in
|
||||
`ae_core__site.ts` via `lookup_site_domain()` using `api.search_ae_obj` with FQDN filter
|
||||
(used by `src/routes/+layout.ts`).
|
||||
Follow-up cleanup complete: retired legacy helper `core__site_domain.ts`. (2026-06-02)
|
||||
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
|
||||
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
|
||||
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 High Priority Workstreams
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration
|
||||
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
|
||||
`$state`-based persistence for fine-grained updates.
|
||||
|
||||
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
|
||||
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
||||
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
||||
|
||||
### [IDB Sort] `build_tmp_sort` rollout
|
||||
Shared utility in `src/lib/ae_core/core__idb_sort.ts` — fixes priority direction (inverted,
|
||||
true→'0' sorts first ASC) and zero-pads sort field (8 chars). No `.reverse()` needed.
|
||||
Sort chain: `group → priority DESC → sort ASC → [module-specific fields] → name`.
|
||||
**⚠️ Never use `.reverse()` on a `tmp_sort_*`-sorted list — inverted priority makes it wrong.**
|
||||
Documented in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` (IDB Sort section).
|
||||
|
||||
- [x] `ae_events__event_presentation` — group + priority + sort + start_datetime + code + name
|
||||
- [x] `ae_journals__journal` + `ae_journals__journal_entry` — group + priority + sort + name + updated_on
|
||||
- [ ] `ae_events__event_session` — roll out when sort behavior is reviewed
|
||||
- [ ] `ae_events__event_presenter` — roll out when sort behavior is reviewed
|
||||
- [ ] `ae_events__event_location` — roll out when sort behavior is reviewed
|
||||
- [x] `ae_posts__post` + `ae_posts__post_comment` — migrated to `build_tmp_sort` with 8-char padding; BB comment list consumer updated for ASC tmp_sort ordering. (2026-06-02)
|
||||
- [ ] `ae_core__person` + `ae_core__account` — roll out when sort behavior is reviewed
|
||||
|
||||
### [Stores] IDB Content Version System
|
||||
- [x] Write `check_and_clear_idb_tables()` helper.
|
||||
- [x] Wire helper into `db_journals.ts` and IDAA layout.
|
||||
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, etc.).
|
||||
- [ ] Roll out to `db_core.ts` (site_domain, person, user).
|
||||
|
||||
### [TypeScript] svelte-check hidden errors
|
||||
- [x] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.**
|
||||
Verified no remaining `children={...}` bindings on `<Modal>` and `npx svelte-check` is clean. (2026-06-02)
|
||||
|
||||
### [Journals] Journal Entry Config follow-ups
|
||||
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
|
||||
- [x] **[Journals] Summary AI shortcut** — added Quick Actions button in entry config modal and wired it to close modal + scroll to AI tools panel in entry edit view. (2026-06-02)
|
||||
|
||||
### [Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`
|
||||
- [x] **[Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`**
|
||||
Migrated all 5 listed files to `@lucide/svelte` and uninstalled `lucide-svelte` from dependencies. (2026-06-02)
|
||||
|
||||
---
|
||||
|
||||
### [Pres Mgmt] Sessions hide/show toggle
|
||||
- [x] **[Pres Mgmt] Hidden sessions blink on initial load** — SCENARIO 2 fallback in
|
||||
`pres_mgmt/+page.svelte` now captures `qry_hidden` as a `$derived.by` dependency and
|
||||
applies the filter in the fallback path. No blink on page load. (2026-05-28)
|
||||
- [x] **[Pres Mgmt] API call uses live store instead of snapshot** — changed
|
||||
`pres_mgmt_loc.current.qry_hidden` → `params.qry_hidden` in `handle_search_refresh`
|
||||
API call to be consistent with fast path snapshot. (2026-05-28)
|
||||
- **Note:** `hide_event_launcher` is still active — used in `menu_session_list.svelte`
|
||||
(Launcher) to CSS-hide sessions from the list. Button to toggle it is in
|
||||
`session_page_menu.svelte`. Not used in Pres Mgmt (intentional — Pres Mgmt always shows all).
|
||||
- **Note:** Non-trusted users always have `!item.hide` applied at the component level
|
||||
in `ae_comp__event_session_obj_li.svelte` regardless of `qry_hidden`. Toggle is
|
||||
trusted-access-only in practice; direct session links still work for non-trusted users.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing & Optimization
|
||||
|
||||
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
|
||||
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
|
||||
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
|
||||
- [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ DevOps & Backend
|
||||
|
||||
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
|
||||
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
|
||||
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
|
||||
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
|
||||
backend field (restoring global/cross-device persistence). Frontend code is in
|
||||
`launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
|
||||
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed (archived)
|
||||
See the full completed history in:
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
|
||||
@@ -1,40 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'node_modules/']
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
// No base path configured — this rule is not applicable to this project
|
||||
'svelte/no-navigation-without-resolve': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
103
osit_ae_theme_1.ts
Normal file
103
osit_ae_theme_1.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import type { CustomThemeConfig } from '@skeletonlabs/tw-plugin';
|
||||
|
||||
export const myCustomTheme: CustomThemeConfig = {
|
||||
name: 'my-custom-theme',
|
||||
properties: {
|
||||
// =~= Theme Properties =~=
|
||||
"--theme-font-family-base": `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
"--theme-font-family-heading": `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
"--theme-font-color-base": "0 0 0",
|
||||
"--theme-font-color-dark": "255 255 255",
|
||||
"--theme-rounded-base": "16px",
|
||||
"--theme-rounded-container": "8px",
|
||||
"--theme-border-base": "1px",
|
||||
// =~= Theme On-X Colors =~=
|
||||
"--on-primary": "0 0 0",
|
||||
"--on-secondary": "0 0 0",
|
||||
"--on-tertiary": "0 0 0",
|
||||
"--on-success": "0 0 0",
|
||||
"--on-warning": "0 0 0",
|
||||
"--on-error": "0 0 0",
|
||||
"--on-surface": "0 0 0",
|
||||
// =~= Theme Colors =~=
|
||||
// primary | #99c1f1
|
||||
"--color-primary-50": "240 246 253", // #f0f6fd
|
||||
"--color-primary-100": "235 243 252", // #ebf3fc
|
||||
"--color-primary-200": "230 240 252", // #e6f0fc
|
||||
"--color-primary-300": "214 230 249", // #d6e6f9
|
||||
"--color-primary-400": "184 212 245", // #b8d4f5
|
||||
"--color-primary-500": "153 193 241", // #99c1f1
|
||||
"--color-primary-600": "138 174 217", // #8aaed9
|
||||
"--color-primary-700": "115 145 181", // #7391b5
|
||||
"--color-primary-800": "92 116 145", // #5c7491
|
||||
"--color-primary-900": "75 95 118", // #4b5f76
|
||||
// secondary | #8ff0a4
|
||||
"--color-secondary-50": "238 253 241", // #eefdf1
|
||||
"--color-secondary-100": "233 252 237", // #e9fced
|
||||
"--color-secondary-200": "227 251 232", // #e3fbe8
|
||||
"--color-secondary-300": "210 249 219", // #d2f9db
|
||||
"--color-secondary-400": "177 245 191", // #b1f5bf
|
||||
"--color-secondary-500": "143 240 164", // #8ff0a4
|
||||
"--color-secondary-600": "129 216 148", // #81d894
|
||||
"--color-secondary-700": "107 180 123", // #6bb47b
|
||||
"--color-secondary-800": "86 144 98", // #569062
|
||||
"--color-secondary-900": "70 118 80", // #467650
|
||||
// tertiary | #f8e45c
|
||||
"--color-tertiary-50": "254 251 231", // #fefbe7
|
||||
"--color-tertiary-100": "254 250 222", // #fefade
|
||||
"--color-tertiary-200": "253 248 214", // #fdf8d6
|
||||
"--color-tertiary-300": "252 244 190", // #fcf4be
|
||||
"--color-tertiary-400": "250 236 141", // #faec8d
|
||||
"--color-tertiary-500": "248 228 92", // #f8e45c
|
||||
"--color-tertiary-600": "223 205 83", // #dfcd53
|
||||
"--color-tertiary-700": "186 171 69", // #baab45
|
||||
"--color-tertiary-800": "149 137 55", // #958937
|
||||
"--color-tertiary-900": "122 112 45", // #7a702d
|
||||
// success | #33d17a
|
||||
"--color-success-50": "224 248 235", // #e0f8eb
|
||||
"--color-success-100": "214 246 228", // #d6f6e4
|
||||
"--color-success-200": "204 244 222", // #ccf4de
|
||||
"--color-success-300": "173 237 202", // #adedca
|
||||
"--color-success-400": "112 223 162", // #70dfa2
|
||||
"--color-success-500": "51 209 122", // #33d17a
|
||||
"--color-success-600": "46 188 110", // #2ebc6e
|
||||
"--color-success-700": "38 157 92", // #269d5c
|
||||
"--color-success-800": "31 125 73", // #1f7d49
|
||||
"--color-success-900": "25 102 60", // #19663c
|
||||
// warning | #ffa348
|
||||
"--color-warning-50": "255 241 228", // #fff1e4
|
||||
"--color-warning-100": "255 237 218", // #ffedda
|
||||
"--color-warning-200": "255 232 209", // #ffe8d1
|
||||
"--color-warning-300": "255 218 182", // #ffdab6
|
||||
"--color-warning-400": "255 191 127", // #ffbf7f
|
||||
"--color-warning-500": "255 163 72", // #ffa348
|
||||
"--color-warning-600": "230 147 65", // #e69341
|
||||
"--color-warning-700": "191 122 54", // #bf7a36
|
||||
"--color-warning-800": "153 98 43", // #99622b
|
||||
"--color-warning-900": "125 80 35", // #7d5023
|
||||
// error | #f66151
|
||||
"--color-error-50": "254 231 229", // #fee7e5
|
||||
"--color-error-100": "253 223 220", // #fddfdc
|
||||
"--color-error-200": "253 216 212", // #fdd8d4
|
||||
"--color-error-300": "251 192 185", // #fbc0b9
|
||||
"--color-error-400": "249 144 133", // #f99085
|
||||
"--color-error-500": "246 97 81", // #f66151
|
||||
"--color-error-600": "221 87 73", // #dd5749
|
||||
"--color-error-700": "185 73 61", // #b9493d
|
||||
"--color-error-800": "148 58 49", // #943a31
|
||||
"--color-error-900": "121 48 40", // #793028
|
||||
// surface | #deddda
|
||||
"--color-surface-50": "250 250 249", // #fafaf9
|
||||
"--color-surface-100": "248 248 248", // #f8f8f8
|
||||
"--color-surface-200": "247 247 246", // #f7f7f6
|
||||
"--color-surface-300": "242 241 240", // #f2f1f0
|
||||
"--color-surface-400": "232 231 229", // #e8e7e5
|
||||
"--color-surface-500": "222 221 218", // #deddda
|
||||
"--color-surface-600": "200 199 196", // #c8c7c4
|
||||
"--color-surface-700": "167 166 164", // #a7a6a4
|
||||
"--color-surface-800": "133 133 131", // #858583
|
||||
"--color-surface-900": "109 108 107", // #6d6c6b
|
||||
|
||||
}
|
||||
}
|
||||
103
osit_ae_theme_2.ts
Normal file
103
osit_ae_theme_2.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import type { CustomThemeConfig } from '@skeletonlabs/tw-plugin';
|
||||
|
||||
export const myCustomTheme: CustomThemeConfig = {
|
||||
name: 'my-custom-theme',
|
||||
properties: {
|
||||
// =~= Theme Properties =~=
|
||||
"--theme-font-family-base": `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
"--theme-font-family-heading": `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
"--theme-font-color-base": "0 0 0",
|
||||
"--theme-font-color-dark": "255 255 255",
|
||||
"--theme-rounded-base": "16px",
|
||||
"--theme-rounded-container": "8px",
|
||||
"--theme-border-base": "1px",
|
||||
// =~= Theme On-X Colors =~=
|
||||
"--on-primary": "0 0 0",
|
||||
"--on-secondary": "0 0 0",
|
||||
"--on-tertiary": "0 0 0",
|
||||
"--on-success": "0 0 0",
|
||||
"--on-warning": "0 0 0",
|
||||
"--on-error": "0 0 0",
|
||||
"--on-surface": "0 0 0",
|
||||
// =~= Theme Colors =~=
|
||||
// primary | #62a0ea
|
||||
"--color-primary-50": "231 241 252", // #e7f1fc
|
||||
"--color-primary-100": "224 236 251", // #e0ecfb
|
||||
"--color-primary-200": "216 231 250", // #d8e7fa
|
||||
"--color-primary-300": "192 217 247", // #c0d9f7
|
||||
"--color-primary-400": "145 189 240", // #91bdf0
|
||||
"--color-primary-500": "98 160 234", // #62a0ea
|
||||
"--color-primary-600": "88 144 211", // #5890d3
|
||||
"--color-primary-700": "74 120 176", // #4a78b0
|
||||
"--color-primary-800": "59 96 140", // #3b608c
|
||||
"--color-primary-900": "48 78 115", // #304e73
|
||||
// secondary | #99c1f1
|
||||
"--color-secondary-50": "240 246 253", // #f0f6fd
|
||||
"--color-secondary-100": "235 243 252", // #ebf3fc
|
||||
"--color-secondary-200": "230 240 252", // #e6f0fc
|
||||
"--color-secondary-300": "214 230 249", // #d6e6f9
|
||||
"--color-secondary-400": "184 212 245", // #b8d4f5
|
||||
"--color-secondary-500": "153 193 241", // #99c1f1
|
||||
"--color-secondary-600": "138 174 217", // #8aaed9
|
||||
"--color-secondary-700": "115 145 181", // #7391b5
|
||||
"--color-secondary-800": "92 116 145", // #5c7491
|
||||
"--color-secondary-900": "75 95 118", // #4b5f76
|
||||
// tertiary | #f8e45c
|
||||
"--color-tertiary-50": "254 251 231", // #fefbe7
|
||||
"--color-tertiary-100": "254 250 222", // #fefade
|
||||
"--color-tertiary-200": "253 248 214", // #fdf8d6
|
||||
"--color-tertiary-300": "252 244 190", // #fcf4be
|
||||
"--color-tertiary-400": "250 236 141", // #faec8d
|
||||
"--color-tertiary-500": "248 228 92", // #f8e45c
|
||||
"--color-tertiary-600": "223 205 83", // #dfcd53
|
||||
"--color-tertiary-700": "186 171 69", // #baab45
|
||||
"--color-tertiary-800": "149 137 55", // #958937
|
||||
"--color-tertiary-900": "122 112 45", // #7a702d
|
||||
// success | #33d17a
|
||||
"--color-success-50": "224 248 235", // #e0f8eb
|
||||
"--color-success-100": "214 246 228", // #d6f6e4
|
||||
"--color-success-200": "204 244 222", // #ccf4de
|
||||
"--color-success-300": "173 237 202", // #adedca
|
||||
"--color-success-400": "112 223 162", // #70dfa2
|
||||
"--color-success-500": "51 209 122", // #33d17a
|
||||
"--color-success-600": "46 188 110", // #2ebc6e
|
||||
"--color-success-700": "38 157 92", // #269d5c
|
||||
"--color-success-800": "31 125 73", // #1f7d49
|
||||
"--color-success-900": "25 102 60", // #19663c
|
||||
// warning | #ffa348
|
||||
"--color-warning-50": "255 241 228", // #fff1e4
|
||||
"--color-warning-100": "255 237 218", // #ffedda
|
||||
"--color-warning-200": "255 232 209", // #ffe8d1
|
||||
"--color-warning-300": "255 218 182", // #ffdab6
|
||||
"--color-warning-400": "255 191 127", // #ffbf7f
|
||||
"--color-warning-500": "255 163 72", // #ffa348
|
||||
"--color-warning-600": "230 147 65", // #e69341
|
||||
"--color-warning-700": "191 122 54", // #bf7a36
|
||||
"--color-warning-800": "153 98 43", // #99622b
|
||||
"--color-warning-900": "125 80 35", // #7d5023
|
||||
// error | #f66151
|
||||
"--color-error-50": "254 231 229", // #fee7e5
|
||||
"--color-error-100": "253 223 220", // #fddfdc
|
||||
"--color-error-200": "253 216 212", // #fdd8d4
|
||||
"--color-error-300": "251 192 185", // #fbc0b9
|
||||
"--color-error-400": "249 144 133", // #f99085
|
||||
"--color-error-500": "246 97 81", // #f66151
|
||||
"--color-error-600": "221 87 73", // #dd5749
|
||||
"--color-error-700": "185 73 61", // #b9493d
|
||||
"--color-error-800": "148 58 49", // #943a31
|
||||
"--color-error-900": "121 48 40", // #793028
|
||||
// surface | #deddda
|
||||
"--color-surface-50": "250 250 249", // #fafaf9
|
||||
"--color-surface-100": "248 248 248", // #f8f8f8
|
||||
"--color-surface-200": "247 247 246", // #f7f7f6
|
||||
"--color-surface-300": "242 241 240", // #f2f1f0
|
||||
"--color-surface-400": "232 231 229", // #e8e7e5
|
||||
"--color-surface-500": "222 221 218", // #deddda
|
||||
"--color-surface-600": "200 199 196", // #c8c7c4
|
||||
"--color-surface-700": "167 166 164", // #a7a6a4
|
||||
"--color-surface-800": "133 133 131", // #858583
|
||||
"--color-surface-900": "109 108 107", // #6d6c6b
|
||||
|
||||
}
|
||||
}
|
||||
103
osit_ae_theme_3.ts
Normal file
103
osit_ae_theme_3.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import type { CustomThemeConfig } from '@skeletonlabs/tw-plugin';
|
||||
|
||||
export const myCustomTheme: CustomThemeConfig = {
|
||||
name: 'my-custom-theme',
|
||||
properties: {
|
||||
// =~= Theme Properties =~=
|
||||
"--theme-font-family-base": `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
"--theme-font-family-heading": `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
"--theme-font-color-base": "0 0 0",
|
||||
"--theme-font-color-dark": "255 255 255",
|
||||
"--theme-rounded-base": "16px",
|
||||
"--theme-rounded-container": "8px",
|
||||
"--theme-border-base": "1px",
|
||||
// =~= Theme On-X Colors =~=
|
||||
"--on-primary": "0 0 0",
|
||||
"--on-secondary": "0 0 0",
|
||||
"--on-tertiary": "0 0 0",
|
||||
"--on-success": "0 0 0",
|
||||
"--on-warning": "0 0 0",
|
||||
"--on-error": "0 0 0",
|
||||
"--on-surface": "0 0 0",
|
||||
// =~= Theme Colors =~=
|
||||
// primary | #3584e4
|
||||
"--color-primary-50": "225 237 251", // #e1edfb
|
||||
"--color-primary-100": "215 230 250", // #d7e6fa
|
||||
"--color-primary-200": "205 224 248", // #cde0f8
|
||||
"--color-primary-300": "174 206 244", // #aecef4
|
||||
"--color-primary-400": "114 169 236", // #72a9ec
|
||||
"--color-primary-500": "53 132 228", // #3584e4
|
||||
"--color-primary-600": "48 119 205", // #3077cd
|
||||
"--color-primary-700": "40 99 171", // #2863ab
|
||||
"--color-primary-800": "32 79 137", // #204f89
|
||||
"--color-primary-900": "26 65 112", // #1a4170
|
||||
// secondary | #99c1f1
|
||||
"--color-secondary-50": "240 246 253", // #f0f6fd
|
||||
"--color-secondary-100": "235 243 252", // #ebf3fc
|
||||
"--color-secondary-200": "230 240 252", // #e6f0fc
|
||||
"--color-secondary-300": "214 230 249", // #d6e6f9
|
||||
"--color-secondary-400": "184 212 245", // #b8d4f5
|
||||
"--color-secondary-500": "153 193 241", // #99c1f1
|
||||
"--color-secondary-600": "138 174 217", // #8aaed9
|
||||
"--color-secondary-700": "115 145 181", // #7391b5
|
||||
"--color-secondary-800": "92 116 145", // #5c7491
|
||||
"--color-secondary-900": "75 95 118", // #4b5f76
|
||||
// tertiary | #f8e45c
|
||||
"--color-tertiary-50": "254 251 231", // #fefbe7
|
||||
"--color-tertiary-100": "254 250 222", // #fefade
|
||||
"--color-tertiary-200": "253 248 214", // #fdf8d6
|
||||
"--color-tertiary-300": "252 244 190", // #fcf4be
|
||||
"--color-tertiary-400": "250 236 141", // #faec8d
|
||||
"--color-tertiary-500": "248 228 92", // #f8e45c
|
||||
"--color-tertiary-600": "223 205 83", // #dfcd53
|
||||
"--color-tertiary-700": "186 171 69", // #baab45
|
||||
"--color-tertiary-800": "149 137 55", // #958937
|
||||
"--color-tertiary-900": "122 112 45", // #7a702d
|
||||
// success | #33d17a
|
||||
"--color-success-50": "224 248 235", // #e0f8eb
|
||||
"--color-success-100": "214 246 228", // #d6f6e4
|
||||
"--color-success-200": "204 244 222", // #ccf4de
|
||||
"--color-success-300": "173 237 202", // #adedca
|
||||
"--color-success-400": "112 223 162", // #70dfa2
|
||||
"--color-success-500": "51 209 122", // #33d17a
|
||||
"--color-success-600": "46 188 110", // #2ebc6e
|
||||
"--color-success-700": "38 157 92", // #269d5c
|
||||
"--color-success-800": "31 125 73", // #1f7d49
|
||||
"--color-success-900": "25 102 60", // #19663c
|
||||
// warning | #ffa348
|
||||
"--color-warning-50": "255 241 228", // #fff1e4
|
||||
"--color-warning-100": "255 237 218", // #ffedda
|
||||
"--color-warning-200": "255 232 209", // #ffe8d1
|
||||
"--color-warning-300": "255 218 182", // #ffdab6
|
||||
"--color-warning-400": "255 191 127", // #ffbf7f
|
||||
"--color-warning-500": "255 163 72", // #ffa348
|
||||
"--color-warning-600": "230 147 65", // #e69341
|
||||
"--color-warning-700": "191 122 54", // #bf7a36
|
||||
"--color-warning-800": "153 98 43", // #99622b
|
||||
"--color-warning-900": "125 80 35", // #7d5023
|
||||
// error | #f66151
|
||||
"--color-error-50": "254 231 229", // #fee7e5
|
||||
"--color-error-100": "253 223 220", // #fddfdc
|
||||
"--color-error-200": "253 216 212", // #fdd8d4
|
||||
"--color-error-300": "251 192 185", // #fbc0b9
|
||||
"--color-error-400": "249 144 133", // #f99085
|
||||
"--color-error-500": "246 97 81", // #f66151
|
||||
"--color-error-600": "221 87 73", // #dd5749
|
||||
"--color-error-700": "185 73 61", // #b9493d
|
||||
"--color-error-800": "148 58 49", // #943a31
|
||||
"--color-error-900": "121 48 40", // #793028
|
||||
// surface | #deddda
|
||||
"--color-surface-50": "250 250 249", // #fafaf9
|
||||
"--color-surface-100": "248 248 248", // #f8f8f8
|
||||
"--color-surface-200": "247 247 246", // #f7f7f6
|
||||
"--color-surface-300": "242 241 240", // #f2f1f0
|
||||
"--color-surface-400": "232 231 229", // #e8e7e5
|
||||
"--color-surface-500": "222 221 218", // #deddda
|
||||
"--color-surface-600": "200 199 196", // #c8c7c4
|
||||
"--color-surface-700": "167 166 164", // #a7a6a4
|
||||
"--color-surface-800": "133 133 131", // #858583
|
||||
"--color-surface-900": "109 108 107", // #6d6c6b
|
||||
|
||||
}
|
||||
}
|
||||
13707
package-lock.json
generated
13707
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
184
package.json
184
package.json
@@ -1,120 +1,68 @@
|
||||
{
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.00.30",
|
||||
"description": "One Sky IT's Aether App created with Svelte, SvelteKit, Tailwind CSS, Lucide, Font Awesome, and Skeleton UI. -Scott Idem",
|
||||
"homepage": "https://oneskyit.com/",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:prod": "vite build --mode prod",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest",
|
||||
"build:docker:dev": "docker compose -f ../aether_container_env/docker-compose.yml build ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
|
||||
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down",
|
||||
"deploy:remote:test": "ssh linode.oneskyit.com 'bash /srv/env/test_aether/deploy.sh test'",
|
||||
"deploy:remote:prod": "ssh linode.oneskyit.com 'bash /srv/env/prod_aether/deploy.sh prod'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@skeletonlabs/skeleton": "^4.*.*",
|
||||
"@skeletonlabs/skeleton-svelte": "^4.*.*",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.48.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/eslint": "^9.0.0",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||
"@typescript-eslint/parser": "^8.47.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"flowbite": "^4.0.0",
|
||||
"globals": "^16.5.0",
|
||||
"highlight.js": "^11.10.0",
|
||||
"lowlight": "^3.2.0",
|
||||
"mode-watcher": "^1.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass-embedded": "^1.81.0",
|
||||
"svelte": "^5.43.10",
|
||||
"svelte-awesome-color-picker": "^4.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-highlight": "^7.8.4",
|
||||
"svelte-idle": "^3.0.1",
|
||||
"tailwind-merge": "^3.0.0",
|
||||
"tailwind-variants": "^3.*.*",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-svelte-plugin": "^0.3.50",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"vitest": {
|
||||
"exclude": [
|
||||
"tests"
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"overrides": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.8",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@lezer/common": "^1.4.0",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.38.8",
|
||||
"@floating-ui/dom": "^1.6.0",
|
||||
"@lucide/svelte": "^0.*.0",
|
||||
"@popperjs/core": "^2.11.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"dayjs": "^1.11.10",
|
||||
"dexie": "^4.0.0",
|
||||
"flowbite-svelte": "^1.28.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"marked": "^17.0.0",
|
||||
"openai": "^6.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"runed": "^0.37.1",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"typescript-eslint": "^8.47.0"
|
||||
}
|
||||
"name": "ae-app-svelte4-tailwind-skeleton",
|
||||
"version": "0.1.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:prod": "cp .env.prod .env.production && vite build",
|
||||
"build:staging": "cp .env.staging .env.production && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@skeletonlabs/skeleton": "^3.0.0-next.5",
|
||||
"@skeletonlabs/skeleton-svelte": "^1.0.0-next.10",
|
||||
"@skeletonlabs/tw-plugin": "^0.4.0",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0-next",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/eslint": "^9.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.9.0",
|
||||
"@typescript-eslint/parser": "^8.9.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0-next",
|
||||
"flowbite": "^2.5.2",
|
||||
"flowbite-svelte": "^0.46.23",
|
||||
"flowbite-svelte-icons": "^1.6.2",
|
||||
"highlight.js": "^11.10.0",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^5.0.0-next",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-highlight": "^7.7.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-svelte-plugin": "^0.3.41",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-tailwind-purgecss": "^0.3.3",
|
||||
"vitest": "^2.1.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"dexie": "^4.0.1-beta.14",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"svelte-persisted-store": "^0.11.0",
|
||||
"tailwind-merge": "^2.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
port: 5173,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: 'tests/**/*.test.ts',
|
||||
testIgnore: ['tests/disabled/**'],
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'http://demo.localhost:5173',
|
||||
// baseURL: 'https://dev-demo.oneskyit.com',
|
||||
trace: 'on-first-retry',
|
||||
// Arch Linux: Playwright's downloaded Chromium requires Ubuntu system libs (libicu74 etc.)
|
||||
// that don't exist on Arch. Use the system Chromium package instead.
|
||||
launchOptions: {
|
||||
executablePath: '/usr/bin/chromium',
|
||||
},
|
||||
}
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
migrate_fa_to_lucide.py — Replace FontAwesome <span class="fas fa-X"> with Lucide components.
|
||||
|
||||
Usage:
|
||||
python3 scripts/migrate_fa_to_lucide.py src/routes/events/[event_id]/\(pres_mgmt\)/
|
||||
|
||||
Skips content inside HTML comments. Adds/merges lucide-svelte imports.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ── FA icon → Lucide component name ─────────────────────────────────────────
|
||||
FA_TO_LUCIDE = {
|
||||
'fa-spinner': 'LoaderCircle',
|
||||
'fa-cog': 'LoaderCircle', # only when fa-spin
|
||||
'fa-sync-alt': 'RefreshCw',
|
||||
'fa-times': 'X',
|
||||
'fa-exclamation-triangle': 'TriangleAlert',
|
||||
'fa-check': 'Check',
|
||||
'fa-check-circle': 'CircleCheck',
|
||||
'fa-plus': 'Plus',
|
||||
'fa-minus': 'Minus',
|
||||
'fa-save': 'Save',
|
||||
'fa-edit': 'Pencil',
|
||||
'fa-eye': 'Eye',
|
||||
'fa-eye-slash': 'EyeOff',
|
||||
'fa-toggle-on': 'ToggleRight',
|
||||
'fa-toggle-off': 'ToggleLeft',
|
||||
'fa-star-of-life': 'Asterisk',
|
||||
'fa-id-card': 'IdCard',
|
||||
'fa-paper-plane': 'Send',
|
||||
'fa-map-marker-alt': 'MapPin',
|
||||
'fa-file-alt': 'FileText',
|
||||
'fa-envelope': 'Mail',
|
||||
'fa-book': 'BookOpen',
|
||||
'fa-angle-right': 'ChevronRight',
|
||||
'fa-user': 'User',
|
||||
'fa-tasks': 'ListChecks',
|
||||
'fa-plane': 'Plane',
|
||||
'fa-list': 'List',
|
||||
'fa-link': 'Link',
|
||||
'fa-file-archive': 'Archive',
|
||||
'fa-comment-dots': 'MessageCircle',
|
||||
'fa-chevron-up': 'ChevronUp',
|
||||
'fa-chevron-down': 'ChevronDown',
|
||||
'fa-camera': 'Camera',
|
||||
'fa-barcode': 'Barcode',
|
||||
'fa-upload': 'Upload',
|
||||
'fa-search': 'Search',
|
||||
'fa-mail-bulk': 'Mails',
|
||||
'fa-laptop-code': 'Laptop',
|
||||
'fa-copy': 'Copy',
|
||||
'fa-user-tag': 'Tag',
|
||||
'fa-user-secret': 'UserRound',
|
||||
'fa-users': 'Users',
|
||||
'fa-user-circle': 'CircleUser',
|
||||
'fa-sort': 'ArrowUpDown',
|
||||
'fa-question': 'HelpCircle',
|
||||
'fa-map-marked': 'MapPinned',
|
||||
'fa-list-ol': 'ListOrdered',
|
||||
'fa-laptop': 'Laptop',
|
||||
'fa-info': 'Info',
|
||||
'fa-building': 'Building2',
|
||||
'fa-user-slash': 'UserX',
|
||||
'fa-user-check': 'UserCheck',
|
||||
'fa-unlink': 'Unlink',
|
||||
'fa-star': 'Star',
|
||||
'fa-search-location': 'MapPin',
|
||||
'fa-remove-format': 'RemoveFormatting',
|
||||
'fa-qrcode': 'QrCode',
|
||||
'fa-key': 'Key',
|
||||
'fa-heartbeat': 'HeartPulse',
|
||||
'fa-hat-wizard': 'Wand2',
|
||||
'fa-fingerprint': 'Fingerprint',
|
||||
'fa-file-csv': 'FileSpreadsheet',
|
||||
'fa-file': 'File',
|
||||
'fa-clock': 'Clock',
|
||||
'fa-clipboard-list': 'ClipboardList',
|
||||
'fa-chart-line': 'TrendingUp',
|
||||
'fa-chalkboard-teacher': 'Presentation',
|
||||
'fa-calendar-day': 'CalendarDays',
|
||||
'fa-bell-slash': 'BellOff',
|
||||
'fa-bell': 'Bell',
|
||||
# ── Additional mappings ──────────────────────────────────────────────────
|
||||
'fa-arrow-left': 'ArrowLeft',
|
||||
'fa-arrow-right': 'ArrowRight',
|
||||
'fa-arrow-up': 'ArrowUp',
|
||||
'fa-arrow-down': 'ArrowDown',
|
||||
'fa-ban': 'Ban',
|
||||
'fa-broom': 'Trash2', # closest semantic match
|
||||
'fa-calendar-alt': 'CalendarDays',
|
||||
'fa-database': 'Database',
|
||||
'fa-door-open': 'DoorOpen',
|
||||
'fa-download': 'Download',
|
||||
'fa-exchange-alt': 'ArrowLeftRight',
|
||||
'fa-file-image': 'FileImage',
|
||||
'fa-lock': 'Lock',
|
||||
'fa-magic': 'Sparkles',
|
||||
'fa-print': 'Printer',
|
||||
'fa-sticky-note': 'StickyNote',
|
||||
'fa-sync': 'RefreshCw',
|
||||
'fa-tag': 'Tag',
|
||||
'fa-trash': 'Trash2',
|
||||
'fa-user-ninja': 'UserRound',
|
||||
'fa-user-tie': 'UserRound',
|
||||
'fa-video': 'Video',
|
||||
'fa-archive': 'Archive',
|
||||
'fa-link-slash': 'Unlink',
|
||||
'fa-question-circle': 'HelpCircle',
|
||||
# ── Additional unmapped icons ──────────────────────────────────────────────
|
||||
'fa-compress-arrows-alt':'Minimize2',
|
||||
'fa-expand-arrows-alt': 'Maximize2',
|
||||
'fa-secret': 'ShieldCheck',
|
||||
'fa-user-shield': 'ShieldUser',
|
||||
'fa-user-nurse': 'UserRound',
|
||||
'fa-user-friends': 'Users',
|
||||
'fa-user-plus': 'UserPlus',
|
||||
'fa-user-edit': 'UserRoundPen',
|
||||
'fa-palette': 'Palette',
|
||||
'fa-eraser': 'Eraser',
|
||||
'fa-code': 'Code',
|
||||
'fa-lock-open': 'LockOpen',
|
||||
'fa-unlock': 'LockOpen',
|
||||
'fa-trash-alt': 'Trash2',
|
||||
'fa-folder-open': 'FolderOpen',
|
||||
'fa-minus-circle': 'MinusCircle',
|
||||
'fa-plus-circle': 'PlusCircle',
|
||||
'fa-window-close': 'X',
|
||||
'fa-cut': 'Scissors',
|
||||
'fa-caret-down': 'ChevronDown',
|
||||
'fa-caret-right': 'ChevronRight',
|
||||
'fa-cogs': 'Settings2',
|
||||
'fa-phone': 'Phone',
|
||||
'fa-phone-slash': 'PhoneOff',
|
||||
'fa-flag': 'Flag',
|
||||
'fa-calendar-week': 'CalendarDays',
|
||||
'fa-address-book': 'BookUser',
|
||||
'fa-info-circle': 'Info',
|
||||
'fa-comment-slash': 'MessageX',
|
||||
'fa-paperclip': 'Paperclip',
|
||||
'fa-keyboard': 'Keyboard',
|
||||
'fa-crosshairs': 'Crosshair',
|
||||
'fa-redo': 'RotateCcw',
|
||||
'fa-tools': 'Wrench',
|
||||
'fa-video-slash': 'VideoOff',
|
||||
'fa-home': 'House',
|
||||
'fa-calendar': 'Calendar',
|
||||
'fa-check-square': 'SquareCheck',
|
||||
'fa-square': 'Square',
|
||||
'fa-times-circle': 'CircleX',
|
||||
'fa-undo': 'RotateCcw',
|
||||
'fa-trash-restore': 'ArchiveRestore',
|
||||
'fa-lock-open': 'LockOpen',
|
||||
'fa-compress': 'Minimize2',
|
||||
'fa-expand': 'Maximize2',
|
||||
'fa-grip-lines': 'GripHorizontal',
|
||||
'fa-bars': 'Menu',
|
||||
'fa-refresh': 'RefreshCw',
|
||||
}
|
||||
|
||||
# Skip modifiers — not real icon names
|
||||
FA_MODIFIERS = {'fas', 'far', 'fab', 'fa-spin', 'fa-fw', 'fa-lg', 'fa-2x', 'fa-sm'}
|
||||
|
||||
# ── Pattern: <span class="fas fa-X [extras]" [other-attrs]></span> ───────────
|
||||
# [^>]* matches newlines too (character class, not dot)
|
||||
SPAN_RE = re.compile(
|
||||
r'<span\s+class="((?:fas|far|fab)\s+fa-[^"]*)"[^>]*>\s*</span>'
|
||||
)
|
||||
|
||||
# ── Comment splitter ─────────────────────────────────────────────────────────
|
||||
COMMENT_RE = re.compile(r'(<!--[\s\S]*?-->)')
|
||||
|
||||
# ── Lucide import line ────────────────────────────────────────────────────────
|
||||
IMPORT_RE = re.compile(r"import\s*\{([^}]+)\}\s*from\s*'@lucide/svelte'\s*;?")
|
||||
|
||||
|
||||
def parse_fa_class(class_str):
|
||||
"""Return (icon_name, extra_classes, has_spin) from a FA class string."""
|
||||
parts = class_str.split()
|
||||
icon_name = None
|
||||
has_spin = 'fa-spin' in parts
|
||||
extra = []
|
||||
for p in parts:
|
||||
if p in FA_MODIFIERS:
|
||||
continue
|
||||
elif p.startswith('fa-'):
|
||||
if icon_name is None:
|
||||
icon_name = p # first real icon name wins
|
||||
extra.append(p)
|
||||
return icon_name, extra, has_spin
|
||||
|
||||
|
||||
def replace_span(m):
|
||||
"""Regex sub callback: replace a single FA span with a Lucide component."""
|
||||
"""
|
||||
if icon_name is None:
|
||||
return m.group(0)
|
||||
|
||||
lucide = FA_TO_LUCIDE.get(icon_name)
|
||||
if lucide is None:
|
||||
print(f' ⚠ no mapping for {icon_name!r} — left as-is', file=sys.stderr)
|
||||
return m.group(0)
|
||||
|
||||
classes = extra[:]
|
||||
if has_spin:
|
||||
classes.append('animate-spin')
|
||||
|
||||
class_attr = f' class="{" ".join(classes)}"' if classes else ''
|
||||
return f'<{lucide} size="1em"{class_attr} />'
|
||||
|
||||
|
||||
def process_content(content):
|
||||
"""Replace FA spans, skip HTML comments. Return (new_content, used_icons)."""
|
||||
used_icons = set()
|
||||
|
||||
def track_and_replace(m):
|
||||
result = replace_span(m)
|
||||
if result != m.group(0):
|
||||
# Extract lucide name from result
|
||||
lucide_name = result.split()[0].lstrip('<')
|
||||
used_icons.add(lucide_name)
|
||||
return result
|
||||
|
||||
# Split by comments; only process non-comment segments
|
||||
parts = COMMENT_RE.split(content)
|
||||
new_parts = []
|
||||
for part in parts:
|
||||
if part.startswith('<!--'):
|
||||
new_parts.append(part)
|
||||
else:
|
||||
new_parts.append(SPAN_RE.sub(track_and_replace, part))
|
||||
|
||||
return ''.join(new_parts), used_icons
|
||||
|
||||
|
||||
def add_import(content, icons):
|
||||
"""Add/merge lucide-svelte import line inside <script>."""
|
||||
if not icons:
|
||||
return content
|
||||
|
||||
sorted_icons = sorted(icons)
|
||||
|
||||
existing = IMPORT_RE.search(content)
|
||||
if existing:
|
||||
current = [s.strip() for s in existing.group(1).split(',') if s.strip()]
|
||||
merged = sorted(set(current) | set(sorted_icons))
|
||||
new_import = f"import {{ {', '.join(merged)} }} from '@lucide/svelte';"
|
||||
return content[:existing.start()] + new_import + content[existing.end():]
|
||||
else:
|
||||
# Insert after the last COMPLETE import statement (handles multiline imports).
|
||||
# A complete import statement ends with: } from '...'; or import '...';
|
||||
complete_import_re = re.compile(
|
||||
r'^[ \t]*import\b[\s\S]*?(?:from\s*[\'"][^\'"]+[\'"]|[\'"][^\'"]+[\'"])\s*;?',
|
||||
re.MULTILINE
|
||||
)
|
||||
all_imports = list(complete_import_re.finditer(content))
|
||||
if all_imports:
|
||||
pos = all_imports[-1].end()
|
||||
new_line = f"\n import {{ {', '.join(sorted_icons)} }} from '@lucide/svelte';"
|
||||
return content[:pos] + new_line + content[pos:]
|
||||
# Fallback: add after <script> tag
|
||||
script_tag = content.find('<script')
|
||||
if script_tag != -1:
|
||||
end = content.index('>', script_tag) + 1
|
||||
return content[:end] + f"\n import {{ {', '.join(sorted_icons)} }} from 'lucide-svelte';" + content[end:]
|
||||
return content
|
||||
|
||||
|
||||
def migrate_file(filepath):
|
||||
path = Path(filepath)
|
||||
original = path.read_text()
|
||||
new_content, used_icons = process_content(original)
|
||||
|
||||
if used_icons:
|
||||
new_content = add_import(new_content, used_icons)
|
||||
|
||||
if new_content != original:
|
||||
path.write_text(new_content)
|
||||
print(f' ✓ {path.name} ({len(used_icons)} icon types: {", ".join(sorted(used_icons))})')
|
||||
return True
|
||||
else:
|
||||
print(f' – {path.name} (no changes)')
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print('Usage: migrate_fa_to_lucide.py <directory_or_file ...>')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# IDAA Guardrail: Abort if any target is src/routes/idaa or a subdirectory
|
||||
for arg in sys.argv[1:]:
|
||||
if Path(arg).resolve().as_posix().endswith('src/routes/idaa') or '/src/routes/idaa/' in Path(arg).resolve().as_posix():
|
||||
print('ABORT: This script must not be run against src/routes/idaa or its subdirectories. IDAA content is private and protected.')
|
||||
sys.exit(1)
|
||||
|
||||
targets = []
|
||||
for arg in sys.argv[1:]:
|
||||
p = Path(arg)
|
||||
if p.is_dir():
|
||||
targets.extend(sorted(p.rglob('*.svelte')))
|
||||
elif p.is_file():
|
||||
targets.append(p)
|
||||
else:
|
||||
print(f'Not found: {arg}', file=sys.stderr)
|
||||
|
||||
changed = 0
|
||||
for t in targets:
|
||||
if migrate_file(t):
|
||||
changed += 1
|
||||
|
||||
print(f'\n{changed}/{len(targets)} files modified.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,166 +0,0 @@
|
||||
[data-theme='AE_c_IDAA_light'] {
|
||||
--text-scaling: 1.067;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.75rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
--body-background-color: var(--color-surface-50);
|
||||
|
||||
--color-primary-50: oklch(85.73% 0.07 251.8deg);
|
||||
--color-primary-100: oklch(78.5% 0.09 252.03deg);
|
||||
--color-primary-200: oklch(71.06% 0.1 253.6deg);
|
||||
--color-primary-300: oklch(63.76% 0.12 253.85deg);
|
||||
--color-primary-400: oklch(56.32% 0.14 255.25deg);
|
||||
--color-primary-500: oklch(49.23% 0.15 256.36deg);
|
||||
--color-primary-600: oklch(43.11% 0.14 258.86deg);
|
||||
--color-primary-700: oklch(36.85% 0.14 261.54deg);
|
||||
--color-primary-800: oklch(30.41% 0.13 263.99deg);
|
||||
--color-primary-900: oklch(23.91% 0.12 265.91deg);
|
||||
--color-primary-950: oklch(16.96% 0.12 264.05deg);
|
||||
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
--color-secondary-50: oklch(96.26% 0.06 196.24deg);
|
||||
--color-secondary-100: oklch(89.14% 0.07 220.79deg);
|
||||
--color-secondary-200: oklch(82.13% 0.08 234.87deg);
|
||||
--color-secondary-300: oklch(75.03% 0.11 245.33deg);
|
||||
--color-secondary-400: oklch(68.15% 0.14 250.72deg);
|
||||
--color-secondary-500: oklch(61.37% 0.16 255.34deg);
|
||||
--color-secondary-600: oklch(55.1% 0.16 256.81deg);
|
||||
--color-secondary-700: oklch(48.64% 0.15 258.4deg);
|
||||
--color-secondary-800: oklch(41.84% 0.15 260.39deg);
|
||||
--color-secondary-900: oklch(35.05% 0.14 262.03deg);
|
||||
--color-secondary-950: oklch(28.12% 0.14 262.47deg);
|
||||
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
--color-tertiary-50: oklch(100% 0 none);
|
||||
--color-tertiary-100: oklch(96.07% 0.01 251.15deg);
|
||||
--color-tertiary-200: oklch(91.88% 0.03 252.69deg);
|
||||
--color-tertiary-300: oklch(87.99% 0.05 253.24deg);
|
||||
--color-tertiary-400: oklch(83.81% 0.06 253.57deg);
|
||||
--color-tertiary-500: oklch(79.93% 0.08 253.32deg);
|
||||
--color-tertiary-600: oklch(72.53% 0.08 251.75deg);
|
||||
--color-tertiary-700: oklch(64.93% 0.08 249.75deg);
|
||||
--color-tertiary-800: oklch(57.14% 0.09 247.99deg);
|
||||
--color-tertiary-900: oklch(49.18% 0.09 246.55deg);
|
||||
--color-tertiary-950: oklch(41.1% 0.09 246.54deg);
|
||||
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
--color-success-50: oklch(95.82% 0.06 184.52deg);
|
||||
--color-success-100: oklch(91.55% 0.08 172.29deg);
|
||||
--color-success-200: oklch(87.44% 0.11 165.22deg);
|
||||
--color-success-300: oklch(83.26% 0.13 161.2deg);
|
||||
--color-success-400: oklch(79.56% 0.16 157.13deg);
|
||||
--color-success-500: oklch(76.12% 0.18 153.61deg);
|
||||
--color-success-600: oklch(69.31% 0.17 151.81deg);
|
||||
--color-success-700: oklch(62.07% 0.16 149.95deg);
|
||||
--color-success-800: oklch(54.9% 0.15 147.65deg);
|
||||
--color-success-900: oklch(47.26% 0.14 145.54deg);
|
||||
--color-success-950: oklch(39.64% 0.13 143.79deg);
|
||||
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
--color-warning-50: oklch(98.26% 0.1 108.02deg);
|
||||
--color-warning-100: oklch(95.84% 0.12 104.66deg);
|
||||
--color-warning-200: oklch(93.48% 0.13 102.21deg);
|
||||
--color-warning-300: oklch(91.49% 0.15 100.17deg);
|
||||
--color-warning-400: oklch(89.28% 0.16 98.19deg);
|
||||
--color-warning-500: oklch(87.14% 0.17 96.01deg);
|
||||
--color-warning-600: oklch(79.88% 0.16 96.31deg);
|
||||
--color-warning-700: oklch(72.35% 0.14 95.62deg);
|
||||
--color-warning-800: oklch(64.73% 0.13 95.92deg);
|
||||
--color-warning-900: oklch(56.77% 0.11 94.87deg);
|
||||
--color-warning-950: oklch(48.63% 0.1 95.22deg);
|
||||
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
--color-error-50: oklch(81.88% 0.1 38.14deg);
|
||||
--color-error-100: oklch(75.88% 0.13 31.15deg);
|
||||
--color-error-200: oklch(70.29% 0.16 27.32deg);
|
||||
--color-error-300: oklch(65.15% 0.19 25.65deg);
|
||||
--color-error-400: oklch(60.98% 0.21 25.56deg);
|
||||
--color-error-500: oklch(57.86% 0.22 26.62deg);
|
||||
--color-error-600: oklch(52.52% 0.2 26.86deg);
|
||||
--color-error-700: oklch(46.81% 0.18 27.02deg);
|
||||
--color-error-800: oklch(41.15% 0.16 27.63deg);
|
||||
--color-error-900: oklch(35.01% 0.14 27.9deg);
|
||||
--color-error-950: oklch(28.69% 0.12 29.23deg);
|
||||
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
--color-surface-50: oklch(100% 0 none);
|
||||
--color-surface-100: oklch(93.98% 0 105.57deg);
|
||||
--color-surface-200: oklch(87.66% 0 67.88deg);
|
||||
--color-surface-300: oklch(81.35% 0 106.1deg);
|
||||
--color-surface-400: oklch(74.79% 0 84.45deg);
|
||||
--color-surface-500: oklch(68.29% 0 91.36deg);
|
||||
--color-surface-600: oklch(60.99% 0 91.38deg);
|
||||
--color-surface-700: oklch(53.5% 0 84.49deg);
|
||||
--color-surface-800: oklch(46.03% 0 91.43deg);
|
||||
--color-surface-900: oklch(37.94% 0 84.52deg);
|
||||
--color-surface-950: oklch(29.34% 0 84.54deg);
|
||||
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
205
src/ae-c-lci.css
205
src/ae-c-lci.css
@@ -1,205 +0,0 @@
|
||||
[data-theme='AE_c_LCI'] {
|
||||
--text-scaling: 1.067;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
--anchor-font-color: var(--color-primary-500);
|
||||
--anchor-font-color-dark: var(--color-primary-500);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.75rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
--color-primary-50: oklch(85.1% 0.07 265.19deg);
|
||||
--color-primary-100: oklch(77.89% 0.08 264.31deg);
|
||||
--color-primary-200: oklch(70.32% 0.08 264.44deg);
|
||||
--color-primary-300: oklch(62.86% 0.09 263.87deg);
|
||||
--color-primary-400: oklch(54.96% 0.1 263.8deg);
|
||||
--color-primary-500: oklch(47.12% 0.11 262.88deg);
|
||||
--color-primary-600: oklch(40.9% 0.1 264.73deg);
|
||||
--color-primary-700: oklch(34.53% 0.1 267.34deg);
|
||||
--color-primary-800: oklch(28.16% 0.09 268.81deg);
|
||||
--color-primary-900: oklch(21.29% 0.09 271.12deg);
|
||||
--color-primary-950: oklch(12.88% 0.09 264.05deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
--color-secondary-50: oklch(96.14% 0.06 196.21deg);
|
||||
--color-secondary-100: oklch(89.81% 0.07 212.45deg);
|
||||
--color-secondary-200: oklch(83.71% 0.08 223.06deg);
|
||||
--color-secondary-300: oklch(77.42% 0.1 231.73deg);
|
||||
--color-secondary-400: oklch(71.44% 0.12 237.59deg);
|
||||
--color-secondary-500: oklch(65.39% 0.14 243.22deg);
|
||||
--color-secondary-600: oklch(58.93% 0.13 245.07deg);
|
||||
--color-secondary-700: oklch(52.09% 0.12 248.03deg);
|
||||
--color-secondary-800: oklch(45.27% 0.12 250.54deg);
|
||||
--color-secondary-900: oklch(38.01% 0.11 254.24deg);
|
||||
--color-secondary-950: oklch(30.67% 0.11 256.73deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
--color-tertiary-50: oklch(87.75% 0.12 326.52deg);
|
||||
--color-tertiary-100: oklch(80.92% 0.13 323.93deg);
|
||||
--color-tertiary-200: oklch(73.87% 0.14 321.55deg);
|
||||
--color-tertiary-300: oklch(66.9% 0.15 319.41deg);
|
||||
--color-tertiary-400: oklch(59.72% 0.16 317.25deg);
|
||||
--color-tertiary-500: oklch(52.73% 0.17 315.13deg);
|
||||
--color-tertiary-600: oklch(46.6% 0.16 314.18deg);
|
||||
--color-tertiary-700: oklch(40.43% 0.14 312.8deg);
|
||||
--color-tertiary-800: oklch(33.85% 0.13 309.88deg);
|
||||
--color-tertiary-900: oklch(27.23% 0.12 306.83deg);
|
||||
--color-tertiary-950: oklch(19.83% 0.1 302.7deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
--color-success-50: oklch(95.23% 0.07 195.99deg);
|
||||
--color-success-100: oklch(90.22% 0.09 189.46deg);
|
||||
--color-success-200: oklch(85.11% 0.1 186.03deg);
|
||||
--color-success-300: oklch(80.35% 0.12 181.75deg);
|
||||
--color-success-400: oklch(75.55% 0.12 178.92deg);
|
||||
--color-success-500: oklch(71.19% 0.13 174.73deg);
|
||||
--color-success-600: oklch(64.29% 0.12 173.65deg);
|
||||
--color-success-700: oklch(57.46% 0.11 171.75deg);
|
||||
--color-success-800: oklch(50.18% 0.1 170.68deg);
|
||||
--color-success-900: oklch(42.87% 0.09 167.65deg);
|
||||
--color-success-950: oklch(34.91% 0.07 164.42deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
--color-warning-50: oklch(95.67% 0.05 84.56deg);
|
||||
--color-warning-100: oklch(92.83% 0.06 82.16deg);
|
||||
--color-warning-200: oklch(90.12% 0.08 80.33deg);
|
||||
--color-warning-300: oklch(87.59% 0.1 80.01deg);
|
||||
--color-warning-400: oklch(85.03% 0.12 78.35deg);
|
||||
--color-warning-500: oklch(82.46% 0.14 76.71deg);
|
||||
--color-warning-600: oklch(76.34% 0.13 72.25deg);
|
||||
--color-warning-700: oklch(70.34% 0.13 68.09deg);
|
||||
--color-warning-800: oklch(63.99% 0.13 63.18deg);
|
||||
--color-warning-900: oklch(57.91% 0.13 57.97deg);
|
||||
--color-warning-950: oklch(51.69% 0.13 51.44deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
--color-error-50: oklch(84.29% 0.09 46.91deg);
|
||||
--color-error-100: oklch(78.63% 0.12 39.19deg);
|
||||
--color-error-200: oklch(72.92% 0.14 34.35deg);
|
||||
--color-error-300: oklch(67.88% 0.17 31.48deg);
|
||||
--color-error-400: oklch(63.09% 0.19 30.02deg);
|
||||
--color-error-500: oklch(59.32% 0.21 29.47deg);
|
||||
--color-error-600: oklch(53.56% 0.19 29.25deg);
|
||||
--color-error-700: oklch(47.75% 0.17 29.2deg);
|
||||
--color-error-800: oklch(41.51% 0.15 28.7deg);
|
||||
--color-error-900: oklch(35.35% 0.14 28.7deg);
|
||||
--color-error-950: oklch(28.69% 0.12 29.23deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
--color-surface-50: oklch(100% 0 none);
|
||||
--color-surface-100: oklch(97.02% 0 none);
|
||||
--color-surface-200: oklch(94.01% 0 none);
|
||||
--color-surface-300: oklch(91.12% 0 196.34deg);
|
||||
--color-surface-400: oklch(88.07% 0 196.37deg);
|
||||
--color-surface-500: oklch(84.99% 0 196.4deg);
|
||||
--color-surface-600: oklch(77.78% 0 196.47deg);
|
||||
--color-surface-700: oklch(70.09% 0 196.54deg);
|
||||
--color-surface-800: oklch(62.51% 0 196.61deg);
|
||||
--color-surface-900: oklch(54.34% 0 196.68deg);
|
||||
--color-surface-950: oklch(46.22% 0 196.73deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
/*
|
||||
* AE Firefly — Axonius variant
|
||||
* Primary: #ff6112 (Axonius orange)
|
||||
* Aether Platform / One Sky IT, LLC — Design System Theme
|
||||
*
|
||||
* Color philosophy:
|
||||
* Primary — Axonius Orange: #ff6112 brand color
|
||||
* Secondary — Warm Amber-Gold: consistent with AE_Firefly
|
||||
* Tertiary — Night-Sky Indigo: consistent with AE_Firefly
|
||||
* Surface — Moonlit Slate: consistent with AE_Firefly
|
||||
*
|
||||
* NOTE: Each data-theme selector is fully self-contained — CSS custom
|
||||
* properties do NOT inherit across theme selectors. All color ramps must
|
||||
* be defined here even if identical to the base Firefly theme.
|
||||
*
|
||||
* Based on: Skeleton v4 theme CSS variable structure
|
||||
* Variant of: src/ae-firefly.css (AE_Firefly)
|
||||
*/
|
||||
|
||||
html[data-theme='AE_Firefly_Axonius'] {
|
||||
--text-scaling: 1.067;
|
||||
--background: var(--color-surface-50) !important;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
/* Anchors: Axonius orange in light, lighter in dark */
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.875rem;
|
||||
--default-border-width: 1px;
|
||||
|
||||
/* PRIMARY — Axonius Orange (#ff6112) */
|
||||
--color-primary-50: #fff5ef;
|
||||
--color-primary-100: #ffe0d1;
|
||||
--color-primary-200: #ffc7a8;
|
||||
--color-primary-300: #ffad7f;
|
||||
--color-primary-400: #ff9356;
|
||||
--color-primary-500: #ff6112;
|
||||
--color-primary-600: #e6550f;
|
||||
--color-primary-700: #bf4b0d;
|
||||
--color-primary-800: #993f0b;
|
||||
--color-primary-900: #7c3509;
|
||||
--color-primary-950: #5f2b08;
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
/* SECONDARY — Warm Amber-Gold (same as AE_Firefly) */
|
||||
--color-secondary-50: oklch(97.5% 0.06 102deg);
|
||||
--color-secondary-100: oklch(93.5% 0.095 100deg);
|
||||
--color-secondary-200: oklch(89.5% 0.128 98deg);
|
||||
--color-secondary-300: oklch(85.5% 0.155 95deg);
|
||||
--color-secondary-400: oklch(81% 0.17 93deg);
|
||||
--color-secondary-500: oklch(76% 0.17 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.16 87deg);
|
||||
--color-secondary-700: oklch(60.5% 0.145 85deg);
|
||||
--color-secondary-800: oklch(52% 0.13 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.11 81deg);
|
||||
--color-secondary-950: oklch(35% 0.09 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
/* TERTIARY — Night-Sky Indigo (same as AE_Firefly) */
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89% 0.068 281deg);
|
||||
--color-tertiary-200: oklch(81.5% 0.092 279deg);
|
||||
--color-tertiary-300: oklch(73.5% 0.112 278deg);
|
||||
--color-tertiary-400: oklch(65% 0.132 277deg);
|
||||
--color-tertiary-500: oklch(55.5% 0.142 276deg);
|
||||
--color-tertiary-600: oklch(48.5% 0.138 275deg);
|
||||
--color-tertiary-700: oklch(41.5% 0.128 274deg);
|
||||
--color-tertiary-800: oklch(34.5% 0.112 273deg);
|
||||
--color-tertiary-900: oklch(27.5% 0.098 272deg);
|
||||
--color-tertiary-950: oklch(20% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
/* SUCCESS */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
/* WARNING */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
/* ERROR */
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
/* SURFACE — Moonlit Slate (same as AE_Firefly) */
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97% 0.006 217deg);
|
||||
--color-surface-200: oklch(93.5% 0.009 215deg);
|
||||
--color-surface-300: oklch(88.5% 0.012 213deg);
|
||||
--color-surface-400: oklch(81.5% 0.015 212deg);
|
||||
--color-surface-500: oklch(70.5% 0.016 215deg);
|
||||
--color-surface-600: oklch(59% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.02 222deg);
|
||||
--color-surface-800: oklch(30.5% 0.022 226deg);
|
||||
--color-surface-900: oklch(24.5% 0.025 229deg);
|
||||
--color-surface-950: oklch(15.5% 0.028 233deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
html.dark[data-theme='AE_Firefly_Axonius'] {
|
||||
--background: var(--color-surface-950) !important;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
|
||||
/* PRIMARY — Axonius Orange */
|
||||
--color-primary-50: #fff5ef;
|
||||
--color-primary-100: #ffe0d1;
|
||||
--color-primary-200: #ffc7a8;
|
||||
--color-primary-300: #ffad7f;
|
||||
--color-primary-400: #ff9356;
|
||||
--color-primary-500: #ff6112;
|
||||
--color-primary-600: #e6550f;
|
||||
--color-primary-700: #bf4b0d;
|
||||
--color-primary-800: #993f0b;
|
||||
--color-primary-900: #7c3509;
|
||||
--color-primary-950: #5f2b08;
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* SECONDARY — Warm Amber-Gold */
|
||||
--color-secondary-50: oklch(97.5% 0.06 102deg);
|
||||
--color-secondary-100: oklch(93.5% 0.095 100deg);
|
||||
--color-secondary-200: oklch(89.5% 0.128 98deg);
|
||||
--color-secondary-300: oklch(85.5% 0.155 95deg);
|
||||
--color-secondary-400: oklch(81% 0.17 93deg);
|
||||
--color-secondary-500: oklch(76% 0.17 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.16 87deg);
|
||||
--color-secondary-700: oklch(60.5% 0.145 85deg);
|
||||
--color-secondary-800: oklch(52% 0.13 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.11 81deg);
|
||||
--color-secondary-950: oklch(35% 0.09 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
/* TERTIARY — Night-Sky Indigo */
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89% 0.068 281deg);
|
||||
--color-tertiary-200: oklch(81.5% 0.092 279deg);
|
||||
--color-tertiary-300: oklch(73.5% 0.112 278deg);
|
||||
--color-tertiary-400: oklch(65% 0.132 277deg);
|
||||
--color-tertiary-500: oklch(55.5% 0.142 276deg);
|
||||
--color-tertiary-600: oklch(48.5% 0.138 275deg);
|
||||
--color-tertiary-700: oklch(41.5% 0.128 274deg);
|
||||
--color-tertiary-800: oklch(34.5% 0.112 273deg);
|
||||
--color-tertiary-900: oklch(27.5% 0.098 272deg);
|
||||
--color-tertiary-950: oklch(20% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
/* SUCCESS */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
/* WARNING */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
/* ERROR */
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
/* SURFACE — Moonlit Slate */
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97% 0.006 217deg);
|
||||
--color-surface-200: oklch(93.5% 0.009 215deg);
|
||||
--color-surface-300: oklch(88.5% 0.012 213deg);
|
||||
--color-surface-400: oklch(81.5% 0.015 212deg);
|
||||
--color-surface-500: oklch(70.5% 0.016 215deg);
|
||||
--color-surface-600: oklch(59% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.02 222deg);
|
||||
--color-surface-800: oklch(35.5% 0.022 226deg);
|
||||
--color-surface-900: oklch(24.5% 0.025 229deg);
|
||||
--color-surface-950: oklch(15.5% 0.028 233deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* AE Firefly — BGH variant
|
||||
* Base color input: #076a72
|
||||
* OKLCH primary ramp centered near hue ≈185° (teal/cyan family)
|
||||
* Variant of: src/ae-firefly.css (AE_Firefly)
|
||||
*/
|
||||
|
||||
html[data-theme='AE_Firefly_BGH'] {
|
||||
--text-scaling: 1.067;
|
||||
--background: var(--color-surface-50) !important;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
/* Anchors: teal in light, lighter teal in dark */
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.875rem;
|
||||
|
||||
/* Map common design-system tokens used by Tailwind/Skeleton presets */
|
||||
/* Set --primary as H S% L% (no wrapper), matching project's convention in src/app.css */
|
||||
--primary: 184.5 88.5% 23.7%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--primary-hex: #076a72;
|
||||
|
||||
/* PRIMARY — OKLCH ramp (hue ≈185°) */
|
||||
--color-primary-50: oklch(96.5% 0.025 189deg);
|
||||
--color-primary-100: oklch(91% 0.05 187deg);
|
||||
--color-primary-200: oklch(84.5% 0.078 186deg);
|
||||
--color-primary-300: oklch(76.5% 0.105 185deg);
|
||||
--color-primary-400: oklch(67.5% 0.125 185deg);
|
||||
--color-primary-500: oklch(50.5% 0.13 185deg);
|
||||
--color-primary-600: oklch(44% 0.125 184deg);
|
||||
--color-primary-700: oklch(37.5% 0.115 183deg);
|
||||
--color-primary-800: oklch(30.5% 0.105 182deg);
|
||||
--color-primary-900: oklch(23.5% 0.09 181deg);
|
||||
--color-primary-950: oklch(16% 0.075 180deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
/* Hex fallback for the core brand color (500) if needed */
|
||||
--color-primary-500-hex: #076a72;
|
||||
|
||||
/* --- Secondary (copied from AE_Firefly defaults) --- */
|
||||
--color-secondary-50: oklch(97.5% 0.06 102deg);
|
||||
--color-secondary-100: oklch(93.5% 0.095 100deg);
|
||||
--color-secondary-200: oklch(89.5% 0.128 98deg);
|
||||
--color-secondary-300: oklch(85.5% 0.155 95deg);
|
||||
--color-secondary-400: oklch(81% 0.17 93deg);
|
||||
--color-secondary-500: oklch(76% 0.17 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.16 87deg);
|
||||
--color-secondary-700: oklch(60.5% 0.145 85deg);
|
||||
--color-secondary-800: oklch(52% 0.13 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.11 81deg);
|
||||
--color-secondary-950: oklch(35% 0.09 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
/* --- Tertiary --- */
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89% 0.068 281deg);
|
||||
--color-tertiary-200: oklch(81.5% 0.092 279deg);
|
||||
--color-tertiary-300: oklch(73.5% 0.112 278deg);
|
||||
--color-tertiary-400: oklch(65% 0.132 277deg);
|
||||
--color-tertiary-500: oklch(55.5% 0.142 276deg);
|
||||
--color-tertiary-600: oklch(48.5% 0.138 275deg);
|
||||
--color-tertiary-700: oklch(41.5% 0.128 274deg);
|
||||
--color-tertiary-800: oklch(34.5% 0.112 273deg);
|
||||
--color-tertiary-900: oklch(27.5% 0.098 272deg);
|
||||
--color-tertiary-950: oklch(20% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
/* --- Success (kept consistent across Firefly variants) --- */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
/* --- Warning --- */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
/* --- Error --- */
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
/* --- Surface (important for light-mode backgrounds) --- */
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97% 0.006 217deg);
|
||||
--color-surface-200: oklch(93.5% 0.009 215deg);
|
||||
--color-surface-300: oklch(88.5% 0.012 213deg);
|
||||
--color-surface-400: oklch(81.5% 0.015 212deg);
|
||||
--color-surface-500: oklch(70.5% 0.016 215deg);
|
||||
--color-surface-600: oklch(59% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.02 222deg);
|
||||
--color-surface-800: oklch(35.5% 0.022 226deg);
|
||||
--color-surface-900: oklch(24.5% 0.02 52deg);
|
||||
--color-surface-950: oklch(15.5% 0.022 48deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
html.dark[data-theme='AE_Firefly_BGH'] {
|
||||
--background: var(--color-surface-950) !important;
|
||||
|
||||
/* Minimal dark-mode contrast tokens for components */
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
/*
|
||||
* AE Firefly — Indigo variant
|
||||
* "Deep night, rich as velvet."
|
||||
* Aether Platform / One Sky IT, LLC — Design System Theme
|
||||
*
|
||||
* Aesthetic vision (Scott Idem, 2026-03-09):
|
||||
* A deep, rich purple-indigo variant of the Firefly system.
|
||||
* Inspired by the deep indigo of a clear night sky — the hours
|
||||
* before dawn when the dark is at its most velvety and profound.
|
||||
* Authoritative and calm, with a warm rose accent for warmth.
|
||||
*
|
||||
* Color philosophy:
|
||||
* Primary — Deep Indigo: rich blue-violet (~266°), luminous depth
|
||||
* Secondary — Violet: companion purple, warmer/rosier tone (~290°)
|
||||
* Tertiary — Dusty Rose: warm complement, plum/rose tones (~341°)
|
||||
* Surface — Velvet Slate: subtly purple-tinged neutral; near-white
|
||||
* (light mode) → deep midnight indigo-grey (dark mode)
|
||||
*
|
||||
* Section 508 / WCAG 2.1 AA:
|
||||
* - Body text (surface-950 on surface-50 light): >15:1 contrast ✓
|
||||
* - Primary-filled buttons meet ≥3:1 for interactive components ✓
|
||||
* - Contrast crossover points calculated per OKLCH approximate RL
|
||||
*
|
||||
* Based on: Skeleton v4 theme CSS variable structure
|
||||
* Variant of: src/ae-firefly.css (AE_Firefly)
|
||||
*/
|
||||
|
||||
html[data-theme='AE_Firefly_Indigo'] {
|
||||
--text-scaling: 1.067;
|
||||
--background: var(--color-surface-50) !important;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
/* Anchors: indigo in light, lighter indigo in dark */
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
}
|
||||
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
html[data-theme='AE_Firefly_Indigo'] {
|
||||
--color-primary-50: oklch(95.5% 0.04 270deg);
|
||||
--color-primary-100: oklch(89.5% 0.072 270deg);
|
||||
--color-primary-200: oklch(82.5% 0.108 269deg);
|
||||
--color-primary-300: oklch(74.5% 0.135 268deg);
|
||||
--color-primary-400: oklch(65% 0.155 267deg);
|
||||
--color-primary-500: oklch(50.5% 0.16 266deg);
|
||||
--color-primary-600: oklch(43.5% 0.152 265deg);
|
||||
--color-primary-700: oklch(37% 0.138 264deg);
|
||||
--color-primary-800: oklch(30% 0.12 263deg);
|
||||
--color-primary-900: oklch(23% 0.1 262deg);
|
||||
--color-primary-950: oklch(15.5% 0.08 261deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(96.5% 0.032 297deg);
|
||||
--color-secondary-100: oklch(91.5% 0.058 295deg);
|
||||
--color-secondary-200: oklch(85.5% 0.09 293deg);
|
||||
--color-secondary-300: oklch(78.5% 0.115 292deg);
|
||||
--color-secondary-400: oklch(70% 0.132 291deg);
|
||||
--color-secondary-500: oklch(60% 0.14 290deg);
|
||||
--color-secondary-600: oklch(52.5% 0.135 289deg);
|
||||
--color-secondary-700: oklch(45% 0.126 288deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 286deg);
|
||||
--color-secondary-900: oklch(30% 0.094 284deg);
|
||||
--color-secondary-950: oklch(22% 0.076 282deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(96.5% 0.022 348deg);
|
||||
--color-tertiary-100: oklch(91% 0.042 346deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.068 344deg);
|
||||
--color-tertiary-300: oklch(76.5% 0.095 343deg);
|
||||
--color-tertiary-400: oklch(68% 0.118 342deg);
|
||||
--color-tertiary-500: oklch(57.5% 0.128 341deg);
|
||||
--color-tertiary-600: oklch(50% 0.122 340deg);
|
||||
--color-tertiary-700: oklch(43% 0.112 339deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.098 338deg);
|
||||
--color-tertiary-900: oklch(28% 0.08 337deg);
|
||||
--color-tertiary-950: oklch(20.5% 0.062 336deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99% 0.003 270deg);
|
||||
--color-surface-100: oklch(96.5% 0.006 268deg);
|
||||
--color-surface-200: oklch(92.5% 0.01 266deg);
|
||||
--color-surface-300: oklch(87% 0.014 265deg);
|
||||
--color-surface-400: oklch(78.5% 0.018 265deg);
|
||||
--color-surface-500: oklch(66.5% 0.02 267deg);
|
||||
--color-surface-600: oklch(54.5% 0.022 269deg);
|
||||
--color-surface-700: oklch(42.5% 0.024 270deg);
|
||||
--color-surface-800: oklch(31% 0.026 272deg);
|
||||
--color-surface-900: oklch(20.5% 0.03 274deg);
|
||||
--color-surface-950: oklch(13% 0.034 276deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
--background: var(--color-surface-950) !important;
|
||||
--radius-container: 0.875rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
|
||||
/* ===================================================================
|
||||
* PRIMARY — Deep Indigo
|
||||
* Hue: ~266°. Rich blue-violet indigo — the color of deep night sky
|
||||
* in the hours before dawn. Luminous depth without harshness.
|
||||
* CSS named "indigo" (#4B0082) is near oklch(20%, 0.18, 302°) but
|
||||
* that's too dark to use as a 500 primary. This palette centers on
|
||||
* a richer, usable indigo that reads clearly as "indigo" while
|
||||
* maintaining sufficient contrast at mid-range shades.
|
||||
* At 500 (L≈50%): sufficient contrast with primary-50 text (≥4:1).
|
||||
* =================================================================== */
|
||||
--color-primary-50: oklch(95.5% 0.04 270deg);
|
||||
--color-primary-100: oklch(89.5% 0.072 270deg);
|
||||
--color-primary-200: oklch(82.5% 0.108 269deg);
|
||||
--color-primary-300: oklch(74.5% 0.135 268deg);
|
||||
--color-primary-400: oklch(65% 0.155 267deg);
|
||||
--color-primary-500: oklch(50.5% 0.16 266deg);
|
||||
--color-primary-600: oklch(43.5% 0.152 265deg);
|
||||
--color-primary-700: oklch(37% 0.138 264deg);
|
||||
--color-primary-800: oklch(30% 0.12 263deg);
|
||||
--color-primary-900: oklch(23% 0.1 262deg);
|
||||
--color-primary-950: oklch(15.5% 0.08 261deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SECONDARY — Violet
|
||||
* Hue: ~290°. A companion purple, slightly warmer and rosier than
|
||||
* the primary indigo. Creates a rich monochromatic depth while
|
||||
* remaining clearly distinct from the primary.
|
||||
* Used for secondary actions, badges, and soft highlights.
|
||||
* =================================================================== */
|
||||
--color-secondary-50: oklch(96.5% 0.032 297deg);
|
||||
--color-secondary-100: oklch(91.5% 0.058 295deg);
|
||||
--color-secondary-200: oklch(85.5% 0.09 293deg);
|
||||
--color-secondary-300: oklch(78.5% 0.115 292deg);
|
||||
--color-secondary-400: oklch(70% 0.132 291deg);
|
||||
--color-secondary-500: oklch(60% 0.14 290deg);
|
||||
--color-secondary-600: oklch(52.5% 0.135 289deg);
|
||||
--color-secondary-700: oklch(45% 0.126 288deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 286deg);
|
||||
--color-secondary-900: oklch(30% 0.094 284deg);
|
||||
--color-secondary-950: oklch(22% 0.076 282deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* TERTIARY — Dusty Rose / Plum
|
||||
* Hue: ~341°. A warm, muted rose-plum that provides the crucial
|
||||
* warm counterpoint to the cool indigo-violet palette. Prevents
|
||||
* the theme from feeling cold — like the warm glow of dawn
|
||||
* breaking against a deep indigo sky.
|
||||
* Used for location chips, warm accents, tertiary elements.
|
||||
* =================================================================== */
|
||||
--color-tertiary-50: oklch(96.5% 0.022 348deg);
|
||||
--color-tertiary-100: oklch(91% 0.042 346deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.068 344deg);
|
||||
--color-tertiary-300: oklch(76.5% 0.095 343deg);
|
||||
--color-tertiary-400: oklch(68% 0.118 342deg);
|
||||
--color-tertiary-500: oklch(57.5% 0.128 341deg);
|
||||
--color-tertiary-600: oklch(50% 0.122 340deg);
|
||||
--color-tertiary-700: oklch(43% 0.112 339deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.098 338deg);
|
||||
--color-tertiary-900: oklch(28% 0.08 337deg);
|
||||
--color-tertiary-950: oklch(20.5% 0.062 336deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SUCCESS — Bioluminescent Green
|
||||
* Hue: ~152°. Consistent with AE_Firefly for recognizable semantic
|
||||
* color meaning across OSIT themes.
|
||||
* =================================================================== */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* WARNING — Amber Orange
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* ERROR — Soft Coral/Rose
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SURFACE — Velvet Slate
|
||||
* A subtly purple-tinged neutral — barely perceptible, it gives
|
||||
* surfaces a velvet quality that harmonizes with the indigo palette
|
||||
* without being purple-on-purple. Light mode: soft white with a
|
||||
* whisper of purple. Dark mode: deep midnight indigo-grey.
|
||||
*
|
||||
* 50 → body-bg light: near-white with ImperceptibleISTIC purple cast
|
||||
* 950 → body-bg dark: deep midnight with indigo depth
|
||||
* =================================================================== */
|
||||
--color-surface-50: oklch(99% 0.003 270deg);
|
||||
--color-surface-100: oklch(96.5% 0.006 268deg);
|
||||
--color-surface-200: oklch(92.5% 0.01 266deg);
|
||||
--color-surface-300: oklch(87% 0.014 265deg);
|
||||
--color-surface-400: oklch(78.5% 0.018 265deg);
|
||||
--color-surface-500: oklch(66.5% 0.02 267deg);
|
||||
--color-surface-600: oklch(54.5% 0.022 269deg);
|
||||
--color-surface-700: oklch(42.5% 0.024 270deg);
|
||||
--color-surface-800: oklch(31% 0.026 272deg);
|
||||
--color-surface-900: oklch(20.5% 0.03 274deg);
|
||||
--color-surface-950: oklch(13% 0.034 276deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
/*
|
||||
* AE Firefly — Rainbow variant
|
||||
* "All the colors of wonder."
|
||||
* Aether Platform / One Sky IT, LLC — Design System Theme
|
||||
*
|
||||
* Aesthetic vision (Scott Idem, 2026-03-09):
|
||||
* A celebration-of-color variant of the Firefly system.
|
||||
* The three brand color slots span the visible spectrum:
|
||||
* Coral-Red (primary) → Emerald-Green (secondary) → Violet (tertiary).
|
||||
* Warm cream surfaces let the saturated accents breathe without
|
||||
* competing. Joyful and energetic — still calm, still Firefly.
|
||||
*
|
||||
* Color philosophy:
|
||||
* Primary — Vivid Coral-Red: ~15°, warm and energetic
|
||||
* Secondary — Emerald Green: ~148°, lush and clear
|
||||
* Tertiary — Rich Violet: ~295°, deep and luminous
|
||||
* Surface — Sunrise Cream: barely warm neutral; warm white
|
||||
* (light mode) → deep warm charcoal (dark mode)
|
||||
*
|
||||
* Section 508 / WCAG 2.1 AA:
|
||||
* - Body text (surface-950 on surface-50 light): >15:1 contrast ✓
|
||||
* - Primary-filled buttons meet ≥3:1 for interactive components ✓
|
||||
* - Chroma values kept within sRGB gamut across the full ramp
|
||||
*
|
||||
* Based on: Skeleton v4 theme CSS variable structure
|
||||
* Variant of: src/ae-firefly.css (AE_Firefly)
|
||||
*/
|
||||
|
||||
html[data-theme='AE_Firefly_Rainbow'] {
|
||||
--text-scaling: 1.067;
|
||||
--background: var(--color-surface-50) !important;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
/* Anchors: coral-red in light, lighter coral in dark */
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
html[data-theme='AE_Firefly_Rainbow'] {
|
||||
--color-primary-50: oklch(97% 0.02 15deg);
|
||||
--color-primary-100: oklch(92% 0.048 14deg);
|
||||
--color-primary-200: oklch(86% 0.085 13deg);
|
||||
--color-primary-300: oklch(79% 0.125 13deg);
|
||||
--color-primary-400: oklch(71% 0.16 13deg);
|
||||
--color-primary-500: oklch(60% 0.19 14deg);
|
||||
--color-primary-600: oklch(52.5% 0.178 15deg);
|
||||
--color-primary-700: oklch(45% 0.162 16deg);
|
||||
--color-primary-800: oklch(37.5% 0.142 17deg);
|
||||
--color-primary-900: oklch(30% 0.118 18deg);
|
||||
--color-primary-950: oklch(22.5% 0.092 19deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(97% 0.04 152deg);
|
||||
--color-secondary-100: oklch(92.5% 0.072 150deg);
|
||||
--color-secondary-200: oklch(87% 0.105 149deg);
|
||||
--color-secondary-300: oklch(81% 0.132 148deg);
|
||||
--color-secondary-400: oklch(74.5% 0.152 148deg);
|
||||
--color-secondary-500: oklch(62% 0.16 148deg);
|
||||
--color-secondary-600: oklch(53.5% 0.148 148deg);
|
||||
--color-secondary-700: oklch(45.5% 0.132 147deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 146deg);
|
||||
--color-secondary-900: oklch(29.5% 0.09 145deg);
|
||||
--color-secondary-950: oklch(21.5% 0.068 144deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(96.5% 0.03 299deg);
|
||||
--color-tertiary-100: oklch(91% 0.058 297deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.092 296deg);
|
||||
--color-tertiary-300: oklch(77% 0.122 295deg);
|
||||
--color-tertiary-400: oklch(68.5% 0.148 295deg);
|
||||
--color-tertiary-500: oklch(57% 0.158 295deg);
|
||||
--color-tertiary-600: oklch(49.5% 0.15 294deg);
|
||||
--color-tertiary-700: oklch(42.5% 0.138 293deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.122 292deg);
|
||||
--color-tertiary-900: oklch(28.5% 0.102 291deg);
|
||||
--color-tertiary-950: oklch(21% 0.08 290deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99.2% 0.004 75deg);
|
||||
--color-surface-100: oklch(97% 0.007 72deg);
|
||||
--color-surface-200: oklch(93.5% 0.01 70deg);
|
||||
--color-surface-300: oklch(88.5% 0.013 68deg);
|
||||
--color-surface-400: oklch(81.5% 0.016 66deg);
|
||||
--color-surface-500: oklch(70.5% 0.018 64deg);
|
||||
--color-surface-600: oklch(59% 0.018 62deg);
|
||||
--color-surface-700: oklch(47.5% 0.018 58deg);
|
||||
--color-surface-800: oklch(35.5% 0.02 55deg);
|
||||
--color-surface-900: oklch(24.5% 0.02 52deg);
|
||||
--color-surface-950: oklch(15.5% 0.022 48deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
--background: var(--color-surface-950) !important;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.875rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
|
||||
/* ===================================================================
|
||||
* PRIMARY — Vivid Coral-Red (warm end of the rainbow)
|
||||
* Hue: ~15°. The warm, energetic anchor of the spectrum —
|
||||
* sits between orange and red for maximum vibrancy and warmth.
|
||||
* Kept within sRGB gamut across the full ramp.
|
||||
* At 500 (L≈60%): sufficient contrast with primary-50 text (≥4:1).
|
||||
* =================================================================== */
|
||||
--color-primary-50: oklch(97% 0.02 15deg);
|
||||
--color-primary-100: oklch(92% 0.048 14deg);
|
||||
--color-primary-200: oklch(86% 0.085 13deg);
|
||||
--color-primary-300: oklch(79% 0.125 13deg);
|
||||
--color-primary-400: oklch(71% 0.16 13deg);
|
||||
--color-primary-500: oklch(60% 0.19 14deg);
|
||||
--color-primary-600: oklch(52.5% 0.178 15deg);
|
||||
--color-primary-700: oklch(45% 0.162 16deg);
|
||||
--color-primary-800: oklch(37.5% 0.142 17deg);
|
||||
--color-primary-900: oklch(30% 0.118 18deg);
|
||||
--color-primary-950: oklch(22.5% 0.092 19deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SECONDARY — Emerald Green (mid-spectrum, the heart of the rainbow)
|
||||
* Hue: ~148°. Clear, lush emerald — the most recognizable
|
||||
* "rainbow green." Positioned at the center of the visible spectrum,
|
||||
* it bridges the warm red primary and the cool violet tertiary.
|
||||
* Used for secondary actions, success-adjacent highlights, badges.
|
||||
* =================================================================== */
|
||||
--color-secondary-50: oklch(97% 0.04 152deg);
|
||||
--color-secondary-100: oklch(92.5% 0.072 150deg);
|
||||
--color-secondary-200: oklch(87% 0.105 149deg);
|
||||
--color-secondary-300: oklch(81% 0.132 148deg);
|
||||
--color-secondary-400: oklch(74.5% 0.152 148deg);
|
||||
--color-secondary-500: oklch(62% 0.16 148deg);
|
||||
--color-secondary-600: oklch(53.5% 0.148 148deg);
|
||||
--color-secondary-700: oklch(45.5% 0.132 147deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 146deg);
|
||||
--color-secondary-900: oklch(29.5% 0.09 145deg);
|
||||
--color-secondary-950: oklch(21.5% 0.068 144deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* TERTIARY — Rich Violet (cool end of the rainbow)
|
||||
* Hue: ~295°. Deep blue-violet — the "indigo and violet" end of
|
||||
* the arc. Completes the warm-cool spectrum span across the three
|
||||
* brand color slots. Creates striking contrast with the warm primary.
|
||||
* Used for location chips, deep accents, tertiary elements.
|
||||
* =================================================================== */
|
||||
--color-tertiary-50: oklch(96.5% 0.03 299deg);
|
||||
--color-tertiary-100: oklch(91% 0.058 297deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.092 296deg);
|
||||
--color-tertiary-300: oklch(77% 0.122 295deg);
|
||||
--color-tertiary-400: oklch(68.5% 0.148 295deg);
|
||||
--color-tertiary-500: oklch(57% 0.158 295deg);
|
||||
--color-tertiary-600: oklch(49.5% 0.15 294deg);
|
||||
--color-tertiary-700: oklch(42.5% 0.138 293deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.122 292deg);
|
||||
--color-tertiary-900: oklch(28.5% 0.102 291deg);
|
||||
--color-tertiary-950: oklch(21% 0.08 290deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SUCCESS — Bioluminescent Green
|
||||
* Hue: ~152°. Consistent with AE_Firefly for recognizable semantic
|
||||
* color meaning across OSIT themes.
|
||||
* =================================================================== */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* WARNING — Amber Orange
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* ERROR — Soft Coral/Rose
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SURFACE — Sunrise Cream
|
||||
* A barely-warm neutral (hue ~70°, chroma 0.004-0.022) that lets
|
||||
* the vibrant brand colors breathe. The warmth prevents the surface
|
||||
* from feeling clinical when the vivid accents are in use.
|
||||
*
|
||||
* 50 → body-bg light: warm near-white, like morning paper
|
||||
* 950 → body-bg dark: deep warm charcoal, like a dim theatre
|
||||
* =================================================================== */
|
||||
--color-surface-50: oklch(99.2% 0.004 75deg);
|
||||
--color-surface-100: oklch(97% 0.007 72deg);
|
||||
--color-surface-200: oklch(93.5% 0.01 70deg);
|
||||
--color-surface-300: oklch(88.5% 0.013 68deg);
|
||||
--color-surface-400: oklch(81.5% 0.016 66deg);
|
||||
--color-surface-500: oklch(70.5% 0.018 64deg);
|
||||
--color-surface-600: oklch(59% 0.018 62deg);
|
||||
--color-surface-700: oklch(47.5% 0.018 58deg);
|
||||
--color-surface-800: oklch(35.5% 0.02 55deg);
|
||||
--color-surface-900: oklch(24.5% 0.02 52deg);
|
||||
--color-surface-950: oklch(15.5% 0.022 48deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
/*
|
||||
* AE Firefly — SteelBlue variant
|
||||
* "Polished metal, like light on still water."
|
||||
* Aether Platform / One Sky IT, LLC — Design System Theme
|
||||
*
|
||||
* Aesthetic vision (Scott Idem, 2026-03-09):
|
||||
* A metallic, professional cool-blue variant of the Firefly system.
|
||||
* Inspired by polished steel and chrome — the reflective shimmer of
|
||||
* cool metal under crisp light. Calm authority, not cold distance.
|
||||
*
|
||||
* Color philosophy:
|
||||
* Primary — Steel Blue: polished metallic blue (~214°), cool and precise
|
||||
* Secondary — Burnished Gold: warm metallic contrast (~52°), brass/copper
|
||||
* Tertiary — Cobalt Navy: deeper blue for depth and dimension (~229°)
|
||||
* Surface — Chrome Silver: barely-cool neutral with a subtle blue cast
|
||||
* (light mode: bright chrome) → (dark mode: gunmetal slate)
|
||||
*
|
||||
* Section 508 / WCAG 2.1 AA:
|
||||
* - Body text (surface-950 on surface-50 light): >15:1 contrast ✓
|
||||
* - Primary-filled buttons meet ≥3:1 for interactive components ✓
|
||||
* - Contrast crossover points calculated per OKLCH approximate RL
|
||||
*
|
||||
* Based on: Skeleton v4 theme CSS variable structure
|
||||
* Variant of: src/ae-firefly.css (AE_Firefly)
|
||||
*/
|
||||
|
||||
html[data-theme='AE_Firefly_SteelBlue'] {
|
||||
--text-scaling: 1.067;
|
||||
--background: var(--color-surface-50) !important;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
/* Anchors: steel blue in light, lighter steel blue in dark */
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.875rem;
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
--color-primary-50: oklch(96.5% 0.022 214deg);
|
||||
--color-primary-100: oklch(91% 0.045 213deg);
|
||||
--color-primary-200: oklch(84.5% 0.072 212deg);
|
||||
--color-primary-300: oklch(76.5% 0.097 212deg);
|
||||
--color-primary-400: oklch(67% 0.115 213deg);
|
||||
--color-primary-500: oklch(56% 0.115 214deg);
|
||||
--color-primary-600: oklch(49% 0.112 214deg);
|
||||
--color-primary-700: oklch(41.5% 0.105 213deg);
|
||||
--color-primary-800: oklch(34% 0.095 212deg);
|
||||
--color-primary-900: oklch(26.5% 0.08 211deg);
|
||||
--color-primary-950: oklch(18.5% 0.065 210deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(97.5% 0.055 56deg);
|
||||
--color-secondary-100: oklch(93.5% 0.09 55deg);
|
||||
--color-secondary-200: oklch(89.5% 0.12 54deg);
|
||||
--color-secondary-300: oklch(85.5% 0.148 53deg);
|
||||
--color-secondary-400: oklch(81.5% 0.162 52deg);
|
||||
--color-secondary-500: oklch(76.5% 0.162 51deg);
|
||||
--color-secondary-600: oklch(68.5% 0.152 50deg);
|
||||
--color-secondary-700: oklch(60.5% 0.138 49deg);
|
||||
--color-secondary-800: oklch(52% 0.122 48deg);
|
||||
--color-secondary-900: oklch(43.5% 0.102 47deg);
|
||||
--color-secondary-950: oklch(35% 0.084 46deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(95.5% 0.025 232deg);
|
||||
--color-tertiary-100: oklch(89.5% 0.048 231deg);
|
||||
--color-tertiary-200: oklch(82.5% 0.072 230deg);
|
||||
--color-tertiary-300: oklch(74.5% 0.095 229deg);
|
||||
--color-tertiary-400: oklch(65.5% 0.12 229deg);
|
||||
--color-tertiary-500: oklch(54.5% 0.135 230deg);
|
||||
--color-tertiary-600: oklch(47% 0.132 230deg);
|
||||
--color-tertiary-700: oklch(39.5% 0.122 229deg);
|
||||
--color-tertiary-800: oklch(32% 0.108 228deg);
|
||||
--color-tertiary-900: oklch(25% 0.09 227deg);
|
||||
--color-tertiary-950: oklch(17.5% 0.072 226deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99% 0.004 220deg);
|
||||
--color-surface-100: oklch(96.5% 0.008 218deg);
|
||||
--color-surface-200: oklch(92.5% 0.012 217deg);
|
||||
--color-surface-300: oklch(87% 0.016 216deg);
|
||||
--color-surface-400: oklch(78.5% 0.02 215deg);
|
||||
--color-surface-500: oklch(66.5% 0.022 217deg);
|
||||
--color-surface-600: oklch(54.5% 0.025 220deg);
|
||||
--color-surface-700: oklch(42.5% 0.028 223deg);
|
||||
--color-surface-800: oklch(31% 0.032 226deg);
|
||||
--color-surface-900: oklch(20.5% 0.035 228deg);
|
||||
--color-surface-950: oklch(13% 0.04 232deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
--background: var(--color-surface-950) !important;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
|
||||
/* ===================================================================
|
||||
* PRIMARY — Polished Steel Blue
|
||||
* Hue: ~214°. Cool metallic blue — the shine of polished steel
|
||||
* under directional light. Professional, precise, and distinctive.
|
||||
* Approx: #4682B4 (CSS SteelBlue) sits at oklch(56%, 0.113, 214°).
|
||||
* At 500 (L≈56%): sufficient contrast with primary-50 text (≥4:1).
|
||||
* =================================================================== */
|
||||
--color-primary-50: oklch(96.5% 0.022 214deg);
|
||||
--color-primary-100: oklch(91% 0.045 213deg);
|
||||
--color-primary-200: oklch(84.5% 0.072 212deg);
|
||||
--color-primary-300: oklch(76.5% 0.097 212deg);
|
||||
--color-primary-400: oklch(67% 0.115 213deg);
|
||||
--color-primary-500: oklch(56% 0.115 214deg);
|
||||
--color-primary-600: oklch(49% 0.112 214deg);
|
||||
--color-primary-700: oklch(41.5% 0.105 213deg);
|
||||
--color-primary-800: oklch(34% 0.095 212deg);
|
||||
--color-primary-900: oklch(26.5% 0.08 211deg);
|
||||
--color-primary-950: oklch(18.5% 0.065 210deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SECONDARY — Burnished Gold (warm metallic contrast)
|
||||
* Hue: ~52°. Warm brass/copper tones that complement the coolness
|
||||
* of steel blue. The classic "metal on metal" contrast pairing —
|
||||
* used for secondary actions, badges, and call-to-action highlights.
|
||||
* =================================================================== */
|
||||
--color-secondary-50: oklch(97.5% 0.055 56deg);
|
||||
--color-secondary-100: oklch(93.5% 0.09 55deg);
|
||||
--color-secondary-200: oklch(89.5% 0.12 54deg);
|
||||
--color-secondary-300: oklch(85.5% 0.148 53deg);
|
||||
--color-secondary-400: oklch(81.5% 0.162 52deg);
|
||||
--color-secondary-500: oklch(76.5% 0.162 51deg);
|
||||
--color-secondary-600: oklch(68.5% 0.152 50deg);
|
||||
--color-secondary-700: oklch(60.5% 0.138 49deg);
|
||||
--color-secondary-800: oklch(52% 0.122 48deg);
|
||||
--color-secondary-900: oklch(43.5% 0.102 47deg);
|
||||
--color-secondary-950: oklch(35% 0.084 46deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* TERTIARY — Cobalt Navy
|
||||
* Hue: ~229°. A deeper, richer blue for depth and dimension —
|
||||
* like the heavy cobalt-blue depths under polished chrome.
|
||||
* Used for accents, location chips, and depth elements.
|
||||
* =================================================================== */
|
||||
--color-tertiary-50: oklch(95.5% 0.025 232deg);
|
||||
--color-tertiary-100: oklch(89.5% 0.048 231deg);
|
||||
--color-tertiary-200: oklch(82.5% 0.072 230deg);
|
||||
--color-tertiary-300: oklch(74.5% 0.095 229deg);
|
||||
--color-tertiary-400: oklch(65.5% 0.12 229deg);
|
||||
--color-tertiary-500: oklch(54.5% 0.135 230deg);
|
||||
--color-tertiary-600: oklch(47% 0.132 230deg);
|
||||
--color-tertiary-700: oklch(39.5% 0.122 229deg);
|
||||
--color-tertiary-800: oklch(32% 0.108 228deg);
|
||||
--color-tertiary-900: oklch(25% 0.09 227deg);
|
||||
--color-tertiary-950: oklch(17.5% 0.072 226deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SUCCESS — Bioluminescent Green
|
||||
* Hue: ~152°. Consistent with AE_Firefly for recognizable semantic
|
||||
* color meaning across OSIT themes.
|
||||
* =================================================================== */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* WARNING — Amber Orange
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* ERROR — Soft Coral/Rose
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
/* ===================================================================
|
||||
* SURFACE — Chrome Silver
|
||||
* A cool-blue-tinted neutral with slightly more chromatic presence
|
||||
* than AE_Firefly's Moonlit Slate — leaning further toward the
|
||||
* steel palette. Light mode: bright chrome. Dark mode: gunmetal.
|
||||
*
|
||||
* 50 → body-bg light: brilliant near-white with a chrome whisper
|
||||
* 950 → body-bg dark: deep gunmetal with subtle cool-blue depth
|
||||
* =================================================================== */
|
||||
--color-surface-50: oklch(99% 0.004 220deg);
|
||||
--color-surface-100: oklch(96.5% 0.008 218deg);
|
||||
--color-surface-200: oklch(92.5% 0.012 217deg);
|
||||
--color-surface-300: oklch(87% 0.016 216deg);
|
||||
--color-surface-400: oklch(78.5% 0.02 215deg);
|
||||
--color-surface-500: oklch(66.5% 0.022 217deg);
|
||||
--color-surface-600: oklch(54.5% 0.025 220deg);
|
||||
--color-surface-700: oklch(42.5% 0.028 223deg);
|
||||
--color-surface-800: oklch(31% 0.032 226deg);
|
||||
--color-surface-900: oklch(20.5% 0.035 228deg);
|
||||
--color-surface-950: oklch(13% 0.04 232deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
/*
|
||||
* AE Firefly — "Shiny serenity, like a firefly."
|
||||
* Aether Platform / One Sky IT, LLC — Design System Theme
|
||||
*
|
||||
* Aesthetic vision (Scott Idem, 2026-03-06):
|
||||
* Calm, focused, softly luminous. A modern Section 508–compliant
|
||||
* theme for One Sky IT, LLC. Inspired by bioluminescence —
|
||||
* the brief, cool glow of a firefly against a deep, serene night.
|
||||
*
|
||||
* Color philosophy:
|
||||
* Primary — Luminescent Teal: cool bioluminescent shimmer
|
||||
* Secondary — Warm Amber-Gold: the firefly's warm body glow
|
||||
* Tertiary — Night-Sky Indigo: depth and serenity of the dark
|
||||
* Surface — Moonlit Slate: barely-cool neutral; crisp white
|
||||
* (light mode) → deep midnight blue-grey (dark mode)
|
||||
*
|
||||
* Section 508 / WCAG 2.1 AA:
|
||||
* - Body text (surface-950 on surface-50 light): >15:1 contrast ✓
|
||||
* - Primary-filled buttons meet ≥3:1 for interactive components ✓
|
||||
* - Contrast crossover points calculated per OKLCH approximate RL
|
||||
*
|
||||
* Based on: Skeleton v4 theme CSS variable structure
|
||||
* Reference: src/ae-osit-default.css, Nouveau preset
|
||||
*/
|
||||
|
||||
html[data-theme='AE_Firefly'] {
|
||||
--text-scaling: 1.067;
|
||||
--background: var(--color-surface-50) !important;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
/* Anchors: teal in light, lighter teal in dark */
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.875rem; /* slightly more rounded than default for modern feel */
|
||||
--default-border-width: 1px;
|
||||
}
|
||||
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
html[data-theme='AE_Firefly'] {
|
||||
--color-primary-50: oklch(96.5% 0.025 192deg);
|
||||
--color-primary-100: oklch(91% 0.05 190deg);
|
||||
--color-primary-200: oklch(84.5% 0.078 188deg);
|
||||
--color-primary-300: oklch(76.5% 0.105 186deg);
|
||||
--color-primary-400: oklch(67.5% 0.125 185deg);
|
||||
--color-primary-500: oklch(50.5% 0.13 184deg);
|
||||
--color-primary-600: oklch(44% 0.125 183deg);
|
||||
--color-primary-700: oklch(37.5% 0.115 182deg);
|
||||
--color-primary-800: oklch(30.5% 0.105 181deg);
|
||||
--color-primary-900: oklch(23.5% 0.09 180deg);
|
||||
--color-primary-950: oklch(16% 0.075 179deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(97.5% 0.06 102deg);
|
||||
--color-secondary-100: oklch(93.5% 0.095 100deg);
|
||||
--color-secondary-200: oklch(89.5% 0.128 98deg);
|
||||
--color-secondary-300: oklch(85.5% 0.155 95deg);
|
||||
--color-secondary-400: oklch(81% 0.17 93deg);
|
||||
--color-secondary-500: oklch(76% 0.17 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.16 87deg);
|
||||
--color-secondary-700: oklch(60.5% 0.145 85deg);
|
||||
--color-secondary-800: oklch(52% 0.13 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.11 81deg);
|
||||
--color-secondary-950: oklch(35% 0.09 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89% 0.068 281deg);
|
||||
--color-tertiary-200: oklch(81.5% 0.092 279deg);
|
||||
--color-tertiary-300: oklch(73.5% 0.112 278deg);
|
||||
--color-tertiary-400: oklch(65% 0.132 277deg);
|
||||
--color-tertiary-500: oklch(55.5% 0.142 276deg);
|
||||
--color-tertiary-600: oklch(48.5% 0.138 275deg);
|
||||
--color-tertiary-700: oklch(41.5% 0.128 274deg);
|
||||
--color-tertiary-800: oklch(34.5% 0.112 273deg);
|
||||
--color-tertiary-900: oklch(27.5% 0.098 272deg);
|
||||
--color-tertiary-950: oklch(20% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97% 0.006 217deg);
|
||||
--color-surface-200: oklch(93.5% 0.009 215deg);
|
||||
--color-surface-300: oklch(88.5% 0.012 213deg);
|
||||
--color-surface-400: oklch(81.5% 0.015 212deg);
|
||||
--color-surface-500: oklch(70.5% 0.016 215deg);
|
||||
--color-surface-600: oklch(59% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.02 222deg);
|
||||
--color-surface-800: oklch(30.5% 0.022 226deg);
|
||||
--color-surface-900: oklch(24.5% 0.025 229deg);
|
||||
--color-surface-950: oklch(15.5% 0.028 233deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
html.dark[data-theme='AE_Firefly'] {
|
||||
--background: var(--color-surface-950) !important;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
|
||||
/* ===================================================================
|
||||
* PRIMARY — Luminescent Firefly Teal
|
||||
* ...existing code...
|
||||
*/
|
||||
--color-primary-50: oklch(96.5% 0.025 192deg);
|
||||
--color-primary-100: oklch(91% 0.05 190deg);
|
||||
--color-primary-200: oklch(84.5% 0.078 188deg);
|
||||
--color-primary-300: oklch(76.5% 0.105 186deg);
|
||||
--color-primary-400: oklch(67.5% 0.125 185deg);
|
||||
--color-primary-500: oklch(50.5% 0.13 184deg);
|
||||
--color-primary-600: oklch(44% 0.125 183deg);
|
||||
--color-primary-700: oklch(37.5% 0.115 182deg);
|
||||
--color-primary-800: oklch(30.5% 0.105 181deg);
|
||||
--color-primary-900: oklch(23.5% 0.09 180deg);
|
||||
--color-primary-950: oklch(16% 0.075 179deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* ...existing code for secondary, tertiary, success, warning, error, surface... */
|
||||
--color-secondary-50: oklch(97.5% 0.06 102deg);
|
||||
--color-secondary-100: oklch(93.5% 0.095 100deg);
|
||||
--color-secondary-200: oklch(89.5% 0.128 98deg);
|
||||
--color-secondary-300: oklch(85.5% 0.155 95deg);
|
||||
--color-secondary-400: oklch(81% 0.17 93deg);
|
||||
--color-secondary-500: oklch(76% 0.17 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.16 87deg);
|
||||
--color-secondary-700: oklch(60.5% 0.145 85deg);
|
||||
--color-secondary-800: oklch(52% 0.13 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.11 81deg);
|
||||
--color-secondary-950: oklch(35% 0.09 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89% 0.068 281deg);
|
||||
--color-tertiary-200: oklch(81.5% 0.092 279deg);
|
||||
--color-tertiary-300: oklch(73.5% 0.112 278deg);
|
||||
--color-tertiary-400: oklch(65% 0.132 277deg);
|
||||
--color-tertiary-500: oklch(55.5% 0.142 276deg);
|
||||
--color-tertiary-600: oklch(48.5% 0.138 275deg);
|
||||
--color-tertiary-700: oklch(41.5% 0.128 274deg);
|
||||
--color-tertiary-800: oklch(34.5% 0.112 273deg);
|
||||
--color-tertiary-900: oklch(27.5% 0.098 272deg);
|
||||
--color-tertiary-950: oklch(20% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152deg);
|
||||
--color-success-200: oklch(87.45% 0.08 152.08deg);
|
||||
--color-success-300: oklch(83.57% 0.09 150.85deg);
|
||||
--color-success-400: oklch(79.47% 0.11 150.71deg);
|
||||
--color-success-500: oklch(75.38% 0.12 149.99deg);
|
||||
--color-success-600: oklch(67.65% 0.11 149.94deg);
|
||||
--color-success-700: oklch(59.71% 0.09 150.42deg);
|
||||
--color-success-800: oklch(51.74% 0.08 150.24deg);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.09 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.12 73deg);
|
||||
--color-warning-300: oklch(85.5% 0.145 70deg);
|
||||
--color-warning-400: oklch(81.5% 0.16 67deg);
|
||||
--color-warning-500: oklch(77% 0.165 65deg);
|
||||
--color-warning-600: oklch(69.5% 0.155 64deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
--color-error-50: oklch(95% 0.04 18deg);
|
||||
--color-error-100: oklch(88% 0.07 20deg);
|
||||
--color-error-200: oklch(80% 0.105 21deg);
|
||||
--color-error-300: oklch(72% 0.14 22deg);
|
||||
--color-error-400: oklch(64.5% 0.17 23deg);
|
||||
--color-error-500: oklch(57.5% 0.195 24deg);
|
||||
--color-error-600: oklch(51.5% 0.182 25deg);
|
||||
--color-error-700: oklch(45.5% 0.165 26deg);
|
||||
--color-error-800: oklch(39.5% 0.148 27deg);
|
||||
--color-error-900: oklch(33% 0.128 28deg);
|
||||
--color-error-950: oklch(26.5% 0.108 29deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97% 0.006 217deg);
|
||||
--color-surface-200: oklch(93.5% 0.009 215deg);
|
||||
--color-surface-300: oklch(88.5% 0.012 213deg);
|
||||
--color-surface-400: oklch(81.5% 0.015 212deg);
|
||||
--color-surface-500: oklch(70.5% 0.016 215deg);
|
||||
--color-surface-600: oklch(59% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.02 222deg);
|
||||
--color-surface-800: oklch(35.5% 0.022 226deg);
|
||||
--color-surface-900: oklch(24.5% 0.025 229deg);
|
||||
--color-surface-950: oklch(15.5% 0.028 233deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
[data-theme='AE_OSIT_default'] {
|
||||
--text-scaling: 1.067;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
--anchor-font-color: var(--color-primary-600);
|
||||
--anchor-font-color-dark: var(--color-primary-400);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.75rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
--color-primary-50: oklch(85.73% 0.07 251.8deg);
|
||||
--color-primary-100: oklch(78.5% 0.09 252.03deg);
|
||||
--color-primary-200: oklch(71.06% 0.1 253.6deg);
|
||||
--color-primary-300: oklch(63.76% 0.12 253.85deg);
|
||||
--color-primary-400: oklch(56.32% 0.14 255.25deg);
|
||||
--color-primary-500: oklch(49.23% 0.15 256.36deg);
|
||||
--color-primary-600: oklch(43.11% 0.14 258.86deg);
|
||||
--color-primary-700: oklch(36.85% 0.14 261.54deg);
|
||||
--color-primary-800: oklch(30.41% 0.13 263.99deg);
|
||||
--color-primary-900: oklch(23.91% 0.12 265.91deg);
|
||||
--color-primary-950: oklch(16.96% 0.12 264.05deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
--color-secondary-50: oklch(96.26% 0.06 196.24deg);
|
||||
--color-secondary-100: oklch(89.14% 0.07 220.79deg);
|
||||
--color-secondary-200: oklch(82.13% 0.08 234.87deg);
|
||||
--color-secondary-300: oklch(75.03% 0.11 245.33deg);
|
||||
--color-secondary-400: oklch(68.15% 0.14 250.72deg);
|
||||
--color-secondary-500: oklch(61.37% 0.16 255.34deg);
|
||||
--color-secondary-600: oklch(55.1% 0.16 256.81deg);
|
||||
--color-secondary-700: oklch(48.64% 0.15 258.4deg);
|
||||
--color-secondary-800: oklch(41.84% 0.15 260.39deg);
|
||||
--color-secondary-900: oklch(35.05% 0.14 262.03deg);
|
||||
--color-secondary-950: oklch(28.12% 0.14 262.47deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
--color-tertiary-50: oklch(100% 0 none);
|
||||
--color-tertiary-100: oklch(96.07% 0.01 251.15deg);
|
||||
--color-tertiary-200: oklch(91.88% 0.03 252.69deg);
|
||||
--color-tertiary-300: oklch(87.99% 0.05 253.24deg);
|
||||
--color-tertiary-400: oklch(83.81% 0.06 253.57deg);
|
||||
--color-tertiary-500: oklch(79.93% 0.08 253.32deg);
|
||||
--color-tertiary-600: oklch(72.53% 0.08 251.75deg);
|
||||
--color-tertiary-700: oklch(64.93% 0.08 249.75deg);
|
||||
--color-tertiary-800: oklch(57.14% 0.09 247.99deg);
|
||||
--color-tertiary-900: oklch(49.18% 0.09 246.55deg);
|
||||
--color-tertiary-950: oklch(41.1% 0.09 246.54deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
--color-success-50: oklch(95.82% 0.06 184.52deg);
|
||||
--color-success-100: oklch(91.55% 0.08 172.29deg);
|
||||
--color-success-200: oklch(87.44% 0.11 165.22deg);
|
||||
--color-success-300: oklch(83.26% 0.13 161.2deg);
|
||||
--color-success-400: oklch(79.56% 0.16 157.13deg);
|
||||
--color-success-500: oklch(76.12% 0.18 153.61deg);
|
||||
--color-success-600: oklch(69.31% 0.17 151.81deg);
|
||||
--color-success-700: oklch(62.07% 0.16 149.95deg);
|
||||
--color-success-800: oklch(54.9% 0.15 147.65deg);
|
||||
--color-success-900: oklch(47.26% 0.14 145.54deg);
|
||||
--color-success-950: oklch(39.64% 0.13 143.79deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
--color-warning-50: oklch(98.26% 0.1 108.02deg);
|
||||
--color-warning-100: oklch(95.84% 0.12 104.66deg);
|
||||
--color-warning-200: oklch(93.48% 0.13 102.21deg);
|
||||
--color-warning-300: oklch(91.49% 0.15 100.17deg);
|
||||
--color-warning-400: oklch(89.28% 0.16 98.19deg);
|
||||
--color-warning-500: oklch(87.14% 0.17 96.01deg);
|
||||
--color-warning-600: oklch(79.88% 0.16 96.31deg);
|
||||
--color-warning-700: oklch(72.35% 0.14 95.62deg);
|
||||
--color-warning-800: oklch(64.73% 0.13 95.92deg);
|
||||
--color-warning-900: oklch(56.77% 0.11 94.87deg);
|
||||
--color-warning-950: oklch(48.63% 0.1 95.22deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
--color-error-50: oklch(81.88% 0.1 38.14deg);
|
||||
--color-error-100: oklch(75.88% 0.13 31.15deg);
|
||||
--color-error-200: oklch(70.29% 0.16 27.32deg);
|
||||
--color-error-300: oklch(65.15% 0.19 25.65deg);
|
||||
--color-error-400: oklch(60.98% 0.21 25.56deg);
|
||||
--color-error-500: oklch(57.86% 0.22 26.62deg);
|
||||
--color-error-600: oklch(52.52% 0.2 26.86deg);
|
||||
--color-error-700: oklch(46.81% 0.18 27.02deg);
|
||||
--color-error-800: oklch(41.15% 0.16 27.63deg);
|
||||
--color-error-900: oklch(35.01% 0.14 27.9deg);
|
||||
--color-error-950: oklch(28.69% 0.12 29.23deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-light);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
--color-surface-50: oklch(100% 0 none);
|
||||
--color-surface-100: oklch(93.98% 0 105.57deg);
|
||||
--color-surface-200: oklch(87.66% 0 67.88deg);
|
||||
--color-surface-300: oklch(81.35% 0 106.1deg);
|
||||
--color-surface-400: oklch(74.79% 0 84.45deg);
|
||||
--color-surface-500: oklch(68.29% 0 91.36deg);
|
||||
--color-surface-600: oklch(60.99% 0 91.38deg);
|
||||
--color-surface-700: oklch(53.5% 0 84.49deg);
|
||||
--color-surface-800: oklch(46.03% 0 91.43deg);
|
||||
--color-surface-900: oklch(37.94% 0 84.52deg);
|
||||
--color-surface-950: oklch(29.34% 0 84.54deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
[data-theme='aeclci'] {
|
||||
--text-scaling: 1.067;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
--base-font-family: system-ui, sans-serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
--heading-font-color: inherit;
|
||||
--heading-font-color-dark: inherit;
|
||||
--heading-font-family: inherit;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
--anchor-font-color: var(--color-primary-500);
|
||||
--anchor-font-color-dark: var(--color-primary-500);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.75rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
--color-primary-50: oklch(85.1% 0.07 265.19deg);
|
||||
--color-primary-100: oklch(77.89% 0.08 264.31deg);
|
||||
--color-primary-200: oklch(70.32% 0.08 264.44deg);
|
||||
--color-primary-300: oklch(62.86% 0.09 263.87deg);
|
||||
--color-primary-400: oklch(54.96% 0.1 263.8deg);
|
||||
--color-primary-500: oklch(47.12% 0.11 262.88deg);
|
||||
--color-primary-600: oklch(40.9% 0.1 264.73deg);
|
||||
--color-primary-700: oklch(34.53% 0.1 267.34deg);
|
||||
--color-primary-800: oklch(28.16% 0.09 268.81deg);
|
||||
--color-primary-900: oklch(21.29% 0.09 271.12deg);
|
||||
--color-primary-950: oklch(12.88% 0.09 264.05deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
--color-secondary-50: oklch(73.24% 0.12 278.78deg);
|
||||
--color-secondary-100: oklch(65.76% 0.12 276.12deg);
|
||||
--color-secondary-200: oklch(58.15% 0.12 273.33deg);
|
||||
--color-secondary-300: oklch(50.59% 0.12 270.28deg);
|
||||
--color-secondary-400: oklch(42.65% 0.12 267.23deg);
|
||||
--color-secondary-500: oklch(34.53% 0.12 264.22deg);
|
||||
--color-secondary-600: oklch(30.3% 0.11 264.59deg);
|
||||
--color-secondary-700: oklch(25.96% 0.09 265.69deg);
|
||||
--color-secondary-800: oklch(21.25% 0.08 267.5deg);
|
||||
--color-secondary-900: oklch(16.42% 0.06 269.55deg);
|
||||
--color-secondary-950: oklch(8.85% 0.06 264.05deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
--color-tertiary-50: oklch(87.75% 0.12 326.52deg);
|
||||
--color-tertiary-100: oklch(80.92% 0.13 323.93deg);
|
||||
--color-tertiary-200: oklch(73.87% 0.14 321.55deg);
|
||||
--color-tertiary-300: oklch(66.9% 0.15 319.41deg);
|
||||
--color-tertiary-400: oklch(59.72% 0.16 317.25deg);
|
||||
--color-tertiary-500: oklch(52.73% 0.17 315.13deg);
|
||||
--color-tertiary-600: oklch(46.6% 0.16 314.18deg);
|
||||
--color-tertiary-700: oklch(40.43% 0.14 312.8deg);
|
||||
--color-tertiary-800: oklch(33.85% 0.13 309.88deg);
|
||||
--color-tertiary-900: oklch(27.23% 0.12 306.83deg);
|
||||
--color-tertiary-950: oklch(19.83% 0.1 302.7deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
--color-success-50: oklch(95.23% 0.07 195.99deg);
|
||||
--color-success-100: oklch(90.22% 0.09 189.46deg);
|
||||
--color-success-200: oklch(85.11% 0.1 186.03deg);
|
||||
--color-success-300: oklch(80.35% 0.12 181.75deg);
|
||||
--color-success-400: oklch(75.55% 0.12 178.92deg);
|
||||
--color-success-500: oklch(71.19% 0.13 174.73deg);
|
||||
--color-success-600: oklch(64.29% 0.12 173.65deg);
|
||||
--color-success-700: oklch(57.46% 0.11 171.75deg);
|
||||
--color-success-800: oklch(50.18% 0.1 170.68deg);
|
||||
--color-success-900: oklch(42.87% 0.09 167.65deg);
|
||||
--color-success-950: oklch(34.91% 0.07 164.42deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
--color-warning-50: oklch(95.67% 0.05 84.56deg);
|
||||
--color-warning-100: oklch(92.83% 0.06 82.16deg);
|
||||
--color-warning-200: oklch(90.12% 0.08 80.33deg);
|
||||
--color-warning-300: oklch(87.59% 0.1 80.01deg);
|
||||
--color-warning-400: oklch(85.03% 0.12 78.35deg);
|
||||
--color-warning-500: oklch(82.46% 0.14 76.71deg);
|
||||
--color-warning-600: oklch(76.34% 0.13 72.25deg);
|
||||
--color-warning-700: oklch(70.34% 0.13 68.09deg);
|
||||
--color-warning-800: oklch(63.99% 0.13 63.18deg);
|
||||
--color-warning-900: oklch(57.91% 0.13 57.97deg);
|
||||
--color-warning-950: oklch(51.69% 0.13 51.44deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
--color-error-50: oklch(84.29% 0.09 46.91deg);
|
||||
--color-error-100: oklch(78.63% 0.12 39.19deg);
|
||||
--color-error-200: oklch(72.92% 0.14 34.35deg);
|
||||
--color-error-300: oklch(67.88% 0.17 31.48deg);
|
||||
--color-error-400: oklch(63.09% 0.19 30.02deg);
|
||||
--color-error-500: oklch(59.32% 0.21 29.47deg);
|
||||
--color-error-600: oklch(53.56% 0.19 29.25deg);
|
||||
--color-error-700: oklch(47.75% 0.17 29.2deg);
|
||||
--color-error-800: oklch(41.51% 0.15 28.7deg);
|
||||
--color-error-900: oklch(35.35% 0.14 28.7deg);
|
||||
--color-error-950: oklch(28.69% 0.12 29.23deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
--color-surface-50: oklch(100% 0 none);
|
||||
--color-surface-100: oklch(97.02% 0 none);
|
||||
--color-surface-200: oklch(94.01% 0 none);
|
||||
--color-surface-300: oklch(91.12% 0 196.34deg);
|
||||
--color-surface-400: oklch(88.07% 0 196.37deg);
|
||||
--color-surface-500: oklch(84.99% 0 196.4deg);
|
||||
--color-surface-600: oklch(77.78% 0 196.47deg);
|
||||
--color-surface-700: oklch(70.09% 0 196.54deg);
|
||||
--color-surface-800: oklch(62.51% 0 196.61deg);
|
||||
--color-surface-900: oklch(54.34% 0 196.68deg);
|
||||
--color-surface-950: oklch(46.22% 0 196.73deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
1015
src/app.css
1015
src/app.css
File diff suppressed because it is too large
Load Diff
35
src/app.d.ts
vendored
35
src/app.d.ts
vendored
@@ -2,35 +2,8 @@
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
userid: string;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Platform {}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
native_app: any;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var native_app: any;
|
||||
}
|
||||
|
||||
// Stripe Buy Button web component — needed so Svelte templates accept the element without TS errors.
|
||||
declare module 'svelte/elements' {
|
||||
interface IntrinsicElements {
|
||||
'stripe-buy-button': {
|
||||
'buy-button-id': string;
|
||||
'publishable-key': string;
|
||||
'client-reference-id'?: string;
|
||||
[attr: string]: any;
|
||||
};
|
||||
}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
108
src/app.html
108
src/app.html
@@ -1,108 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="">
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const hostname = window.location.hostname;
|
||||
const is_local_dev =
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '[::1]' ||
|
||||
hostname.endsWith('.localhost');
|
||||
|
||||
if (!is_local_dev || !('serviceWorker' in navigator)) return;
|
||||
|
||||
// Prevent the app bootstrap from re-registering a worker on localhost.
|
||||
// The browser can otherwise keep routing fetches through a stale SW
|
||||
// during iterative iframe testing, which is exactly the noise we want to avoid.
|
||||
try {
|
||||
Object.defineProperty(navigator.serviceWorker, 'register', {
|
||||
configurable: true,
|
||||
value: async () => ({})
|
||||
});
|
||||
} catch {
|
||||
// If the property is not writable in this browser, the unregister
|
||||
// pass below still removes any existing worker registration.
|
||||
}
|
||||
|
||||
// Local iframe testing should not keep an older worker alive, because
|
||||
// Chromium can continue to route fetches through the stale worker until
|
||||
// it is explicitly unregistered.
|
||||
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||
for (const registration of registrations) {
|
||||
registration.unregister().catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Clear any stale runtime caches as well; local testing should always
|
||||
// rebuild from the current source rather than reusing old worker output.
|
||||
if ('caches' in window) {
|
||||
caches.keys().then((cache_keys) => {
|
||||
for (const cache_key of cache_keys) {
|
||||
caches.delete(cache_key).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<!-- Google Fonts: commented out 2026-05-19 — no theme or component applies these families;
|
||||
all themes use system-ui/sans-serif. Re-enable if a theme is added that references them.
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
|
||||
rel="stylesheet" />
|
||||
-->
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- <link href="app.css" rel="stylesheet"> -->
|
||||
|
||||
<!-- Pre-JS loading indicator. Removed by root +layout.svelte onMount once Svelte
|
||||
bootstraps and the existing is_hydrating overlay takes over. Pointer-events:none
|
||||
so it never blocks interaction if something goes wrong with the remove call. -->
|
||||
<style>
|
||||
#ae_loader {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
#ae_loader::after {
|
||||
content: '';
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(120, 120, 120, 0.15);
|
||||
border-top-color: rgba(120, 120, 120, 0.45);
|
||||
animation: ae_loader_spin 0.75s linear infinite;
|
||||
}
|
||||
@keyframes ae_loader_spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<!-- h-full w-full overflow-auto -->
|
||||
<!-- overflow-x-scroll -->
|
||||
<body data-sveltekit-preload-data="hover" class="h-full w-full">
|
||||
<div id="ae_loader" aria-hidden="true"></div>
|
||||
<div style="display: contents" class="">%sveltekit.body%</div>
|
||||
<body data-sveltekit-preload-data="hover" data-theme="my-custom-theme">
|
||||
<div style="display: contents" class="h-full w-full overflow-hidden">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
371
src/app.postcss
Normal file
371
src/app.postcss
Normal file
@@ -0,0 +1,371 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
/* There are no more Tailwind layers. */
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply h-full overflow-hidden;
|
||||
|
||||
/* font-family: 'Liberation Sans', sans-serif; */
|
||||
/* font-family: 'Noto Sans', sans-serif; */
|
||||
}
|
||||
|
||||
/* default theme */
|
||||
/* @font-face {
|
||||
font-family: 'Liberation Sans', sans-serif;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
src: url('/fonts/liberation/LiberationSans-Regular.ttf');
|
||||
src: url('/fonts/noto/NotoSans-Regular.ttf');
|
||||
font-display: swap;
|
||||
} */
|
||||
|
||||
/* modern theme */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
src: url('/fonts/Quicksand.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* :root [data-theme='modern'] { */
|
||||
/* --theme-rounded-base: 20px;
|
||||
--theme-rounded-container: 4px; */
|
||||
|
||||
/* --theme-font-family-base: 'Liberation Sans', sans-serif; */
|
||||
/* --theme-font-family-heading: 'Liberation Sans', sans-serif; */
|
||||
/* } */
|
||||
|
||||
.card-footer {
|
||||
border-top: 1px solid hsla(0, 0%, 0%, 0.5);
|
||||
margin-top: 1em;
|
||||
padding-top: 1em;
|
||||
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
|
||||
/* Tailwind: This "fixes" Tailwind's default group button styles that do not seem to allow hidding buttons. */
|
||||
.btn-group a.hidden, .btn-group button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.ae_d_none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Allow content to scroll horizontal if too wide */
|
||||
.ae_h_scrollfix {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* These helps with the Skeleton Tailwind modal utility. */
|
||||
.ae_modal_scrollfix {
|
||||
/* Allow modal content to scroll if it's too long */
|
||||
overflow-y: auto;
|
||||
max-height: 96vh;
|
||||
/* max-height: 99%; */
|
||||
|
||||
}
|
||||
|
||||
.ae_debug {
|
||||
/* A darker pink outline */
|
||||
outline: thin dashed;
|
||||
outline-color: hsla(0, 100%, 50%, 0.15);
|
||||
/* A light pink background color */
|
||||
background-color: hsla(0, 100%, 50%, 0.15);
|
||||
}
|
||||
.ae_debug:hover {
|
||||
/* A darker pink outline */
|
||||
outline-color: hsla(0, 100%, 50%, 0.50);
|
||||
/* A light pink background color */
|
||||
background-color: hsla(0, 100%, 50%, 0.40);
|
||||
}
|
||||
|
||||
|
||||
/* Deal with being in an iframe */
|
||||
#appShell #shell-header.iframe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appShell #shell-footer.iframe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Remove the background from the body in all cases */
|
||||
/* body[data-theme] { */
|
||||
/* background: none; */
|
||||
/* background-image: none; */
|
||||
/* } */
|
||||
|
||||
/* Remove the background from the body if using iframes */
|
||||
/* body[data-theme]:has(#page.iframe) { */
|
||||
/* background: none; */
|
||||
/* background-image: none; */
|
||||
/* background-image: url('https://static.oneskyit.com/c/CHOW/images/CHOW_2024_yellow_background.png'); */
|
||||
/* background-size: cover; */
|
||||
/* } */
|
||||
|
||||
main {
|
||||
/* background: none;
|
||||
background-color: hsla(0, 0%, 100%, 0.92); */
|
||||
}
|
||||
|
||||
main>section {
|
||||
background: none;
|
||||
background-color: hsla(0, 0%, 100%, 0.92);
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
/* @media (min-width: 640px) {
|
||||
main>div, main>section {
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
} */
|
||||
|
||||
/* @media (min-width: 768px) {
|
||||
main>div, main>section {
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
} */
|
||||
|
||||
.ae_sponsorships {
|
||||
/* background: none; */
|
||||
/* background-color: hsla(0, 0%, 100%, 0.92); */
|
||||
/* background-image: url('https://static.oneskyit.com/c/CHOW/images/CHOW_2024_yellow_background.png'); */
|
||||
/* background-size: cover; */
|
||||
}
|
||||
|
||||
pre.pre_wrap {
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
|
||||
border: none;
|
||||
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
||||
input.required {
|
||||
/* border-right: solid medium var(--color-warning-500); */
|
||||
/* color: var(--color-warning-500); */
|
||||
}
|
||||
|
||||
input:required {
|
||||
/* background-color: var(--alert-color-lightest); */
|
||||
/* border: solid 2px red; */
|
||||
/* outline: dashed thin var(--alert-color-lighter); */
|
||||
|
||||
/* border-right: solid medium var(--alert-color-mid); */
|
||||
/* border-right: solid medium var(--warning-color-mid); */
|
||||
/* border-right: solid medium var(--error-color-mid); */
|
||||
}
|
||||
/* input:required:hover {
|
||||
background-color: var(--alert-color-lighter);
|
||||
border-right: solid thick var(--alert-color-darker);
|
||||
} */
|
||||
|
||||
/* input:required::before {
|
||||
display: block;
|
||||
|
||||
content: '*';
|
||||
color: var(--warning-color-darker);
|
||||
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
} */
|
||||
|
||||
.input_required::after {
|
||||
content: '*';
|
||||
color: rgb(var(--color-error-500) / 0.9);
|
||||
|
||||
position: relative;
|
||||
/* top: 0em; */
|
||||
left: .25em;
|
||||
}
|
||||
|
||||
/* Make the group a flex row by default */
|
||||
/* div.btn-group { */
|
||||
/* display: flex; */
|
||||
/* gap: 0; */
|
||||
/* flex-direction: row; */
|
||||
/* justify-content: space-around; */
|
||||
/* align-items: center; */
|
||||
/* margin: 0;
|
||||
padding: 0; */
|
||||
|
||||
/* } */
|
||||
/* Make all button elements except for the the first button element not rounded on the left. */
|
||||
/* Make all button elements except for the fhe last button element not rounded on the right. */
|
||||
/* These helps with the Skeleton (Tailwind?) button group element. */
|
||||
.btn-group button {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* .md:btn-group button,
|
||||
.lg:btn-group button {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
} */
|
||||
|
||||
/* div.btn-group button:first-child {
|
||||
border-top-left-radius: .25rem;
|
||||
border-bottom-left-radius: .25rem;
|
||||
}
|
||||
div.btn-group button:last-child {
|
||||
border-top-right-radius: .25rem;
|
||||
border-bottom-right-radius: .25rem;
|
||||
} */
|
||||
|
||||
.ae_obj_prop .label {
|
||||
|
||||
}
|
||||
|
||||
.ae_obj_prop .value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.ae_md_hide {
|
||||
/* outline: medium dashed green; */
|
||||
/* display: none; */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.ae_md_hide {
|
||||
/* outline: medium dashed red; */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.ae_lg_hide {
|
||||
/* outline: medium dashed blue; */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Use the div.ae_quick_modal_container to block background clicks when using the section.ae_quick_popover. */
|
||||
div.ae_quick_modal_container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: hsla(0, 0%, 0%, .5);
|
||||
}
|
||||
|
||||
/* The section.ae_quick_popover should be above the rest of the content and centered on the page. */
|
||||
section.ae_quick_popover {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 100;
|
||||
background-color: hsla(0, 0%, 100%, .95);
|
||||
padding: 1rem;
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0 0 1rem hsla(0, 0%, 0%, .5);
|
||||
|
||||
min-height: 98%;
|
||||
min-width: 98%;
|
||||
}
|
||||
|
||||
section.ae_quick_popover_small {
|
||||
position: fixed;
|
||||
top: 1em;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
z-index: 100;
|
||||
background-color: hsla(0, 0%, 100%, .95);
|
||||
padding: 1rem;
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0 0 1rem hsla(0, 0%, 0%, .5);
|
||||
|
||||
min-height: 24rem;
|
||||
max-height: 95%;
|
||||
min-width: 50%;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
|
||||
.fade_50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.fade_50:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.auth_view_only {
|
||||
display: none;
|
||||
}
|
||||
.ae_root--auth_access .auth_view_only {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
|
||||
|
||||
img.qr_code {
|
||||
/* outline: solid thin hsla(30, 100%, 50%, .1); */
|
||||
/* width: 1.50in; */
|
||||
}
|
||||
|
||||
img.qr_code:hover {
|
||||
/* outline: solid thin green; */
|
||||
/* width: 2.50in; */
|
||||
}
|
||||
|
||||
img.qr_code:focus {
|
||||
/* outline: solid thin red; */
|
||||
/* width: 2.50in; */
|
||||
}
|
||||
|
||||
|
||||
.dim {
|
||||
opacity: 0.5;
|
||||
color: hsla(0, 0%, 50%, .95);
|
||||
}
|
||||
.dim_warning {
|
||||
opacity: 0.5;
|
||||
/* color: hsla(0, 100%, 50%, .95); */
|
||||
/* background should be hash marks */
|
||||
background-image: repeating-linear-gradient(-45deg, hsla(0, 100%, 50%, .25), hsla(0, 100%, 50%, .25) 10px, transparent 10px, transparent 20px);
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.sk_header.hide_sm {
|
||||
display: none;
|
||||
}
|
||||
.sk_header.show_sm {
|
||||
display: initial;
|
||||
}
|
||||
.sk_header.show_md {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sk_header.hide_md {
|
||||
display: none;
|
||||
}
|
||||
.sk_header.show_md {
|
||||
display: initial;
|
||||
}
|
||||
.sk_header.show_sm {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
7
src/index.test.ts
Normal file
7
src/index.test.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -1,255 +1,63 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Performs a DELETE request to the Aether API.
|
||||
* Refactored 2026-01-08 to use standard fetch with timeout, custom fetch injection,
|
||||
* standardized kebab-case headers, and robust V3 response handling.
|
||||
*/
|
||||
export const delete_object = async function delete_object({
|
||||
api_cfg = null,
|
||||
endpoint = '',
|
||||
headers = {},
|
||||
params = {},
|
||||
data = {},
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
log_lvl = 0,
|
||||
retry_count = 5
|
||||
}: {
|
||||
api_cfg: any;
|
||||
endpoint: string;
|
||||
headers?: any;
|
||||
params?: any;
|
||||
data?: any;
|
||||
timeout?: number;
|
||||
return_meta?: boolean;
|
||||
log_lvl?: number;
|
||||
retry_count?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** delete_object() *** Endpoint: ${endpoint}`);
|
||||
console.log('Params:', params);
|
||||
if (log_lvl > 1) {
|
||||
console.log('Data:', data);
|
||||
console.log(`Base URL: ${api_cfg?.['base_url']}`);
|
||||
}
|
||||
// Updated 2024-05-23
|
||||
export let delete_object = async function delete_object(
|
||||
{
|
||||
api_cfg=null,
|
||||
endpoint='',
|
||||
params={},
|
||||
data={},
|
||||
return_meta=false,
|
||||
log_lvl=0
|
||||
}: {
|
||||
api_cfg: any,
|
||||
endpoint: string,
|
||||
params?: any,
|
||||
data?: any,
|
||||
return_meta?: boolean,
|
||||
log_lvl?: number
|
||||
}
|
||||
|
||||
if (!api_cfg) {
|
||||
console.error('No API Config was provided. Returning false.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct the URL with query parameters
|
||||
const url = new URL(endpoint, api_cfg['base_url']);
|
||||
if (params) {
|
||||
Object.keys(params).forEach((key) =>
|
||||
url.searchParams.append(key, params[key])
|
||||
);
|
||||
}
|
||||
|
||||
// Clean and merge headers without mutating the original api_cfg
|
||||
const headers_cleaned: key_val = {};
|
||||
const merged_headers = { ...api_cfg['headers'], ...headers };
|
||||
|
||||
// Handle "Bootstrap Paradox" for unauthenticated requests
|
||||
if (merged_headers.hasOwnProperty('x-no-account-id')) {
|
||||
delete merged_headers['x-account-id'];
|
||||
if (merged_headers['x-no-account-id'] === null) {
|
||||
merged_headers['x-no-account-id'] = 'Nothing to See Here';
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop in merged_headers) {
|
||||
const prop_cleaned = prop.replaceAll('_', '-');
|
||||
let value = merged_headers[prop];
|
||||
if (value === null || value === undefined) continue;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
headers_cleaned[prop_cleaned] = value;
|
||||
}
|
||||
|
||||
// Auto-inject Authorization header if JWT is present but header is missing
|
||||
const jwt =
|
||||
headers_cleaned['jwt'] || headers_cleaned['JWT'] || api_cfg['jwt'];
|
||||
if (
|
||||
jwt &&
|
||||
!headers_cleaned['Authorization'] &&
|
||||
!headers_cleaned['authorization']
|
||||
) {
|
||||
headers_cleaned['Authorization'] = `Bearer ${jwt}`;
|
||||
}
|
||||
|
||||
headers_cleaned['Content-Type'] = 'application/json';
|
||||
console.log('*** delete_object() ***');
|
||||
|
||||
if (log_lvl) {
|
||||
// console.log(api_cfg);
|
||||
console.log(endpoint);
|
||||
console.log(params);
|
||||
if (log_lvl > 1) {
|
||||
console.log('Final cleaned headers:', headers_cleaned);
|
||||
console.log('Data:', data);
|
||||
console.log(typeof data);
|
||||
}
|
||||
// console.log(return_meta);
|
||||
// console.log(as_list);
|
||||
}
|
||||
|
||||
let fetch_method: any = fetch;
|
||||
if (api_cfg.fetch) {
|
||||
if (log_lvl > 1) {
|
||||
console.log('Using custom fetch function from api_cfg!!!');
|
||||
}
|
||||
fetch_method = api_cfg.fetch;
|
||||
// https://stackoverflow.com/questions/51069552/axios-delete-request-with-body-and-headers
|
||||
|
||||
let axios_api = axios.create({
|
||||
baseURL: api_cfg['base_url'],
|
||||
// timeout: 2000,
|
||||
/* other custom settings */
|
||||
});
|
||||
axios_api.defaults.headers = api_cfg['headers'];
|
||||
|
||||
//OLD: axios_api.delete(endpoint, { 'data': data })
|
||||
let response_data = await axios_api.delete(endpoint, { params: params, 'data': data })
|
||||
.then(function (response) {
|
||||
console.log(response.data);
|
||||
return response.data;
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error.response && error.response.status === 404) {
|
||||
return null; // Returning null since there were no results
|
||||
}
|
||||
console.log(error);
|
||||
return false; // Returning false since something may have gone wrong. Also more in line with what the API returns.
|
||||
// return error;
|
||||
});
|
||||
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
// Keep timeout handle at attempt scope so catch can always clear it.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
// AbortError alone is ambiguous. Track helper-timeout aborts so
|
||||
// caller/navigation aborts can still fail fast with no retry.
|
||||
let did_timeout_abort = false;
|
||||
timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.error(
|
||||
`API DELETE request timed out after ${timeout}ms.`
|
||||
);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'DELETE',
|
||||
headers: headers_cleaned,
|
||||
body:
|
||||
Object.keys(data).length > 0
|
||||
? JSON.stringify(data)
|
||||
: undefined,
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
const response = await fetch_method(
|
||||
url.toString(),
|
||||
fetchOptions
|
||||
).catch(function (error: any) {
|
||||
if (
|
||||
error?.name === 'AbortError' ||
|
||||
error?.name === 'TypeError' ||
|
||||
error?.message?.includes('aborted')
|
||||
) {
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'API DELETE: Request aborted or browser-terminated.',
|
||||
error
|
||||
);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'API DELETE Object *fetch* request was aborted or failed in an unexpected way.',
|
||||
error
|
||||
);
|
||||
return error;
|
||||
});
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
// Error object was returned from fetch catch block; decide retry class.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'AbortError' ||
|
||||
response.name === 'TypeError'))
|
||||
) {
|
||||
if (response.name === 'AbortError') {
|
||||
if (did_timeout_abort) {
|
||||
throw new Error(
|
||||
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Network error (attempt ${attempt}): ${response.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error(
|
||||
`HTTP fetch request was aborted or failed in an unexpected way! URL = ${url.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`Response: status=${response.status} attempt=${attempt}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
console.warn('404 Not Found. Returning null.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorBody = await response.text();
|
||||
console.error(
|
||||
`HTTP error! status: ${response.status}`,
|
||||
errorBody
|
||||
);
|
||||
|
||||
// Fail fast on client/auth/validation failures.
|
||||
if (
|
||||
response.status === 400 ||
|
||||
response.status === 401 ||
|
||||
response.status === 403 ||
|
||||
response.status === 422
|
||||
) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(
|
||||
`AUTH DIAGNOSTICS (DELETE): Headers sent for ${endpoint}:`,
|
||||
{
|
||||
has_api_key: !!headers_cleaned['x-aether-api-key'],
|
||||
has_account_id: !!headers_cleaned['x-account-id']
|
||||
}
|
||||
);
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status} - ${errorBody}`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log('Response JSON:', json);
|
||||
}
|
||||
|
||||
// Return the response data or metadata
|
||||
// Robustly handle V3 response envelopes
|
||||
return return_meta
|
||||
? json
|
||||
: json.data !== undefined
|
||||
? json.data
|
||||
: json;
|
||||
} catch (error) {
|
||||
// Ensure per-attempt timeout is always cleared on failure.
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
console.error(`API DELETE error on attempt ${attempt}:`, error);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
console.error('Max retry attempts reached. Returning false.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy.
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(
|
||||
`API DELETE: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
|
||||
);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
if (log_lvl > 1) {
|
||||
console.log(response_data);
|
||||
}
|
||||
return response_data;
|
||||
}
|
||||
|
||||
@@ -1,94 +1,172 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { key_val } from '$lib/ae_stores';
|
||||
import { get_object } from './api_get_object';
|
||||
|
||||
/**
|
||||
* Fetches a single Aether object by its ID using the CRUD endpoint.
|
||||
* Refactored 2026-01-08 to properly handle unauthenticated lookups (Bootstrap Paradox)
|
||||
* and ensure clean header passing to get_object without mutating the global config.
|
||||
*/
|
||||
export async function get_ae_obj_id_crud({
|
||||
api_cfg,
|
||||
no_account_id = false,
|
||||
obj_type,
|
||||
obj_id,
|
||||
use_alt_table = false,
|
||||
use_alt_base = false,
|
||||
inc = {},
|
||||
enabled = 'enabled',
|
||||
hidden = 'not_hidden',
|
||||
limit = 999999,
|
||||
offset = 0,
|
||||
data = {},
|
||||
headers = {},
|
||||
params = {},
|
||||
timeout = 60000,
|
||||
return_meta = false,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
api_cfg: any;
|
||||
no_account_id?: boolean;
|
||||
obj_type: string;
|
||||
obj_id: string;
|
||||
use_alt_table?: boolean;
|
||||
use_alt_base?: boolean;
|
||||
inc?: any;
|
||||
enabled?: 'enabled' | 'all' | 'not_enabled' | undefined;
|
||||
hidden?: 'hidden' | 'all' | 'not_hidden' | undefined;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
data?: any;
|
||||
headers?: any;
|
||||
params?: key_val;
|
||||
timeout?: number;
|
||||
return_meta?: boolean;
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
// Updated 2023-12-01
|
||||
export async function get_ae_obj_id_crud(
|
||||
{
|
||||
api_cfg,
|
||||
no_account_id = false,
|
||||
obj_type,
|
||||
obj_id,
|
||||
use_alt_table = false,
|
||||
use_alt_base = false,
|
||||
inc = {},
|
||||
enabled = 'enabled',
|
||||
hidden = 'not_hidden',
|
||||
limit = 999999,
|
||||
offset = 0,
|
||||
data = {},
|
||||
// key,
|
||||
// jwt = null,
|
||||
headers = {},
|
||||
params = {},
|
||||
timeout = 25000,
|
||||
return_meta = false,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
api_cfg: any,
|
||||
no_account_id?: boolean,
|
||||
obj_type: string,
|
||||
obj_id: string,
|
||||
use_alt_table?: boolean,
|
||||
use_alt_base?: boolean,
|
||||
inc?: any,
|
||||
enabled?: string,
|
||||
hidden?: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
data?: any,
|
||||
// key: string,
|
||||
// jwt?: string,
|
||||
headers?: any,
|
||||
params?: key_val,
|
||||
timeout?: number,
|
||||
return_meta?: boolean,
|
||||
log_lvl?: number
|
||||
}
|
||||
) {
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`*** get_ae_obj_id_crud() *** Type: ${obj_type} ID: ${obj_id}`
|
||||
);
|
||||
console.log('*** get_ae_obj_id_crud() ***');
|
||||
}
|
||||
|
||||
// V3 Standard: Unified endpoint for all objects
|
||||
const endpoint = `/v3/crud/${obj_type}/${obj_id}`;
|
||||
// data = {};
|
||||
// data['super_key'] = key;
|
||||
// data['jwt'] = jwt;
|
||||
// NOTE: The key and or JWT should be in the header of the DELETE, GET, PATCH, POST
|
||||
|
||||
if (log_lvl > 1) {
|
||||
let endpoint = '';
|
||||
if (obj_type == 'account') {
|
||||
endpoint = `/crud/account/${obj_id}`;
|
||||
} else if (obj_type == 'address') {
|
||||
endpoint = `/crud/address/${obj_id}`;
|
||||
} else if (obj_type == 'archive') {
|
||||
endpoint = `/crud/archive/${obj_id}`;
|
||||
} else if (obj_type == 'archive_content') {
|
||||
endpoint = `/crud/archive/content/${obj_id}`;
|
||||
} else if (obj_type == 'contact') {
|
||||
endpoint = `/crud/contact/${obj_id}`;
|
||||
} else if (obj_type == 'data_store') {
|
||||
endpoint = `/crud/data_store/${obj_id}`;
|
||||
} else if (obj_type == 'event') {
|
||||
endpoint = `/crud/event/${obj_id}`;
|
||||
} else if (obj_type == 'event_abstract') {
|
||||
endpoint = `/crud/event/abstract/${obj_id}`;
|
||||
} else if (obj_type == 'event_badge') {
|
||||
endpoint = `/crud/event/badge/${obj_id}`;
|
||||
} else if (obj_type == 'event_device') {
|
||||
endpoint = `/crud/event/device/${obj_id}`;
|
||||
} else if (obj_type == 'event_exhibit') {
|
||||
endpoint = `/crud/event/exhibit/${obj_id}`;
|
||||
} else if (obj_type == 'event_exhibit_tracking') {
|
||||
endpoint = `/crud/event/exhibit/tracking/${obj_id}`;
|
||||
} else if (obj_type == 'event_file') {
|
||||
endpoint = `/crud/event/file/${obj_id}`;
|
||||
} else if (obj_type == 'event_location') {
|
||||
endpoint = `/crud/event/location/${obj_id}`;
|
||||
} else if (obj_type == 'event_person') {
|
||||
endpoint = `/crud/event/person/${obj_id}`;
|
||||
} else if (obj_type == 'event_presentation') {
|
||||
endpoint = `/crud/event/presentation/${obj_id}`;
|
||||
} else if (obj_type == 'event_presenter') {
|
||||
endpoint = `/crud/event/presenter/${obj_id}`;
|
||||
} else if (obj_type == 'event_session') {
|
||||
endpoint = `/crud/event/session/${obj_id}`;
|
||||
} else if (obj_type == 'event_track') {
|
||||
endpoint = `/crud/event/track/${obj_id}`;
|
||||
} else if (obj_type == 'grant') {
|
||||
endpoint = `/crud/grant/${obj_id}`;
|
||||
} else if (obj_type == 'hosted_file') {
|
||||
endpoint = `/crud/hosted_file/${obj_id}`;
|
||||
} else if (obj_type == 'journal') {
|
||||
endpoint = `/crud/journal/${obj_id}`;
|
||||
} else if (obj_type == 'journal_entry') {
|
||||
endpoint = `/crud/journal/entry/${obj_id}`;
|
||||
} else if (obj_type == 'order') {
|
||||
endpoint = `/crud/order/${obj_id}`;
|
||||
} else if (obj_type == 'order_line') {
|
||||
endpoint = `/crud/order/line/${obj_id}`;
|
||||
} else if (obj_type == 'page') {
|
||||
endpoint = `/crud/page/${obj_id}`;
|
||||
} else if (obj_type == 'person') {
|
||||
endpoint = `/crud/person/${obj_id}`;
|
||||
} else if (obj_type == 'post') {
|
||||
endpoint = `/crud/post/${obj_id}`;
|
||||
} else if (obj_type == 'post_comment') {
|
||||
endpoint = `/crud/post/comment/${obj_id}`;
|
||||
} else if (obj_type == 'site') {
|
||||
endpoint = `/crud/site/${obj_id}`;
|
||||
} else if (obj_type == 'site_domain') {
|
||||
endpoint = `/crud/site/domain/${obj_id}`;
|
||||
} else if (obj_type == 'sponsorship_cfg') {
|
||||
endpoint = `/crud/sponsorship/cfg/${obj_id}`;
|
||||
} else if (obj_type == 'sponsorship') {
|
||||
endpoint = `/crud/sponsorship/${obj_id}`;
|
||||
// } else if (obj_type == 'user') {
|
||||
// endpoint = `/crud/user/${obj_id}`;
|
||||
} else {
|
||||
console.log(`Unknown object type: ${obj_type}`);
|
||||
return false;
|
||||
}
|
||||
if (log_lvl) {
|
||||
console.log('Endpoint:', endpoint);
|
||||
}
|
||||
|
||||
const final_params = {
|
||||
...params,
|
||||
use_alt_table: use_alt_table,
|
||||
use_alt_base: use_alt_base
|
||||
};
|
||||
params['use_alt_table'] = use_alt_table;
|
||||
params['use_alt_base'] = use_alt_base;
|
||||
|
||||
const final_headers = { ...headers };
|
||||
if (log_lvl) {
|
||||
console.log('Params:', params);
|
||||
}
|
||||
|
||||
if (no_account_id) {
|
||||
// This instructs get_object to skip account-id requirements
|
||||
final_headers['x-no-account-id'] = 'Nothing to See Here';
|
||||
final_headers['x-account-id'] = null; // Explicitly null to trigger removal in get_object
|
||||
headers['x-no-account-id'] = 'Nothing to See Here';
|
||||
delete headers['x-account-id'];
|
||||
delete api_cfg['headers']['x-account-id'];
|
||||
// headers['x-account-id'] = null;
|
||||
// headers['x-account-id'] = '_XY7DXtc9Mxx';
|
||||
// params['x_no_account_id_token'] = 'Nothing to See Here';
|
||||
|
||||
// Remove the x-account-id header
|
||||
// if (headers['x-account-id']) {
|
||||
// delete headers['x-account-id'];
|
||||
// }
|
||||
|
||||
// headers['x-account-id'] = null;
|
||||
// headers['x-no-account-id-token'] = 'Nothing to See Here'; // get_object() will fix the underscores to dashes
|
||||
}
|
||||
|
||||
const result = await get_object({
|
||||
let object_obj_get_promise = await get_object({
|
||||
api_cfg: api_cfg,
|
||||
endpoint: endpoint,
|
||||
headers: final_headers,
|
||||
params: final_params,
|
||||
headers: headers,
|
||||
params: params,
|
||||
timeout: timeout,
|
||||
log_lvl: log_lvl,
|
||||
return_meta: return_meta
|
||||
}).catch(function (error: any) {
|
||||
console.error(
|
||||
`API GET CRUD object ID request failed for ${obj_type}/${obj_id}`,
|
||||
error
|
||||
);
|
||||
return false;
|
||||
});
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log('GET Object result =', result);
|
||||
console.log(object_obj_get_promise);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return object_obj_get_promise;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user