Compare commits
518 Commits
929f08b656
...
ae_app_3x_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
246d4f8ef3 | ||
|
|
666b54bd36 | ||
|
|
89c1decf8d | ||
|
|
b9d70b616f | ||
|
|
e8a49562a9 | ||
|
|
e909c34874 | ||
|
|
48bc52899f | ||
|
|
6b122a065e | ||
|
|
1b81b8873c | ||
|
|
4f74cf1353 | ||
|
|
6dc6be9926 | ||
|
|
97c4c1cd6b | ||
|
|
b6481a3507 | ||
|
|
7b45b548e4 | ||
|
|
a1057fd776 | ||
|
|
d0286f7868 | ||
|
|
1c541cd090 | ||
|
|
868b4017f2 | ||
|
|
3122725610 | ||
|
|
6e04145514 | ||
|
|
b8ceed69d0 | ||
|
|
71aacb6346 | ||
|
|
04f3b82d59 | ||
|
|
cc04411d23 | ||
|
|
55f3e3a5a4 | ||
|
|
955d28d9c5 | ||
|
|
7c5cf53106 | ||
|
|
24b52b8027 | ||
|
|
6b3fb36926 | ||
|
|
5cad150b0a | ||
|
|
8a41f02f0d | ||
|
|
88ab5b27d4 | ||
|
|
b623557795 | ||
|
|
7d2b30b7ce | ||
|
|
c9b0acfa06 | ||
|
|
29a24812f4 | ||
|
|
70fda25c95 | ||
|
|
8f815b7033 | ||
|
|
ba4a0dc828 | ||
|
|
35c1324824 | ||
|
|
1a53a20995 | ||
|
|
04c2042060 | ||
|
|
4831f4b81b | ||
|
|
7bf76bf766 | ||
|
|
3ae5b30c37 | ||
|
|
b04202ecec | ||
|
|
84c4a2aa43 | ||
|
|
399f98ce8e | ||
|
|
f5ccd2e3cf | ||
|
|
94a3cb0644 | ||
|
|
9d904446d4 | ||
|
|
b45a27481a | ||
|
|
26ab5dda75 | ||
|
|
0511d9591f | ||
|
|
59fc7cabc6 | ||
|
|
41b352bc0a | ||
|
|
a4fed750fa | ||
|
|
74e0f752a6 | ||
|
|
be3e56eece | ||
|
|
e2d3c5a822 | ||
|
|
1b6aeb5b02 | ||
|
|
04018a27ed | ||
|
|
4292aebc56 | ||
|
|
3466d6552c | ||
|
|
b7969bc46e | ||
|
|
99b8eb0b5e | ||
|
|
9e361eae9b | ||
|
|
e735d0c213 | ||
|
|
d05cc63459 | ||
|
|
ac17417f3c | ||
|
|
3773758eb5 | ||
|
|
60bdd2fdba | ||
|
|
72c8f9b502 | ||
|
|
1de87b6c5f | ||
|
|
84a9d0fffc | ||
|
|
87084f0f71 | ||
|
|
de048a084b | ||
|
|
ee79e33a2a | ||
|
|
a5243fa820 | ||
|
|
5fce149808 | ||
|
|
a74effa6ff | ||
|
|
33d48e7e78 | ||
|
|
65e48c764e | ||
|
|
f6c950abdf | ||
|
|
3d6f9035c8 | ||
|
|
535efd9c4b | ||
|
|
4a39ca1468 | ||
|
|
182a066d38 | ||
|
|
35fed53e2a | ||
|
|
322abc2691 | ||
|
|
cfaf687717 | ||
|
|
e7620a1c06 | ||
|
|
e4e2174c97 | ||
|
|
d3bf314c62 | ||
|
|
d32355a1a2 | ||
|
|
213eabd8c1 | ||
|
|
872291b0a0 | ||
|
|
25d17841e4 | ||
|
|
6282fb167f | ||
|
|
a90572bcb8 | ||
|
|
194c89f6d1 | ||
|
|
469729ce22 | ||
|
|
d1f5d0e2fd | ||
|
|
9c83567430 | ||
|
|
b4d0d82141 | ||
|
|
15bfe6d5d6 | ||
|
|
dddf4b6170 | ||
|
|
587b815446 | ||
|
|
ca51a82dae | ||
|
|
a38320c7f5 | ||
|
|
c76fb8f2b5 | ||
|
|
a26ea8b49c | ||
|
|
21fad1a698 | ||
|
|
33e9eeef78 | ||
|
|
172ea994c7 | ||
|
|
17b549a75c | ||
|
|
3de01af1a1 | ||
|
|
518a450b91 | ||
|
|
cb767ed115 | ||
|
|
86201f0fc1 | ||
|
|
60e3fc539e | ||
|
|
b3029a4d27 | ||
|
|
ea765d8ad2 | ||
|
|
db5acdd30a | ||
|
|
a000e07647 | ||
|
|
7f9368589a | ||
|
|
55d3d49595 | ||
|
|
f5cf1ef398 | ||
|
|
d5d552a029 | ||
|
|
689bb326cb | ||
|
|
e6db2b4d6a | ||
|
|
cfc5d237c7 | ||
|
|
a56f520d4e | ||
|
|
c0f828ec2c | ||
|
|
5bb06c4904 | ||
|
|
7621e044b4 | ||
|
|
76569a872f | ||
|
|
d9726d062e | ||
|
|
91f40c4a89 | ||
|
|
e63a17865c | ||
|
|
a59e53aec5 | ||
|
|
6042095147 | ||
|
|
932deced12 | ||
|
|
861385b4ff | ||
|
|
42f40e990e | ||
|
|
3ea362c166 | ||
|
|
400312456b | ||
|
|
6755a68b13 | ||
|
|
71e79f032d | ||
|
|
53fd5e7de4 | ||
|
|
14e84884cd | ||
|
|
e921ca973f | ||
|
|
2855e091f7 | ||
|
|
f37c64c68b | ||
|
|
615af58a11 | ||
|
|
76e21b08ff | ||
|
|
4ada5c4a8f | ||
|
|
8850db89c6 | ||
|
|
ccacdc3f4b | ||
|
|
128944c7ab | ||
|
|
c0386f27bc | ||
|
|
ee506832e7 | ||
|
|
bbab9e7c8c | ||
|
|
daf1570781 | ||
|
|
c69e40829f | ||
|
|
429f38996a | ||
|
|
6857f1226c | ||
|
|
bb84117991 | ||
|
|
7a1099bbbe | ||
|
|
3a81887c56 | ||
|
|
730fb19d60 | ||
|
|
b32fb05138 | ||
|
|
12429ccf2e | ||
|
|
2d552b36fd | ||
|
|
3ed1a2a6c4 | ||
|
|
ab9e54d768 | ||
|
|
5bb2df1bd9 | ||
|
|
50c484a4cc | ||
|
|
26fde2a566 | ||
|
|
21a44f96fa | ||
|
|
631a77158c | ||
|
|
1296b1077e | ||
|
|
1fe2f6f4d2 | ||
|
|
73f97ee17b | ||
|
|
ad6b390fd9 | ||
|
|
f297c7c018 | ||
|
|
48a748d314 | ||
|
|
c4e2e64a7e | ||
|
|
76b4adef74 | ||
|
|
95a86b16fa | ||
|
|
042265008c | ||
|
|
a5f2ae3835 | ||
|
|
b3ce65f7f6 | ||
|
|
054775b0f8 | ||
|
|
af28fba263 | ||
|
|
17e522f826 | ||
|
|
324f3a97ac | ||
|
|
530853a78d | ||
|
|
453fcf581d | ||
|
|
530b53aa6d | ||
|
|
cc990084fb | ||
|
|
5fdb0d1d87 | ||
|
|
44cc538ce0 | ||
|
|
978a9a6960 | ||
|
|
82430649db | ||
|
|
cdcec259f7 | ||
|
|
e5883cd53c | ||
|
|
8d5c5e39c9 | ||
|
|
39749c608a | ||
|
|
4923099cfb | ||
|
|
36bd32f172 | ||
|
|
1374f0728e | ||
|
|
c79ae92be0 | ||
|
|
49c6a2351e | ||
|
|
b697126495 | ||
|
|
8dd22912c3 | ||
|
|
f8fe4ac5a2 | ||
|
|
2c1e9d294e | ||
|
|
768fdbfb21 | ||
|
|
e74dc7a388 | ||
|
|
a3f2f17480 | ||
|
|
6c73812187 | ||
|
|
ff824ebbe5 | ||
|
|
422c9c341c | ||
|
|
a3d229c803 | ||
|
|
f72454f379 | ||
|
|
c5c5292715 | ||
|
|
8ed7e0f8d7 | ||
|
|
68e5e01df1 | ||
|
|
611b1e6b51 | ||
|
|
1ef9080cda | ||
|
|
66c0be65c4 | ||
|
|
bdba092de0 | ||
|
|
0fa93d7ee5 | ||
|
|
847d653b5e | ||
|
|
cd01a87143 | ||
|
|
60ecd221b4 | ||
|
|
e3a3ab7de8 | ||
|
|
3553809f27 | ||
|
|
6e700e7b4d | ||
|
|
d7b4d8c37c | ||
|
|
d9f704fe25 | ||
|
|
af74b52481 | ||
|
|
730eea4ce7 | ||
|
|
09f1a6ee57 | ||
|
|
64c3afe564 | ||
|
|
54dfd734e6 | ||
|
|
c7ebeebe29 | ||
|
|
c71fc65be9 | ||
|
|
8b7597906f | ||
|
|
c289268550 | ||
|
|
09a5178b89 | ||
|
|
e64252b839 | ||
|
|
25e35f6f96 | ||
|
|
74bc3b3625 | ||
|
|
cd868460fe | ||
|
|
6ebf4f125d | ||
|
|
0ae8cf63d7 | ||
|
|
d32312653d | ||
|
|
f5155eba50 | ||
|
|
392217e66c | ||
|
|
7497bfb9f8 | ||
|
|
3ae9d0a884 | ||
|
|
409308d2be | ||
|
|
62cc26d1f9 | ||
|
|
8b087edeb9 | ||
|
|
54707a00e3 | ||
|
|
e5c8500bc1 | ||
|
|
07dd213cfd | ||
|
|
1c20038a55 | ||
|
|
d8616ea5fd | ||
|
|
0b04ce7c0c | ||
|
|
146682a30b | ||
|
|
20d8a6975d | ||
|
|
80957316f2 | ||
|
|
0d0cec9819 | ||
|
|
0705fa8de4 | ||
|
|
5846981c48 | ||
|
|
3ca0f0bad9 | ||
|
|
7486150aab | ||
|
|
c3a346cc9a | ||
|
|
7fd8c976bf | ||
|
|
9ed2d21757 | ||
|
|
38a752fbae | ||
|
|
285ef84b7e | ||
|
|
5cbdec3b5c | ||
|
|
8a23e7b7b3 | ||
|
|
cc5a6887c0 | ||
|
|
89c05cc323 | ||
|
|
0631937e18 | ||
|
|
20bf1d94eb | ||
|
|
878ff91c30 | ||
|
|
7cef6be54c | ||
|
|
19822c4eaf | ||
|
|
d5e5cb7ada | ||
|
|
e7b6045580 | ||
|
|
a1ebeddf9d | ||
|
|
2f5ad8ccc0 | ||
|
|
90adb19f5d | ||
|
|
7be60c2b8b | ||
|
|
bb6782cc32 | ||
|
|
51b7f267e9 | ||
|
|
de07fa0e0e | ||
|
|
b4f0ca3e64 | ||
|
|
6507fb82c0 | ||
|
|
d692d7cfde | ||
|
|
fdee7c16ca | ||
|
|
4d08994e79 | ||
|
|
bbdfe75866 | ||
|
|
56e23f3da0 | ||
|
|
4ae9ecc381 | ||
|
|
3fd6b33d6f | ||
|
|
e15a26f6c6 | ||
|
|
f8e34b10b8 | ||
|
|
29c5a9fa82 | ||
|
|
18cbe256de | ||
|
|
2b2324ee8a | ||
|
|
6c6fccdfb4 | ||
|
|
ef5188aa6d | ||
|
|
c4fdc8efa4 | ||
|
|
66310adb22 | ||
|
|
b94516ce26 | ||
|
|
b8e6bcaf03 | ||
|
|
dea599bd9c | ||
|
|
4d5081582f | ||
|
|
1381b81bf0 | ||
|
|
686b289bdb | ||
|
|
6d8f767e45 | ||
|
|
61c9a6766d | ||
|
|
ff4295b24c | ||
|
|
9d8c0e5dd4 | ||
|
|
236a5513ee | ||
|
|
868f4b3390 | ||
|
|
aebbcf5b47 | ||
|
|
9baffc4407 | ||
|
|
898afd9775 | ||
|
|
74e65ea892 | ||
|
|
1ad3d2030d | ||
|
|
721facf7ba | ||
|
|
a42b49dd50 | ||
|
|
278a40c981 | ||
|
|
5fcf2e86f1 | ||
|
|
7543bf6ae5 | ||
|
|
9af5a292b6 | ||
|
|
2595664dd1 | ||
|
|
e4265f69af | ||
|
|
1df17e68bb | ||
|
|
deea250a85 | ||
|
|
4d43fd8a67 | ||
|
|
9180384ec1 | ||
|
|
75f755c660 | ||
|
|
4780be7a00 | ||
|
|
5d6cc4ca35 | ||
|
|
a68b85e1f1 | ||
|
|
0199c2e2c9 | ||
|
|
126eb77be2 | ||
|
|
7733ef8708 | ||
|
|
a81d65ce7e | ||
|
|
3d81cb5a83 | ||
|
|
345641e4c4 | ||
|
|
71c615bf4a | ||
|
|
81aa0eefcd | ||
|
|
430d39231d | ||
|
|
5203104fef | ||
|
|
bf31f13650 | ||
|
|
7bc7bf5554 | ||
|
|
6aeaef6f1d | ||
|
|
ae00ddffb0 | ||
|
|
8d430a9c31 | ||
|
|
f6051156cf | ||
|
|
d64222ca91 | ||
|
|
acf0a13955 | ||
|
|
5826b21821 | ||
|
|
ad3b27b747 | ||
|
|
15566efec1 | ||
|
|
5e07f2822c | ||
|
|
d35a28f912 | ||
|
|
2e01e7f115 | ||
|
|
f7ddcaa448 | ||
|
|
940e25d549 | ||
|
|
22d62ba3b1 | ||
|
|
c7fa75afc7 | ||
|
|
cfdec1e305 | ||
|
|
bfe02727bf | ||
|
|
e542c55500 | ||
|
|
c9e2284758 | ||
|
|
941ad6ae88 | ||
|
|
d05420d9c1 | ||
|
|
76c28a7e22 | ||
|
|
a84ea4cbcb | ||
|
|
dd4c558d1b | ||
|
|
d7b49efdde | ||
|
|
fec08fdfbf | ||
|
|
32ed4e47a8 | ||
|
|
534bda9203 | ||
|
|
8aef519aa6 | ||
|
|
dd339a7280 | ||
|
|
3659fef17c | ||
|
|
d5b2b557f3 | ||
|
|
0aa32a5293 | ||
|
|
ef5c807c27 | ||
|
|
b02843e467 | ||
|
|
56b4e5c627 | ||
|
|
b64db756ad | ||
|
|
590139e63a | ||
|
|
372d79df2b | ||
|
|
c979454d84 | ||
|
|
8d30e01ad4 | ||
|
|
f2765d6a5e | ||
|
|
ef45a0ca0f | ||
|
|
b01478a87f | ||
|
|
f34074cdd6 | ||
|
|
ae9cdaf9f1 | ||
|
|
be0b8baf62 | ||
|
|
93fea0d165 | ||
|
|
988ba75df3 | ||
|
|
34bf823987 | ||
|
|
1e178c14e7 | ||
|
|
50e83502ff | ||
|
|
10e9206ca4 | ||
|
|
f95243a9c7 | ||
|
|
d340bbbe94 | ||
|
|
a952c5ddbe | ||
|
|
7f79c1857a | ||
|
|
48c5515131 | ||
|
|
c8eb904eb0 | ||
|
|
d80202e35b | ||
|
|
055bbd9ffd | ||
|
|
0e0fc071c7 | ||
|
|
5971ca6143 | ||
|
|
cf7203daaf | ||
|
|
0ca2408111 | ||
|
|
62ae376e67 | ||
|
|
034e25d6c4 | ||
|
|
08fdb2bddf | ||
|
|
84875d1daa | ||
|
|
09757d249c | ||
|
|
fae4bba037 | ||
|
|
7b2694e9b7 | ||
|
|
e27ff2c67f | ||
|
|
c198ca2454 | ||
|
|
0ab8b936ce | ||
|
|
4a5b4bf7cd | ||
|
|
1935564645 | ||
|
|
fface58751 | ||
|
|
4a1b0dac86 | ||
|
|
fd9e5f6dc0 | ||
|
|
21f0fe69af | ||
|
|
01c895f7ba | ||
|
|
3a4c4a1e64 | ||
|
|
75664ad2e1 | ||
|
|
2a5adda6cb | ||
|
|
be3634d750 | ||
|
|
fd5d5e371b | ||
|
|
75d85bf904 | ||
|
|
5e0f35d3df | ||
|
|
0767e2ff82 | ||
|
|
38c5345060 | ||
|
|
601bcf94b0 | ||
|
|
197d136c59 | ||
|
|
7d8981bcb5 | ||
|
|
828a2a0b10 | ||
|
|
665eb48280 | ||
|
|
d12a4bf71f | ||
|
|
214fca3713 | ||
|
|
802d0ec368 | ||
|
|
113aae23a7 | ||
|
|
62e1115b05 | ||
|
|
63ec7f4cc2 | ||
|
|
8fabaf28f7 | ||
|
|
f1bce485ab | ||
|
|
58dbb68601 | ||
|
|
9b0c05b80c | ||
|
|
ae4b94f1b2 | ||
|
|
e6daf6b503 | ||
|
|
84dc3dd158 | ||
|
|
aa5ba8c9c6 | ||
|
|
c53a993bab | ||
|
|
d8ce04304b | ||
|
|
525ce1db79 | ||
|
|
6559e3393c | ||
|
|
702a7a73de | ||
|
|
847d89054d | ||
|
|
0d49ff3b8d | ||
|
|
0e9a26cdca | ||
|
|
83e271a323 | ||
|
|
ace90ad043 | ||
|
|
d139ed1bd0 | ||
|
|
3d988222a1 | ||
|
|
5433a906bb | ||
|
|
d89218be15 | ||
|
|
a8e9bd6694 | ||
|
|
6cd3b5f8f9 | ||
|
|
b33c1b16f6 | ||
|
|
d7a0857bed | ||
|
|
6939c058d8 | ||
|
|
b88a7de358 | ||
|
|
27f0bd21fb | ||
|
|
f111670f60 | ||
|
|
045efa71e1 | ||
|
|
1e2c9d9b74 | ||
|
|
e64001cf63 | ||
|
|
4137d8677d | ||
|
|
19d0145d00 | ||
|
|
9d44b9341c | ||
|
|
bc67ff5798 | ||
|
|
f87ab10251 | ||
|
|
35c4341c34 | ||
|
|
bd5759f037 | ||
|
|
872e381c00 | ||
|
|
64402e8e2a | ||
|
|
88b11b8318 | ||
|
|
65e0477761 | ||
|
|
98736ae1bc | ||
|
|
7308a4773d | ||
|
|
99541f0f9d | ||
|
|
f950c22a59 | ||
|
|
b63f8eed0c |
15
.ae_brief
15
.ae_brief
@@ -1,22 +1,15 @@
|
|||||||
# Aether Project Brief: aether_app_sveltekit
|
# Aether Project Brief: aether_app_sveltekit
|
||||||
**Last Updated:** 2026-02-09 22:03:56
|
**Last Updated:** 2026-05-21 22:25:05
|
||||||
**Current Agent:** mcp_agent
|
**Current Agent:** mcp_agent
|
||||||
|
|
||||||
## 🛠️ What I Just Did
|
## 🛠️ What I Just Did
|
||||||
Addressed multiple Svelte compiler warnings:
|
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.
|
||||||
1. Converted ~30 decorative labels to spans (a11y).
|
|
||||||
2. Applied Svelte 5 untrack() pattern to initial state from props.
|
|
||||||
3. Fixed CSS scoping for TipTap editor.
|
|
||||||
4. Added rel="noopener noreferrer" to external links.
|
|
||||||
5. Commited changes in two atomic batches.
|
|
||||||
|
|
||||||
## 🚧 Current Blockers
|
## 🚧 Current Blockers
|
||||||
None. Remaining svelte-check warnings (219) require more granular ID/for linking in complex forms.
|
None.
|
||||||
|
|
||||||
## ➡️ Exact Next Steps
|
## ➡️ Exact Next Steps
|
||||||
1. Granular fix for remaining 68 label/ID associations in address/person forms.
|
User to review changes. Ready for onsite testing/deployment.
|
||||||
2. Systematic application of untrack() for remaining state-from-props warnings.
|
|
||||||
3. Clean up unused TipTap CSS selectors identified by svelte-check.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
*Generated by ae_brief*
|
*Generated by ae_brief*
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
.svelte-kit
|
# Build artifacts and local state
|
||||||
.vite
|
.svelte-kit/
|
||||||
node_modules
|
.vite/
|
||||||
dist
|
node_modules/
|
||||||
build
|
build/
|
||||||
.cache
|
dist/
|
||||||
.DS_Store
|
.cache/
|
||||||
.vscode
|
|
||||||
.idea
|
# VCS and IDE
|
||||||
.git
|
.git/
|
||||||
.gitignore
|
.gitignore
|
||||||
coverage
|
.vscode/
|
||||||
tests
|
.idea/
|
||||||
documentation
|
|
||||||
backups
|
# OS junk
|
||||||
|
.DS_Store
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Logs and temp files
|
||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
*.tgz
|
*.tgz
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
npm-debug.log
|
|
||||||
package-lock.json.bak
|
|
||||||
yarn-error.log
|
|
||||||
/.cache
|
|
||||||
/.parcel-cachenode_modules/
|
|
||||||
build/
|
|
||||||
.svelte-kit/
|
|
||||||
.git/
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.staging
|
|
||||||
!.env.prod
|
|
||||||
npm_deploy/
|
|
||||||
test-results/
|
|
||||||
test_results/
|
|
||||||
documentation/
|
|
||||||
backups/
|
|
||||||
*.log
|
|
||||||
*.bak
|
|
||||||
.claude/
|
|
||||||
.vscode/
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.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
|
||||||
|
|||||||
19
.env.dev.default
Normal file
19
.env.dev.default
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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,9 +1,5 @@
|
|||||||
# One Sky IT's Aether Framework and System — PRODUCTION template
|
# One Sky IT's Aether Framework and System — PROD template (api.oneskyit.com)
|
||||||
# Copy to .env.prod and fill in real values.
|
# Copy to .env.prod and fill in real values.
|
||||||
# AE_CFG_ID: 1=Default, 5=Home Dev, 7=Live Testing/Prod
|
|
||||||
|
|
||||||
# Shared config record (controls SMTP, API routing, external keys from DB)
|
|
||||||
AE_CFG_ID=7
|
|
||||||
|
|
||||||
# Aether API access
|
# Aether API access
|
||||||
PUBLIC_AE_API_PROTOCOL=https
|
PUBLIC_AE_API_PROTOCOL=https
|
||||||
@@ -13,16 +9,10 @@ PUBLIC_AE_API_PORT=443
|
|||||||
PUBLIC_AE_API_PATH=
|
PUBLIC_AE_API_PATH=
|
||||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
# 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).
|
# Separate from the main API key — has limited permissions (no account_id required).
|
||||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
|
|
||||||
|
|
||||||
# SvelteKit app config
|
|
||||||
AE_APP_NODE_PORT=3001
|
|
||||||
|
|
||||||
# Default demo/client context (set to the target account for this deployment)
|
|
||||||
PUBLIC_AE_ACCOUNT_ID=XXXX
|
|
||||||
PUBLIC_AE_EVENT_ID=XXXX
|
|
||||||
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# One Sky IT's Aether Framework and System — STAGING / HOME DEV template
|
|
||||||
# Copy to .env.staging and fill in real values.
|
|
||||||
# AE_CFG_ID: 1=Default, 5=Home Dev, 7=Live Testing/Prod
|
|
||||||
|
|
||||||
# Shared config record (controls SMTP, API routing, external keys from DB)
|
|
||||||
AE_CFG_ID=5
|
|
||||||
|
|
||||||
# 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
|
|
||||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
|
|
||||||
|
|
||||||
# SvelteKit app config
|
|
||||||
AE_APP_NODE_PORT=3001
|
|
||||||
|
|
||||||
# Default demo/client context (set to the target account for this deployment)
|
|
||||||
PUBLIC_AE_ACCOUNT_ID=XXXX # OSIT = _XY7DXtc9MY
|
|
||||||
PUBLIC_AE_EVENT_ID=XXXX # OSIT = pjrcghqwert
|
|
||||||
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX
|
|
||||||
18
.env.test.default
Normal file
18
.env.test.default
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,7 +8,8 @@ node_modules
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
!.env.prod.default
|
!.env.prod.default
|
||||||
!.env.staging.default
|
!.env.test.default
|
||||||
|
!.env.dev.default
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
1. **Before starting:** Read `documentation/TODO__Agents.md` for active tasks
|
1. **Before starting:** Read `documentation/TODO__Agents.md` for active tasks
|
||||||
2. **Before committing:** Run `npx svelte-check` — no exceptions
|
2. **Before committing:** Run `npx svelte-check` — no exceptions
|
||||||
3. **Commits:** Atomic — one component or fix per commit
|
3. **Commits:** Atomic — one component or fix per commit
|
||||||
4. **Never delete files with `rm`** — move to `~/tmp/gemini_trash`
|
4. **Never delete files with `rm`** — move to `~/tmp/agents_trash`
|
||||||
5. **Backend coordination:** Use `ae_send_message` or flag changes clearly
|
5. **Backend coordination:** Use `ae_send_message` or flag changes clearly
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
24
Dockerfile
24
Dockerfile
@@ -9,18 +9,32 @@ RUN npm install
|
|||||||
# Copy the rest of the source code.
|
# Copy the rest of the source code.
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build Argument to determine build environment (staging, prod, or production).
|
# Build Argument to determine build environment (dev, test, prod).
|
||||||
ARG BUILD_MODE=staging
|
ARG BUILD_MODE=dev
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Sync the SvelteKit project to generate ./.svelte-kit/tsconfig.json
|
# Sync the SvelteKit project to generate ./.svelte-kit/tsconfig.json
|
||||||
RUN npx svelte-kit sync
|
RUN npx svelte-kit sync
|
||||||
|
|
||||||
# Perform the build based on the BUILD_MODE argument.
|
# 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 \
|
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
|
||||||
npm run build:prod; \
|
npm run build:prod; \
|
||||||
|
elif [ "$BUILD_MODE" = "test" ]; then \
|
||||||
|
npm run build:test; \
|
||||||
else \
|
else \
|
||||||
npm run build:staging; \
|
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
|
fi
|
||||||
|
|
||||||
# Stage 2: Final runtime image
|
# Stage 2: Final runtime image
|
||||||
@@ -35,8 +49,8 @@ COPY --from=builder /app/package-lock.json .
|
|||||||
# Install only production dependencies.
|
# Install only production dependencies.
|
||||||
RUN npm install --omit=dev
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
# Copy the resulting .env.production file to .env.
|
# Copy the runtime env file (non-PUBLIC vars for the Node server).
|
||||||
COPY --from=builder /app/.env.production .env
|
COPY --from=builder /app/.env.runtime .env
|
||||||
|
|
||||||
# SvelteKit (via adapter-node) defaults to port 3000.
|
# SvelteKit (via adapter-node) defaults to port 3000.
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
89
README.md
89
README.md
@@ -123,32 +123,72 @@ Developer sandbox pages — not for production use.
|
|||||||
|
|
||||||
# How to build and deploy SvelteKit:
|
# How to build and deploy SvelteKit:
|
||||||
|
|
||||||
The deployment is now fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built directly from source inside a clean Docker container, ensuring consistent environment handling across staging and production.
|
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.
|
||||||
|
|
||||||
### Deployment Commands
|
## Environments
|
||||||
|
|
||||||
Run these commands from the root of the `aether_app_sveltekit` project:
|
| 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/`)
|
||||||
|
|
||||||
#### 1. Deploy to Staging (Dev Workstation)
|
|
||||||
This triggers an autonomous build inside the Docker container using your `.env.staging` file and restarts the service.
|
|
||||||
```bash
|
```bash
|
||||||
npm run deploy:staging
|
# Active development — Vite HMR, no Docker
|
||||||
```
|
npm run dev
|
||||||
|
|
||||||
#### 2. Deploy to Production
|
# Build Vite output only (no Docker)
|
||||||
This builds the image using production flags (using `.env.prod`) and restarts the production container.
|
npm run build:dev
|
||||||
```bash
|
npm run build:test
|
||||||
npm run deploy:prod
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Technical Details
|
### Technical Details
|
||||||
|
|
||||||
- **Unified Orchestration**: All services (API, UI, Redis) are managed via `~/OSIT_dev/aether_container_env/docker-compose.yml`.
|
- **Unified Orchestration**: All services (API, UI, Redis) are managed via `~/OSIT_dev/aether_container_env/docker-compose.yml`.
|
||||||
- **Dockerfile**: Uses a multi-stage build. Stage 1 (builder) installs dependencies and builds the app using the `BUILD_MODE` argument passed from Docker Compose. Stage 2 (runtime) creates the final lightweight image.
|
- **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**:
|
- **Environment Handling**:
|
||||||
- `PUBLIC_` variables are baked into the image during the build step.
|
- `PUBLIC_` variables are baked into the image at build time via the `.env.<mode>` file.
|
||||||
- Private runtime variables are passed via the orchestration's `.env` file.
|
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
|
||||||
- **Networking**: The frontend communicates with the backend via the high-speed internal Docker network (`http://ae_api:5005`).
|
- **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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -229,8 +269,9 @@ npm install @tiptap/extension-link @tiptap/extension-bullet-list @tiptap/extensi
|
|||||||
|
|
||||||
The application uses standard SvelteKit `.env` files for build-time configuration (specifically for `PUBLIC_` prefixed variables).
|
The application uses standard SvelteKit `.env` files for build-time configuration (specifically for `PUBLIC_` prefixed variables).
|
||||||
|
|
||||||
- **`.env.staging`**: Used by `npm run deploy:staging`.
|
- **`.env.dev`**: Used by `npm run build:docker:dev` and `npm run build:dev`.
|
||||||
- **`.env.prod`**: Used by `npm run deploy:prod`.
|
- **`.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`).
|
- **`.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.
|
**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.
|
||||||
@@ -248,16 +289,16 @@ npm run dev
|
|||||||
npm run dev -- --open
|
npm run dev -- --open
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deployment (Production/Staging)
|
## Deployment
|
||||||
|
|
||||||
To create a production or staging version and deploy it to the local containers:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# For Staging/Dev
|
# Build Docker image locally and restart container
|
||||||
npm run deploy:staging
|
npm run build:docker:dev
|
||||||
|
npm run build:docker:prod
|
||||||
|
|
||||||
# For Production
|
# Deploy to remote server (linode.oneskyit.com)
|
||||||
npm run deploy:prod
|
npm run deploy:remote:test
|
||||||
|
npm run deploy:remote:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
These commands use the multi-stage **Dockerfile** to build the app in a clean environment and automatically restart the corresponding Docker containers.
|
These commands use the multi-stage **Dockerfile** to build the app in a clean environment and automatically restart the corresponding Docker containers.
|
||||||
|
|||||||
@@ -7,12 +7,17 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"autofetch",
|
"autofetch",
|
||||||
|
"Axonius",
|
||||||
"displayplacer",
|
"displayplacer",
|
||||||
|
"elif",
|
||||||
"filelist",
|
"filelist",
|
||||||
"gsettings",
|
"gsettings",
|
||||||
"onsave"
|
"onsave"
|
||||||
],
|
],
|
||||||
"git.autofetch": true,
|
"git.autofetch": true,
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"chat.tools.terminal.autoApprove": {
|
||||||
|
"npx svelte-check": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
conductor/fix-idaa-breakout-links.md
Normal file
33
conductor/fix-idaa-breakout-links.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 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.
|
||||||
53
conductor/launcher-ux-refinement.md
Normal file
53
conductor/launcher-ux-refinement.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# 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.
|
||||||
@@ -1211,6 +1211,8 @@ This document provides a reference for the data structures of the core Aether AP
|
|||||||
|
|
||||||
**Source Model:** `Journal_Entry_Base` in `models/journal_entry_models.py`
|
**Source Model:** `Journal_Entry_Base` in `models/journal_entry_models.py`
|
||||||
|
|
||||||
|
**UI note:** The current Journals Entry Config modal treats `summary` as metadata, keeps `alert` and `alert_msg` as separate fields, and uses `priority` as a boolean flag while `sort` remains the numeric ordering field.
|
||||||
|
|
||||||
- `id_random`: `Optional[str]`
|
- `id_random`: `Optional[str]`
|
||||||
- `id`: `Optional[int]`
|
- `id`: `Optional[int]`
|
||||||
- `journal_id_random`: `Optional[str]`
|
- `journal_id_random`: `Optional[str]`
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ The Aether project is a Svelte and SvelteKit based application, utilizing Tailwi
|
|||||||
- **Markdown Parsing:** `marked` library
|
- **Markdown Parsing:** `marked` library
|
||||||
- **State Management:** Svelte stores, potentially with `liveQuery` from Dexie for reactive IndexedDB interactions.
|
- **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
|
## 3. Module Structure
|
||||||
|
|
||||||
The Aether project is organized into several modules, categorized as Core, Extended, and Custom.
|
The Aether project is organized into several modules, categorized as Core, Extended, and Custom.
|
||||||
@@ -77,7 +86,7 @@ Used for more structured client-side data storage, often for caching and offline
|
|||||||
|
|
||||||
Standardized sorting orders are applied across various data lists.
|
Standardized sorting orders are applied across various data lists.
|
||||||
|
|
||||||
- **Default/General:** `group > priority > sort > updated_on/created_on`
|
- **Default/General:** `group > priority (flag) > sort > updated_on/created_on`
|
||||||
- **Specific (e.g., Events):** `type > start_date/time > code or name`
|
- **Specific (e.g., Events):** `type > start_date/time > code or name`
|
||||||
|
|
||||||
## 6. Object Properties and Fields
|
## 6. Object Properties and Fields
|
||||||
@@ -95,8 +104,8 @@ These fields are expected to be present in most Aether objects.
|
|||||||
- `name`: Display name.
|
- `name`: Display name.
|
||||||
- `enable`: Boolean for active/inactive status.
|
- `enable`: Boolean for active/inactive status.
|
||||||
- `hide`: Boolean for visibility.
|
- `hide`: Boolean for visibility.
|
||||||
- `priority`: Numeric value for ordering.
|
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||||
- `sort`: Numeric value for ordering.
|
- `sort`: Numeric value for ordering within a priority group.
|
||||||
- `group`: Categorization string.
|
- `group`: Categorization string.
|
||||||
- `notes`: General notes/comments.
|
- `notes`: General notes/comments.
|
||||||
- `created_on`: Timestamp of creation.
|
- `created_on`: Timestamp of creation.
|
||||||
@@ -153,7 +162,7 @@ The Electron app (`aether_app_native_electron/`) exists **solely** to support th
|
|||||||
- Hardware telemetry for connected devices
|
- Hardware telemetry for connected devices
|
||||||
|
|
||||||
**What Electron is NOT used for:**
|
**What Electron is NOT used for:**
|
||||||
- Badge printing (browser works fine)
|
- Badge printing (browser works well)
|
||||||
- Any other Aether module
|
- Any other Aether module
|
||||||
- Any general-purpose Aether functionality
|
- Any general-purpose Aether functionality
|
||||||
|
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ A standardized menu for interacting with objects.
|
|||||||
- **Actions:** `create`, `view`, `edit`, `update`, `hide`, `disable`, `delete`, `alert` (message), `archive` (not yet ready).
|
- **Actions:** `create`, `view`, `edit`, `update`, `hide`, `disable`, `delete`, `alert` (message), `archive` (not yet ready).
|
||||||
- **Future Actions:** `copy`, `import`.
|
- **Future Actions:** `copy`, `import`.
|
||||||
- **Sort Options:**
|
- **Sort Options:**
|
||||||
- `[default]`: `group > priority > sort (ASC/DESC) > alert > name`
|
- `[default]`: `group > priority (flag) > sort (ASC/DESC) > alert > name`
|
||||||
- `[sort_updated]`: `group > priority > sort (ASC/DESC) > alert > updated_on > created_on`
|
- `[sort_updated]`: `group > priority (flag) > sort (ASC/DESC) > alert > updated_on > created_on`
|
||||||
- `[priority_updated]`: `group > priority > updated_on (ASC/DESC) > created_on`
|
- `[priority_updated]`: `group > priority (flag) > updated_on (ASC/DESC) > created_on`
|
||||||
- `[priority_name]`: `group > priority > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
- `[priority_name]`: `group > priority (flag) > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||||
- `[name]`: `priority > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
- `[name]`: `priority (flag) > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||||
- `[created_on]`: `priority > created_on (ASC/DESC)`
|
- `[created_on]`: `priority (flag) > created_on (ASC/DESC)`
|
||||||
- `[updated_on]`: `priority > updated_on (ASC/DESC) > created_on`
|
- `[updated_on]`: `priority (flag) > updated_on (ASC/DESC) > created_on`
|
||||||
|
|
||||||
## 2. Pop-ups
|
## 2. Pop-ups
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,31 @@ This document outlines the key data structures and their properties used within
|
|||||||
|
|
||||||
These fields are expected to be present in most Aether objects, providing a consistent base structure.
|
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 a UUID).
|
- `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).
|
- `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`).
|
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
|
||||||
- `code`: Short, unique identifier.
|
- `code`: Short, unique identifier.
|
||||||
- `name`: Display name.
|
- `name`: Display name.
|
||||||
- `enable`: Boolean for active/inactive status.
|
- `enable`: Boolean for active/inactive status.
|
||||||
- `hide`: Boolean for visibility.
|
- `hide`: Boolean for visibility.
|
||||||
- `priority`: Numeric value for ordering.
|
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||||
- `sort`: Numeric value for ordering.
|
- `sort`: Numeric value for ordering within a priority group.
|
||||||
- `group`: Categorization string.
|
- `group`: Categorization string.
|
||||||
- `notes`: General notes/comments.
|
- `notes`: General notes/comments.
|
||||||
- `created_on`: Timestamp of creation.
|
- `created_on`: Timestamp of creation.
|
||||||
- `updated_on`: Timestamp of last update.
|
- `updated_on`: Timestamp of last update.
|
||||||
|
|
||||||
### 1.2. Special Use Fields
|
### 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.
|
Fields with specific purposes or conditional usage across different object types.
|
||||||
|
|
||||||
@@ -32,7 +42,7 @@ Fields with specific purposes or conditional usage across different object types
|
|||||||
- `passcode`: A password or access code associated with an object.
|
- `passcode`: A password or access code associated with an object.
|
||||||
- `external_id`: An identifier from an external system.
|
- `external_id`: An identifier from an external system.
|
||||||
|
|
||||||
### 1.3. Configuration and JSON Fields
|
### 1.4. Configuration and JSON Fields
|
||||||
|
|
||||||
Fields designed to store structured data in JSON format.
|
Fields designed to store structured data in JSON format.
|
||||||
|
|
||||||
@@ -40,7 +50,7 @@ Fields designed to store structured data in JSON format.
|
|||||||
- `data_json`: General purpose 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.
|
- `linked_li_json`: A list of linked items, stored as a JSON string.
|
||||||
|
|
||||||
### 1.4. Special Generated Fields (Client-side)
|
### 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.
|
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.
|
||||||
|
|
||||||
@@ -48,7 +58,7 @@ These fields are generated on the client-side, primarily for facilitating UI log
|
|||||||
- `tmp_sort_2`: Temporary sort field 2.
|
- `tmp_sort_2`: Temporary sort field 2.
|
||||||
- `tmp_sort_3`: Temporary sort field 3.
|
- `tmp_sort_3`: Temporary sort field 3.
|
||||||
|
|
||||||
### 1.5. Future Standard Fields
|
### 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.
|
A list of potential future standard fields, often prefixed with `obj_`. These are conceptual and represent planned expansions to the data model.
|
||||||
|
|
||||||
@@ -58,7 +68,7 @@ A list of potential future standard fields, often prefixed with `obj_`. These ar
|
|||||||
|
|
||||||
Standardized sorting orders are applied across various data lists to ensure consistent presentation.
|
Standardized sorting orders are applied across various data lists to ensure consistent presentation.
|
||||||
|
|
||||||
- **Default/General Sorting:** `group > priority > sort > updated_on/created_on`
|
- **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`
|
- **Specific Sorting (e.g., for time-based events):** `type > start_date/time > code or name`
|
||||||
|
|
||||||
## 3. Data Storage Mechanisms
|
## 3. Data Storage Mechanisms
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
- `description`: Longer text description.
|
- `description`: Longer text description.
|
||||||
- `enable`: Boolean for active/inactive status.
|
- `enable`: Boolean for active/inactive status.
|
||||||
- `hide`: Boolean for visibility.
|
- `hide`: Boolean for visibility.
|
||||||
- `priority`: Numeric value for ordering.
|
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||||
- `sort`: Numeric value for ordering.
|
- `sort`: Numeric value for ordering within a priority group.
|
||||||
- `group`: Categorization string.
|
- `group`: Categorization string.
|
||||||
- `notes`: General notes/comments.
|
- `notes`: General notes/comments.
|
||||||
- `created_on`: Timestamp of creation.
|
- `created_on`: Timestamp of creation.
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
## 9. Data Sorting
|
## 9. Data Sorting
|
||||||
|
|
||||||
- **Standard Order:** `group > priority > sort > updated_on/created_on`
|
- **Standard Order:** `group > priority (flag) > sort > updated_on/created_on`
|
||||||
- **Specific Order:** `type > start_date/time > code or name`
|
- **Specific Order:** `type > start_date/time > code or name`
|
||||||
|
|
||||||
## 10. Local Storage and IndexedDB Keys
|
## 10. Local Storage and IndexedDB Keys
|
||||||
|
|||||||
@@ -86,6 +86,18 @@ site_access_code_kv: {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `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
|
## Utility Functions
|
||||||
@@ -113,6 +125,37 @@ Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for thre
|
|||||||
- IDAA users authenticate via Novi UUID at `authenticated` level or higher.
|
- 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.
|
- 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
|
### Journals
|
||||||
- Private personal data. Always authenticated. Passcode/encryption features exist.
|
- Private personal data. Always authenticated. Passcode/encryption features exist.
|
||||||
- Never expose journal content publicly.
|
- Never expose journal content publicly.
|
||||||
@@ -123,6 +166,11 @@ Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for thre
|
|||||||
- Security model: API key is one layer; JWT + `x-account-id` scoping provides the primary auth.
|
- 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.
|
- 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
|
### Email Display
|
||||||
Non-trusted users must never see a full email address. Obscure using:
|
Non-trusted users must never see a full email address. Obscure using:
|
||||||
```typescript
|
```typescript
|
||||||
@@ -139,6 +187,12 @@ This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if n
|
|||||||
|
|
||||||
## Module-Specific Permission Patterns
|
## 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
|
### Events — Badges
|
||||||
|
|
||||||
| Scenario | Visibility | Print Action | Review Actions |
|
| Scenario | Visibility | Print Action | Review Actions |
|
||||||
|
|||||||
569
documentation/AE__UI_UX_future_ideas.md
Normal file
569
documentation/AE__UI_UX_future_ideas.md
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
527
documentation/BOOTSTRAP__AI_Agent_Quickstart.md
Normal file
527
documentation/BOOTSTRAP__AI_Agent_Quickstart.md
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
# 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` |
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
**Client:** International Doctors in Alcoholics Anonymous (IDAA)
|
**Client:** International Doctors in Alcoholics Anonymous (IDAA)
|
||||||
**Module Path:** `src/routes/idaa/`
|
**Module Path:** `src/routes/idaa/`
|
||||||
**State Stores:** `src/lib/stores/ae_idaa_stores.ts`
|
**State Stores:** `src/lib/stores/ae_idaa_stores.ts`
|
||||||
**Last Updated:** 2026-03-09 (Novi UUID verification upgrade)
|
**Last Updated:** 2026-05-18 (Default limit and stepper update)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,6 +31,17 @@ IDAA is a private membership organization for physicians in recovery. They use t
|
|||||||
|
|
||||||
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.
|
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
|
## Architecture: Composite Module
|
||||||
@@ -85,8 +96,8 @@ src/routes/idaa/
|
|||||||
│ │ └── [event_id]/
|
│ │ └── [event_id]/
|
||||||
│ │ ├── +page.svelte # Meeting detail page — renders view OR edit based on session flag
|
│ │ ├── +page.svelte # Meeting detail page — renders view OR edit based on session flag
|
||||||
│ │ └── +page.ts
|
│ │ └── +page.ts
|
||||||
│ └── video_conferences/ # Jitsi video conference integration
|
│ ├── video_conferences/ # Jitsi video conference integration
|
||||||
└── jitsi_reports/ # External Jitsi reporting
|
│ └── jitsi_reports/ # Jitsi meeting activity log report (trusted_access only)
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Recovery Meetings has **two UI entry points**:
|
> **Note:** Recovery Meetings has **two UI entry points**:
|
||||||
@@ -113,21 +124,22 @@ IDAA members do not log in through Aether — they log in through Novi (idaa.org
|
|||||||
|
|
||||||
> **Security note (2026-03-09):** The iframe HTML files previously also passed `email` and `full_name`
|
> **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.
|
> via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed.
|
||||||
> The SvelteKit layout now fetches verified identity directly from the Novi API.
|
> 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.
|
> See "Iframe Integration" → "Novi UUID Verification Flow" below.
|
||||||
|
|
||||||
### Verification Flow (`(idaa)/+layout.svelte`)
|
### Verification Flow (`(idaa)/+layout.svelte`)
|
||||||
|
|
||||||
When a `uuid` param is present in the URL, the layout performs an **async Novi API call** to verify:
|
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)
|
1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
|
||||||
2. Gets verified name and email directly from Novi — these can't be forged via URL
|
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`
|
3. Sets `$idaa_loc.novi_uuid`, `$idaa_loc.novi_email`, `$idaa_loc.novi_full_name`
|
||||||
4. Sets `$idaa_loc.novi_verified = true` on success
|
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.
|
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, or the verification call fails, access is denied. There is no URL-param fallback.
|
**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:**
|
**Required `site_cfg_json` fields:**
|
||||||
```json
|
```json
|
||||||
@@ -148,20 +160,26 @@ This section documents the exact way Aether uses the Novi API for the IDAA integ
|
|||||||
|
|
||||||
- **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".
|
- **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)
|
### Verification Flow (implementation)
|
||||||
|
|
||||||
1. The IDAA iframe loads Aether pages with a `?uuid=<uuid>&iframe=true` param.
|
1. The IDAA iframe loads Aether pages with a `?uuid=<uuid>&iframe=true` param.
|
||||||
2. When the `uuid` param is present the IDAA layout performs an authenticated GET against the Novi customers endpoint:
|
2. When the `uuid` param is present the IDAA layout calls the Aether server-side proxy:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// simplified
|
// simplified
|
||||||
fetch(`${api_root_url}/customers/${uuid}`, {
|
fetch(`${aether_api_url}/v3/action/idaa/novi_member/${uuid}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `Basic ${api_key}` }
|
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 the layout uses the returned JSON to build a display name and normalized email, then writes these values to the IDAA store and marks verification success.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -169,9 +187,9 @@ fetch(`${api_root_url}/customers/${uuid}`, {
|
|||||||
|
|
||||||
### Key `site_cfg_json` fields and where they are used
|
### Key `site_cfg_json` fields and where they are used
|
||||||
|
|
||||||
- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Required for the verification request. Accessed in code as `$ae_loc.site_cfg_json.novi_idaa_api_key` and passed in the `Authorization: Basic <key>` header. If missing, Novi-based access is denied.
|
- **`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 API root (defaults to `https://www.idaa.org/api`). Used to form the verification URL.
|
- **`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_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.
|
||||||
|
|
||||||
@@ -181,6 +199,17 @@ fetch(`${api_root_url}/customers/${uuid}`, {
|
|||||||
|
|
||||||
- **`novi_bb_base_url`**: (optional) Base URL used to build links for Bulletin Board notification emails.
|
- **`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).
|
- **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
|
### Stores / runtime fields set by verification
|
||||||
@@ -201,12 +230,32 @@ These fields are read elsewhere in the IDAA UI to enable flows for verified user
|
|||||||
### Security notes and operational guidance
|
### 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 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 config and do not expose it in client-side code or public repositories.
|
- 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.
|
||||||
- The verification request uses Basic auth with the provided `novi_idaa_api_key` (already Base64-encoded by Novi) — treat the token like a password.
|
- 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 Novi changes their customer API shape, update the layout parsing (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.
|
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)
|
### Permission Levels (Ascending)
|
||||||
| Level | Condition | Access |
|
| Level | Condition | Access |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -217,6 +266,47 @@ If you need a compact checklist for re-creating this flow in another integration
|
|||||||
|
|
||||||
`novi_trusted_li` and `novi_admin_li` are managed in Aether site config (not in Novi directly).
|
`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
|
### Permission Upgrade Rule
|
||||||
```
|
```
|
||||||
// RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade.
|
// RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade.
|
||||||
@@ -232,7 +322,12 @@ This ensures that OSIT staff with `super` or `manager` roles retain full access
|
|||||||
|
|
||||||
### Access Gate (`(idaa)/+layout.svelte`)
|
### Access Gate (`(idaa)/+layout.svelte`)
|
||||||
The inner layout blocks ALL rendering if the user is not authorized:
|
The inner layout blocks ALL rendering if the user is not authorized:
|
||||||
- `novi_verifying = true` → "Verifying identity..." spinner
|
- `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
|
- Verification failed or no UUID → "Access Denied" error page
|
||||||
- Access check runs before any child routes render
|
- Access check runs before any child routes render
|
||||||
|
|
||||||
@@ -322,12 +417,87 @@ Recovery Meetings reuses the Aether Events object to represent AA recovery meeti
|
|||||||
|
|
||||||
### Search Filters
|
### Search Filters
|
||||||
Members can filter meetings by:
|
Members can filter meetings by:
|
||||||
- **Fulltext search** — name, location
|
- **Fulltext search** — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
|
||||||
- **Physical** — in-person meetings
|
- **Virtual** — online meetings (Zoom, Jitsi, other)
|
||||||
- **Virtual** — online meetings (Zoom, Google Meet, etc.)
|
- **In-person** — physical location meetings
|
||||||
- **Meeting type** — specific meeting format categories
|
- **Meeting type** — IDAA / Caduceus / Family Recovery
|
||||||
|
- **My Meetings** — star toggle; shows only meetings the member has starred (favorites)
|
||||||
|
|
||||||
Search is debounced (250ms) and uses the standard Aether SWR pattern.
|
**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
|
### Edit Form — Sections and Key Fields
|
||||||
|
|
||||||
@@ -388,6 +558,136 @@ 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`)
|
## State Management (`ae_idaa_stores.ts`)
|
||||||
|
|
||||||
Four stores manage all IDAA state:
|
Four stores manage all IDAA state:
|
||||||
@@ -405,18 +705,36 @@ Stores Novi auth context and per-submodule query settings:
|
|||||||
novi_jitsi_mod_li: string[] // Jitsi moderator UUIDs
|
novi_jitsi_mod_li: string[] // Jitsi moderator UUIDs
|
||||||
|
|
||||||
archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_obj }
|
archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_obj }
|
||||||
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj }
|
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj,
|
||||||
recovery_meetings: { qry__fulltext_str, qry__physical, qry__virtual, qry__type, qry__limit, edit__event_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` (sessionStorage — cleared on tab close)
|
### `idaa_sess` (in-memory only — resets on page load)
|
||||||
UI state per submodule:
|
UI state per submodule:
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id, obj_changed }
|
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id,
|
||||||
bb: { qry__status, show__modal_edit__post_id, show__modal_view__post_id, obj_changed }
|
show__modal_edit__archive_content_id, show__modal_view__archive_content_id, obj_changed }
|
||||||
recovery_meetings: { qry__status, show__modal_edit, show__modal_view, attend_platform, 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -477,6 +795,10 @@ https://assets-staging.noviams.com/novi-core-assets/css/c/idaa/idaa.css — Boo
|
|||||||
- `<section>` and heading elements may get unexpected margins/padding from Bootstrap's typography reset
|
- `<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)
|
- 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
|
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
|
**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>`,
|
same Skeleton/Tailwind component classes as the rest of the app. Avoid using bare `<section>`,
|
||||||
@@ -517,13 +839,16 @@ ae_loc.trusted_access = true;
|
|||||||
ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
||||||
```
|
```
|
||||||
|
|
||||||
### Current Test Coverage (as of 2026-02-26)
|
### Current Test Coverage (as of 2026-04-07)
|
||||||
| Module | State | Notes |
|
| Module | State | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Archives | ⚠️ Smoke only | `archive_content.test.ts` — no auth gate test |
|
| Archives | ⚠️ Smoke only | `archive_content.test.ts` — no auth gate test |
|
||||||
| Bulletin Board | ❌ None | Priority — most sensitive module |
|
| Bulletin Board | ❌ None | Priority — most sensitive module |
|
||||||
| Recovery Meetings | ❌ None | — |
|
| 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 |
|
| 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -540,7 +865,38 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
|||||||
- [Naming Conventions](./AE__Naming_Conventions.md)
|
- [Naming Conventions](./AE__Naming_Conventions.md)
|
||||||
- [Playwright Test README](../tests/README.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
|
**Document Status:** ✅ Current
|
||||||
**Last Verified:** 2026-03-09 — updated for Novi UUID verification upgrade
|
**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)
|
||||||
|
|||||||
@@ -19,8 +19,15 @@ Required for any non-public data (Journals, Badges, Users, etc.).
|
|||||||
* **Header:** `x-account-id: <account_id>`
|
* **Header:** `x-account-id: <account_id>`
|
||||||
2. **Administrative Bypass**: For authorized scripts needing global access.
|
2. **Administrative Bypass**: For authorized scripts needing global access.
|
||||||
* **Header:** `x-no-account-id: bypass`
|
* **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.
|
3. **Token Access**: Provide a **JWT** in the query string.
|
||||||
* **Query Param:** `?jwt=<token>`
|
* **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]
|
> [!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.
|
> **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.
|
||||||
@@ -44,6 +51,37 @@ When the frontend first loads and doesn't know the `account_id`, it performs a "
|
|||||||
* Returns 200 + a list containing the `account_id` (random string ID) and `site_id` (random string ID).
|
* 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.
|
* ** デザイン 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
|
## 3. Standard CRUD Patterns
|
||||||
@@ -58,6 +96,26 @@ The primary way to retrieve data.
|
|||||||
* **Endpoint:** `POST /v3/crud/{obj_type}/search`
|
* **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.
|
* **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.
|
||||||
|
|
||||||
|
#### Sorting with `order_by_li`
|
||||||
|
|
||||||
|
Pass a JSON object as the `order_by_li` query parameter to sort results:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ?order_by_li={"filename":"ASC","created_on":"DESC"}
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
order_by_li: JSON.stringify({ filename: 'ASC', created_on: 'DESC' })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **`order_by_li` only accepts columns from the raw base table** — not view-only join columns.
|
||||||
|
>
|
||||||
|
> Some object types (e.g. `event_file`) have enriched views that JOIN other tables to expose convenience fields like `event_presenter_family_name`. These are available in search results when using `?view=alt`, but they **cannot** be used in `order_by_li`. Attempting to sort by them silently drops those sort keys (the query proceeds without them).
|
||||||
|
>
|
||||||
|
> If you need to sort by a joined field, sort client-side on the returned list.
|
||||||
|
>
|
||||||
|
> **Columns safe to sort on for `event_file`:** any field in the `event_file` table itself — `filename`, `title`, `extension`, `created_on`, `updated_on`, `sort`, `enable`, etc.
|
||||||
|
|
||||||
### C. POST Create / PATCH Update
|
### C. POST Create / PATCH Update
|
||||||
Modify data in the system.
|
Modify data in the system.
|
||||||
* **Endpoints:**
|
* **Endpoints:**
|
||||||
@@ -68,12 +126,28 @@ Modify data in the system.
|
|||||||
* **Header:** `x-ae-ignore-extra-fields: true`
|
* **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.
|
* **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)
|
### D. ID Fields in Responses (Vision ID Convention)
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **V3 responses always use random string IDs — never database integers.**
|
> **V3 responses always use random string IDs — never database integers.**
|
||||||
|
|
||||||
After a successful `POST` create or any `GET`, the response contains:
|
All V3 responses — `POST` create, `GET` single, `GET` list, search, and `PATCH` update — contain:
|
||||||
|
|
||||||
| Field | Type | Use |
|
| Field | Type | Use |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
@@ -212,7 +286,7 @@ When seeding new lookup data (e.g., adding timezones in bulk):
|
|||||||
|
|
||||||
## 5. Event File Data Retrieval (Hosted Files)
|
## 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:
|
Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File is a metadata record for binary content, accessed via dedicated Action endpoints. To download an event file use `/v3/action/event_file/{event_file_id}/download` — not the hosted_file endpoint directly (each endpoint only accepts its own ID type). To retrieve hosted file metadata alongside an event file record:
|
||||||
|
|
||||||
* **Endpoint:** `GET /v3/crud/event_file/{event_file_id}`
|
* **Endpoint:** `GET /v3/crud/event_file/{event_file_id}`
|
||||||
* **Query Parameter:** Add `inc_hosted_file=true`
|
* **Query Parameter:** Add `inc_hosted_file=true`
|
||||||
@@ -227,6 +301,48 @@ Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file
|
|||||||
* `hosted_file_size` (string - in bytes)
|
* `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.
|
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.
|
||||||
|
|
||||||
|
### Direct Download Links (Shareable / External)
|
||||||
|
|
||||||
|
Event files can be downloaded without standard auth headers using one of two bypass mechanisms. This is useful for generating shareable links for staff or external recipients.
|
||||||
|
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Path:** `/v3/action/event_file/{event_file_id}/download`
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **Breaking change (2026-06-10):** This endpoint now requires an `event_file_id`. Previously it accepted `hosted_file_id` or `archive_content_id` and resolved the chain automatically — that cross-resolution has been removed. Pass the correct ID type for the endpoint you are calling. If you were routing downloads through `/v3/action/hosted_file/{hosted_file_id}/download` as a workaround, switch to this endpoint using `event_file_id`. *(Remove this note after ~2026-06-24.)*
|
||||||
|
|
||||||
|
#### Auth bypass options
|
||||||
|
|
||||||
|
| Query param | Value | When to use |
|
||||||
|
|---|---|---|
|
||||||
|
| `?key=<account_id_random>` | Any valid account random ID | Staff sharing within a known account context |
|
||||||
|
| `?site_key=<site_access_key>` | The site's `access_key` value | Public or semi-public distribution tied to a specific site |
|
||||||
|
|
||||||
|
Either param replaces the need for `x-aether-api-key` / `x-account-id` headers, so the URL is self-contained and works in a plain browser tab or `<a href>` link.
|
||||||
|
|
||||||
|
#### Optional params
|
||||||
|
|
||||||
|
| Query param | Description |
|
||||||
|
|---|---|
|
||||||
|
| `filename` | Override the download filename (min 4 chars). Useful for giving files clean display names. |
|
||||||
|
|
||||||
|
#### Building a shareable link
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Build a self-contained download URL for staff/external use
|
||||||
|
function makeDownloadUrl(eventFileId: string, accountId: string, displayName?: string): string {
|
||||||
|
const base = `https://dev-api.oneskyit.com/v3/action/event_file/${eventFileId}/download`;
|
||||||
|
const params = new URLSearchParams({ key: accountId });
|
||||||
|
if (displayName) params.set('filename', displayName);
|
||||||
|
return `${base}?${params}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The endpoint supports byte-range requests (`Range` header), so it works correctly for in-browser media streaming as well as direct file downloads.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The `?key=` bypass verifies only that the account ID exists — it does not confirm the file belongs to that account. It is appropriate for internal staff tools. For publicly distributed links, prefer `?site_key=` which ties access to a specific site's configured key.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Hosted File Actions: Convert & Clip (Frontend Notes)
|
## 6. Hosted File Actions: Convert & Clip (Frontend Notes)
|
||||||
@@ -247,22 +363,274 @@ These helper endpoints let the frontend request small server-side transformation
|
|||||||
- Required query params: `link_to_type`, `link_to_id`, `start_time`, `end_time` (format `HH:MM:SS`)
|
- 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)
|
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool)
|
||||||
- Auth: standard V3 headers
|
- 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`.
|
||||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
|
- Defaults to stream-copying (fast); set `reencode=true` to force H.264 or `scale_down=true` to resize.
|
||||||
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize.
|
- Add `?background=true` to schedule the clip asynchronously — returns `202 Accepted` immediately; poll the `hosted_file` record for completion.
|
||||||
- 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; 202 when scheduled successfully.
|
||||||
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
|
|
||||||
|
|
||||||
Frontend guidance:
|
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.
|
- 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.
|
- 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.
|
- Prefer `?background=true` for large inputs to avoid request timeouts. 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Event Exhibit Tracking Export (Leads Export)
|
## 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.
|
Allows an exhibitor to download all lead-capture records for their exhibit as a CSV or XLSX file.
|
||||||
|
|
||||||
@@ -330,10 +698,67 @@ const url = URL.createObjectURL(blob);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Troubleshooting 403 Forbidden
|
## 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 | Surface as `'api_error'`; advise retry |
|
||||||
|
|
||||||
|
### Migration from direct Novi call
|
||||||
|
|
||||||
|
The frontend's `+layout.svelte:verify_novi_uuid()` currently calls Novi directly from the browser. Replace that `fetch()` with this endpoint. Response code mapping:
|
||||||
|
|
||||||
|
| Direct Novi result | This endpoint returns | Frontend state |
|
||||||
|
|---|---|---|
|
||||||
|
| `200` with identity data | `200` | `verified` |
|
||||||
|
| `200` with no identity data | `404` | `denied` |
|
||||||
|
| `404` | `404` | `denied` |
|
||||||
|
| `429` | `429` | `'rate_limited'` |
|
||||||
|
| Network error / Novi 5xx | `503` | `'api_error'` |
|
||||||
|
|
||||||
|
### 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:
|
If you receive a 403 on a valid ID:
|
||||||
1. Verify `x-aether-api-key` is correct.
|
1. Verify `x-aether-api-key` is correct.
|
||||||
2. Ensure you are sending `x-account-id` and NOT `x-aether-api-token`.
|
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.
|
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).
|
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.
|
||||||
|
|||||||
@@ -157,26 +157,58 @@ This layout hides `.badge_back` in `@media print` — only the front face prints
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Epson — Fan-Fold / Label Printer
|
### Epson ColorWorks C3500 — Fan-Fold Label Printer
|
||||||
|
|
||||||
**Status:** Not yet tested. Section to be filled in after testing.
|
**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.
|
||||||
|
|
||||||
Common Epson models used for fan-fold name badge stock: TM-T88 series, C3500, LX series.
|
The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
|
||||||
Fan-fold stock is typically 4" × 3" or 4" × 6" paper labels.
|
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
|
#### CSS Layout
|
||||||
|
|
||||||
Fan-fold badges would use a layout sized to the specific label stock.
|
The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
|
||||||
A new CSS layout file will need to be created per stock size if not already present.
|
`src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
|
||||||
Naming convention: `badge_layout_epson_[model]_[size].css`
|
|
||||||
|
|
||||||
#### Setup Notes
|
Created 2026-05-15 for Axonius Adapt DC. Key specs:
|
||||||
|
- `badge_front` 4" × 6", portrait orientation
|
||||||
*(To be filled in after testing — cover: driver source, CUPS setup, paper size, Chrome settings)*
|
- `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
|
#### Known Behaviors
|
||||||
|
|
||||||
*(To be filled in after testing)*
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
132
documentation/GUIDE__AE_Events_Onsite_Runbook.md
Normal file
132
documentation/GUIDE__AE_Events_Onsite_Runbook.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# 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. |
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
- **Zero Tolerance:** If a task introduces even a single svelte-check warning or error, it must not be merged. Resolve all warnings before committing.
|
- **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.
|
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`.
|
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:staging` to ensure no SSR or build-time failures.
|
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):
|
5. **Integration Tests:** For changes to badge print, event layouts, or auth/store logic, run the relevant Playwright test file(s):
|
||||||
```bash
|
```bash
|
||||||
npx playwright test tests/event_badge_render.test.ts tests/event_badge_attendee_workflow.test.ts
|
npx playwright test tests/event_badge_render.test.ts tests/event_badge_attendee_workflow.test.ts
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
## 2. Commit Policy
|
## 2. Commit Policy
|
||||||
- **Atomic Commits:** One component or one logic fix per commit. Do not batch unrelated changes.
|
- **Atomic Commits:** One component or one logic fix per commit. Do not batch unrelated changes.
|
||||||
- **Safety:** Use `~/tmp/gemini_trash` for file removal; never use `rm` directly on source files.
|
- **Safety:** Use `~/tmp/agents_trash` for file removal; never use `rm` directly on source files.
|
||||||
- **Secrets:** Never commit `.env`, API keys, or passwords.
|
- **Secrets:** Never commit `.env`, API keys, or passwords.
|
||||||
|
|
||||||
## 3. Coordination (The Handshake)
|
## 3. Coordination (The Handshake)
|
||||||
|
|||||||
@@ -25,13 +25,54 @@ let lq__obj = $derived(
|
|||||||
|
|
||||||
- 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.
|
- 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
|
### 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.
|
**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:
|
**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**.
|
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.
|
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:**
|
**Example of the Bug:**
|
||||||
```typescript
|
```typescript
|
||||||
@@ -89,14 +130,114 @@ $effect(() => {
|
|||||||
|
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||
## Practical Patterns from Aether (Journals & Events)
|
## 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: 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:
|
- 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.
|
- 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.
|
- 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.
|
- 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):
|
Example (presentation list pattern):
|
||||||
```typescript
|
```typescript
|
||||||
let lq__event_presentation_obj_li = $derived(
|
let lq__event_presentation_obj_li = $derived(
|
||||||
@@ -113,6 +254,8 @@ let lq__event_presentation_obj_li = $derived(
|
|||||||
- Add a small `console.log` inside each `liveQuery` closure to confirm when it runs and what `id` it sees.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
@@ -233,6 +376,79 @@ export function createLiveQueryStore<T>(query: () => T | Promise<T>) {
|
|||||||
|
|
||||||
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.
|
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")
|
## 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.
|
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.
|
||||||
@@ -282,7 +498,7 @@ If you must use non-blocking loads, you must pass the initial data to the compon
|
|||||||
|
|
||||||
## The `untrack()` Reactive-Tracking Trap
|
## 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.
|
`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
|
### Symptom
|
||||||
|
|
||||||
@@ -346,7 +562,7 @@ Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re
|
|||||||
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
|
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):**
|
**❌ Invalid Pattern (Causes Compile Error):**
|
||||||
Attempting to normalize a value *inside* the binding will fail.
|
Attempting to normalize a value _inside_ the binding will fail.
|
||||||
```svelte
|
```svelte
|
||||||
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
||||||
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
||||||
|
|||||||
@@ -114,17 +114,19 @@ corresponding `ticket_N_text` on the template provides the HTML rendered on the
|
|||||||
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
|
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
|
||||||
| `notes` | str | Internal notes |
|
| `notes` | str | Internal notes |
|
||||||
|
|
||||||
### New Field (pending backend addition)
|
### Duplex / Single-Sided
|
||||||
| Field | Type | Notes |
|
| Field | Type | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) |
|
| `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.
|
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
|
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.
|
prints. The back section still displays on screen for configuration reference.
|
||||||
|
|
||||||
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
|
`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
|
||||||
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
|
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
|
||||||
|
|
||||||
|
Axonius events use `duplex = false` — single-sided printing only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ The print page (`print/+page.svelte`) or the badge view should conditionally add
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
```
|
```
|
||||||
|
|
||||||
This is not yet implemented — tracked as a pending Phase 1 item.
|
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
|
### CSS Scope
|
||||||
|
|
||||||
@@ -180,7 +182,7 @@ The `layout` field encodes physical badge stock dimensions. Standard codes to us
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `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_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_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) | *(none — Tailwind defaults)* | Generic fanfold fallback; dimensions match the hardcoded Tailwind values |
|
| `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 |
|
| `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
|
Layout CSS files live in `src/lib/ae_events/badges/css/` and are imported by
|
||||||
@@ -192,6 +194,124 @@ wrapper so multiple layouts can coexist in the bundle without conflict.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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)
|
## Template-Derived Features (component behavior)
|
||||||
|
|
||||||
### badge_type_list → badge type select
|
### badge_type_list → badge type select
|
||||||
@@ -218,7 +338,6 @@ The `properties_to_save` array in `ae_events__event_badge_template.ts` controls
|
|||||||
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
|
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
|
||||||
in DB and may be needed:
|
in DB and may be needed:
|
||||||
|
|
||||||
- `style_href` — needed once external CSS is wired via `<svelte:head>`
|
|
||||||
- `passcode` — not needed client-side
|
- `passcode` — not needed client-side
|
||||||
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
||||||
- `header_background`, `footer_background` — not needed (legacy)
|
- `header_background`, `footer_background` — not needed (legacy)
|
||||||
@@ -359,6 +478,9 @@ Firefox users can use "Save to PDF" directly — it just works.
|
|||||||
- [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] 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` 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] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
|
||||||
- [ ] Make `layout` field drive actual card dimensions in the badge component — currently the Zebra ZC10L layout CSS (`badge_layout_zebra_zc10l_pvc.css`) sets dimensions correctly via `[data-layout="..."]` scoping, but fanfold layouts still use Tailwind defaults. Needs proper CSS for each layout code.
|
- [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)
|
- [ ] 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)
|
||||||
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)
|
|
||||||
|
|||||||
@@ -1,843 +1,130 @@
|
|||||||
# MODULE: Aether Events — Badges
|
# Aether Events — Badges
|
||||||
|
|
||||||
**Module Path:** `src/routes/events/[event_id]/(badges)/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.
|
||||||
**API Module:** `src/lib/ae_events/ae_events__event_badge.ts`
|
|
||||||
**Database:** `db_events.badge` (Dexie IndexedDB table)
|
|
||||||
**Last Updated:** 2026-02-27 (rev 6)
|
|
||||||
**Related Docs:** `documentation/PROJECT__AE_Events_Badges_Review_Print.md` (implementation guide)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## Data Model & Hierarchy
|
||||||
|
|
||||||
The Badges module manages event attendee badges with support for:
|
### Core Objects
|
||||||
- **External system imports as needed** (CSV/Excel, iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others)
|
- **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
|
||||||
- **Field override protection** to prevent staff/attendee edits from being overwritten by automated syncs
|
- **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
|
||||||
- **Multi-tier access control** for field editing
|
|
||||||
- **QR code generation** for badge scanning
|
### Relationships
|
||||||
- **Print tracking** (count, first/last print datetime)
|
- **Badge → Event:** Many-to-one.
|
||||||
- **Advanced search and filtering**
|
- **Badge → Template:** Many-to-one (via `event_badge_template_id`).
|
||||||
- **HTML rendering** in display fields for rich text formatting
|
- **Badge → Person:** Optional link to core Aether Person record for unified profiles.
|
||||||
- **Accessibility features** (text enlargement toggle)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Critical Design Pattern: Override Fields
|
## Critical Design Pattern: Override Fields
|
||||||
|
|
||||||
### Purpose
|
### Purpose
|
||||||
The `*_override` fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because:
|
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.
|
||||||
1. Staff may need to correct imported data
|
|
||||||
2. Attendees may be allowed to self-update certain fields (e.g., preferred name, pronouns)
|
|
||||||
3. External systems often have outdated or incorrect data
|
|
||||||
4. Changes should persist across multiple sync cycles
|
|
||||||
|
|
||||||
### How It Works
|
### 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.
|
||||||
|
|
||||||
**Import Behavior:**
|
### Standard Override Pairs
|
||||||
```
|
|
||||||
External System → Aether API → Populates REGULAR fields only
|
|
||||||
(never touches *_override fields)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Display Behavior:**
|
| Regular Field | Override Field | Editable By | HTML? |
|
||||||
```
|
|---|---|---|---|
|
||||||
UI Display Logic:
|
| `full_name` | `full_name_override` | Staff, Attendee | ✅ |
|
||||||
1. IF `*_override` field has value → USE IT (highest priority)
|
| `professional_title` | `professional_title_override` | Staff, Attendee | ✅ |
|
||||||
2. ELSE IF regular field has value → USE IT (fallback)
|
| `affiliations` | `affiliations_override` | Staff, Attendee | ✅ |
|
||||||
3. ELSE → Display placeholder/empty
|
| `location` | `location_override` | Staff, Attendee | ✅ |
|
||||||
```
|
| `email` | `email_override` | Staff Only | No |
|
||||||
|
| `badge_type` | `badge_type_override` | Staff Only | No |
|
||||||
**HTML Rendering (implemented 2026-02-27):**
|
|
||||||
|
|
||||||
Certain fields support HTML markup for rich text formatting. When viewing (not editing), these fields use Svelte's `{@html}` directive to render the markup:
|
|
||||||
- `full_name` / `full_name_override`
|
|
||||||
- `professional_title` / `professional_title_override`
|
|
||||||
- `affiliations` / `affiliations_override`
|
|
||||||
- `location` / `location_override`
|
|
||||||
|
|
||||||
This allows for formatting like:
|
|
||||||
- Bold/italic: `<b>Dr.</b> Jane Smith` or `<i>Chief Medical Officer</i>`
|
|
||||||
- Line breaks: `Hospital Name<br>Department Name`
|
|
||||||
- Special characters and entities
|
|
||||||
|
|
||||||
**Example — Full Name:**
|
|
||||||
```typescript
|
|
||||||
// API imports from iMIS
|
|
||||||
badge.given_name = "Robert"
|
|
||||||
badge.family_name = "Smith"
|
|
||||||
badge.full_name = "Robert Smith" // Auto-computed
|
|
||||||
|
|
||||||
// Staff edits to preferred name with HTML
|
|
||||||
badge.full_name_override = "<b>Bob</b> Smith"
|
|
||||||
|
|
||||||
// Display in UI (review form)
|
|
||||||
{@html badge.full_name_override || badge.full_name || "— no name —"}
|
|
||||||
// Result: **Bob** Smith (bold rendered)
|
|
||||||
|
|
||||||
// Edit mode shows raw HTML
|
|
||||||
<input bind:value={editable_full_name_override} />
|
|
||||||
// Shows: <b>Bob</b> Smith (editable as text)
|
|
||||||
|
|
||||||
// Next cron sync from iMIS
|
|
||||||
// ✅ badge.full_name updated to "Robert J. Smith" (middle initial added)
|
|
||||||
// ✅ badge.full_name_override remains "<b>Bob</b> Smith" (PROTECTED)
|
|
||||||
// ✅ Display still shows **Bob** Smith (bold rendered)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Override Fields
|
|
||||||
|
|
||||||
| Regular Field | Override Field | Purpose | Editable By | HTML Rendering |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `pronouns` | `pronouns_override` | Preferred pronouns | Staff, Attendee | No |
|
|
||||||
| `professional_title` | `professional_title_override` | Job title display | Staff, Attendee | ✅ Yes |
|
|
||||||
| `full_name` | `full_name_override` | Preferred name display | Staff, Attendee | ✅ Yes |
|
|
||||||
| `affiliations` | `affiliations_override` | Organization display | Staff, Attendee | ✅ Yes |
|
|
||||||
| `phone` | `phone_override` | Phone number | Staff, Attendee | No |
|
|
||||||
| `email` | `email_override` | Contact email override | Staff only | No |
|
|
||||||
| `location` | `location_override` | City/State/Country display | Staff, Attendee | ✅ Yes |
|
|
||||||
| `badge_type` | `badge_type_override` | Badge category label text | Staff only | No |
|
|
||||||
| `badge_type_code` | `badge_type_code_override` | Badge access level code | Staff only | No |
|
|
||||||
| `registration_type` | `registration_type_override` | Registration category label text | Staff only | No |
|
|
||||||
| `registration_type_code` | `registration_type_code_override` | Registration category code | Staff only | No |
|
|
||||||
|
|
||||||
> **Note:** `phone`, `phone_override`, `pronouns_override`, `registration_type`, `registration_type_code`, `registration_type_override`, `registration_type_code_override` may need to be confirmed against the DB schema via `ae_describe event_badge` and added to `properties_to_save` in `ae_events__event_badge.ts` if not already present.
|
|
||||||
|
|
||||||
### Sync Safety Rules
|
|
||||||
|
|
||||||
**Automated Sync (Cron Jobs):**
|
|
||||||
- ✅ CAN update: All regular fields (`given_name`, `family_name`, `email`, `affiliations`, etc.)
|
|
||||||
- ❌ CANNOT update: Any `*_override` field
|
|
||||||
- ❌ CANNOT delete: Any `*_override` value
|
|
||||||
|
|
||||||
**Manual Staff Edit:**
|
|
||||||
- ✅ CAN update: Any field (including overrides)
|
|
||||||
- ✅ CAN clear: Override fields (reverts to regular field)
|
|
||||||
|
|
||||||
**Attendee Self-Service Edit:**
|
|
||||||
- ✅ CAN update: Only specific override fields (per event config)
|
|
||||||
- ✅ CAN clear: Their own override fields
|
|
||||||
- ❌ CANNOT edit: Regular fields, badge_type, email_override
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## External System Integration
|
## External System Integration
|
||||||
|
|
||||||
### Supported Import Sources
|
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."
|
||||||
- **iMIS** (Association Management)
|
|
||||||
- **Zoom** (Virtual event registration)
|
|
||||||
- **Novi AMS** (Association Management)
|
|
||||||
- **Impexium** (Association Management)
|
|
||||||
- **Confex** (Event abstract management)
|
|
||||||
- **Cvent** (Event registration)
|
|
||||||
- **Custom CSV/Excel** imports
|
|
||||||
|
|
||||||
### Data Flow Direction
|
### Supported Sources
|
||||||
```
|
- **iMIS**, **Novi AMS**, **Impexium** (Associations)
|
||||||
External Systems ─────────> Aether
|
- **Zoom**, **Cvent** (Registrations)
|
||||||
(READ ONLY) (WRITE + DISPLAY)
|
- **Confex** (Abstracts/Presenters)
|
||||||
```
|
- **Custom CSV/Excel**
|
||||||
|
|
||||||
**Important:** Aether is **pull-only** — does not push changes back to external systems. This prevents sync conflicts and maintains external systems as the source of truth for base data.
|
|
||||||
|
|
||||||
### Sync Behavior
|
|
||||||
- **Frequency:** Scheduled cron jobs (typically hourly, daily, or on-demand)
|
|
||||||
- **Method:** Full sync or incremental (depends on external system API)
|
|
||||||
- **Conflict Resolution:** Override fields always win
|
|
||||||
|
|
||||||
**Pseudocode:**
|
|
||||||
```python
|
|
||||||
def sync_badge_from_external(external_badge_data, existing_badge):
|
|
||||||
# Update regular fields from external source
|
|
||||||
existing_badge.given_name = external_badge_data.first_name
|
|
||||||
existing_badge.family_name = external_badge_data.last_name
|
|
||||||
existing_badge.email = external_badge_data.email
|
|
||||||
existing_badge.affiliations = external_badge_data.organization
|
|
||||||
existing_badge.badge_type_code = external_badge_data.registration_type
|
|
||||||
|
|
||||||
# NEVER TOUCH OVERRIDE FIELDS
|
|
||||||
# existing_badge.full_name_override ← PROTECTED
|
|
||||||
# existing_badge.affiliations_override ← PROTECTED
|
|
||||||
# existing_badge.email_override ← PROTECTED
|
|
||||||
|
|
||||||
return existing_badge
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Access Control & Edit Permissions
|
## Access Control & Permissions
|
||||||
|
|
||||||
### Access Levels (Ascending)
|
| Level | Access |
|
||||||
1. **Anonymous** — No access to badges
|
|---|---|
|
||||||
2. **Public** — View public event info only (no badge access)
|
| **Authenticated** | View own badge, limited self-edit (overrides only). |
|
||||||
3. **Authenticated** — View own badge, limited self-edit
|
| **Trusted** | Search all badges, view all, reprint existing badges. |
|
||||||
4. **Trusted** — Search all badges, view all, edit own
|
| **Administrator** | Full CRUD, bulk operations, override any field. |
|
||||||
5. **Administrator** — Full CRUD, bulk operations, override any field
|
| **Manager** | All Admin + Event/Template configuration. |
|
||||||
6. **Manager** — All administrator + event configuration
|
|
||||||
7. **Super** — All manager + cross-event operations
|
|
||||||
|
|
||||||
### Current Implementation (v3) — 2026-02-27
|
### 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.
|
||||||
#### Badge Search Results Visibility
|
|
||||||
|
|
||||||
| Access Level | Sees |
|
|
||||||
| --- | --- |
|
|
||||||
| Below Trusted (incl. anonymous) | Only badges where `print_count < 1` and not hidden |
|
|
||||||
| Trusted, not Edit Mode | Only badges where `print_count < 1` and not hidden |
|
|
||||||
| Trusted + Edit Mode | All badges where `hide === false` (including already-printed) |
|
|
||||||
|
|
||||||
#### Print Button Behavior (per result row)
|
|
||||||
|
|
||||||
| Access Level | Print Action |
|
|
||||||
| --- | --- |
|
|
||||||
| Below Trusted | No print action — name shown with User icon, non-interactive |
|
|
||||||
| Trusted, `print_count < 1` | Clickable link → `/print` page, Printer icon |
|
|
||||||
| Trusted, `print_count >= 1`, not Edit Mode | Disabled (already printed safety lock), shows `Nx` count |
|
|
||||||
| Trusted, `print_count >= 1`, Edit Mode | Clickable reprint — shows `Nx` count badge next to icon |
|
|
||||||
|
|
||||||
Print count displayed as `[Printer][2×] Name` when `print_count >= 1`.
|
|
||||||
|
|
||||||
#### Review Area Buttons (per result row, up to 3 buttons total)
|
|
||||||
|
|
||||||
| Button | Visible To | Behavior |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Email Review Link | All users | Placeholder `alert()` — will trigger email API |
|
|
||||||
| Review Link (clipboard) | Trusted + Edit Mode only | Copies `/review` URL to clipboard; shows `Copied!` feedback |
|
|
||||||
| *(direct Review link)* | *(future)* | *(not yet implemented as separate nav button)* |
|
|
||||||
|
|
||||||
#### Badge Edit Form (`ae_comp__badge_obj_view.svelte`)
|
|
||||||
|
|
||||||
**Currently editable fields (local `edit_mode_active`, not global `edit_mode`):**
|
|
||||||
```typescript
|
|
||||||
editable_full_name_override: string | null
|
|
||||||
editable_professional_title_override: string | null
|
|
||||||
editable_affiliations_override: string | null // textarea
|
|
||||||
editable_location_override: string | null
|
|
||||||
editable_allow_tracking: boolean | null
|
|
||||||
editable_email: string | null
|
|
||||||
editable_badge_type_code: string | null
|
|
||||||
```
|
|
||||||
|
|
||||||
- Save button → `handle_save_changes()` — only changed fields sent to API
|
|
||||||
- Cancel button → `handle_cancel_changes()` — reverts to IDB values
|
|
||||||
- **IMPORTANT:** This component must NEVER write to `$ae_loc.edit_mode` — it uses its own local `edit_mode_active` flag only. (Bug fixed 2026-02-27)
|
|
||||||
|
|
||||||
#### Badge Review Form (`ae_comp__badge_review_form.svelte`)
|
|
||||||
|
|
||||||
Form-based review (NOT a badge render). Used by the `/review` page.
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
- `can_edit_fields: string[]` prop controls which fields are editable per user level
|
|
||||||
- `['*']` = administrator (all fields)
|
|
||||||
- `is_staff: boolean` prop shows/hides the staff-only fields
|
|
||||||
- Fields show "(overridden)" label when an override value differs from the base field
|
|
||||||
|
|
||||||
**Features (implemented 2026-02-27):**
|
|
||||||
- **HTML Rendering**: `full_name_override`, `professional_title_override`, `affiliations_override`, and `location_override` fields render HTML markup using `{@html}` directive when viewing (not when editing)
|
|
||||||
- **Accessibility**: Text enlargement toggle button switches between text-2xl (normal) and text-4xl (enlarged) for improved readability
|
|
||||||
- **Help Modal**: Flowbite Modal component with 6 help sections (Reviewing Badge, Editing Info, Accessibility, QR Code, Lead Scanning, Assistance)
|
|
||||||
- **QR Code Display**: Generates QR code using `core_func.js_generate_qr_code()` with badge ID, supports hover zoom and click-to-expand
|
|
||||||
- **Print Status**: Shows print count, first print datetime, and last print datetime at top of form
|
|
||||||
- **Local Edit Mode**: Independent `local_edit_active` state (never writes to `$ae_loc.edit_mode`)
|
|
||||||
- **Save/Cancel**: Only changed fields sent to API; revert button for override fields
|
|
||||||
|
|
||||||
**Editable Fields:**
|
|
||||||
- Pronouns, Full Name, Professional Title, Affiliations, Phone, Location (all with override support)
|
|
||||||
- Allow Tracking checkbox (lead scanning permission)
|
|
||||||
- Staff-only: Email, Badge Type, Registration Type, Hide, Priority, Notes
|
|
||||||
- Staff-only: Options (`other_1_code` through `other_8_code`) and Tickets (`ticket_1_code` through `ticket_8_code`)
|
|
||||||
- Agree to Terms & Conditions checkbox (attendee-visible when in can_edit_fields)
|
|
||||||
|
|
||||||
#### Badge Review Page — Header Buttons (implemented 2026-02-27)
|
|
||||||
|
|
||||||
| Button | Visible To | Behavior |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Back → Search (ArrowLeft) | Staff (`has_staff_access`) only | `<a href="/events/{id}/badges">` |
|
|
||||||
| Print (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | `<a href="/print">`, shows `Nx` count if reprinting |
|
|
||||||
| Copy Link (clipboard) | Trusted + Edit Mode only | Copies review URL to clipboard; `Copied!` feedback for 2s |
|
|
||||||
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
|
|
||||||
|
|
||||||
#### Badge Print Page — Header Buttons (implemented 2026-02-27)
|
|
||||||
|
|
||||||
| Button | Visible To | Behavior |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Back → Search (ArrowLeft) | Always (when badge loaded) | `<a href="/events/{id}/badges">` |
|
|
||||||
| Print Now (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | Calls `window.print()` directly (convenience duplicate); print count tracked by component button |
|
|
||||||
| Review (Eye icon) | Trusted + Edit Mode only | `<a href="/review">` nav link |
|
|
||||||
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
|
|
||||||
|
|
||||||
#### Badge Review Page — Display Sections (implemented 2026-02-27)
|
|
||||||
|
|
||||||
The review form (`ae_comp__badge_review_form.svelte`) displays:
|
|
||||||
|
|
||||||
1. **Print status** ✅ — print count + first/last print timestamps (read-only, hidden if never printed)
|
|
||||||
2. **QR Code** ✅ — the attendee's badge QR code for scanning at the badge kiosk (for automatic badge search + print flow). Generated using `core_func.js_generate_qr_code()` with `obj_type: 'event_badge'` and badge ID. Supports hover zoom overlay and click-to-expand.
|
|
||||||
3. **Editable Fields** ✅ — all fields with access-level gating, override support, and HTML rendering for display fields
|
|
||||||
4. **Options** ✅ (`other_1_code` through `other_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Option X` checkmark display only when value exists
|
|
||||||
5. **Tickets** ✅ (`ticket_1_code` through `ticket_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Ticket X` checkmark display only when value exists
|
|
||||||
6. **Accessibility Toggle** ✅ — Font size enlargement button in sticky header (text-2xl ↔ text-4xl)
|
|
||||||
7. **Help Modal** ✅ — Attendee guidance modal with 6 sections explaining the review process, editing, QR codes, and lead scanning
|
|
||||||
|
|
||||||
#### Default Field Permissions (hardcoded for now — Axonius first show, mid-April 2026)
|
|
||||||
|
|
||||||
These are hardcoded in `review/+page.svelte` pending connection to `mod_badges_json.edit_permissions`.
|
|
||||||
|
|
||||||
**Attendee (passcode-authenticated / anonymous with link):**
|
|
||||||
```typescript
|
|
||||||
[
|
|
||||||
'pronouns_override',
|
|
||||||
'full_name_override',
|
|
||||||
'professional_title_override',
|
|
||||||
'affiliations_override',
|
|
||||||
'phone_override',
|
|
||||||
'location_override',
|
|
||||||
'allow_tracking', // Exhibitor Leads opt-in
|
|
||||||
'agree_to_tc', // Terms & Conditions placeholder
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Trusted Staff and above:**
|
|
||||||
```typescript
|
|
||||||
[
|
|
||||||
'pronouns_override',
|
|
||||||
'full_name_override',
|
|
||||||
'professional_title_override',
|
|
||||||
'affiliations_override',
|
|
||||||
'email_override',
|
|
||||||
'phone_override',
|
|
||||||
'location_override',
|
|
||||||
'badge_type_code_override', // + badge_type_override (text label)
|
|
||||||
'registration_type_code_override', // + registration_type_override (text label)
|
|
||||||
'option_1' ... 'option_8', // i.e. other_1_code ... other_8_code
|
|
||||||
'ticket_1_code' ... 'ticket_8_code',
|
|
||||||
'allow_tracking',
|
|
||||||
'agree_to_tc',
|
|
||||||
'hide',
|
|
||||||
'priority',
|
|
||||||
'notes',
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Administrator** — `can_edit_fields = ['*']` (all fields)
|
|
||||||
|
|
||||||
**Badge type options (hardcoded for now):** `member`, `non-member`, `guest`, `exhibitor`, `staff`, `test`
|
|
||||||
(In future: read from Event Badge Template's configured list)
|
|
||||||
|
|
||||||
**Registration type options:** Same list as badge type for now — identical select options.
|
|
||||||
|
|
||||||
#### Future: Per-Event Configuration
|
|
||||||
|
|
||||||
`event.mod_badges_json.edit_permissions` — placeholder settings UI exists in
|
|
||||||
`ae_comp__event_settings_badges_form.svelte`. Review page uses hardcoded defaults for now.
|
|
||||||
The settings form and review page are not yet connected.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"authenticated": {
|
|
||||||
"can_edit": ["pronouns_override", "full_name_override", "professional_title_override", "affiliations_override", "phone_override", "location_override", "allow_tracking", "agree_to_tc"]
|
|
||||||
},
|
|
||||||
"trusted": {
|
|
||||||
"can_edit": ["*attendee_fields", "email_override", "badge_type_code_override", "registration_type_code_override", "option_x", "ticket_x_code", "allow_tracking", "agree_to_tc", "hide", "priority", "notes"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Search & Filter Capabilities
|
## Search & Filter Capabilities
|
||||||
|
|
||||||
### Search Component
|
- **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
|
||||||
**File:** `ae_comp__badge_search.svelte`
|
- **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.
|
||||||
|
|
||||||
### Multi-Word Search Fix (2026-02-26)
|
### Visibility Filter (Trusted + Edit Mode)
|
||||||
Fulltext search now correctly handles multi-word queries by splitting on whitespace and applying AND logic per word:
|
|
||||||
```typescript
|
|
||||||
// "scott idem" → LIKE '%scott%' AND LIKE '%idem%'
|
|
||||||
// Previously: LIKE '%scott idem%' (failed to match)
|
|
||||||
const words = qry.split(/\s+/).filter(w => w.length > 0);
|
|
||||||
for (const word of words) {
|
|
||||||
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${word}%` });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Committed:** dc0f3066
|
|
||||||
|
|
||||||
### Available Filters
|
Three-option select controlling which records are shown:
|
||||||
|
|
||||||
**Fulltext Search** (All Users)
|
| Option | Who can set it | Effect |
|
||||||
- Searches: `default_qry_str` database field
|
| --- | --- | --- |
|
||||||
- Includes: Name, email, external IDs
|
| **Default** | Any | Hides hidden and disabled badges |
|
||||||
- Type: `LIKE %query%` (case-insensitive)
|
| **Show Hidden** | Trusted | Shows hidden badges alongside normal ones |
|
||||||
- Trigger: Enter key or 3+ characters typed
|
| **Show Disabled + Hidden** | Manager only | Shows all records regardless of enable/hide flags |
|
||||||
|
|
||||||
**Advanced Filters** (Trusted Access & Above)
|
### Result Limit Stepper (Edit Mode)
|
||||||
```typescript
|
|
||||||
// Badge Type Filter
|
|
||||||
badge_type_code: 'current_member' | 'inactive_member' | 'ex_all' | 'staff' | etc.
|
|
||||||
// Note: Badge types are defined per Event and Event Badge Template in database table records.
|
|
||||||
// Common types include: member, nonmember, guest, exhibitor, staff
|
|
||||||
// This is a work in progress - types vary by event configuration.
|
|
||||||
|
|
||||||
// Print Status Filter
|
Controls the maximum number of results returned. Only visible in edit mode.
|
||||||
qry_printed_status: 'all' | 'printed' | 'not_printed'
|
|
||||||
|
|
||||||
// Affiliations Search
|
| Access Level | Range | Step |
|
||||||
qry_affiliations: string // Separate filter for organization search
|
| --- | --- | --- |
|
||||||
|
| Below Trusted | Fixed 25 | — |
|
||||||
|
| Trusted | 25 – 250 | 25 |
|
||||||
|
| Manager+ | 25 – 2550 | 25 up to 250, then 100 |
|
||||||
|
|
||||||
// Sort Options
|
### Badge Type Filter — Known Limitation
|
||||||
qry_sort_order:
|
|
||||||
- 'name_asc' / 'name_desc'
|
|
||||||
- 'updated_desc' / 'updated_asc'
|
|
||||||
- 'print_count_desc'
|
|
||||||
- 'print_first_desc' / 'print_last_desc'
|
|
||||||
- 'badge_type_asc'
|
|
||||||
- 'affiliations_asc'
|
|
||||||
```
|
|
||||||
|
|
||||||
### QR Scan Search
|
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.
|
||||||
- Scans badge QR code
|
|
||||||
- Extracts badge ID
|
|
||||||
- Auto-fills search with ID
|
|
||||||
- Jumps to badge detail view
|
|
||||||
|
|
||||||
### Search Implementation Pattern
|
|
||||||
**File:** `badges/+page.svelte` (Lines 117-365)
|
|
||||||
|
|
||||||
**Strategy:** Standardized Reactive Search Pattern (Aether UI V3)
|
|
||||||
1. **Isolate dependencies** into stable `$derived` object
|
|
||||||
2. **Debounced effect** (300ms) triggers search
|
|
||||||
3. **Fast Path:** Search IDB first (if not `remote_first`)
|
|
||||||
4. **Revalidate:** API request updates IDB
|
|
||||||
5. **LiveQuery:** UI auto-updates from IDB changes
|
|
||||||
|
|
||||||
**Search API:** `events_func.search__event_badge()`
|
|
||||||
```typescript
|
|
||||||
await search__event_badge({
|
|
||||||
api_cfg: $ae_api,
|
|
||||||
event_id: event_id,
|
|
||||||
fulltext_search_qry_str: qry_str || null,
|
|
||||||
type_code: type_code || null,
|
|
||||||
printed_status: printed_status,
|
|
||||||
affiliations_qry_str: aff_str || null,
|
|
||||||
order_by_li: order_by_li,
|
|
||||||
limit: 150,
|
|
||||||
log_lvl: 0
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Badge Display Logic
|
|
||||||
|
|
||||||
### Name Display Priority
|
|
||||||
```typescript
|
|
||||||
// Component: ae_comp__badge_obj_li.svelte (Lines 113-121)
|
|
||||||
if (event_badge_obj?.full_name_override)
|
|
||||||
display: full_name_override
|
|
||||||
else if (event_badge_obj?.full_name)
|
|
||||||
display: full_name
|
|
||||||
else
|
|
||||||
display: given_name + ' ' + family_name
|
|
||||||
```
|
|
||||||
|
|
||||||
### Badge View Page
|
|
||||||
**Route:** `/events/[event_id]/badges/[badge_id]`
|
|
||||||
|
|
||||||
**Components:**
|
|
||||||
- `+page.svelte` — Container with LiveQuery for badge data
|
|
||||||
- `ae_comp__badge_obj_view.svelte` — Full badge display + edit UI
|
|
||||||
|
|
||||||
**LiveQueries:**
|
|
||||||
```typescript
|
|
||||||
lq__event_badge_obj = liveQuery(() => db_events.badge.get(event_badge_id))
|
|
||||||
lq__event_badge_template_obj = liveQuery(() =>
|
|
||||||
db_events.badge_template.get(badge.event_badge_template_id)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Loading States:**
|
|
||||||
- `is_loading_idb` — Waiting for initial IDB lookup
|
|
||||||
- If badge not found → "Badge Not Found" error with reload button
|
|
||||||
- Loader spinner while fetching
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Badge Templates
|
|
||||||
|
|
||||||
### Purpose
|
|
||||||
Badge templates define the visual layout and content structure for printed badges:
|
|
||||||
- Header images/logos
|
|
||||||
- Field positions and font sizes
|
|
||||||
- QR code placement
|
|
||||||
- Ticket/option indicator display
|
|
||||||
- WiFi credentials display
|
|
||||||
|
|
||||||
### Template Selection
|
|
||||||
Each badge references an `event_badge_template_id`. The template controls:
|
|
||||||
- Layout (front/back)
|
|
||||||
- Branding elements
|
|
||||||
- Which fields to show
|
|
||||||
- Field formatting rules
|
|
||||||
|
|
||||||
### Template Loading
|
|
||||||
Templates are loaded alongside badges via `inc_template` parameter:
|
|
||||||
```typescript
|
|
||||||
load_ae_obj_id__event_badge({
|
|
||||||
event_badge_id: badge_id,
|
|
||||||
inc_template: true // Also loads template
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Print Tracking
|
## Print Tracking
|
||||||
|
|
||||||
### Print Fields
|
Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
|
||||||
```typescript
|
|
||||||
print_count: number // Increments each print
|
|
||||||
print_first_datetime: string // ISO datetime of first print
|
|
||||||
print_last_datetime: string // ISO datetime of most recent print
|
|
||||||
```
|
|
||||||
|
|
||||||
### Print Button (Implemented 2026-02-26)
|
| Field | Purpose |
|
||||||
The `handle_print_badge()` function in `ae_comp__badge_obj_view.svelte` increments the count and records timestamps:
|
|---|---|
|
||||||
```typescript
|
| `print_count` | Increments on every "Print Badge" action. |
|
||||||
async function handle_print_badge() {
|
| `print_first_datetime` | Timestamp of the very first print. |
|
||||||
const now = new Date().toISOString();
|
| `print_last_datetime` | Timestamp of the most recent print. |
|
||||||
const current_print_count = $lq__event_badge_obj.print_count ?? 0;
|
|
||||||
const data_to_update = {
|
|
||||||
print_count: current_print_count + 1,
|
|
||||||
print_last_datetime: now
|
|
||||||
};
|
|
||||||
if (current_print_count === 0) {
|
|
||||||
data_to_update.print_first_datetime = now; // Only set on first print
|
|
||||||
}
|
|
||||||
await events_func.update_ae_obj__event_badge({ ... });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Button has `data-testid="badge-print-btn"` and shows loading/done/error states with icon feedback.
|
> **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
|
||||||
|
|
||||||
### Print Workflow
|
|
||||||
1. **Pre-Print:** Badge print page (`/print`) shows "Already printed N times" warning in screen-only header if `print_count >= 1`
|
|
||||||
2. **Record:** `handle_print_badge()` updates `print_count`, `print_last_datetime`, and `print_first_datetime` (first print only) via API before printing
|
|
||||||
3. **Print:** `window.print()` — standard browser print dialog, wired and working (2026-02-27)
|
|
||||||
4. **Redirect:** After 1 second, `goto(/events/{id}/badges)` returns to search
|
|
||||||
5. **Audit:** `print_first_datetime` and `print_last_datetime` visible in Edit Mode debug row
|
|
||||||
|
|
||||||
**Browser vs Electron:** Badge printing does NOT require the Electron native app. The standard browser print dialog works well across Chrome, Chromium, and Firefox. The Electron native app is specialized for the **Events Pres Mgmt Launcher only** and should not be assumed available for badge stations.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database Schema
|
## Route Map (Badges)
|
||||||
|
|
||||||
### IndexedDB Table: `badge`
|
| URL | Purpose |
|
||||||
**File:** `src/lib/ae_events/db_events.ts` (Lines 841-852)
|
|---|---|
|
||||||
|
| `/events/[id]/badges` | Main search and attendee list. |
|
||||||
**Indexed Fields:**
|
| `/events/[id]/badges/templates` | Badge template management. |
|
||||||
```typescript
|
| `/events/[id]/badges/[id]/print` | The actual print-ready render page. |
|
||||||
badge: `
|
| `/events/[id]/badges/[id]/review` | Attendee-facing self-service form. |
|
||||||
event_badge_id, id,
|
|
||||||
event_id,
|
|
||||||
full_name, full_name_override, email, email_override,
|
|
||||||
affiliations, affiliations_override,
|
|
||||||
badge_type, badge_type_code, badge_type_code_override, badge_type_override,
|
|
||||||
external_event_id, external_id, external_person_id,
|
|
||||||
default_qry_str,
|
|
||||||
alert,
|
|
||||||
tmp_sort_1, tmp_sort_2,
|
|
||||||
print_count, print_first_datetime, print_last_datetime,
|
|
||||||
enable, hide, priority, sort, group, notes, created_on, updated_on
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
### Saved Properties
|
|
||||||
**File:** `ae_events__event_badge.ts` (Lines 495-563)
|
|
||||||
|
|
||||||
**Complete field list** (67 fields total):
|
|
||||||
- Identity: `id`, `event_badge_id`, `event_id`, `event_badge_template_id`
|
|
||||||
- Name: `pronouns`, `informal_name`, `title_names`, `given_name`, `middle_name`, `family_name`, `designations`
|
|
||||||
- Professional: `professional_title`, `professional_title_override`
|
|
||||||
- Display: `full_name`, `full_name_override`
|
|
||||||
- Organization: `affiliations`, `affiliations_override`
|
|
||||||
- Contact: `email`, `email_override`
|
|
||||||
- Address: `address_line_1`, `address_line_2`, `address_line_3`, `city`, `country_subdivision_code`, `state_province`, `state_province_abb`, `postal_code`, `country_alpha_2_code`, `country`, `full_address`
|
|
||||||
- Location: `location`, `location_override`
|
|
||||||
- Classification: `badge_type`, `badge_type_code`, `badge_type_override`, `badge_type_code_override`
|
|
||||||
- External: `external_event_id`, `external_id`, `external_person_id`
|
|
||||||
- Search: `query_str`, `default_qry_str`
|
|
||||||
- System: `alert`, `enable`, `hide`, `priority`, `sort`, `group`, `notes`, `created_on`, `updated_on`
|
|
||||||
- Print: `print_count`, `print_first_datetime`, `print_last_datetime`
|
|
||||||
- Sorting: `tmp_sort_1`, `tmp_sort_2`
|
|
||||||
- Person Link: `person_external_id`, `person_external_sys_id`, `person_given_name`, `person_family_name`, `person_full_name`, `person_professional_title`, `person_affiliations`, `person_primary_email`, `person_passcode`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Functions
|
|
||||||
|
|
||||||
### CRUD Operations
|
|
||||||
**File:** `src/lib/ae_events/ae_events__event_badge.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Load single badge
|
|
||||||
load_ae_obj_id__event_badge({ event_badge_id, event_id, inc_template })
|
|
||||||
|
|
||||||
// Load badge list
|
|
||||||
load_ae_obj_li__event_badge({ event_id, view, limit, order_by_li })
|
|
||||||
|
|
||||||
// Search badges (V3 API)
|
|
||||||
search__event_badge({
|
|
||||||
event_id,
|
|
||||||
fulltext_search_qry_str,
|
|
||||||
type_code,
|
|
||||||
printed_status,
|
|
||||||
affiliations_qry_str,
|
|
||||||
order_by_li
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create badge
|
|
||||||
create_ae_obj__event_badge({ event_id, data_kv })
|
|
||||||
|
|
||||||
// Update badge
|
|
||||||
update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })
|
|
||||||
|
|
||||||
// Delete badge
|
|
||||||
delete_ae_obj_id__event_badge({ event_badge_id, event_id, method })
|
|
||||||
```
|
|
||||||
|
|
||||||
### Field Processing
|
|
||||||
**Function:** `process_ae_obj__event_badge_props()`
|
|
||||||
|
|
||||||
**Processing Steps:**
|
|
||||||
1. Map `*_random` fields to clean names (`event_badge_id_random` → `event_badge_id`)
|
|
||||||
2. Set primary `id` field from `event_badge_id`
|
|
||||||
3. Ensure `event_id` is set (from function parameter if missing)
|
|
||||||
4. Calculate `tmp_sort_1` and `tmp_sort_2` for efficient sorting
|
|
||||||
5. Return processed objects
|
|
||||||
|
|
||||||
**Critical Fix (2026-02-26):** All CRUD functions now return **processed** data (matches IDB cache) instead of raw API responses. This ensures consistency between function return values and cached data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Component Architecture
|
|
||||||
|
|
||||||
### Route Structure
|
|
||||||
```
|
|
||||||
/events/[event_id]/(badges)/badges/
|
|
||||||
├── +layout.svelte # Layout wrapper (minimal)
|
|
||||||
├── +page.svelte # Badge list + search
|
|
||||||
├── ae_comp__badge_search.svelte # Search form + filters
|
|
||||||
├── ae_comp__badge_obj_li.svelte # Badge list display (results)
|
|
||||||
├── ae_comp__badge_create_form.svelte # (Not actively used)
|
|
||||||
├── ae_comp__badge_upload_form.svelte # Bulk CSV upload
|
|
||||||
└── [badge_id]/
|
|
||||||
├── ae_comp__badge_obj_view.svelte # Badge rendering + staff edit + print button
|
|
||||||
├── ae_comp__badge_review_form.svelte # Form-based field review/edit (attendee + staff)
|
|
||||||
├── print/
|
|
||||||
│ ├── +page.ts # Non-blocking badge loader (inc_template: true)
|
|
||||||
│ └── +page.svelte # Print-focused page — screen header + badge render
|
|
||||||
└── review/
|
|
||||||
├── +page.ts # Non-blocking badge loader (inc_template: false)
|
|
||||||
└── +page.svelte # Passcode-gated review page
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** The old `[badge_id]/+page.svelte` placeholder was removed (2026-02-27). The name link in the search results list now goes directly to `/print`.
|
|
||||||
|
|
||||||
#### Badge Print Page (`/print`)
|
|
||||||
- Screen-only header (`print:hidden`): "Back to Search" link + "Already printed N times" warning
|
|
||||||
- Badge rendered via `ae_comp__badge_obj_view` with `is_review_mode={false}`
|
|
||||||
- Print button inside `ae_comp__badge_obj_view` handles count update → `window.print()` → redirect to search
|
|
||||||
- Page `<title>` includes badge name + event name
|
|
||||||
|
|
||||||
#### Badge Review Page (`/review`)
|
|
||||||
- Passcode-gated for attendees — URL `?passcode=...` matched against `badge.person_passcode`
|
|
||||||
- **Note:** `person_passcode` field is not yet in the DB (as of 2026-02-27). Review page accessible to staff via `trusted_access` without a passcode.
|
|
||||||
- Access hierarchy (checked in order):
|
|
||||||
1. Administrator → full access (`can_edit_fields = ['*']`)
|
|
||||||
2. Trusted Staff → staff field set
|
|
||||||
3. Attendee with valid passcode → attendee field set
|
|
||||||
4. No access → passcode entry form shown
|
|
||||||
- Uses `ae_comp__badge_review_form.svelte` (NOT badge render)
|
|
||||||
- "Back to Search" link shown for staff only
|
|
||||||
|
|
||||||
### Key Components
|
|
||||||
|
|
||||||
**Badge List Page** (`+page.svelte`)
|
|
||||||
- **LiveQuery:** Reactive badge list from IDB
|
|
||||||
- **Search Pattern:** Debounced search with fast path + revalidation
|
|
||||||
- **ID List:** `event_badge_id_li` drives LiveQuery
|
|
||||||
- **Loading State:** Shows spinner when `search_status === 'loading'`
|
|
||||||
|
|
||||||
**Badge Search** (`ae_comp__badge_search.svelte`)
|
|
||||||
- **Form Mode:** Toggle between search form and QR scanner
|
|
||||||
- **Filters:** Badge type, print status, affiliations, sort order (trusted+ only)
|
|
||||||
- **Fulltext:** Name/email search (all users)
|
|
||||||
- **QR Scan:** Integrated QR scanner for badge ID lookup
|
|
||||||
|
|
||||||
**Badge List Display** (`ae_comp__badge_obj_li.svelte`)
|
|
||||||
- **Visibility Filter:** Respects `hide` flag (trusted+ sees all)
|
|
||||||
- **Display Logic:** Override → regular → fallback pattern
|
|
||||||
- **Print Indicator:** Green checkmark badge shows `print_count`
|
|
||||||
- **Metadata:** ID, created/updated timestamps (edit mode only)
|
|
||||||
|
|
||||||
**Badge Detail View** (`ae_comp__badge_obj_view.svelte`)
|
|
||||||
- **Edit Mode:** Activated by edit button (or `#review` URL hash for future self-service)
|
|
||||||
- **Condition:** Renders only when BOTH `$lq__event_badge_obj` AND `$lq__event_badge_template_obj` are non-null
|
|
||||||
- **Form Binding:** Direct `bind:value` on editable fields
|
|
||||||
- **Dynamic Sizing:** Font size adjusts based on text length
|
|
||||||
- **Print Preview:** Full badge layout with template
|
|
||||||
- **Save Handler:** Only sends changed fields to API
|
|
||||||
- **`data-testid` attributes:** `badge-edit-btn`, `badge-save-btn`, `badge-cancel-btn`, `badge-print-btn`, `badge-professional-title-input` — use these in tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Status
|
|
||||||
|
|
||||||
### Current Test Coverage
|
|
||||||
- ✅ Badge list loads (all 6 data integrity tests passing)
|
|
||||||
- ✅ Badge template list loads and displays
|
|
||||||
- ✅ Badge template form renders and populates correctly
|
|
||||||
- ✅ Badge template values persist in edit form
|
|
||||||
- ✅ Electron bridge compatibility (graceful degradation in browser)
|
|
||||||
- ✅ Badge field processor handles missing optional fields
|
|
||||||
- ✅ Badge type filter tests
|
|
||||||
- ✅ Badge template relationship tests
|
|
||||||
- ✅ **Attendee workflow test** — navigate → edit professional title → print → return (d1ded2d4)
|
|
||||||
|
|
||||||
### Key Test Lessons Learned
|
|
||||||
|
|
||||||
**Search API path is FLAT, not nested.** `search_ae_obj` builds `/v3/crud/{obj_type}/search` — always flat regardless of the parent relationship. Mocks must match this:
|
|
||||||
```typescript
|
|
||||||
// CORRECT — flat path
|
|
||||||
url.includes('/v3/crud/event_badge/search') && method === 'POST'
|
|
||||||
// WRONG — nested path, mock will never fire
|
|
||||||
url.includes(`/v3/crud/event/${event_id}/event_badge/search`) && method === 'POST'
|
|
||||||
```
|
|
||||||
|
|
||||||
**List API (GET) is also FLAT with query params.** `get_ae_obj_li` builds `/v3/crud/{obj_type}/?for_obj_id=...` — always flat. Mocks must check `url.includes('/v3/crud/event_badge_template/') && url.includes('for_obj_id')`.
|
|
||||||
|
|
||||||
**CSS `input[value*=...]` selectors don't work with Svelte bind:value.** The CSS selector checks the HTML *attribute*; Svelte's `bind:value` sets the DOM *property* only. In Playwright tests, use `page.getByLabel()` or `locator.inputValue()` instead.
|
|
||||||
|
|
||||||
**Dexie requires `_random` ID fields.** Badge objects saved to IDB must include:
|
|
||||||
```typescript
|
|
||||||
event_badge_id_random: string // Must be present or Dexie skips the object
|
|
||||||
id_random: string // Also checked
|
|
||||||
// Error: "Object is missing a valid ID for table 'badge'"
|
|
||||||
```
|
|
||||||
All API mock responses in tests need these fields.
|
|
||||||
|
|
||||||
**Badge view requires both badge AND template.** `ae_comp__badge_obj_view.svelte` wraps everything in `{#if $lq__event_badge_obj && $lq__event_badge_template_obj}` — if the template isn't loaded, edit/print buttons and the badge itself don't render. Tests must mock the badge template endpoint.
|
|
||||||
|
|
||||||
**Badge GET endpoint (single object):** `/v3/crud/event_badge/{id}` (NOT nested under event). Matches `api.get_ae_obj()` which uses the flat path.
|
|
||||||
|
|
||||||
**Badge PATCH endpoint (update):** `/v3/crud/event/${event_id}/event_badge/${badge_id}` (nested under event). Matches `api.patch_ae_obj()` which uses the nested path.
|
|
||||||
|
|
||||||
**Use `data-testid` for test selectors.** Key buttons have targets: `badge-edit-btn`, `badge-save-btn`, `badge-cancel-btn`, `badge-print-btn`, `badge-professional-title-input`.
|
|
||||||
|
|
||||||
### Remaining Test Issues
|
|
||||||
None — all current badge tests passing as of 2026-02-26 (f5e98b8c).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Issues & Future Enhancements
|
|
||||||
|
|
||||||
### Known Issues
|
|
||||||
1. **Session Cold-Start:** Potential race condition on first load (same as pres mgmt module)
|
|
||||||
2. **Type Definitions:** Some pre-existing TypeScript errors on external package types (not introduced by badge work)
|
|
||||||
3. **`person_passcode` not in DB:** Attendee-gated review URL (`?passcode=...`) cannot function until this field is added to the `event_badge` schema. The review page falls back to passcode entry form for non-staff.
|
|
||||||
4. **Print page CSS:** Badge print rendering and `@page` print styles not yet fine-tuned — expected to need work
|
|
||||||
5. **`mod_badges_json.edit_permissions` not connected:** Settings UI exists but review page uses hardcoded field defaults
|
|
||||||
|
|
||||||
### Implemented (2026-02-27)
|
|
||||||
- ✅ `window.print()` wired to print button (records count first, then prints, then redirects)
|
|
||||||
- ✅ Dedicated `/print` page — replaces old `[badge_id]/+page.svelte` placeholder
|
|
||||||
- ✅ Dedicated `/review` page — passcode-gated, access-tiered
|
|
||||||
- ✅ `ae_comp__badge_review_form.svelte` — stub created, full form fields pending
|
|
||||||
- ✅ Badge search results visibility rules (unprinted-only for non-edit, all for trusted+edit)
|
|
||||||
- ✅ Badge list: 4 action buttons per row (Print, Review nav, Copy Link, Email Link) — all Lucide icons
|
|
||||||
- ✅ Print page: 3 action buttons in header (Print Now, Review nav, Email Link) — all Lucide icons
|
|
||||||
- ✅ Review page: 3 action buttons in header (Print nav, Copy Link, Email Link) — all Lucide icons
|
|
||||||
- ✅ Print button: not shown when already printed (unless Edit Mode)
|
|
||||||
- ✅ Print count shown as `Nx` badge next to printer icon
|
|
||||||
- ✅ Email obscuring for non-trusted users
|
|
||||||
- ✅ Email Review Link button (placeholder alert — email API pending)
|
|
||||||
- ✅ Direct Review Link clipboard copy (trusted + Edit Mode only)
|
|
||||||
- ✅ Fixed: components no longer write to `$ae_loc.edit_mode`
|
|
||||||
- ✅ Settings UI for `edit_permissions` per event (`ae_comp__event_settings_badges_form.svelte`)
|
|
||||||
- ✅ All badge module icons converted to Lucide (Font Awesome removed from badge routes)
|
|
||||||
|
|
||||||
### Recently Completed (2026-02-27)
|
|
||||||
- ✅ **Badge Review Form** — `ae_comp__badge_review_form.svelte` fully implemented (fields, QR, save/cancel, options/tickets, accessibility, help modal)
|
|
||||||
- ✅ **Print font size controls (v1)** — Screen-only `[−]/[+]/[↺]` panel on print page; 4 px props added to `ae_comp__badge_obj_view.svelte`; auto-sizing unchanged when props absent
|
|
||||||
- ✅ **Bug fix** — `default_authenticated_fields` / `default_trusted_fields` in `review/+page.svelte` corrected (wrong field names caused silent save drops)
|
|
||||||
|
|
||||||
### Still Needed — HIGH PRIORITY (first show: April 2026)
|
|
||||||
|
|
||||||
### Still Needed — MEDIUM PRIORITY
|
|
||||||
1. **Email API for review links:** `send_review_email()` is a placeholder `alert()`. Needs actual email send endpoint.
|
|
||||||
2. **`person_passcode` DB field:** Add to `event_badge` schema to enable attendee-gated review URLs.
|
|
||||||
3. **Connect `edit_permissions` config:** Read `mod_badges_json.edit_permissions` in review page instead of hardcoded defaults.
|
|
||||||
4. **Print page CSS / `@page` styles:** Badge rendering, sizing, and print-specific stylesheet.
|
|
||||||
|
|
||||||
### Still Needed — FUTURE / LOW PRIORITY
|
|
||||||
1. **Batch Operations:** Bulk update, bulk print, bulk export
|
|
||||||
2. **Audit Log:** Track who edited which fields and when
|
|
||||||
3. **Photo Badges:** Support badge photo upload and display
|
|
||||||
4. **Real-Time Sync:** WebSocket updates for multi-device badge printing stations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### Adding New Override Fields
|
|
||||||
1. Add `{field}_override` to database schema
|
|
||||||
2. Add to `properties_to_save` array in `ae_events__event_badge.ts`
|
|
||||||
3. Update display logic to check override first
|
|
||||||
4. Add to editable fields in `ae_comp__badge_obj_view.svelte`
|
|
||||||
5. Update access control config
|
|
||||||
6. Document in this file
|
|
||||||
|
|
||||||
### Testing Override Fields
|
|
||||||
```typescript
|
|
||||||
// Simulate external sync
|
|
||||||
badge.given_name = "External Value"
|
|
||||||
|
|
||||||
// User edits
|
|
||||||
badge.given_name_override = "User Value"
|
|
||||||
|
|
||||||
// Next sync (should NOT change override)
|
|
||||||
badge.given_name = "Updated External Value"
|
|
||||||
|
|
||||||
// Display should still show "User Value"
|
|
||||||
assert(display === badge.given_name_override)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging Search Issues
|
|
||||||
```typescript
|
|
||||||
// Enable search logging
|
|
||||||
log_lvl: 2
|
|
||||||
|
|
||||||
// Check search params object
|
|
||||||
console.log('Search params:', search_params)
|
|
||||||
|
|
||||||
// Verify API request
|
|
||||||
console.log('API request:', { event_id, fulltext_search_qry_str, type_code })
|
|
||||||
|
|
||||||
// Check returned IDs
|
|
||||||
console.log('Badge IDs:', event_badge_id_li)
|
|
||||||
|
|
||||||
// Verify IDB contents
|
|
||||||
db_events.badge.toArray().then(console.log)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md)
|
👉 **[MODULE__AE_Events_Badge_Templates.md](./MODULE__AE_Events_Badge_Templates.md)** (Technical reference for layouts)
|
||||||
- [Development Guide](./GUIDE__Development.md)
|
👉 **[GUIDE__AE_Events_Badges_Onsite.md](./GUIDE__AE_Events_Badges_Onsite.md)** (Hardware & station setup)
|
||||||
- [Events Launcher Native Integration](./PROJECT__AE_Events_Launcher_Native_integration.md)
|
👉 **[GUIDE__AE_Events_Onsite_Runbook.md](./GUIDE__AE_Events_Onsite_Runbook.md)** (Onsite operational checklists)
|
||||||
- [Naming Conventions](./AE__Naming_Conventions.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Status:** 🔄 In Progress
|
|
||||||
**Last Verified:** 2026-02-27 (rev 5 — field permissions spec added, header buttons implemented, review form fields pending)
|
|
||||||
**Verified Against:** Code as of 2026-02-27 (branch ae_app_3x_llm)
|
|
||||||
|
|||||||
79
documentation/MODULE__AE_Events_Launcher.md
Normal file
79
documentation/MODULE__AE_Events_Launcher.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# 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) |
|
||||||
94
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
94
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 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).
|
||||||
110
documentation/MODULE__AE_Events_Launcher_Config_Menu_new.md
Normal file
110
documentation/MODULE__AE_Events_Launcher_Config_Menu_new.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# 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.
|
||||||
275
documentation/MODULE__AE_Events_Launcher_Native.md
Normal file
275
documentation/MODULE__AE_Events_Launcher_Native.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# 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.
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
**Platform:** PWA only — mobile-first, offline-capable.
|
**Platform:** PWA only — mobile-first, offline-capable.
|
||||||
**Target users:** Conference exhibitors scanning attendee badges at their booths.
|
**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
|
## What It Does
|
||||||
@@ -17,7 +23,7 @@ Key capabilities:
|
|||||||
|
|
||||||
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
|
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
|
||||||
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
|
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
|
||||||
- **Lead detail** — custom question responses, notes (rich text), priority flag, hide/unhide
|
- **Lead detail** — custom question responses, notes (rich text), priority boolean flag, hide/unhide
|
||||||
- **Export** — CSV/XLSX download of all leads for an exhibit
|
- **Export** — CSV/XLSX download of all leads for an exhibit
|
||||||
- **License management** — assign staff accounts (email + passcode) per max license count
|
- **License management** — assign staff accounts (email + passcode) per max license count
|
||||||
- **Custom questions** — configurable per-exhibit follow-up questions (ratings, dropdowns, text)
|
- **Custom questions** — configurable per-exhibit follow-up questions (ratings, dropdowns, text)
|
||||||
@@ -103,7 +109,7 @@ The main lead management view.
|
|||||||
Exhibit configuration and app settings.
|
Exhibit configuration and app settings.
|
||||||
|
|
||||||
**Admin Tools** (manager_access only):
|
**Admin Tools** (manager_access only):
|
||||||
- Payment status toggle (`priority` field)
|
- Payment status toggle (`priority` boolean field)
|
||||||
- Max licenses, small/large device counts
|
- Max licenses, small/large device counts
|
||||||
|
|
||||||
**Booth Profile** (all signed-in users):
|
**Booth Profile** (all signed-in users):
|
||||||
@@ -114,13 +120,12 @@ Exhibit configuration and app settings.
|
|||||||
- Sign out button
|
- Sign out button
|
||||||
|
|
||||||
**Lead Retrieval Config**:
|
**Lead Retrieval Config**:
|
||||||
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` only; gap: should also allow shared-passcode users — see Known Gaps)
|
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` OR signed in via shared exhibit passcode)
|
||||||
- Qualifiers & Questions — custom question config
|
- Qualifiers & Questions — custom question config
|
||||||
- Licenses & Billing — stub (Stripe not yet implemented)
|
- Licenses & Billing — Stripe payment (only shown when `event.mod_exhibits_json.leads_require_payment = true`)
|
||||||
|
|
||||||
**App Settings**:
|
**App Settings**:
|
||||||
- Auto-hide header/footer toggle
|
- Auto-hide header/footer toggle
|
||||||
- Show Payment Tab toggle
|
|
||||||
- Show Extra Details toggle
|
- Show Extra Details toggle
|
||||||
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
||||||
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
||||||
@@ -239,17 +244,27 @@ Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors
|
|||||||
|
|
||||||
## Known Gaps
|
## Known Gaps
|
||||||
|
|
||||||
|
None currently. See TODO__Agents.md for remaining smoke test items.
|
||||||
|
|
||||||
|
## Implemented (previously listed as gaps)
|
||||||
|
|
||||||
### Payment / Stripe
|
### Payment / Stripe
|
||||||
`ae_comp__exhibit_payment.svelte` is a stub. The Stripe integration is not implemented.
|
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
|
||||||
The payment tab can be hidden via "Show Payment Tab" in App Settings.
|
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
|
### License Management — Shared Passcode Access
|
||||||
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
|
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'`).
|
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
|
||||||
Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte):
|
|
||||||
```svelte
|
### "My Leads" filter for shared-passcode users
|
||||||
{#if $ae_loc.administrator_access || $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.type === 'shared'}
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -260,3 +275,10 @@ Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhi
|
|||||||
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
|
- 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)
|
- 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]/`
|
- 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
|
||||||
139
documentation/MODULE__AE_Events_Presentation_Management.md
Normal file
139
documentation/MODULE__AE_Events_Presentation_Management.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# 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` |
|
||||||
@@ -124,7 +124,7 @@ the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badg
|
|||||||
- `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
|
- `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
|
||||||
"Staff adjustments" divider before badge_type field.
|
"Staff adjustments" divider before badge_type field.
|
||||||
- `print_list/+page.svelte` — Updated to import v2.
|
- `print_list/+page.svelte` — Updated to import v2.
|
||||||
- `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/gemini_trash/**
|
- `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/agents_trash/**
|
||||||
|
|
||||||
**Kiosk UX improvements (2026-03-12):**
|
**Kiosk UX improvements (2026-03-12):**
|
||||||
- Print page header: cleaner, shows name + "Ready"/"Printed N×" status chip, event name.
|
- Print page header: cleaner, shows name + "Ready"/"Printed N×" status chip, event name.
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
# Aether Events Launcher: Native Electron Integration
|
|
||||||
|
|
||||||
> **Status:** Operational / Phase 5 Implementation
|
|
||||||
> **Last Updated:** 2026-03-11
|
|
||||||
> **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.
|
|
||||||
- Implementing "Smart Fallbacks" (e.g., resolving `[home]` placeholders if the bridge is partially hydrated).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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/data_store/code/{device_code}` or `GET /v3/crud/event_device/{id}` to pull operational context.
|
|
||||||
3. **Hydrate:** Authenticates with the Aether V3 API and injects the **JWT** and **Device Config** into the UI environment.
|
|
||||||
4. **Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
1. Extracts all `event_file_id` values for that session.
|
|
||||||
2. Checks the native cache via `aetherNative.check_cache`.
|
|
||||||
3. Triggers background downloads for missing files via `aetherNative.download_to_cache`.
|
|
||||||
|
|
||||||
### 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})` — Mirror / Extend via `displayplacer`. macOS only.
|
|
||||||
- **Power Control:** `power_control({action})` — Shutdown, reboot, sleep. macOS + Linux.
|
|
||||||
- **Window Control:** `window_control({action})` — Maximize, minimize, fullscreen, kiosk mode.
|
|
||||||
- **Wallpaper:** `set_wallpaper({path})` — macOS (AppleScript) + Linux (gsettings).
|
|
||||||
|
|
||||||
> **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_hash_file_cache({cache_root, hash, hash_prefix_length?})` — Verifies a file exists in the local hashed cache.
|
|
||||||
- `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.
|
|
||||||
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?})` — Atomic "Safe Handover": copy from cache → tmp → rename → execute.
|
|
||||||
- `cleanup_tmp_files({cache_root, max_age_minutes?})` — Removes stale `*.tmp` download artifacts. Default: 1440 min (24h). Called at launcher startup.
|
|
||||||
|
|
||||||
> `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.
|
|
||||||
- `kill_processes({process_name_li})` — Gracefully terminates processes by name.
|
|
||||||
- `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})` — Sets desktop wallpaper. macOS (AppleScript) + Linux (gsettings).
|
|
||||||
- `window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk.
|
|
||||||
- `set_display_layout({mode, configStr?})` — Mirror or extend displays via displayplacer. macOS only.
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
238
documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md
Normal file
238
documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
# 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.
|
||||||
408
documentation/PROJECT__AE_Site_Passcode_Security.md
Normal file
408
documentation/PROJECT__AE_Site_Passcode_Security.md
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
# 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,7 +1,7 @@
|
|||||||
# Aether Journals UI Update (2026)
|
# Aether Journals UI Update (2026)
|
||||||
|
|
||||||
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Style pass complete)
|
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Journal Entry config rework in progress)
|
||||||
> **Last Updated:** 2026-03-06
|
> **Last Updated:** 2026-05-05
|
||||||
> **Primary Agent:** Frontend SvelteKit Agent
|
> **Primary Agent:** Frontend SvelteKit Agent
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@@ -72,6 +72,10 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
- [x] Implement Auto-Save toggle and visual status indicators.
|
- [x] Implement Auto-Save toggle and visual status indicators.
|
||||||
- [x] Extract decryption workflow to non-reactive helper.
|
- [x] Extract decryption workflow to non-reactive helper.
|
||||||
- [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
|
- [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] **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] **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] **Dark mode fixes:** Entry content hover, journal view section/description background and text colors.
|
||||||
@@ -100,6 +104,7 @@ We have established a unified design language for configuration interfaces and a
|
|||||||
* **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.
|
* **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).
|
* **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.
|
* **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.
|
* **Explicit Persistence:** Follow "Edit working copy → Save Changes" pattern to prevent accidental store/API churn.
|
||||||
|
|
||||||
#### Skeleton v4 Style Token Reference (Journals = canonical example)
|
#### Skeleton v4 Style Token Reference (Journals = canonical example)
|
||||||
@@ -111,6 +116,7 @@ We have established a unified design language for configuration interfaces and a
|
|||||||
| Success (confirmed save) | `btn preset-filled-success` |
|
| Success (confirmed save) | `btn preset-filled-success` |
|
||||||
| Warning (caution action) | `btn preset-tonal-warning hover:preset-filled-warning-500` |
|
| 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` |
|
| 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` |
|
| Active tab | `preset-filled-primary` |
|
||||||
| Inactive tab | `preset-tonal-surface` |
|
| Inactive tab | `preset-tonal-surface` |
|
||||||
| Icon button | `btn-icon btn-icon-sm preset-tonal-surface` |
|
| Icon button | `btn-icon btn-icon-sm preset-tonal-surface` |
|
||||||
@@ -143,6 +149,14 @@ 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 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.
|
* **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`)
|
### 3. Concurrency Locking (`is_processing`)
|
||||||
* **The Problem:** Decryption (Async) and Auto-Save (Debounced Async) can fire nearly simultaneously.
|
* **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`.
|
* **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`.
|
||||||
|
|||||||
97
documentation/PROJECT__Stores_Svelte5_Migration.md
Normal file
97
documentation/PROJECT__Stores_Svelte5_Migration.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 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,7 +1,7 @@
|
|||||||
# Project: CRUD V3 Final Migration
|
# Project: CRUD V3 Final Migration
|
||||||
|
|
||||||
> **Status:** Active / In Progress
|
> **Status:** 🟡 Surgical Cleanup (90% Complete — Events Module Fully Migrated)
|
||||||
> **Last Updated:** 2026-01-20
|
> **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/...`).
|
> **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/...`).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -21,23 +21,23 @@ While the **Journals** and **Identity (User/Account)** modules have been success
|
|||||||
The following files have been identified as using legacy CRUD wrappers.
|
The following files have been identified as using legacy CRUD wrappers.
|
||||||
|
|
||||||
### 🔴 High Priority: Events Module
|
### 🔴 High Priority: Events Module
|
||||||
The entire `ae_events` library is heavily dependent on legacy `v2` list and `v1` CRUD wrappers.
|
- [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)
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_session.ts`
|
- [x] `src/lib/ae_events/ae_events__event_presentation.ts` (Migrated 2026-01-30)
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_presenter.ts`
|
- [x] `src/lib/ae_events/ae_events__event_location.ts` (Migrated 2026-01-30)
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_presentation.ts`
|
- [x] `src/lib/ae_events/ae_events__event_badge_template.ts` (Migrated 2026-01-30)
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_location.ts`
|
- [x] `src/lib/ae_events/ae_events__event_device.ts` (Migrated 2026-01-30)
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_badge_template.ts`
|
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_device.ts`
|
|
||||||
- [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28)
|
- [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28)
|
||||||
- [ ] `src/lib/ae_events/ae_events__event_file.ts`
|
- [x] `src/lib/ae_events/ae_events__event_file.ts` (Migrated 2026-01-30)
|
||||||
|
|
||||||
### 🟠 Medium Priority: Core & Sponsorships
|
### 🟠 Medium Priority: Core & Sponsorships
|
||||||
Legacy patterns persisting in core logic and config modules.
|
Legacy patterns persisting in core logic and config modules.
|
||||||
|
|
||||||
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
||||||
- [ ] `src/lib/ae_core/core__hosted_files.ts` (Uses `get_ae_obj_id_crud`)
|
- [x] `src/lib/ae_core/core__hosted_files.ts` (Migrated 2026-01-20)
|
||||||
- [ ] `src/lib/ae_core/core__site.ts` (Uses `get_ae_obj_id_crud`)
|
- [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__country_subdivisions.ts`
|
||||||
- [ ] `src/lib/ae_core/core__time_zones.ts`
|
- [ ] `src/lib/ae_core/core__time_zones.ts`
|
||||||
- [ ] `src/lib/ae_core/core__countries.ts`
|
- [ ] `src/lib/ae_core/core__countries.ts`
|
||||||
@@ -48,9 +48,10 @@ Specific UI components that make direct API calls instead of using store functio
|
|||||||
- [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`)
|
- [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`)
|
||||||
- [x] `src/lib/elements/element_data_store_v2.svelte`
|
- [x] `src/lib/elements/element_data_store_v2.svelte`
|
||||||
- [ ] `src/routes/events/[event_id]/event_page_menu.svelte`
|
- [ ] `src/routes/events/[event_id]/event_page_menu.svelte`
|
||||||
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.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/events/ae_comp__event_session_obj_li.svelte`
|
||||||
- [ ] `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.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`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
58
documentation/PROPOSAL__IDAA_UI_UX_Roadmap_2026.md
Normal file
58
documentation/PROPOSAL__IDAA_UI_UX_Roadmap_2026.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# 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,137 +1,101 @@
|
|||||||
# Frontend Agent Task List
|
# Frontend Agent Task List
|
||||||
> Use this file to track steps for complex features or bug fixes.
|
> Use this file to track steps for complex features or bug fixes.
|
||||||
> **Status:** Stable — ongoing development.
|
> **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**
|
||||||
|
|
||||||
## 🚧 Upcoming High Priority
|
- [ ] **[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.
|
||||||
|
|
||||||
### [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
|
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||||
`$ae_loc.*` auth-field read sites across 150+ files. Deferred until a Svelte runes migration
|
**Setup/Registration:** June 8 | **Show:** June 9
|
||||||
of the store layer itself (touching every component anyway makes the callsite sweep cheap).
|
|
||||||
|
|
||||||
### [Backend] Join event_location_id onto event_presenter API view
|
- [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
|
||||||
The `event_presenter` object currently has `event_session_id` but not `event_location_id`.
|
wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
|
||||||
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.
|
|
||||||
- Backend: add `event_location_id` (and `event_location_id_random`) to the `event_presenter`
|
|
||||||
view or API response
|
|
||||||
- Frontend: add `event_location_id` to `ae_EventPresenter` type and `properties_to_save`;
|
|
||||||
pass as `events__launcher_id` in `presenter_page_menu.svelte`
|
|
||||||
|
|
||||||
### [Launcher] Active features (identified 2026-03-06)
|
---
|
||||||
|
|
||||||
- [x] **Font size cycler (Launcher sidebar):** Font size cycler and light/dark toggle added to new `menu_launcher_controls.svelte` component; wired into `launcher_menu.svelte`. Visibility toggles (All Files / All Sessions) moved to same component and restyled to `preset-tonal-tertiary`. (2026-03-11)
|
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||||
|
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||||
|
|
||||||
- [x] **Minor Svelte warning:** `slct_event_location_id` in `menu_location_list.svelte` — prop already has `$bindable(null)`; stale comment in file updated. (2026-03-11)
|
- [ ] **[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`.
|
||||||
|
|
||||||
### [Svelte] State reference warnings
|
---
|
||||||
- [x] **`svelte-check` fully clean — 0 errors, 0 warnings.** All 42 `state_referenced_locally` warnings fixed (2026-03-11). CSS `@apply`/`@reference` warnings in `ae_idaa_comp__event_obj_id_edit.svelte` also resolved — Tailwind utilities inlined, `<style>` block removed. (2026-03-16)
|
|
||||||
|
|
||||||
### [Badges] Remaining badge work before first live event
|
## 🚧 High Priority Workstreams
|
||||||
- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
|
|
||||||
File: `ae_comp__badge_print_controls.svelte`.
|
|
||||||
|
|
||||||
### [Badges] Zebra ZC10L Hardware Testing — ~week of 2026-03-16
|
### [Stores] Svelte 4 → Svelte 5 State Migration
|
||||||
Scott is renting a Zebra ZC10L for one day to do real-world badge printing tests before
|
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
|
||||||
Axonius (mid-April). See `documentation/PROJECT__AE_Events_Zebra_Hardware_Test_Day.md`
|
`$state`-based persistence for fine-grained updates.
|
||||||
for the full checklist and prep plan.
|
|
||||||
|
|
||||||
**Pre-test work (do before printer arrives):**
|
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||||
- [x] **Debug outlines are gated** — `print/+page.svelte` prints nothing; outlines live in
|
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
|
||||||
`static/ae-print-badge.css` behind `html.debug_outlines` class (toggled by the "Show debug
|
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
||||||
outlines" checkbox in the controls panel, trusted-only). Won't appear in print unless explicitly
|
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
||||||
enabled. No action needed. (verified 2026-03-18)
|
|
||||||
- [ ] **Zebra ZC10L Linux driver** — install CUPS driver package ahead of time; verify card prints
|
|
||||||
before burning rental time on driver setup. Check Zebra's site for Linux/CUPS driver.
|
|
||||||
- [x] **`style_href` wired** — `print/+page.svelte` already loads `style_href` via `<svelte:head>`
|
|
||||||
and it's in `properties_to_save`. (verified 2026-03-18)
|
|
||||||
- [x] **`duplex=0` hides badge back** — `duplex` is in `properties_to_save`; v2 badge render
|
|
||||||
gates `{#if show_badge_back}` on `duplex != null && !!duplex`. Set `duplex=0` on the template
|
|
||||||
to suppress the back section for single-sided PVC. (verified 2026-03-18)
|
|
||||||
- [ ] **Set up test event + PVC template** in dev DB with `layout: badge_3.5x5.5_pvc`,
|
|
||||||
`duplex=0`, a few badge records with varied name lengths, HTML in fields, different badge_type_codes.
|
|
||||||
- [ ] **Test data set:** include edge cases — very long name, HTML markup in name/affiliations,
|
|
||||||
badge with no affiliations, badge with all ticket/option codes set.
|
|
||||||
|
|
||||||
### [Leads] Exhibitor Lead Scanning — IN PROGRESS (demo-ready prep)
|
### [Data Layer] IDB sorting + content version rollout
|
||||||
Module is substantially built as a PWA (no Electron). Core flow works end-to-end.
|
Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
|
||||||
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:**
|
**⚠️ Exception:** `ae_events__event.ts` and `ae_events__event_session.ts` use **legacy encoding**
|
||||||
- Exhibit search/landing (`/leads/`) — SWR, local + API search, sort
|
(`priority ? 1 : 0`, priority=true→`'1'`). Their sort comparators must remain **descending**
|
||||||
- Exhibit detail page — 4-tab layout, sticky header with Add/List toggle, auto-refresh timer
|
until the modules are migrated to `build_tmp_sort`. `ae_events__event_presentation.ts` already
|
||||||
- Tab 1 (Start): sign-in via shared passcode OR licensed user (email + passcode)
|
uses `build_tmp_sort` (overrides generic encoding in its `specific_processor`). See
|
||||||
- Tab 2 (Add): QR scan (rapid vs. qualify mode) + manual badge search; duplicate detection on both
|
`CLIENT__IDAA_and_customized_mods.md` → "Sort Encoding" for full table.
|
||||||
- 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:**
|
- [ ] **[IDB Sort] Migrate `ae_events__event.ts` to `build_tmp_sort`** — requires bumping
|
||||||
- [x] **Export endpoint** — V3 action endpoint confirmed live on backend (2026-03-16). Returns 403 if
|
`IDB_CONTENT_VERSIONS.events.event` (currently v3) and switching all event sort comparators
|
||||||
`leads_api_access` is not enabled on the exhibit — expected behavior. Export button now gated in
|
to ascending. Check all pages that sort events before doing this.
|
||||||
UI: only renders when `$lq__exhibit_obj?.leads_api_access === true`. Enable via:
|
- [ ] **[IDB Sort] Roll out to `ae_events__event_session`** after sort behavior review.
|
||||||
`PATCH /v3/crud/event_exhibit/{id}` with `{ "leads_api_access": true }`.
|
- [ ] **[IDB Sort] Roll out to `ae_events__event_presenter`** after sort behavior review.
|
||||||
- [x] **`allow_tracking` gate** — implemented (2026-03-16). QR scanner shows a warning card and
|
- [ ] **[IDB Sort] Roll out to `ae_events__event_location`** after sort behavior review.
|
||||||
blocks the add. Manual search shows a ShieldOff "Opt-Out" badge per row and guards `add_as_lead`.
|
- [ ] **[IDB Sort] Roll out to `ae_core__person` + `ae_core__account`** after sort behavior review.
|
||||||
Opt-in model: `allow_tracking` must be explicitly `true` on the badge. Also added `allow_tracking`
|
- [ ] **[IDB Version] Roll out to `db_events.ts`** (session, presenter, badge, etc.).
|
||||||
and `agree_to_tc` to `ae_EventBadge` in `ae_types.ts`.
|
- [ ] **[IDB Version] Roll out to `db_core.ts`** (site_domain, person, user).
|
||||||
**Demo note:** ensure test badges have `allow_tracking = true` or no one can be added.
|
|
||||||
- [ ] **Payment component** — `ae_comp__exhibit_payment.svelte` is a stub (Stripe placeholder only);
|
|
||||||
omit from demo or hide the payment tab via "Show Payment Tab" toggle in Manage settings
|
|
||||||
- [ ] **End-to-end smoke test** — sign in with shared passcode, scan/search a badge, add a lead,
|
|
||||||
view detail, add notes/responses, export CSV; verify on mobile (Chrome/Safari PWA)
|
|
||||||
- [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
|
### [Journals] Journal Entry Config follow-ups
|
||||||
- [x] **Wire AE_APP_REPLICAS:** `docker-compose.yml` line 147 already has `scale: ${AE_APP_REPLICAS:-1}`. (verified 2026-03-11)
|
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
|
||||||
- [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)
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### [General]
|
## 🧪 Testing & Optimization
|
||||||
- [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)
|
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
|
||||||
- [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)
|
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
|
||||||
- [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)
|
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
|
||||||
- [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)
|
- [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
|
||||||
- [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)
|
## ⚙️ DevOps & Backend
|
||||||
- [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)
|
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
|
||||||
- [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)
|
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
|
||||||
- [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)
|
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
|
||||||
- [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)
|
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
|
||||||
- [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)
|
backend field (restoring global/cross-device persistence). Frontend code is in
|
||||||
- [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)
|
`launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||||
- [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)
|
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||||
- [x] **[DevOps]** `/health` endpoint live at `src/routes/health/+server.ts`. Docker `HEALTHCHECK` uses it. (2026-03-10)
|
- [x] **[DevOps] Service worker `skipWaiting` + `clients.claim`** — Root cause of "users see
|
||||||
- [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)
|
old code / can't reproduce in dev testing": the SW sat in waiting state until all tabs closed.
|
||||||
- [x] **[Launcher]** Location select → session auto-load bug fixed via `$derived.by()` liveQuery pattern. (2026-03-10)
|
IDAA members leave idaa.org open all day. Fixed 2026-06-03: both calls added to
|
||||||
- [x] **[Svelte]** `state_referenced_locally` warning fixes — 10 warnings resolved in IDAA archives/BB. (2026-03-09)
|
`src/service-worker.js`. See mistake #16 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||||
- [x] **[TypeScript]** Sign In/Out TS errors fixed — `user_id` / `person_id` typed as `string | null`. (2026-03-09)
|
- [ ] **[DevOps] Nginx proxy buffer tuning** — Buffer settings copied from PHP guide; not
|
||||||
- [x] **[Tests]** All badge data integrity and attendee workflow Playwright tests passing. Root causes documented in `tests/README.md`. (2026-03)
|
optimal for Node.js. `proxy_busy_buffers_size` technically exceeds safe limit. Re-examine
|
||||||
- [x] **[Badges]** Badge print controls panel, QR code, duplex wiring, review form, print button, multi-word fulltext search, `data-testid` attributes. (2026-03)
|
when enabling compression (now re-enabled) stabilizes.
|
||||||
- [x] **[UI]** Firefly Theme + Pres Mgmt Visual Redesign (5 files). (2026-03-06)
|
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||||
- [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)
|
## ✅ Completed (archived)
|
||||||
- [x] **[Security]** Playwright integration tests replace `verify_jwt_logic.js` simulation tests. (2026-03)
|
See the full completed history in:
|
||||||
- [x] **[Framework]** `AE_Obj_Field_Editor_V3` with Svelte 5 Runes. CRUD v2 fully retired. (2026-03-05)
|
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
|
||||||
- [x] **[IDAA]** Bulletin Board and Recovery Meetings functionality verified. (2026-02)
|
[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)
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
# 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.
|
||||||
31
documentation/archive/TODO__Agents__ARCHIVE_2026-03.md
Normal file
31
documentation/archive/TODO__Agents__ARCHIVE_2026-03.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## ✅ 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)
|
||||||
227
documentation/archive/TODO__Agents__ARCHIVE_2026-04.md
Normal file
227
documentation/archive/TODO__Agents__ARCHIVE_2026-04.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# 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).
|
||||||
54
documentation/archive/TODO__Agents__ARCHIVE_2026-05.md
Normal file
54
documentation/archive/TODO__Agents__ARCHIVE_2026-05.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 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.
|
||||||
139
documentation/archive/TODO__Agents__ARCHIVE_2026-06.md
Normal file
139
documentation/archive/TODO__Agents__ARCHIVE_2026-06.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# 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)
|
||||||
401
package-lock.json
generated
401
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "osit-aether-app-svelte",
|
"name": "osit-aether-app-svelte",
|
||||||
"version": "3.00.05",
|
"version": "3.00.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "osit-aether-app-svelte",
|
"name": "osit-aether-app-svelte",
|
||||||
"version": "3.00.05",
|
"version": "3.00.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.0",
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
"@codemirror/commands": "^6.10.0",
|
"@codemirror/commands": "^6.10.0",
|
||||||
@@ -26,17 +26,15 @@
|
|||||||
"@lucide/svelte": "^0.*.0",
|
"@lucide/svelte": "^0.*.0",
|
||||||
"@popperjs/core": "^2.11.0",
|
"@popperjs/core": "^2.11.0",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"axios": "^1.7.0",
|
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dexie": "^4.0.0",
|
"dexie": "^4.0.0",
|
||||||
"flowbite-svelte": "^1.28.1",
|
"flowbite-svelte": "^1.28.1",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"lucide-svelte": "^0.*.0",
|
|
||||||
"marked": "^17.0.0",
|
"marked": "^17.0.0",
|
||||||
"openai": "^6.10.0",
|
"openai": "^6.10.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"shadcn-svelte": "^1.0.11",
|
"runed": "^0.37.1",
|
||||||
"svelte-persisted-store": "^0.12.0",
|
"svelte-persisted-store": "^0.12.0",
|
||||||
"typescript-eslint": "^8.47.0"
|
"typescript-eslint": "^8.47.0"
|
||||||
},
|
},
|
||||||
@@ -57,7 +55,6 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||||
"@typescript-eslint/parser": "^8.47.0",
|
"@typescript-eslint/parser": "^8.47.0",
|
||||||
"bits-ui": "^2.14.3",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
@@ -1786,7 +1783,7 @@
|
|||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
@@ -2330,7 +2327,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/acorn-typescript": {
|
"node_modules/@sveltejs/acorn-typescript": {
|
||||||
@@ -2382,7 +2379,7 @@
|
|||||||
"version": "2.53.4",
|
"version": "2.53.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz",
|
||||||
"integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==",
|
"integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
@@ -2424,7 +2421,7 @@
|
|||||||
"version": "6.2.4",
|
"version": "6.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||||
@@ -2445,7 +2442,7 @@
|
|||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
||||||
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"obug": "^2.1.0"
|
"obug": "^2.1.0"
|
||||||
@@ -2793,7 +2790,7 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/deep-eql": {
|
"node_modules/@types/deep-eql": {
|
||||||
@@ -3930,23 +3927,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/axios": {
|
|
||||||
"version": "1.13.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
|
||||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"follow-redirects": "^1.15.11",
|
|
||||||
"form-data": "^4.0.5",
|
|
||||||
"proxy-from-env": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@@ -3965,31 +3945,6 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
|
||||||
"version": "2.16.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.16.3.tgz",
|
|
||||||
"integrity": "sha512-5hJ5dEhf5yPzkRFcxzgQHScGodeo0gK0MUUXrdLlRHWaBOBGZiacWLG96j/wwFatKwZvouw7q+sn14i0fx3RIg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/core": "^1.7.1",
|
|
||||||
"@floating-ui/dom": "^1.7.1",
|
|
||||||
"esm-env": "^1.1.2",
|
|
||||||
"runed": "^0.35.1",
|
|
||||||
"svelte-toolbelt": "^0.10.6",
|
|
||||||
"tabbable": "^6.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/huntabyte"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@internationalized/date": "^3.8.1",
|
|
||||||
"svelte": "^5.33.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||||
@@ -4002,19 +3957,6 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/call-bind-apply-helpers": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"function-bind": "^1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -4127,27 +4069,6 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "14.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
|
||||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/commondir": {
|
"node_modules/commondir": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
@@ -4165,7 +4086,7 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -4275,20 +4196,10 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -4335,20 +4246,6 @@
|
|||||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dunder-proto": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"gopd": "^1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@@ -4368,24 +4265,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-define-property": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/es-errors": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@@ -4393,33 +4272,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/es-object-atoms": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/es-set-tostringtag": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"get-intrinsic": "^1.2.6",
|
|
||||||
"has-tostringtag": "^1.0.2",
|
|
||||||
"hasown": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.3",
|
"version": "0.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||||
@@ -4971,42 +4823,6 @@
|
|||||||
"mini-svg-data-uri": "^1.4.3"
|
"mini-svg-data-uri": "^1.4.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
|
||||||
"version": "1.15.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"debug": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/form-data": {
|
|
||||||
"version": "4.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"es-set-tostringtag": "^2.1.0",
|
|
||||||
"hasown": "^2.0.2",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
@@ -5039,43 +4855,6 @@
|
|||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-intrinsic": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
|
||||||
"es-define-property": "^1.0.1",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"es-object-atoms": "^1.1.1",
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"get-proto": "^1.0.1",
|
|
||||||
"gopd": "^1.2.0",
|
|
||||||
"has-symbols": "^1.1.0",
|
|
||||||
"hasown": "^2.0.2",
|
|
||||||
"math-intrinsics": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-proto": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"dunder-proto": "^1.0.1",
|
|
||||||
"es-object-atoms": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -5101,18 +4880,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gopd": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -5128,33 +4895,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-symbols": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-tostringtag": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-symbols": "^1.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -5350,7 +5090,7 @@
|
|||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -5690,20 +5430,10 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-svelte": {
|
|
||||||
"version": "0.577.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.577.0.tgz",
|
|
||||||
"integrity": "sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"peerDependencies": {
|
|
||||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
@@ -5730,36 +5460,6 @@
|
|||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/math-intrinsics": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mini-svg-data-uri": {
|
"node_modules/mini-svg-data-uri": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||||
@@ -5865,7 +5565,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -5908,17 +5608,11 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch-native": {
|
|
||||||
"version": "1.6.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
|
||||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/obug": {
|
"node_modules/obug": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/sxzz",
|
"https://github.com/sponsors/sxzz",
|
||||||
"https://opencollective.com/debug"
|
"https://opencollective.com/debug"
|
||||||
@@ -6360,12 +6054,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -6495,10 +6183,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/runed": {
|
"node_modules/runed": {
|
||||||
"version": "0.35.1",
|
"version": "0.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.37.1.tgz",
|
||||||
"integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==",
|
"integrity": "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/huntabyte",
|
"https://github.com/sponsors/huntabyte",
|
||||||
"https://github.com/sponsors/tglide"
|
"https://github.com/sponsors/tglide"
|
||||||
@@ -6511,11 +6198,15 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sveltejs/kit": "^2.21.0",
|
"@sveltejs/kit": "^2.21.0",
|
||||||
"svelte": "^5.7.0"
|
"svelte": "^5.7.0",
|
||||||
|
"zod": "^4.1.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@sveltejs/kit": {
|
"@sveltejs/kit": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6946,23 +6637,9 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/shadcn-svelte": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/shadcn-svelte/-/shadcn-svelte-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-ojCwEOK4ggawNogoHyN51PqxDYT/6Qzi9TdEu6gfTb/ITRCU4mEQBGylf2hqB+nfKcj9xXv99CR9b0bEKmAC5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^14.0.0",
|
|
||||||
"node-fetch-native": "^1.6.4",
|
|
||||||
"postcss": "^8.5.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"shadcn-svelte": "dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -6995,7 +6672,7 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polka/url": "^1.0.0-next.24",
|
"@polka/url": "^1.0.0-next.24",
|
||||||
@@ -7272,27 +6949,6 @@
|
|||||||
"svelte": "^3.48.0 || ^4 || ^5"
|
"svelte": "^3.48.0 || ^4 || ^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-toolbelt": {
|
|
||||||
"version": "0.10.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
|
||||||
"integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
"https://github.com/sponsors/huntabyte"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"runed": "^0.35.1",
|
|
||||||
"style-to-object": "^1.0.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18",
|
|
||||||
"pnpm": ">=8.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"svelte": "^5.30.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svelte/node_modules/is-reference": {
|
"node_modules/svelte/node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -7340,13 +6996,6 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tabbable": {
|
|
||||||
"version": "6.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
|
||||||
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||||
@@ -7452,7 +7101,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -7658,7 +7307,7 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz",
|
||||||
"integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==",
|
"integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"tests/deps/*",
|
"tests/deps/*",
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "osit-aether-app-svelte",
|
"name": "osit-aether-app-svelte",
|
||||||
"version": "3.00.07",
|
"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",
|
"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/",
|
"homepage": "https://oneskyit.com/",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:prod": "cp .env.prod .env.production && vite build",
|
"build:dev": "vite build --mode dev",
|
||||||
"build:staging": "cp .env.staging .env.production && vite build",
|
"build:test": "vite build --mode test",
|
||||||
|
"build:prod": "vite build --mode prod",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "npm run test:integration && npm run test:unit",
|
"test": "npm run test:integration && npm run test:unit",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
@@ -17,9 +18,10 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test:integration": "playwright test",
|
"test:integration": "playwright test",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"deploy:staging": "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",
|
"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",
|
||||||
"deploy:prod": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=prod ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d --remove-orphans ae_app",
|
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down",
|
||||||
"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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -38,7 +40,6 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||||
"@typescript-eslint/parser": "^8.47.0",
|
"@typescript-eslint/parser": "^8.47.0",
|
||||||
"bits-ui": "^2.14.3",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
@@ -104,17 +105,15 @@
|
|||||||
"@lucide/svelte": "^0.*.0",
|
"@lucide/svelte": "^0.*.0",
|
||||||
"@popperjs/core": "^2.11.0",
|
"@popperjs/core": "^2.11.0",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"axios": "^1.7.0",
|
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dexie": "^4.0.0",
|
"dexie": "^4.0.0",
|
||||||
"flowbite-svelte": "^1.28.1",
|
"flowbite-svelte": "^1.28.1",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"lucide-svelte": "^0.*.0",
|
|
||||||
"marked": "^17.0.0",
|
"marked": "^17.0.0",
|
||||||
"openai": "^6.10.0",
|
"openai": "^6.10.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"shadcn-svelte": "^1.0.11",
|
"runed": "^0.37.1",
|
||||||
"svelte-persisted-store": "^0.12.0",
|
"svelte-persisted-store": "^0.12.0",
|
||||||
"typescript-eslint": "^8.47.0"
|
"typescript-eslint": "^8.47.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ FA_TO_LUCIDE = {
|
|||||||
'fa-times': 'X',
|
'fa-times': 'X',
|
||||||
'fa-exclamation-triangle': 'TriangleAlert',
|
'fa-exclamation-triangle': 'TriangleAlert',
|
||||||
'fa-check': 'Check',
|
'fa-check': 'Check',
|
||||||
'fa-check-circle': 'CheckCircle',
|
'fa-check-circle': 'CircleCheck',
|
||||||
'fa-plus': 'Plus',
|
'fa-plus': 'Plus',
|
||||||
'fa-minus': 'Minus',
|
'fa-minus': 'Minus',
|
||||||
'fa-save': 'Save',
|
'fa-save': 'Save',
|
||||||
|
|||||||
353
src/ae-firefly-axonius.css
Normal file
353
src/ae-firefly-axonius.css
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
169
src/ae-firefly-bgh.css
Normal file
169
src/ae-firefly-bgh.css
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
29
src/app.css
29
src/app.css
@@ -41,6 +41,8 @@ html.light {
|
|||||||
@import './ae-firefly-steelblue.css';
|
@import './ae-firefly-steelblue.css';
|
||||||
@import './ae-firefly-indigo.css';
|
@import './ae-firefly-indigo.css';
|
||||||
@import './ae-firefly-rainbow.css';
|
@import './ae-firefly-rainbow.css';
|
||||||
|
@import './ae-firefly-axonius.css';
|
||||||
|
@import './ae-firefly-bgh.css';
|
||||||
|
|
||||||
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';
|
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';
|
||||||
|
|
||||||
@@ -161,9 +163,16 @@ html.light {
|
|||||||
background-color: rgb(55 65 81); /* gray-700 */
|
background-color: rgb(55 65 81); /* gray-700 */
|
||||||
border-color: rgb(75 85 99); /* gray-600 */
|
border-color: rgb(75 85 99); /* gray-600 */
|
||||||
}
|
}
|
||||||
|
.input::placeholder,
|
||||||
|
.textarea::placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
.dark .input::placeholder,
|
.dark .input::placeholder,
|
||||||
.dark .textarea::placeholder {
|
.dark .textarea::placeholder {
|
||||||
color: rgb(156 163 175); /* gray-400 — legible at reduced opacity */
|
color: rgb(156 163 175); /* gray-400 */
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.8; /* gray-400 is already dim; subtle additional fade */
|
||||||
}
|
}
|
||||||
/* Option elements in dark selects — forces browser native dark chrome */
|
/* Option elements in dark selects — forces browser native dark chrome */
|
||||||
.dark .select option {
|
.dark .select option {
|
||||||
@@ -231,12 +240,14 @@ html.trusted_access #appShell {
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
/* modern theme */
|
/* modern theme — @font-face commented out 2026-05-19: Quicksand is declared but no
|
||||||
@font-face {
|
CSS rule applies font-family:'Quicksand' to any element, so the browser never
|
||||||
|
fetches this file. Re-enable if a theme or component starts using it. */
|
||||||
|
/* @font-face {
|
||||||
font-family: 'Quicksand';
|
font-family: 'Quicksand';
|
||||||
src: url('/fonts/Quicksand.ttf');
|
src: url('/fonts/Quicksand.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
} */
|
||||||
|
|
||||||
/* :root [data-theme='modern'] { */
|
/* :root [data-theme='modern'] { */
|
||||||
/* --theme-rounded-base: 20px;
|
/* --theme-rounded-base: 20px;
|
||||||
@@ -914,8 +925,14 @@ img.qr_code:focus {
|
|||||||
/* BEGIN: Overrides and fixes specific to Novi and IDAA */
|
/* BEGIN: Overrides and fixes specific to Novi and IDAA */
|
||||||
.iframe .novi_btn {
|
.iframe .novi_btn {
|
||||||
border-radius: 60px;
|
border-radius: 60px;
|
||||||
/* border-color: hsla(0, 0%, 50%, .5); */
|
/* Bootstrap v3 (.btn) sets border:1px solid transparent and wins over
|
||||||
/* border-color: hsla(0, 0%, 0%, .15); */
|
Skeleton/Tailwind preset-outlined classes when loaded last. Use box-shadow
|
||||||
|
instead — Bootstrap does not set box-shadow on .btn so it cannot strip it. */
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.iframe .novi_btn:hover {
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.5);
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iframe .novi_m0 {
|
.iframe .novi_m0 {
|
||||||
|
|||||||
12
src/app.d.ts
vendored
12
src/app.d.ts
vendored
@@ -22,3 +22,15 @@ declare global {
|
|||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var native_app: any;
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
76
src/app.html
76
src/app.html
@@ -3,11 +3,58 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<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="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- 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.gstatic.com" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link
|
<link
|
||||||
@@ -19,14 +66,43 @@
|
|||||||
<link
|
<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"
|
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" />
|
rel="stylesheet" />
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- <link href="app.css" 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>
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<!-- h-full w-full overflow-auto -->
|
<!-- h-full w-full overflow-auto -->
|
||||||
<!-- overflow-x-scroll -->
|
<!-- overflow-x-scroll -->
|
||||||
<body data-sveltekit-preload-data="hover" class="h-full w-full">
|
<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>
|
<div style="display: contents" class="">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||||
import type { key_val } from '$lib/stores/ae_stores';
|
import type { key_val } from '$lib/stores/ae_stores';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +13,7 @@ export const delete_object = async function delete_object({
|
|||||||
headers = {},
|
headers = {},
|
||||||
params = {},
|
params = {},
|
||||||
data = {},
|
data = {},
|
||||||
timeout = 60000,
|
timeout = 20000,
|
||||||
return_meta = false,
|
return_meta = false,
|
||||||
log_lvl = 0,
|
log_lvl = 0,
|
||||||
retry_count = 5
|
retry_count = 5
|
||||||
@@ -97,9 +99,15 @@ export const delete_object = async function delete_object({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
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 {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => {
|
// 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(
|
console.error(
|
||||||
`API DELETE request timed out after ${timeout}ms.`
|
`API DELETE request timed out after ${timeout}ms.`
|
||||||
);
|
);
|
||||||
@@ -120,12 +128,48 @@ export const delete_object = async function delete_object({
|
|||||||
url.toString(),
|
url.toString(),
|
||||||
fetchOptions
|
fetchOptions
|
||||||
).catch(function (error: any) {
|
).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(
|
console.log(
|
||||||
'API DELETE Object *fetch* request was aborted or failed in an unexpected way.',
|
'API DELETE Object *fetch* request was aborted or failed in an unexpected way.',
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
return error;
|
||||||
});
|
});
|
||||||
clearTimeout(timeoutId);
|
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) {
|
if (!response) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -151,7 +195,24 @@ export const delete_object = async function delete_object({
|
|||||||
errorBody
|
errorBody
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status >= 400 && response.status < 404) {
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +235,8 @@ export const delete_object = async function delete_object({
|
|||||||
? json.data
|
? json.data
|
||||||
: json;
|
: json;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Ensure per-attempt timeout is always cleared on failure.
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
console.error(`API DELETE error on attempt ${attempt}:`, error);
|
console.error(`API DELETE error on attempt ${attempt}:`, error);
|
||||||
|
|
||||||
if (attempt === retry_count) {
|
if (attempt === retry_count) {
|
||||||
@@ -181,9 +244,12 @@ export const delete_object = async function delete_object({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log_lvl) {
|
// Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy.
|
||||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ interface GetDataStoreV3Params {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a Data Store object by its human-friendly code (V3)
|
* Get a Data Store object by its human-friendly code (V3)
|
||||||
* Uses hierarchical fallback logic (Specific -> Account -> Global)
|
* Uses hierarchical fallback logic (Specific -> Account -> Global).
|
||||||
|
* TEMPORARY: the global fallback is a stopgap until the backend can
|
||||||
|
* serve account-scoped defaults via JWT-backed access only.
|
||||||
* Path: GET /v3/data_store/code/{code}
|
* Path: GET /v3/data_store/code/{code}
|
||||||
*/
|
*/
|
||||||
export async function get_data_store({
|
export async function get_data_store({
|
||||||
@@ -36,8 +38,10 @@ export async function get_data_store({
|
|||||||
|
|
||||||
const headers: key_val = {};
|
const headers: key_val = {};
|
||||||
if (no_account_id) {
|
if (no_account_id) {
|
||||||
// This token allows bypassing the mandatory account_id requirement for global defaults
|
// TEMPORARY: keep this narrow global-default escape hatch until the
|
||||||
headers['x-no-account-id-token'] = 'Nothing to See Here';
|
// backend can answer the data_store request with account-scoped JWT
|
||||||
|
// access only.
|
||||||
|
headers['x-no-account-id'] = 'Nothing to See Here';
|
||||||
}
|
}
|
||||||
|
|
||||||
return await get_object({
|
return await get_object({
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const get_object = async function get_object({
|
|||||||
headers = {},
|
headers = {},
|
||||||
params = {},
|
params = {},
|
||||||
data = {},
|
data = {},
|
||||||
timeout = 90000,
|
timeout = 20000,
|
||||||
return_meta = false,
|
return_meta = false,
|
||||||
return_blob = false,
|
return_blob = false,
|
||||||
filename = '',
|
filename = '',
|
||||||
@@ -73,9 +73,6 @@ export const get_object = async function get_object({
|
|||||||
url.searchParams.append(key, params[key])
|
url.searchParams.append(key, params[key])
|
||||||
);
|
);
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
// Clean and merge headers without mutating the original api_cfg
|
// Clean and merge headers without mutating the original api_cfg
|
||||||
const headers_cleaned: key_val = {};
|
const headers_cleaned: key_val = {};
|
||||||
const merged_headers = { ...api_cfg['headers'], ...headers };
|
const merged_headers = { ...api_cfg['headers'], ...headers };
|
||||||
@@ -108,7 +105,6 @@ export const get_object = async function get_object({
|
|||||||
const is_valid_bypass =
|
const is_valid_bypass =
|
||||||
bypass_val === 'bypass' ||
|
bypass_val === 'bypass' ||
|
||||||
bypass_val === 'Nothing to See Here' ||
|
bypass_val === 'Nothing to See Here' ||
|
||||||
params['key'] ||
|
|
||||||
bypass_val === 'direct-download';
|
bypass_val === 'direct-download';
|
||||||
|
|
||||||
if (is_valid_bypass) {
|
if (is_valid_bypass) {
|
||||||
@@ -170,10 +166,17 @@ export const get_object = async function get_object({
|
|||||||
console.log('Final cleaned headers:', headers_cleaned);
|
console.log('Final cleaned headers:', headers_cleaned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signal is injected per-attempt inside the retry loop so each retry gets
|
||||||
|
// a fresh AbortController with its own independent timeout.
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: headers_cleaned,
|
headers: headers_cleaned,
|
||||||
signal: controller.signal
|
// Be explicit about CORS behavior and redirect handling to avoid
|
||||||
|
// environment-dependent defaults that can cause opaque failures.
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: 'omit',
|
||||||
|
redirect: 'follow',
|
||||||
|
cache: 'no-store'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (log_lvl > 1) {
|
if (log_lvl > 1) {
|
||||||
@@ -198,10 +201,24 @@ export const get_object = async function get_object({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fresh AbortController per attempt — ensures each retry has its own
|
||||||
|
// independent timeout. Sharing a single controller across retries leaves
|
||||||
|
// retries unprotected once the first attempt's clearTimeout() runs.
|
||||||
|
const controller = new AbortController();
|
||||||
|
// Track whether THIS helper's timeout fired. AbortError alone is ambiguous:
|
||||||
|
// it can mean timeout OR intentional caller abort (navigation/unmount).
|
||||||
|
// We only retry timeout-aborts; intentional aborts should fail fast.
|
||||||
|
let did_timeout_abort = false;
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
did_timeout_abort = true;
|
||||||
|
console.warn(`API GET: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
|
||||||
|
controller.abort();
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch_method(
|
const response = await fetch_method(
|
||||||
url.toString(),
|
url.toString(),
|
||||||
fetchOptions
|
{ ...fetchOptions, signal: controller.signal }
|
||||||
).catch(function (error: any) {
|
).catch(function (error: any) {
|
||||||
// SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
|
// SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
|
||||||
if (
|
if (
|
||||||
@@ -226,21 +243,36 @@ export const get_object = async function get_object({
|
|||||||
});
|
});
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
// Check if we should stop due to abort or network failure
|
// Check if we should stop due to abort or network failure.
|
||||||
if (
|
if (
|
||||||
response instanceof Error ||
|
response instanceof Error ||
|
||||||
(response &&
|
(response &&
|
||||||
(response.name === 'TypeError' ||
|
(response.name === 'TypeError' ||
|
||||||
response.name === 'AbortError'))
|
response.name === 'AbortError'))
|
||||||
) {
|
) {
|
||||||
// If it was an explicit abort, definitely stop
|
// AbortError can be either timeout or intentional abort.
|
||||||
if (response.name === 'AbortError') return false;
|
// Retry only helper-owned timeout aborts; fail fast on caller abort.
|
||||||
|
if (response.name === 'AbortError') {
|
||||||
|
if (did_timeout_abort) {
|
||||||
|
throw new Error(
|
||||||
|
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (log_lvl > 1)
|
// TypeError = transient network failure (ERR_NETWORK_CHANGED,
|
||||||
console.log(
|
// ERR_NETWORK_IO_SUSPENDED, hotel/conference WiFi blip, etc.).
|
||||||
'API GET Object: Detected NetworkError or TypeError. Failing fast.'
|
// IMPORTANT: throw here so the retry loop's catch block handles it with
|
||||||
);
|
// backoff. Returning false would bypass retries entirely.
|
||||||
return false;
|
//
|
||||||
|
// WHY THIS WAS BROKEN: The Jan 2026 "offline-first fast-paths" commit
|
||||||
|
// (a10accfaa) changed .catch() to return the error as a value instead of
|
||||||
|
// not returning (undefined). The undefined path fell through to the
|
||||||
|
// `if (!response)` throw which DID retry. The explicit `return error` +
|
||||||
|
// this `return false` block silently killed the retry for the most common
|
||||||
|
// failure mode on conference/hotel WiFi.
|
||||||
|
throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
@@ -259,6 +291,14 @@ export const get_object = async function get_object({
|
|||||||
console.log(
|
console.log(
|
||||||
`Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}`
|
`Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}`
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
'Response headers:',
|
||||||
|
Object.fromEntries(response.headers.entries())
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore header read errors */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (log_lvl > 1) {
|
if (log_lvl > 1) {
|
||||||
console.log('Response:', response);
|
console.log('Response:', response);
|
||||||
@@ -425,6 +465,8 @@ export const get_object = async function get_object({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Ensure the per-attempt timeout timer is always cancelled on failure.
|
||||||
|
clearTimeout(timeoutId);
|
||||||
console.log(
|
console.log(
|
||||||
`API GET object request *fetch* error on attempt ${attempt}:`,
|
`API GET object request *fetch* error on attempt ${attempt}:`,
|
||||||
error
|
error
|
||||||
@@ -435,10 +477,13 @@ export const get_object = async function get_object({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log retry information
|
// Backoff before retrying. Without a delay, rapid retries on a flaky
|
||||||
if (log_lvl) {
|
// connection accomplish nothing and add noise. Caps at 8s so later
|
||||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
// attempts don't wait excessively. Gives the network time to recover
|
||||||
}
|
// (ERR_NETWORK_CHANGED is typically a sub-second WiFi roam event).
|
||||||
|
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||||
|
console.log(`API GET: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const patch_object = async function patch_object({
|
|||||||
headers = {},
|
headers = {},
|
||||||
params = {},
|
params = {},
|
||||||
data = {},
|
data = {},
|
||||||
timeout = 60000,
|
timeout = 20000,
|
||||||
return_meta = false,
|
return_meta = false,
|
||||||
log_lvl = 0,
|
log_lvl = 0,
|
||||||
retry_count = 5
|
retry_count = 5
|
||||||
@@ -82,7 +82,6 @@ export const patch_object = async function patch_object({
|
|||||||
const is_valid_bypass =
|
const is_valid_bypass =
|
||||||
bypass_val === 'bypass' ||
|
bypass_val === 'bypass' ||
|
||||||
bypass_val === 'Nothing to See Here' ||
|
bypass_val === 'Nothing to See Here' ||
|
||||||
params['key'] ||
|
|
||||||
bypass_val === 'direct-download';
|
bypass_val === 'direct-download';
|
||||||
|
|
||||||
if (is_valid_bypass) {
|
if (is_valid_bypass) {
|
||||||
@@ -154,9 +153,15 @@ export const patch_object = async function patch_object({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
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 {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => {
|
// AbortError alone is ambiguous. Track whether the helper timeout
|
||||||
|
// fired so we can retry timeout-aborts but fail fast on caller abort.
|
||||||
|
let did_timeout_abort = false;
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
did_timeout_abort = true;
|
||||||
console.error(
|
console.error(
|
||||||
`API PATCH request timed out after ${timeout}ms.`
|
`API PATCH request timed out after ${timeout}ms.`
|
||||||
);
|
);
|
||||||
@@ -174,12 +179,52 @@ export const patch_object = async function patch_object({
|
|||||||
url.toString(),
|
url.toString(),
|
||||||
fetchOptions
|
fetchOptions
|
||||||
).catch(function (error: any) {
|
).catch(function (error: any) {
|
||||||
|
// Keep noisy abort/network conditions out of high-level logs.
|
||||||
|
if (
|
||||||
|
error?.name === 'AbortError' ||
|
||||||
|
error?.name === 'TypeError' ||
|
||||||
|
error?.message?.includes('aborted')
|
||||||
|
) {
|
||||||
|
if (log_lvl > 1) {
|
||||||
|
console.log(
|
||||||
|
'API PATCH: Request aborted or browser-terminated.',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'API PATCH Object *fetch* request was aborted or failed in an unexpected way.',
|
'API PATCH Object *fetch* request was aborted or failed in an unexpected way.',
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
return error;
|
||||||
});
|
});
|
||||||
clearTimeout(timeoutId);
|
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') {
|
||||||
|
// Retry only helper-timeout aborts. Caller/navigation aborts
|
||||||
|
// should fail fast to avoid duplicate mutation side-effects.
|
||||||
|
if (did_timeout_abort) {
|
||||||
|
throw new Error(
|
||||||
|
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transient browser/network failure class.
|
||||||
|
throw new Error(
|
||||||
|
`Network error (attempt ${attempt}): ${response.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -293,6 +338,8 @@ export const patch_object = async function patch_object({
|
|||||||
? json.data
|
? json.data
|
||||||
: json;
|
: json;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Ensure per-attempt timeout is always cleared on failure.
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
console.error(`API PATCH error on attempt ${attempt}:`, error);
|
console.error(`API PATCH error on attempt ${attempt}:`, error);
|
||||||
|
|
||||||
if (attempt === retry_count) {
|
if (attempt === retry_count) {
|
||||||
@@ -300,9 +347,12 @@ export const patch_object = async function patch_object({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log_lvl) {
|
// Backoff before retrying. Caps at 8s to match GET/POST policy.
|
||||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||||
}
|
console.log(
|
||||||
|
`API PATCH: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,22 @@ import { post_object } from './api_post_object';
|
|||||||
import { patch_object } from './api_patch_object';
|
import { patch_object } from './api_patch_object';
|
||||||
import { delete_object } from './api_delete_object';
|
import { delete_object } from './api_delete_object';
|
||||||
|
|
||||||
|
const JSON_PRETTY_SPACES = 2;
|
||||||
|
|
||||||
|
function serialize_json_field_pretty(value: unknown) {
|
||||||
|
if (value === null || value === undefined) return value;
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(value), null, JSON_PRETTY_SPACES);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value, null, JSON_PRETTY_SPACES);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --- POST (CREATE) WRAPPERS ---
|
* --- POST (CREATE) WRAPPERS ---
|
||||||
*/
|
*/
|
||||||
@@ -33,13 +49,11 @@ export async function create_ae_obj({
|
|||||||
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
||||||
const cleaned_fields = { ...fields };
|
const cleaned_fields = { ...fields };
|
||||||
for (const key in cleaned_fields) {
|
for (const key in cleaned_fields) {
|
||||||
if (
|
if (key.endsWith('_json') && cleaned_fields[key] !== null) {
|
||||||
key.endsWith('_json') &&
|
|
||||||
cleaned_fields[key] !== null &&
|
|
||||||
typeof cleaned_fields[key] === 'object'
|
|
||||||
) {
|
|
||||||
if (log_lvl) console.log(`Auto-serializing field: ${key}`);
|
if (log_lvl) console.log(`Auto-serializing field: ${key}`);
|
||||||
cleaned_fields[key] = JSON.stringify(cleaned_fields[key]);
|
cleaned_fields[key] = serialize_json_field_pretty(
|
||||||
|
cleaned_fields[key]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +112,10 @@ export async function create_nested_obj({
|
|||||||
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
||||||
const cleaned_fields = { ...fields };
|
const cleaned_fields = { ...fields };
|
||||||
for (const key in cleaned_fields) {
|
for (const key in cleaned_fields) {
|
||||||
if (
|
if (key.endsWith('_json') && cleaned_fields[key] !== null) {
|
||||||
key.endsWith('_json') &&
|
cleaned_fields[key] = serialize_json_field_pretty(
|
||||||
cleaned_fields[key] !== null &&
|
cleaned_fields[key]
|
||||||
typeof cleaned_fields[key] === 'object'
|
);
|
||||||
) {
|
|
||||||
cleaned_fields[key] = JSON.stringify(cleaned_fields[key]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,13 +160,11 @@ export async function update_ae_obj({
|
|||||||
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
||||||
const cleaned_fields = { ...fields };
|
const cleaned_fields = { ...fields };
|
||||||
for (const key in cleaned_fields) {
|
for (const key in cleaned_fields) {
|
||||||
if (
|
if (key.endsWith('_json') && cleaned_fields[key] !== null) {
|
||||||
key.endsWith('_json') &&
|
|
||||||
cleaned_fields[key] !== null &&
|
|
||||||
typeof cleaned_fields[key] === 'object'
|
|
||||||
) {
|
|
||||||
if (log_lvl > 1) console.log(`Auto-serializing field: ${key}`);
|
if (log_lvl > 1) console.log(`Auto-serializing field: ${key}`);
|
||||||
cleaned_fields[key] = JSON.stringify(cleaned_fields[key]);
|
cleaned_fields[key] = serialize_json_field_pretty(
|
||||||
|
cleaned_fields[key]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,12 +224,10 @@ export async function update_nested_obj({
|
|||||||
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
// Standard Aether Pattern: Auto-serialize any key ending in _json
|
||||||
const cleaned_fields = { ...fields };
|
const cleaned_fields = { ...fields };
|
||||||
for (const key in cleaned_fields) {
|
for (const key in cleaned_fields) {
|
||||||
if (
|
if (key.endsWith('_json') && cleaned_fields[key] !== null) {
|
||||||
key.endsWith('_json') &&
|
cleaned_fields[key] = serialize_json_field_pretty(
|
||||||
cleaned_fields[key] !== null &&
|
cleaned_fields[key]
|
||||||
typeof cleaned_fields[key] === 'object'
|
);
|
||||||
) {
|
|
||||||
cleaned_fields[key] = JSON.stringify(cleaned_fields[key]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const post_object = async function post_object({
|
|||||||
params = {},
|
params = {},
|
||||||
data = {},
|
data = {},
|
||||||
form_data = null,
|
form_data = null,
|
||||||
timeout = 90000,
|
timeout = 20000,
|
||||||
return_meta = false,
|
return_meta = false,
|
||||||
return_blob = false,
|
return_blob = false,
|
||||||
filename = '',
|
filename = '',
|
||||||
@@ -23,7 +23,10 @@ export const post_object = async function post_object({
|
|||||||
// The task_id value should be a random string that is unique to the task. This is used to identify the task in the message event.
|
// The task_id value should be a random string that is unique to the task. This is used to identify the task in the message event.
|
||||||
task_id = crypto.randomUUID(),
|
task_id = crypto.randomUUID(),
|
||||||
log_lvl = 0,
|
log_lvl = 0,
|
||||||
retry_count = 5
|
retry_count = 5,
|
||||||
|
// When true: use XHR instead of fetch so xhr.upload.onprogress can fire
|
||||||
|
// progress postMessages into api_upload_kv. Only meaningful for form_data uploads.
|
||||||
|
track_progress = false
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -39,6 +42,7 @@ export const post_object = async function post_object({
|
|||||||
task_id?: string;
|
task_id?: string;
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
retry_count?: number;
|
retry_count?: number;
|
||||||
|
track_progress?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -104,7 +108,6 @@ export const post_object = async function post_object({
|
|||||||
const is_valid_bypass =
|
const is_valid_bypass =
|
||||||
bypass_val === 'bypass' ||
|
bypass_val === 'bypass' ||
|
||||||
bypass_val === 'Nothing to See Here' ||
|
bypass_val === 'Nothing to See Here' ||
|
||||||
params['key'] ||
|
|
||||||
bypass_val === 'direct-download';
|
bypass_val === 'direct-download';
|
||||||
|
|
||||||
if (is_valid_bypass) {
|
if (is_valid_bypass) {
|
||||||
@@ -181,14 +184,35 @@ export const post_object = async function post_object({
|
|||||||
fetch_method = api_cfg.fetch;
|
fetch_method = api_cfg.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
// XHR path — only for form_data uploads with progress tracking requested.
|
||||||
try {
|
// fetch() has no upload progress events; XHR.upload.onprogress does.
|
||||||
const controller = new AbortController();
|
if (track_progress && form_data) {
|
||||||
const timeoutId = setTimeout(() => {
|
return _post_with_xhr({
|
||||||
console.error(`API POST request timed out after ${timeout}ms.`);
|
url_str: url.toString(),
|
||||||
controller.abort();
|
headers_cleaned,
|
||||||
}, timeout);
|
form_data,
|
||||||
|
task_id,
|
||||||
|
endpoint,
|
||||||
|
timeout,
|
||||||
|
return_meta,
|
||||||
|
log_lvl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||||
|
// Declared at loop scope (not inside try) so the catch block can clearTimeout.
|
||||||
|
// Fresh controller per attempt — same rationale as api_get_object.ts.
|
||||||
|
const controller = new AbortController();
|
||||||
|
// AbortError is not specific enough by itself. Distinguish timeout-aborts
|
||||||
|
// (retryable transient class) from intentional caller aborts (fail-fast).
|
||||||
|
let did_timeout_abort = false;
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
did_timeout_abort = true;
|
||||||
|
console.warn(`API POST: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
|
||||||
|
controller.abort();
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers_cleaned,
|
headers: headers_cleaned,
|
||||||
@@ -227,19 +251,28 @@ export const post_object = async function post_object({
|
|||||||
});
|
});
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
// Check if we should stop due to abort or network failure
|
// Check if we should stop due to abort or network failure.
|
||||||
if (
|
if (
|
||||||
response instanceof Error ||
|
response instanceof Error ||
|
||||||
(response &&
|
(response &&
|
||||||
(response.name === 'TypeError' ||
|
(response.name === 'TypeError' ||
|
||||||
response.name === 'AbortError'))
|
response.name === 'AbortError'))
|
||||||
) {
|
) {
|
||||||
if (response.name === 'AbortError') return false;
|
// Retry timeout-aborts from this helper; do not retry caller aborts
|
||||||
if (log_lvl > 1)
|
// (route change/unmount/manual cancellation).
|
||||||
console.log(
|
if (response.name === 'AbortError') {
|
||||||
'API POST Object: Detected NetworkError or TypeError. Failing fast.'
|
if (did_timeout_abort) {
|
||||||
);
|
throw new Error(
|
||||||
return false;
|
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeError = transient network failure. Throw into the retry loop
|
||||||
|
// so backoff-and-retry applies. Same fix as api_get_object.ts — see
|
||||||
|
// comment there for the full history of why this was broken.
|
||||||
|
throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
@@ -393,6 +426,8 @@ export const post_object = async function post_object({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Ensure the per-attempt timeout timer is always cancelled on failure.
|
||||||
|
clearTimeout(timeoutId);
|
||||||
console.error(`API POST error on attempt ${attempt}:`, error);
|
console.error(`API POST error on attempt ${attempt}:`, error);
|
||||||
|
|
||||||
if (attempt === retry_count) {
|
if (attempt === retry_count) {
|
||||||
@@ -400,9 +435,134 @@ export const post_object = async function post_object({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log_lvl) {
|
// Backoff before retrying — same rationale as api_get_object.ts.
|
||||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||||
}
|
console.log(`API POST: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function _post_with_xhr({
|
||||||
|
url_str,
|
||||||
|
headers_cleaned,
|
||||||
|
form_data,
|
||||||
|
task_id,
|
||||||
|
endpoint,
|
||||||
|
timeout,
|
||||||
|
return_meta,
|
||||||
|
log_lvl
|
||||||
|
}: {
|
||||||
|
url_str: string;
|
||||||
|
headers_cleaned: key_val;
|
||||||
|
form_data: FormData;
|
||||||
|
task_id: string;
|
||||||
|
endpoint: string;
|
||||||
|
timeout: number;
|
||||||
|
return_meta: boolean;
|
||||||
|
log_lvl: number;
|
||||||
|
}): Promise<any> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', url_str);
|
||||||
|
xhr.timeout = timeout;
|
||||||
|
|
||||||
|
// Apply auth/custom headers. Content-Type is intentionally omitted —
|
||||||
|
// the browser sets the multipart boundary automatically for FormData.
|
||||||
|
for (const [key, value] of Object.entries(headers_cleaned)) {
|
||||||
|
xhr.setRequestHeader(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable && typeof window !== 'undefined') {
|
||||||
|
const pct = Math.round((event.loaded / event.total) * 100);
|
||||||
|
try {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: 'api_post_json_form',
|
||||||
|
status: 'uploading',
|
||||||
|
task_id,
|
||||||
|
endpoint,
|
||||||
|
size_total: event.total,
|
||||||
|
size_loaded: event.loaded,
|
||||||
|
percent_completed: pct,
|
||||||
|
progress: pct,
|
||||||
|
rate: 0
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.ontimeout = () => {
|
||||||
|
console.error(`XHR upload timed out after ${timeout}ms. Endpoint: ${endpoint}`);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
console.error(`XHR upload network error. Endpoint: ${endpoint}`);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (log_lvl)
|
||||||
|
console.log(`XHR response: status=${xhr.status} endpoint=${endpoint}`);
|
||||||
|
|
||||||
|
if (xhr.status === 401 || xhr.status === 403) {
|
||||||
|
console.warn(`XHR AUTH FAILURE (${xhr.status}): ${endpoint}`);
|
||||||
|
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.status === 404) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.status < 200 || xhr.status >= 300) {
|
||||||
|
console.error(`XHR upload failed: HTTP ${xhr.status}. Endpoint: ${endpoint}`);
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire completion postMessage (matches the fetch path shape)
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: 'api_post_json_form',
|
||||||
|
status: 'complete',
|
||||||
|
task_id,
|
||||||
|
endpoint,
|
||||||
|
size_total: 0,
|
||||||
|
size_loaded: 0,
|
||||||
|
percent_completed: 100,
|
||||||
|
progress: 100,
|
||||||
|
rate: 0
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(xhr.responseText);
|
||||||
|
if (log_lvl > 1) console.log('XHR Response JSON:', json);
|
||||||
|
resolve(
|
||||||
|
return_meta
|
||||||
|
? json
|
||||||
|
: json.data !== undefined
|
||||||
|
? json.data
|
||||||
|
: json
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('XHR: Failed to parse response JSON.', e);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(form_data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export async function load_ae_obj_li__archive({
|
|||||||
if (inc_content_li && archive_obj_li && Array.isArray(archive_obj_li)) {
|
if (inc_content_li && archive_obj_li && Array.isArray(archive_obj_li)) {
|
||||||
for (let i = 0; i < archive_obj_li.length; i++) {
|
for (let i = 0; i < archive_obj_li.length; i++) {
|
||||||
const archive_obj = archive_obj_li[i];
|
const archive_obj = archive_obj_li[i];
|
||||||
const archive_id = archive_obj.archive_id_random;
|
const archive_id = archive_obj.archive_id;
|
||||||
|
|
||||||
const content_li = await load_ae_obj_li__archive_content({
|
const content_li = await load_ae_obj_li__archive_content({
|
||||||
api_cfg: api_cfg,
|
api_cfg: api_cfg,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export const editable_fields__archive_content = [
|
export const editable_fields__archive_content = [
|
||||||
'archive_id',
|
'archive_id',
|
||||||
'archive_id_random',
|
|
||||||
'archive_content_type',
|
'archive_content_type',
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
@@ -9,7 +8,6 @@ export const editable_fields__archive_content = [
|
|||||||
'url',
|
'url',
|
||||||
'url_text',
|
'url_text',
|
||||||
'hosted_file_id',
|
'hosted_file_id',
|
||||||
'hosted_file_id_random',
|
|
||||||
'file_path',
|
'file_path',
|
||||||
'filename',
|
'filename',
|
||||||
'file_extension',
|
'file_extension',
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export const properties_to_save = [
|
|||||||
'archive_id',
|
'archive_id',
|
||||||
'archive_content_type',
|
'archive_content_type',
|
||||||
'name',
|
'name',
|
||||||
|
'code',
|
||||||
'description',
|
'description',
|
||||||
'content_html',
|
'content_html',
|
||||||
'content_json',
|
'content_json',
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface Archive_Content {
|
|||||||
archive_content_type: string;
|
archive_content_type: string;
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
|
code?: null | string;
|
||||||
description?: null | string;
|
description?: null | string;
|
||||||
|
|
||||||
content_html?: null | string;
|
content_html?: null | string;
|
||||||
@@ -168,7 +169,28 @@ export class MySubClassedDexie extends Dexie {
|
|||||||
tmp_sort_1, tmp_sort_2,
|
tmp_sort_1, tmp_sort_2,
|
||||||
enable, hide, priority, sort, group, notes, created_on, updated_on, [group+priority+sort+updated_on]`
|
enable, hide, priority, sort, group, notes, created_on, updated_on, [group+priority+sort+updated_on]`
|
||||||
});
|
});
|
||||||
|
// v2: add code index to content table
|
||||||
|
this.version(2).stores({
|
||||||
|
archive: `
|
||||||
|
id, archive_id,
|
||||||
|
code,
|
||||||
|
account_id,
|
||||||
|
name,
|
||||||
|
original_datetime, original_timezone, original_location,
|
||||||
|
tmp_sort_1, tmp_sort_2,
|
||||||
|
enable, hide, priority, sort, group, notes, created_on, updated_on`,
|
||||||
|
content: `
|
||||||
|
id, archive_content_id,
|
||||||
|
archive_id,
|
||||||
|
archive_content_type,
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
hosted_file_id,
|
||||||
|
original_datetime, original_timezone, original_location,
|
||||||
|
[group+original_datetime],
|
||||||
|
tmp_sort_1, tmp_sort_2,
|
||||||
|
enable, hide, priority, sort, group, notes, created_on, updated_on, [group+priority+sort+updated_on]`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function handle_clip_video(event: Event) {
|
|||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
params: params,
|
params: params,
|
||||||
timeout: 300000, // 5 minutes
|
timeout: 1800000, // 30 minutes — clip_video runs ffmpeg which can take 5-15 min for long recordings
|
||||||
// return_blob: true,
|
// return_blob: true,
|
||||||
// filename: event.target.new_filename.value,
|
// filename: event.target.new_filename.value,
|
||||||
// auto_download: false,
|
// auto_download: false,
|
||||||
@@ -157,12 +157,24 @@ function handle_clip_video(event: Event) {
|
|||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|
||||||
|
// WHY: get_object() returns false/null on network failure (CORS drop,
|
||||||
|
// timeout, Gunicorn worker killed). Without this guard, result.hosted_file_id
|
||||||
|
// is undefined — JS uses it as an object key without throwing, so all the
|
||||||
|
// success state below would run and show "Clipped" on a failed request.
|
||||||
|
if (!result || !result.hosted_file_id) {
|
||||||
|
console.log('clip_video: request failed or returned no result');
|
||||||
|
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
|
||||||
|
'error';
|
||||||
|
$ae_loc.files.processed_file_kv[hosted_file_id].submit_status =
|
||||||
|
'error';
|
||||||
|
submit_status = 'error';
|
||||||
|
clip_complete = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
video_clip_file_kv[result.hosted_file_id] = {};
|
video_clip_file_kv[result.hosted_file_id] = {};
|
||||||
video_clip_file_kv[result.hosted_file_id] = result;
|
video_clip_file_kv[result.hosted_file_id] = result;
|
||||||
|
|
||||||
// $ae_loc.files.video_clip_file_kv[result.hosted_file_id] = {};
|
|
||||||
// $ae_loc.files.video_clip_file_kv[result.hosted_file_id] = result;
|
|
||||||
|
|
||||||
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
|
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
|
||||||
'clipped';
|
'clipped';
|
||||||
$ae_sess.files.processed_file_kv[hosted_file_id].clip_complete =
|
$ae_sess.files.processed_file_kv[hosted_file_id].clip_complete =
|
||||||
@@ -176,13 +188,6 @@ function handle_clip_video(event: Event) {
|
|||||||
submit_status = 'clipped';
|
submit_status = 'clipped';
|
||||||
clip_complete = true;
|
clip_complete = true;
|
||||||
|
|
||||||
// let file_blob = new Blob([result.data]);
|
|
||||||
// // console.log(file_blob);
|
|
||||||
// let file_obj_url = window.URL.createObjectURL(file_blob); // The img src
|
|
||||||
// // const url = window.URL.createObjectURL(new Blob([result.data]));
|
|
||||||
// download_clip_src = file_obj_url;
|
|
||||||
// // download_filename = file_obj_url;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -363,21 +368,25 @@ function handle_clip_video(event: Event) {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-lg btn-primary preset-tonal-primary border-primary-500 hover:preset-filled-primary-500 border transition-colors"
|
class="btn btn-lg btn-primary border transition-colors"
|
||||||
|
class:preset-tonal-primary={submit_status !== 'error'}
|
||||||
|
class:border-primary-500={submit_status !== 'error'}
|
||||||
|
class:hover:preset-filled-primary-500={submit_status !== 'error'}
|
||||||
|
class:preset-tonal-error={submit_status === 'error'}
|
||||||
|
class:border-error-500={submit_status === 'error'}
|
||||||
disabled={submit_status == 'clipping'}>
|
disabled={submit_status == 'clipping'}>
|
||||||
<!-- {#await ae_promises[hosted_file_id]} -->
|
|
||||||
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipping'}
|
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipping'}
|
||||||
<LoaderCircle size="1em" class="m-1 animate-spin" />
|
<LoaderCircle size="1em" class="m-1 animate-spin" />
|
||||||
<span class="highlight">Clipping...</span>
|
<span class="highlight">Clipping...</span>
|
||||||
|
{:else if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'error'}
|
||||||
|
<span class="fas fa-exclamation-triangle m-1"></span>
|
||||||
|
Failed — Retry?
|
||||||
|
{:else if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
|
||||||
|
<Check size="1em" class="m-1" />
|
||||||
|
Clipped
|
||||||
{:else}
|
{:else}
|
||||||
<!-- {#if ae_promises[hosted_file_id]} -->
|
<Scissors size="1em" class="m-1" />
|
||||||
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
|
Clip Video
|
||||||
<Check size="1em" class="m-1" />
|
|
||||||
Clipped
|
|
||||||
{:else}
|
|
||||||
<Scissors size="1em" class="m-1" />
|
|
||||||
Clip Video
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
<!-- <span class="fas fa-cut m-1"></span>
|
<!-- <span class="fas fa-cut m-1"></span>
|
||||||
Clip Video -->
|
Clip Video -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// *** Import Svelte specific
|
// *** Import Svelte specific
|
||||||
import * as Lucide from 'lucide-svelte';
|
import * as Lucide from '@lucide/svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
// *** Import Aether specific variables and functions
|
||||||
@@ -35,6 +35,7 @@ interface Props {
|
|||||||
require_auth?: boolean;
|
require_auth?: boolean;
|
||||||
classes?: string;
|
classes?: string;
|
||||||
click?: () => void | Promise<any>;
|
click?: () => void | Promise<any>;
|
||||||
|
track_click_promise?: boolean;
|
||||||
label?: import('svelte').Snippet;
|
label?: import('svelte').Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ let {
|
|||||||
require_auth = true,
|
require_auth = true,
|
||||||
classes = '',
|
classes = '',
|
||||||
click,
|
click,
|
||||||
|
track_click_promise = true,
|
||||||
label
|
label
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -128,10 +130,7 @@ $effect(() => {
|
|||||||
let ae_promises: key_val = $state({});
|
let ae_promises: key_val = $state({});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const file_id =
|
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||||
hosted_file_obj?.id ||
|
|
||||||
hosted_file_obj?.hosted_file_id ||
|
|
||||||
hosted_file_id;
|
|
||||||
if (file_id && $ae_sess?.api_download_kv[file_id]?.percent_completed) {
|
if (file_id && $ae_sess?.api_download_kv[file_id]?.percent_completed) {
|
||||||
download_percent = $ae_sess.api_download_kv[file_id].percent_completed;
|
download_percent = $ae_sess.api_download_kv[file_id].percent_completed;
|
||||||
}
|
}
|
||||||
@@ -139,10 +138,7 @@ $effect(() => {
|
|||||||
|
|
||||||
// Reactive timer to alternate views during active download
|
// Reactive timer to alternate views during active download
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const file_id =
|
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||||
hosted_file_obj?.id ||
|
|
||||||
hosted_file_obj?.hosted_file_id ||
|
|
||||||
hosted_file_id;
|
|
||||||
const is_actively_downloading =
|
const is_actively_downloading =
|
||||||
ae_promises[file_id] && download_complete === undefined;
|
ae_promises[file_id] && download_complete === undefined;
|
||||||
|
|
||||||
@@ -178,32 +174,50 @@ let shortened_filename = $derived(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let is_url_file = $derived.by(() => {
|
||||||
|
const raw_filename = (hosted_file_obj?.filename ?? '').toLowerCase();
|
||||||
|
const extension = (hosted_file_obj?.extension ?? '').toLowerCase();
|
||||||
|
return (
|
||||||
|
raw_filename.startsWith('http://') ||
|
||||||
|
raw_filename.startsWith('https://') ||
|
||||||
|
extension === 'url'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
let direct_download_url = $derived.by(() => {
|
let direct_download_url = $derived.by(() => {
|
||||||
if (!show_direct_download || !hosted_file_obj) return '';
|
if (!show_direct_download || !hosted_file_obj) return '';
|
||||||
// IMPORTANT: For Direct Link Mode, we MUST use the V3 Action endpoint to support Random String IDs.
|
// Use event_file endpoint when event_file_id is present (canonical per API guide §5).
|
||||||
// Legacy endpoints often expect integer IDs and will return 404 for string IDs.
|
// Fall back to hosted_file endpoint for standalone hosted_file objects.
|
||||||
const file_id =
|
if (hosted_file_obj.event_file_id) {
|
||||||
hosted_file_obj.event_file_id ||
|
return `${$ae_api.base_url}/v3/action/event_file/${hosted_file_obj.event_file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
|
||||||
hosted_file_obj.hosted_file_id ||
|
}
|
||||||
hosted_file_id;
|
const file_id = hosted_file_obj.hosted_file_id || hosted_file_id;
|
||||||
const obj_type_path = hosted_file_obj.event_file_id
|
return `${$ae_api.base_url}/v3/action/hosted_file/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
|
||||||
? 'event_file'
|
|
||||||
: 'hosted_file';
|
|
||||||
return `${$ae_api.base_url}/v3/action/${obj_type_path}/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handle_click() {
|
async function handle_click() {
|
||||||
const file_id =
|
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||||
hosted_file_obj?.id ||
|
|
||||||
hosted_file_obj?.hosted_file_id ||
|
// URL-backed records are intentionally not downloaded. Callers are expected
|
||||||
hosted_file_id;
|
// to provide a custom click handler that opens the URL directly.
|
||||||
|
if (is_url_file) {
|
||||||
|
if (click) {
|
||||||
|
const result = click();
|
||||||
|
if (track_click_promise && result instanceof Promise) {
|
||||||
|
ae_promises[file_id] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
download_complete = undefined;
|
download_complete = undefined;
|
||||||
download_status_msg = 'Downloading...';
|
download_status_msg = 'Downloading...';
|
||||||
|
|
||||||
if (click) {
|
if (click) {
|
||||||
const result = click();
|
const result = click();
|
||||||
// If the override returns a promise, track it so the UI shows progress
|
// If the override returns a promise, track it so the UI shows progress.
|
||||||
if (result instanceof Promise) {
|
// Launcher open flows can opt out so native status messages stay authoritative.
|
||||||
|
if (track_click_promise && result instanceof Promise) {
|
||||||
ae_promises[file_id] = result;
|
ae_promises[file_id] = result;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -238,10 +252,7 @@ async function handle_click() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
{@const file_id =
|
{@const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id}
|
||||||
hosted_file_obj?.id ||
|
|
||||||
hosted_file_obj?.hosted_file_id ||
|
|
||||||
hosted_file_id}
|
|
||||||
{#await ae_promises[file_id]}
|
{#await ae_promises[file_id]}
|
||||||
<div class="flex min-h-[1.5rem] w-full items-center">
|
<div class="flex min-h-[1.5rem] w-full items-center">
|
||||||
<div
|
<div
|
||||||
@@ -316,8 +327,7 @@ async function handle_click() {
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#if hosted_file_id && hosted_file_obj}
|
{#if hosted_file_id && hosted_file_obj}
|
||||||
{@const file_id =
|
{@const file_id = hosted_file_obj.hosted_file_id ?? hosted_file_id}
|
||||||
hosted_file_obj.id || hosted_file_obj.hosted_file_id || hosted_file_id}
|
|
||||||
|
|
||||||
{#if show_direct_download}
|
{#if show_direct_download}
|
||||||
<a
|
<a
|
||||||
@@ -333,7 +343,20 @@ async function handle_click() {
|
|||||||
disabled={require_auth && !$ae_loc.authenticated_access}
|
disabled={require_auth && !$ae_loc.authenticated_access}
|
||||||
class={variant_classes}
|
class={variant_classes}
|
||||||
onclick={handle_click}
|
onclick={handle_click}
|
||||||
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}>
|
title={
|
||||||
|
`Download this file:
|
||||||
|
${final_filename}
|
||||||
|
[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...
|
||||||
|
Hosted ID: ${file_id}
|
||||||
|
|
||||||
|
File size: ${hosted_file_obj.file_size ? ae_util.format_bytes(hosted_file_obj.file_size) : 'Unknown size'}
|
||||||
|
Created on: ${ae_util.iso_datetime_formatter(hosted_file_obj.created_on, 'datetime_short')}
|
||||||
|
Updated on: ${ae_util.iso_datetime_formatter(hosted_file_obj.updated_on, 'datetime_short')}
|
||||||
|
|
||||||
|
Open with: ${hosted_file_obj.open_in_os == 'win' ? 'Windows' : hosted_file_obj.open_in_os == 'mac' ? 'macOS' : hosted_file_obj.open_in_os == 'linux' ? 'Linux' : '--not set--'}
|
||||||
|
|
||||||
|
Linked to Type: ${linked_to_type ?? '--none--'} ID: ${linked_to_id ?? '---'}`
|
||||||
|
}>
|
||||||
{@render content()}
|
{@render content()}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// untrack import removed — task_id sync now uses direct $effect (no untrack needed)
|
// untrack import removed — task_id sync now uses direct $effect (no untrack needed)
|
||||||
// Imports
|
// Imports
|
||||||
// Import components and elements
|
// Import components and elements
|
||||||
import * as Lucide from 'lucide-svelte';
|
import * as Lucide from '@lucide/svelte';
|
||||||
import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte';
|
import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte';
|
||||||
|
|
||||||
// Import storage, functions, and libraries
|
// Import storage, functions, and libraries
|
||||||
@@ -49,7 +49,7 @@ let {
|
|||||||
input_name = 'file_list',
|
input_name = 'file_list',
|
||||||
multiple = true,
|
multiple = true,
|
||||||
required = true,
|
required = true,
|
||||||
accept = 'audio/*, image/*, video/*, .bak, .cfg, .css, .csv, .doc, .docx, .gz, .htm, .html, .ini, .iso, .j2, .json, .key, .keynote, .md, .pdf, .ppt, .pptx, .rar, .rtf, .sql, .svelte, ttf, .txt, .xls, .xlsx, .xz, .zip, .bin, .dmg, .exe, .js, .msi, .php, .py, .sh',
|
accept = 'audio/*, image/*, video/*, .bak, .cfg, .css, .csv, .doc, .docx, .gz, .htm, .html, .ini, .iso, .j2, .json, .key, .keynote, .md, .pdf, .ppt, .pptx, .rar, .rtf, .sql, .svelte, .ttf, .txt, .xls, .xlsx, .xz, .zip, .bin, .dmg, .exe, .js, .msi, .php, .py, .sh',
|
||||||
class_li_default = 'flex flex-col gap-1 items-center justify-center w-full max-w-2xl mx-auto my-1',
|
class_li_default = 'flex flex-col gap-1 items-center justify-center w-full max-w-2xl mx-auto my-1',
|
||||||
class_li = '',
|
class_li = '',
|
||||||
input_class_li = ['file_drop_area'],
|
input_class_li = ['file_drop_area'],
|
||||||
@@ -66,7 +66,7 @@ let {
|
|||||||
let task_id: string = $state('');
|
let task_id: string = $state('');
|
||||||
let input_file_list: any = $state(null);
|
let input_file_list: any = $state(null);
|
||||||
let ae_promises: key_val = $state({}); // Promise<any>;
|
let ae_promises: key_val = $state({}); // Promise<any>;
|
||||||
let ae_triggers: key_val = {};
|
// let ae_triggers: key_val = {};
|
||||||
|
|
||||||
let input_element_id = 'ae_comp__hosted_files_upload__input';
|
let input_element_id = 'ae_comp__hosted_files_upload__input';
|
||||||
|
|
||||||
@@ -78,18 +78,22 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Sync task_id with link_to_id prop so it resets when navigating to a different object.
|
// Only sync task_id when idle — don't reset during an in-flight upload.
|
||||||
task_id = link_to_id;
|
if (!ae_promises.upload__hosted_file_obj) {
|
||||||
|
task_id = link_to_id;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||||
|
return function (event: T) {
|
||||||
|
event.preventDefault();
|
||||||
|
fn(event);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// *** Functions and Logic
|
// *** Functions and Logic
|
||||||
async function handle_submit_form_files(event: SubmitEvent) {
|
async function handle_submit_form_files(event: SubmitEvent) {
|
||||||
console.log('*** handle_submit_form() ***');
|
if (log_lvl) console.log('*** handle_submit_form() ***');
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ae_sess.files.disable_submit__hosted_file_obj = true;
|
$ae_sess.files.disable_submit__hosted_file_obj = true;
|
||||||
$ae_sess.files.submit_status = 'saving';
|
$ae_sess.files.submit_status = 'saving';
|
||||||
@@ -113,42 +117,17 @@ async function handle_submit_form_files(event: SubmitEvent) {
|
|||||||
file_input.files &&
|
file_input.files &&
|
||||||
file_input.files.length > 0
|
file_input.files.length > 0
|
||||||
) {
|
) {
|
||||||
task_id = link_to_id; // Ideally this should be the file hash, but we may be uploading multiple files at once. This should be done with a loop instead?
|
|
||||||
|
|
||||||
// Loop through each file and upload them individually in event.target[input_element_id].files
|
|
||||||
// The task_id should be the file hash.
|
|
||||||
// processed_file_list[i] has the file hash_sha256, hash_sha256_match, warnings, uploaded, uploaded_bytes, filename, and file_size_bytes.
|
|
||||||
for (let i = 0; i < file_input.files.length; i++) {
|
for (let i = 0; i < file_input.files.length; i++) {
|
||||||
let tmp_file = file_input.files[i];
|
|
||||||
|
|
||||||
task_id = $ae_sess.files.processed_file_list[i].hash_sha256;
|
task_id = $ae_sess.files.processed_file_list[i].hash_sha256;
|
||||||
|
|
||||||
// hosted_file_results = await handle_input_upload_files([tmp_file], task_id);
|
|
||||||
hosted_file_results = await handle_input_upload_files({
|
hosted_file_results = await handle_input_upload_files({
|
||||||
input_upload_files: [tmp_file],
|
input_upload_files: [file_input.files[i]],
|
||||||
task_id: task_id
|
task_id: task_id
|
||||||
});
|
});
|
||||||
|
if (log_lvl > 1) console.log('hosted_file_results:', hosted_file_results);
|
||||||
if (hosted_file_results) {
|
|
||||||
console.log(`hosted_file_results:`, hosted_file_results);
|
|
||||||
} else {
|
|
||||||
console.log(`hosted_file_results:`, hosted_file_results);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// hosted_file_results = await handle_input_upload_files(event.target[input_element_id].files, task_id);
|
|
||||||
$ae_sess.files.processed_file_list = [];
|
$ae_sess.files.processed_file_list = [];
|
||||||
$ae_sess = $ae_sess; // Is this needed? 2025-03-17
|
|
||||||
target.reset();
|
target.reset();
|
||||||
// await tick();
|
if (log_lvl) console.log('hosted_file_id_li:', hosted_file_id_li);
|
||||||
|
|
||||||
if (log_lvl) {
|
|
||||||
console.log(
|
|
||||||
`hosted_file_id_li: ${hosted_file_id_li}`,
|
|
||||||
hosted_file_id_li
|
|
||||||
);
|
|
||||||
} else if (log_lvl > 1) {
|
|
||||||
console.log('hosted_file_results:', hosted_file_results);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$ae_sess.files.disable_submit__hosted_file_obj = false;
|
$ae_sess.files.disable_submit__hosted_file_obj = false;
|
||||||
@@ -164,120 +143,72 @@ async function handle_input_upload_files({
|
|||||||
input_upload_files: any[];
|
input_upload_files: any[];
|
||||||
task_id: string;
|
task_id: string;
|
||||||
}) {
|
}) {
|
||||||
console.log('*** handle_input_upload_files() ***');
|
if (log_lvl) console.log('*** handle_input_upload_files() ***');
|
||||||
|
|
||||||
const form_data = new FormData();
|
const form_data = new FormData();
|
||||||
|
|
||||||
form_data.append('account_id', $ae_loc.account_id);
|
form_data.append('account_id', $ae_loc.account_id);
|
||||||
form_data.append('link_to_type', link_to_type);
|
form_data.append('link_to_type', link_to_type);
|
||||||
form_data.append('link_to_id', link_to_id);
|
form_data.append('link_to_id', link_to_id);
|
||||||
|
|
||||||
for (let i = 0; i < input_upload_files.length; i++) {
|
for (let i = 0; i < input_upload_files.length; i++) {
|
||||||
form_data.append(`file_list`, input_upload_files[i]);
|
form_data.append('file_list', input_upload_files[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hash_sha256, uploaded, uploaded_bytes
|
// Promise assigned to state so {#await ae_promises.upload__hosted_file_obj} in the
|
||||||
// $ae_sess.files.processed_file_list[i] = {
|
// template can track it. Using await here instead would hide the promise from the template.
|
||||||
// ...$ae_sess.files.processed_file_list[i],
|
|
||||||
// uploaded: $ae_sess.api_upload_kv[link_to_id].percent_completed,
|
|
||||||
// uploaded_bytes: $ae_sess.api_upload_kv[link_to_id].uploaded_bytes,
|
|
||||||
// };
|
|
||||||
|
|
||||||
let params = null;
|
|
||||||
|
|
||||||
let endpoint = '/v3/action/hosted_file/upload';
|
|
||||||
|
|
||||||
console.log(form_data);
|
|
||||||
|
|
||||||
params = null;
|
|
||||||
|
|
||||||
// Uncomment and the post_promise is not seen by the "await" below
|
|
||||||
// post_promise = await api.post_object({api_cfg: $cfg.api, endpoint: endpoint, params: params, data:form_data});
|
|
||||||
// Uncomment so that the post_promise is not seen by the "await" below
|
|
||||||
ae_promises.upload__hosted_file_obj = api
|
ae_promises.upload__hosted_file_obj = api
|
||||||
.post_object({
|
.post_object({
|
||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
endpoint: endpoint,
|
endpoint: '/v3/action/hosted_file/upload',
|
||||||
// params: params,
|
|
||||||
form_data: form_data,
|
form_data: form_data,
|
||||||
|
timeout: 1200000, // 20 min — large video/audio files
|
||||||
task_id: task_id,
|
task_id: task_id,
|
||||||
|
track_progress: true,
|
||||||
log_lvl: log_lvl
|
log_lvl: log_lvl
|
||||||
// retry_count: 1,
|
|
||||||
})
|
})
|
||||||
.then(async function (result) {
|
.then(function (result) {
|
||||||
// WARNING!!!! ONLY ONE FILE IS EXPECTED TO BE UPLOADED AT A TIME!!!
|
// Endpoint always returns a list; we upload one file at a time.
|
||||||
// NOTE: The upload endpoint always returns a list of successfully uploaded files. In this case we are only uploading one file and expecting a list of one item.
|
if (!result || !result[0]) {
|
||||||
let x = 0;
|
console.error('Upload failed — no result returned.');
|
||||||
console.log(result[x]);
|
return false;
|
||||||
let hosted_file_obj = result[x];
|
}
|
||||||
|
const hosted_file_obj = result[0];
|
||||||
let hosted_file_id = hosted_file_obj.hosted_file_id;
|
const hosted_file_id = hosted_file_obj.hosted_file_id;
|
||||||
|
|
||||||
hosted_file_id_li.push(hosted_file_id);
|
hosted_file_id_li.push(hosted_file_id);
|
||||||
hosted_file_obj_li.push(hosted_file_obj);
|
hosted_file_obj_li.push(hosted_file_obj);
|
||||||
|
|
||||||
let hosted_file_data: key_val = {};
|
const hosted_file_data: key_val = {
|
||||||
hosted_file_data['id'] = hosted_file_id; // Same as the hosted_file_id
|
id: hosted_file_id,
|
||||||
hosted_file_data['hosted_file_id'] = hosted_file_id;
|
hosted_file_id: hosted_file_id,
|
||||||
hosted_file_data['for_type'] = link_to_type;
|
for_type: link_to_type,
|
||||||
hosted_file_data['for_id'] = link_to_id;
|
for_id: link_to_id,
|
||||||
hosted_file_data['hash_sha256'] = hosted_file_obj.hash_sha256;
|
hash_sha256: hosted_file_obj.hash_sha256,
|
||||||
hosted_file_data['filename'] = hosted_file_obj.filename;
|
filename: hosted_file_obj.filename,
|
||||||
hosted_file_data['extension'] = hosted_file_obj.extension;
|
extension: hosted_file_obj.extension,
|
||||||
hosted_file_data['content_type'] = hosted_file_obj.content_type;
|
content_type: hosted_file_obj.content_type,
|
||||||
hosted_file_data['size'] = hosted_file_obj.size;
|
size: hosted_file_obj.size,
|
||||||
hosted_file_data['enable'] = true;
|
enable: true,
|
||||||
hosted_file_data['created_on'] = hosted_file_obj.created_on;
|
created_on: hosted_file_obj.created_on,
|
||||||
hosted_file_data['updated_on'] = hosted_file_obj.updated_on;
|
updated_on: hosted_file_obj.updated_on
|
||||||
console.log(hosted_file_data);
|
};
|
||||||
|
|
||||||
hosted_file_obj_kv[hosted_file_id] = hosted_file_data;
|
hosted_file_obj_kv[hosted_file_id] = hosted_file_data;
|
||||||
|
if (log_lvl) console.log('hosted_file_data:', hosted_file_data);
|
||||||
if (log_lvl) {
|
|
||||||
console.log(`hosted_file_data:`, hosted_file_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hosted_file_data;
|
return hosted_file_data;
|
||||||
|
|
||||||
// $ae_sess.files.new_upload_list[i].uploaded_bytes = 10; // fake 10 bytes at least...
|
|
||||||
|
|
||||||
// let event_file_id = await events_func.create_hosted_file_obj_from_hosted_file_async({
|
|
||||||
// api_cfg: $ae_api,
|
|
||||||
// hosted_file_id: hosted_file_id,
|
|
||||||
// data: event_file_data,
|
|
||||||
// log_lvl: log_lvl
|
|
||||||
// })
|
|
||||||
// .then(function (create_result) {
|
|
||||||
// console.log(create_result); // NOTE: This should be the event_file_id string
|
|
||||||
// // let event_file_id = create_result;
|
|
||||||
// return create_result;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return event_file_id;
|
|
||||||
})
|
})
|
||||||
// .then(function (hosted_file_data) {
|
|
||||||
// return hosted_file_data;
|
|
||||||
// })
|
|
||||||
.catch(function (error: any) {
|
.catch(function (error: any) {
|
||||||
console.log('Something went wrong.');
|
console.error('Upload failed:', error);
|
||||||
console.log(error);
|
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
$slct_trigger = 'load__hosted_file_obj_li';
|
$slct_trigger = 'load__hosted_file_obj_li';
|
||||||
});
|
});
|
||||||
|
|
||||||
if (log_lvl) {
|
return ae_promises.upload__hosted_file_obj;
|
||||||
console.log(`Waiting for upload__hosted_file_obj promise...`);
|
|
||||||
}
|
|
||||||
let hosted_file_result = ae_promises.upload__hosted_file_obj;
|
|
||||||
|
|
||||||
return hosted_file_result;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- class:hidden={!$ae_loc.trusted_access} -->
|
<!-- class:hidden={!$ae_loc.trusted_access} -->
|
||||||
<form onsubmit={handle_submit_form_files} class="{class_li_default} {class_li}">
|
<form onsubmit={prevent_default(handle_submit_form_files)} class="{class_li_default} {class_li}">
|
||||||
{#await ae_promises.upload__hosted_file_obj}
|
{#await ae_promises.upload__hosted_file_obj}
|
||||||
<div class="flex flex-row items-center justify-center gap-1 text-lg">
|
<div class="flex flex-row items-center justify-center gap-1 text-lg">
|
||||||
<Lucide.LoaderCircle class="m-1 animate-spin" />
|
<Lucide.LoaderCircle class="m-1 animate-spin" />
|
||||||
@@ -291,7 +222,7 @@ async function handle_input_upload_files({
|
|||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
<label
|
<label
|
||||||
for="ae_comp__hosted_files_upload__input"
|
for={input_element_id}
|
||||||
class="svelte_input_file_label text-center"
|
class="svelte_input_file_label text-center"
|
||||||
class:hidden={$ae_sess.files.disable_submit__hosted_file_obj}>
|
class:hidden={$ae_sess.files.disable_submit__hosted_file_obj}>
|
||||||
{#if label}{@render label()}{:else}
|
{#if label}{@render label()}{:else}
|
||||||
@@ -337,9 +268,19 @@ async function handle_input_upload_files({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-lg btn-primary preset-tonal-primary border-primary-500 hover:preset-tonal-success hover:border-success-500 w-54 border"
|
class="
|
||||||
|
btn btn-lg btn-primary
|
||||||
|
preset-tonal-primary
|
||||||
|
border border-primary-500
|
||||||
|
hover:preset-tonal-success hover:border-success-500
|
||||||
|
w-54
|
||||||
|
transition-all
|
||||||
|
"
|
||||||
|
class:opacity-30={$ae_sess.files.disable_submit__hosted_file_obj ||
|
||||||
|
$ae_sess.files.status__file_list != 'ready'}
|
||||||
disabled={$ae_sess.files.disable_submit__hosted_file_obj ||
|
disabled={$ae_sess.files.disable_submit__hosted_file_obj ||
|
||||||
$ae_sess.files.status__file_list != 'ready'}>
|
$ae_sess.files.status__file_list != 'ready'}
|
||||||
|
>
|
||||||
{#await ae_promises.upload__hosted_file_obj}
|
{#await ae_promises.upload__hosted_file_obj}
|
||||||
<Lucide.LoaderCircle class="m-1 animate-spin" />
|
<Lucide.LoaderCircle class="m-1 animate-spin" />
|
||||||
<span class="">
|
<span class="">
|
||||||
@@ -350,18 +291,18 @@ async function handle_input_upload_files({
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{:then}
|
{:then}
|
||||||
<Lucide.UploadCloud class="m-1" size={20} />
|
<Lucide.CloudUpload class="m-1" size={20} />
|
||||||
<span class="text-sm"> Upload </span>
|
<span class="text-sm"> Upload </span>
|
||||||
|
{#if $ae_sess.files.processed_file_list?.length > 0}
|
||||||
<span class="ml-2 grow font-bold">
|
<span class="ml-2 grow font-bold">
|
||||||
{#if $ae_sess.files.processed_file_list?.length > 0}
|
|
||||||
{$ae_sess.files.processed_file_list.length}
|
{$ae_sess.files.processed_file_list.length}
|
||||||
{$ae_sess.files.processed_file_list.length === 1
|
{$ae_sess.files.processed_file_list.length === 1
|
||||||
? 'file'
|
? 'file'
|
||||||
: 'files'}
|
: 'files'}
|
||||||
{:else}
|
|
||||||
<span class="text-xs"> 0 </span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-xs"> none </span>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { untrack } from 'svelte';
|
|||||||
* Specialized UI for managing site.cfg_json settings.
|
* Specialized UI for managing site.cfg_json settings.
|
||||||
* Supports General, AI, Performance, and IDAA-specific configurations.
|
* Supports General, AI, Performance, and IDAA-specific configurations.
|
||||||
*/
|
*/
|
||||||
import { Modal } from 'flowbite-svelte';
|
|
||||||
import {
|
import {
|
||||||
Brain,
|
Brain,
|
||||||
CodeXml,
|
CodeXml,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
Globe,
|
Globe,
|
||||||
Mail,
|
Mail,
|
||||||
Minus,
|
Minus,
|
||||||
@@ -34,7 +35,7 @@ $effect(() => {
|
|||||||
if (typeof cfg_json === 'string') {
|
if (typeof cfg_json === 'string') {
|
||||||
try {
|
try {
|
||||||
cfg_json = JSON.parse(cfg_json);
|
cfg_json = JSON.parse(cfg_json);
|
||||||
} catch (e) {
|
} catch {
|
||||||
cfg_json = {};
|
cfg_json = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,13 +46,14 @@ $effect(() => {
|
|||||||
let active_tab: 'visuals' | 'email' | 'ai' | 'refresh' | 'idaa' | 'raw' =
|
let active_tab: 'visuals' | 'email' | 'ai' | 'refresh' | 'idaa' | 'raw' =
|
||||||
$state('visuals');
|
$state('visuals');
|
||||||
let raw_json_str = $state('');
|
let raw_json_str = $state('');
|
||||||
|
let show_llm_api_token = $state(false);
|
||||||
|
|
||||||
// Ensure we have a valid object
|
// Ensure we have a valid object
|
||||||
if (!cfg_json) cfg_json = {};
|
if (!cfg_json) cfg_json = {};
|
||||||
|
|
||||||
function add_to_list(key: string) {
|
function add_to_list(key: string, prompt_label: string) {
|
||||||
if (!cfg_json[key]) cfg_json[key] = [];
|
if (!cfg_json[key]) cfg_json[key] = [];
|
||||||
const val = prompt('Enter Novi UUID:');
|
const val = prompt(prompt_label);
|
||||||
if (val) cfg_json[key].push(val);
|
if (val) cfg_json[key].push(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,10 @@ function remove_from_list(key: string, index: number) {
|
|||||||
cfg_json[key].splice(index, 1);
|
cfg_json[key].splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function list_count(key: string): number {
|
||||||
|
return Array.isArray(cfg_json[key]) ? cfg_json[key].length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Sync Raw JSON string when entering the tab
|
// Sync Raw JSON string when entering the tab
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (active_tab === 'raw') {
|
if (active_tab === 'raw') {
|
||||||
@@ -74,7 +80,7 @@ $effect(() => {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw_json_str);
|
const parsed = JSON.parse(raw_json_str);
|
||||||
cfg_json = parsed;
|
cfg_json = parsed;
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignore invalid JSON while typing
|
// Ignore invalid JSON while typing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,6 +242,14 @@ $effect(() => {
|
|||||||
>LLM Model</span>
|
>LLM Model</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
name="site_llm_model"
|
||||||
|
data-bwignore="true"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
bind:value={cfg_json.llm__api_model}
|
bind:value={cfg_json.llm__api_model}
|
||||||
class="input variant-form-material" />
|
class="input variant-form-material" />
|
||||||
</label>
|
</label>
|
||||||
@@ -243,10 +257,34 @@ $effect(() => {
|
|||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-xs font-bold uppercase opacity-50"
|
<span class="text-xs font-bold uppercase opacity-50"
|
||||||
>API Token</span>
|
>API Token</span>
|
||||||
<input
|
<div class="flex gap-2">
|
||||||
type="password"
|
<input
|
||||||
bind:value={cfg_json.llm__api_token}
|
type={show_llm_api_token ? 'text' : 'password'}
|
||||||
class="input variant-form-material font-mono" />
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
name="site_llm_api_token"
|
||||||
|
data-bwignore="true"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
bind:value={cfg_json.llm__api_token}
|
||||||
|
class="input variant-form-material grow font-mono" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm variant-soft-surface"
|
||||||
|
onclick={() =>
|
||||||
|
(show_llm_api_token = !show_llm_api_token)}
|
||||||
|
title={show_llm_api_token
|
||||||
|
? 'Hide API Token'
|
||||||
|
: 'Show API Token'}>
|
||||||
|
{#if show_llm_api_token}
|
||||||
|
<EyeOff size="1.1em" />
|
||||||
|
{:else}
|
||||||
|
<Eye size="1.1em" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-xs font-bold uppercase opacity-50"
|
<span class="text-xs font-bold uppercase opacity-50"
|
||||||
@@ -331,24 +369,32 @@ $effect(() => {
|
|||||||
|
|
||||||
<!-- UUID Lists -->
|
<!-- UUID Lists -->
|
||||||
<section class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<section class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
{#each [{ key: 'novi_admin_li', label: 'Novi Admins', color: 'text-error-500' }, { key: 'novi_trusted_li', label: 'Novi Trusted', color: 'text-warning-500' }, { key: 'novi_jitsi_mod_li', label: 'Jitsi Moderators', color: 'text-primary-500' }, { key: 'novi_idaa_group_guid_li', label: 'Member Group GUIDs', color: 'text-secondary-500' }] as list (list.key)}
|
{#each [{ key: 'novi_admin_li', label: 'Novi Admins', color: 'text-error-500', prompt: 'Enter Novi UUID:' }, { key: 'novi_trusted_li', label: 'Novi Trusted', color: 'text-warning-500', prompt: 'Enter Novi UUID:' }, { key: 'novi_jitsi_mod_li', label: 'Jitsi Moderators', color: 'text-primary-500', prompt: 'Enter Novi UUID:' }, { key: 'novi_idaa_group_guid_li', label: 'Member Group GUIDs', color: 'text-secondary-500', prompt: 'Enter Group GUID:' }, { key: 'jitsi_exclude_uuids', label: 'Jitsi Excluded UUIDs', color: 'text-error-500', prompt: 'Enter Novi UUID to exclude:' }, { key: 'jitsi_known_meetings', label: 'Known IDAA Meetings', color: 'text-primary-500', prompt: 'Enter meeting name to allow:' }] as list (list.key)}
|
||||||
<div class="bg-surface-500/5 space-y-2 rounded-lg p-3">
|
<details class="bg-surface-500/5 rounded-lg p-3">
|
||||||
<header class="flex items-center justify-between">
|
<summary
|
||||||
<span
|
class="flex cursor-pointer list-none items-center justify-between gap-2 [&::-webkit-details-marker]:hidden">
|
||||||
class="text-[10px] font-black tracking-wider uppercase {list.color}"
|
<span class="min-w-0">
|
||||||
>{list.label}</span>
|
<span
|
||||||
|
class="text-[10px] font-black tracking-wider uppercase {list.color}"
|
||||||
|
>{list.label}</span>
|
||||||
|
<span class="ml-2 text-[10px] opacity-50"
|
||||||
|
>({list_count(list.key)})</span>
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
class="btn btn-icon btn-icon-sm variant-soft-primary"
|
class="btn btn-icon btn-icon-sm variant-soft-primary"
|
||||||
onclick={() => add_to_list(list.key)}>
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
add_to_list(list.key, list.prompt);
|
||||||
|
}}>
|
||||||
<Plus size="12" />
|
<Plus size="12" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</summary>
|
||||||
<div class="space-y-1">
|
<div class="mt-3 space-y-1">
|
||||||
{#each cfg_json[list.key] ?? [] as uuid, i (uuid)}
|
{#each cfg_json[list.key] ?? [] as item, i (i)}
|
||||||
<div
|
<div
|
||||||
class="bg-surface-500/10 flex items-center gap-1 rounded p-1 font-mono text-[10px]">
|
class="bg-surface-500/10 flex items-center gap-1 rounded p-1 font-mono text-[10px]">
|
||||||
<span class="grow truncate"
|
<span class="grow truncate">{item}</span>
|
||||||
>{uuid}</span>
|
|
||||||
<button
|
<button
|
||||||
class="text-error-500 transition-transform hover:scale-110"
|
class="text-error-500 transition-transform hover:scale-110"
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
@@ -358,7 +404,7 @@ $effect(() => {
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,9 @@ export const properties_to_save = [
|
|||||||
'user_id',
|
'user_id',
|
||||||
'user_id_random',
|
'user_id_random',
|
||||||
'external_client_id',
|
'external_client_id',
|
||||||
|
'url_root',
|
||||||
|
'url_full_path',
|
||||||
|
'url_params',
|
||||||
'source',
|
'source',
|
||||||
'object_type',
|
'object_type',
|
||||||
'object_id',
|
'object_id',
|
||||||
|
|||||||
@@ -99,39 +99,46 @@ export async function lookup_site_domain({
|
|||||||
api_cfg,
|
api_cfg,
|
||||||
fqdn,
|
fqdn,
|
||||||
view = 'default',
|
view = 'default',
|
||||||
log_lvl = 0
|
log_lvl = 0,
|
||||||
|
access_key
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
fqdn: string;
|
fqdn: string;
|
||||||
view?: string;
|
view?: string;
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
|
access_key?: string;
|
||||||
}): Promise<ae_SiteDomain | null> {
|
}): Promise<ae_SiteDomain | null> {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(`*** lookup_site_domain() *** fqdn=${fqdn} (Cache-First)`);
|
console.log(`*** lookup_site_domain() *** fqdn=${fqdn} (Cache-First)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. FAST PATH: Check local cache first
|
// 1. FAST PATH: Check local cache first.
|
||||||
let cached = null;
|
// Skip when access_key is provided — the key must be validated server-side.
|
||||||
try {
|
// A stale cached entry with a previously-valid key must not grant access if the
|
||||||
cached = await db_core.site_domain.where('fqdn').equals(fqdn).first();
|
// URL key has changed or been revoked.
|
||||||
if (cached) {
|
if (!access_key) {
|
||||||
if (log_lvl)
|
try {
|
||||||
console.log(
|
const cached = await db_core.site_domain.where('fqdn').equals(fqdn).first();
|
||||||
'BOOTSTRAP: Cache hit. Returning cached site domain immediately.'
|
if (cached) {
|
||||||
);
|
if (log_lvl)
|
||||||
|
console.log(
|
||||||
|
'BOOTSTRAP: Cache hit. Returning cached site domain immediately.'
|
||||||
|
);
|
||||||
|
|
||||||
// Trigger background refresh to keep cache fresh, but don't await it
|
// Trigger background refresh to keep cache fresh, but don't await it
|
||||||
_refresh_site_domain_background({
|
_refresh_site_domain_background({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
fqdn,
|
fqdn,
|
||||||
view,
|
view,
|
||||||
log_lvl: 0
|
log_lvl: 0,
|
||||||
});
|
access_key
|
||||||
|
});
|
||||||
|
|
||||||
return cached as any;
|
return cached as ae_SiteDomain;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('BOOTSTRAP: Cache read failed.', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.warn('BOOTSTRAP: Cache read failed.', err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. SLOW PATH: Wait for API if cache is empty
|
// 2. SLOW PATH: Wait for API if cache is empty
|
||||||
@@ -139,7 +146,8 @@ export async function lookup_site_domain({
|
|||||||
api_cfg,
|
api_cfg,
|
||||||
fqdn,
|
fqdn,
|
||||||
view,
|
view,
|
||||||
log_lvl
|
log_lvl,
|
||||||
|
access_key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +158,8 @@ async function _refresh_site_domain_background({
|
|||||||
api_cfg,
|
api_cfg,
|
||||||
fqdn,
|
fqdn,
|
||||||
view,
|
view,
|
||||||
log_lvl
|
log_lvl,
|
||||||
|
access_key
|
||||||
}: any) {
|
}: any) {
|
||||||
try {
|
try {
|
||||||
const guest_api_cfg = { ...api_cfg };
|
const guest_api_cfg = { ...api_cfg };
|
||||||
@@ -172,10 +181,20 @@ async function _refresh_site_domain_background({
|
|||||||
delete guest_api_cfg.jwt;
|
delete guest_api_cfg.jwt;
|
||||||
delete guest_api_cfg.account_id;
|
delete guest_api_cfg.account_id;
|
||||||
|
|
||||||
const search_query = {
|
const search_query: any = {
|
||||||
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
|
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If the caller provided an access_key (from the URL), include it
|
||||||
|
// so the server can match restricted domains. Omit empty/falsy keys.
|
||||||
|
if (access_key) {
|
||||||
|
search_query.and.push({
|
||||||
|
field: 'access_key',
|
||||||
|
op: 'eq',
|
||||||
|
value: access_key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result_li = await api.search_ae_obj({
|
const result_li = await api.search_ae_obj({
|
||||||
api_cfg: guest_api_cfg,
|
api_cfg: guest_api_cfg,
|
||||||
obj_type: 'site_domain',
|
obj_type: 'site_domain',
|
||||||
@@ -202,23 +221,37 @@ async function _refresh_site_domain_background({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// WHY: The fast-path returns stale Dexie cache, then this background refresh
|
// WHY: The fast-path returns stale Dexie cache, then this background refresh
|
||||||
// runs after the page renders. If cfg_json changed server-side (e.g. a Novi
|
// runs after the page renders. Push any fields that may have been missing from
|
||||||
// API key was added), the stale cfg is already in $ae_loc. We push the fresh
|
// the stale cache (e.g. account_name, cfg_json) back into $ae_loc so the UI
|
||||||
// cfg_json into the store here so any layout tracking it (e.g. IDAA Novi
|
// reflects the correct values without requiring a second full page reload.
|
||||||
// verification) gets notified and can retry with the correct config.
|
const loc_patch: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (result.cfg_json) {
|
if (result.cfg_json) {
|
||||||
const current_cfg = get(ae_loc).site_cfg_json;
|
const current_cfg = get(ae_loc).site_cfg_json;
|
||||||
if (
|
if (JSON.stringify(current_cfg) !== JSON.stringify(result.cfg_json)) {
|
||||||
JSON.stringify(current_cfg) !==
|
loc_patch.site_cfg_json = result.cfg_json;
|
||||||
JSON.stringify(result.cfg_json)
|
|
||||||
) {
|
|
||||||
ae_loc.update((loc) => ({
|
|
||||||
...loc,
|
|
||||||
site_cfg_json: result.cfg_json
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.account_name) {
|
||||||
|
const current_name = get(ae_loc).account_name;
|
||||||
|
// Only overwrite the default placeholder — don't stomp a real value.
|
||||||
|
if (!current_name || current_name === 'Account Name Not Set' || current_name === 'Ghost Account') {
|
||||||
|
loc_patch.account_name = result.account_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.account_code) {
|
||||||
|
const current_code = get(ae_loc).account_code;
|
||||||
|
if (!current_code || current_code === 'not_set' || current_code === 'ghost') {
|
||||||
|
loc_patch.account_code = result.account_code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(loc_patch).length > 0) {
|
||||||
|
ae_loc.update((loc) => ({ ...loc, ...loc_patch }));
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -715,6 +748,7 @@ const properties_to_save__site_domain = [
|
|||||||
'account_name',
|
'account_name',
|
||||||
'fqdn',
|
'fqdn',
|
||||||
'access_key',
|
'access_key',
|
||||||
|
'site_domain_access_key',
|
||||||
'enable',
|
'enable',
|
||||||
'enable_from',
|
'enable_from',
|
||||||
'enable_to',
|
'enable_to',
|
||||||
|
|||||||
@@ -328,117 +328,76 @@ export async function delete_ae_obj_id__user({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* *** LEGACY AUTHENTICATION HEADER LOGIC ***
|
* *** V3 AUTHENTICATION FUNCTIONS ***
|
||||||
*
|
*
|
||||||
* The functions in this section interact with legacy Aether API authentication endpoints
|
* All functions below use the V3 action endpoints:
|
||||||
* (e.g., /user/authenticate, /user/lookup_email).
|
* POST /v3/action/user/authenticate (was: GET /user/authenticate)
|
||||||
|
* GET /v3/action/user/{id}/email_auth_key_url (was: GET /user/{id}/email_auth_key_url)
|
||||||
|
* POST /v3/crud/user/search (was: GET /user/lookup_email)
|
||||||
|
* POST /v3/action/user/{id}/change_password (was: PATCH /user/{id}/change_password)
|
||||||
*
|
*
|
||||||
* Unlike V3 endpoints which handle context automatically or via standard headers,
|
* Key differences from the legacy routes:
|
||||||
* these legacy endpoints have specific requirements:
|
* - Credentials are in the POST body, not query params (safer — not logged in URLs)
|
||||||
*
|
* - x-account-id header still required to scope username/email lookups to the account
|
||||||
* 1. They often require the `x-account-id` header to be explicitly set to the target
|
* - x-no-account-id must still be removed when we have a real account context
|
||||||
* account ID to find the user within that specific account context.
|
|
||||||
* 2. The standard API wrapper logic might strip `x-account-id` if `x-no-account-id`
|
|
||||||
* is present (Bootstrap Paradox logic). We must explicitly remove `x-no-account-id`
|
|
||||||
* and set `x-account-id` to ensure the request is routed correctly.
|
|
||||||
* 3. Some endpoints accept `account_id` as a query parameter, while others (like email sending)
|
|
||||||
* may crash (500 Error) if unexpected parameters are passed.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Updated 2025-04-04
|
// Updated 2026-04-25 — migrated from GET /user/authenticate to POST /v3/action/user/authenticate
|
||||||
// This function handles username/password authentication.
|
|
||||||
// It explicitly sets the x-account-id header to ensure the user is looked up in the correct account.
|
|
||||||
export async function auth_ae_obj__username_password({
|
export async function auth_ae_obj__username_password({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
account_id,
|
account_id,
|
||||||
null_account_id = false,
|
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
params = {},
|
|
||||||
try_cache = true,
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
null_account_id?: boolean;
|
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
params?: key_val;
|
|
||||||
try_cache?: boolean;
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(
|
console.log(
|
||||||
`*** auth_ae_obj__username_password() *** account_id=${account_id} username=${username} password=${password}`
|
`*** auth_ae_obj__username_password() *** account_id=${account_id} username=${username}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = '/user/authenticate';
|
// WHY: Must set x-account-id explicitly so the backend scopes the username lookup
|
||||||
|
// to the correct account. Remove x-no-account-id which is only used during bootstrap.
|
||||||
// Prepare API config with correct headers to override global guest settings
|
|
||||||
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
||||||
if (account_id) {
|
use_api_cfg.headers['x-account-id'] = account_id;
|
||||||
use_api_cfg.headers['x-account-id'] = account_id;
|
delete use_api_cfg.headers['x-no-account-id'];
|
||||||
delete use_api_cfg.headers['x-no-account-id'];
|
|
||||||
params['account_id'] = account_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null_account_id) {
|
|
||||||
params['null_account_id'] = true;
|
|
||||||
}
|
|
||||||
params['username'] = username; // Required
|
|
||||||
params['password'] = password; // Required
|
|
||||||
params['inc_jwt'] = true; // Request a JWT in the response
|
|
||||||
if (log_lvl > 1) {
|
|
||||||
console.log(`auth_ae_obj__username_password() - params:`, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
ae_promises.auth__username_password = await api
|
ae_promises.auth__username_password = await api
|
||||||
.get_object({
|
.post_object({
|
||||||
api_cfg: use_api_cfg,
|
api_cfg: use_api_cfg,
|
||||||
endpoint: endpoint,
|
endpoint: '/v3/action/user/authenticate',
|
||||||
params: params,
|
data: { username, password },
|
||||||
// data: {},
|
log_lvl
|
||||||
log_lvl: log_lvl
|
|
||||||
})
|
})
|
||||||
.then(async function (user_obj_get_result) {
|
.then(function (result: any) {
|
||||||
if (user_obj_get_result) {
|
return result ?? null;
|
||||||
return user_obj_get_result;
|
|
||||||
} else {
|
|
||||||
console.log('No results returned.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (error: any) {
|
.catch(function (error: any) {
|
||||||
console.log('No results returned or failed.', error);
|
console.log('auth_ae_obj__username_password failed:', error);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (log_lvl) {
|
|
||||||
console.log(
|
|
||||||
'ae_promises.auth__username_password:',
|
|
||||||
ae_promises.auth__username_password
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ae_promises.auth__username_password;
|
return ae_promises.auth__username_password;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated 2025-04-04
|
// Updated 2026-04-25 — migrated from GET /user/authenticate to POST /v3/action/user/authenticate
|
||||||
// This function handles authentication using a User ID and a one-time auth key.
|
|
||||||
export async function auth_ae_obj__user_id_user_auth_key({
|
export async function auth_ae_obj__user_id_user_auth_key({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
account_id,
|
account_id,
|
||||||
user_id,
|
user_id,
|
||||||
user_auth_key,
|
user_auth_key,
|
||||||
params = {},
|
|
||||||
try_cache = true,
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
user_auth_key: string;
|
user_auth_key: string;
|
||||||
params?: key_val;
|
|
||||||
try_cache?: boolean;
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
@@ -447,61 +406,36 @@ export async function auth_ae_obj__user_id_user_auth_key({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = '/user/authenticate';
|
|
||||||
|
|
||||||
// Prepare API config with correct headers to override global guest settings
|
|
||||||
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
||||||
if (account_id) {
|
use_api_cfg.headers['x-account-id'] = account_id;
|
||||||
use_api_cfg.headers['x-account-id'] = account_id;
|
delete use_api_cfg.headers['x-no-account-id'];
|
||||||
delete use_api_cfg.headers['x-no-account-id'];
|
|
||||||
params['account_id'] = account_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
params['user_id'] = user_id; // Required
|
|
||||||
params['auth_key'] = user_auth_key; // Required
|
|
||||||
params['inc_jwt'] = true; // Request a JWT in the response
|
|
||||||
if (log_lvl > 1) {
|
|
||||||
console.log(`auth_ae_obj__user_id_user_auth_key() - params:`, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
ae_promises.auth__user_id_user_key = await api
|
ae_promises.auth__user_id_user_key = await api
|
||||||
.get_object({
|
.post_object({
|
||||||
api_cfg: use_api_cfg,
|
api_cfg: use_api_cfg,
|
||||||
endpoint: endpoint,
|
endpoint: '/v3/action/user/authenticate',
|
||||||
params: params,
|
// WHY: valid_email=true marks the user's email as verified on successful magic-link auth
|
||||||
log_lvl: log_lvl
|
data: { user_id, auth_key: user_auth_key, valid_email: true },
|
||||||
|
log_lvl
|
||||||
})
|
})
|
||||||
.then(async function (user_obj_get_result) {
|
.then(function (result: any) {
|
||||||
if (user_obj_get_result) {
|
return result ?? null;
|
||||||
return user_obj_get_result;
|
|
||||||
} else {
|
|
||||||
console.log('No results returned.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (error: any) {
|
.catch(function (error: any) {
|
||||||
console.log('No results returned or failed.', error);
|
console.log('auth_ae_obj__user_id_user_auth_key failed:', error);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (log_lvl) {
|
|
||||||
console.log(
|
|
||||||
'ae_promises.auth__user_id_user_key:',
|
|
||||||
ae_promises.auth__user_id_user_key
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ae_promises.auth__user_id_user_key;
|
return ae_promises.auth__user_id_user_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an email to the user with a new one time use authentication key.
|
// Updated 2026-04-25 — migrated from GET /user/{id}/email_auth_key_url to V3 action path
|
||||||
// Updated 2025-04-08
|
|
||||||
// NOTE: This legacy endpoint is sensitive to extra query parameters and will 500 if account_id is passed in the URL.
|
|
||||||
export async function send_email_auth_ae_obj__user_id({
|
export async function send_email_auth_ae_obj__user_id({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
account_id,
|
account_id,
|
||||||
user_id,
|
user_id,
|
||||||
base_url,
|
base_url,
|
||||||
key_param_name = 'user_key', // API defaults to 'auth_key'
|
key_param_name = 'user_key',
|
||||||
params = {},
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
@@ -509,7 +443,6 @@ export async function send_email_auth_ae_obj__user_id({
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
base_url?: string;
|
base_url?: string;
|
||||||
key_param_name?: string;
|
key_param_name?: string;
|
||||||
params?: key_val;
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
@@ -518,47 +451,30 @@ export async function send_email_auth_ae_obj__user_id({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const email_auth_key_endpoint = `/user/${user_id}/email_auth_key_url`;
|
|
||||||
params = {
|
|
||||||
root_url: base_url,
|
|
||||||
key_param_name: key_param_name
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare API config with correct headers
|
|
||||||
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
||||||
if (account_id) {
|
use_api_cfg.headers['x-account-id'] = account_id;
|
||||||
use_api_cfg.headers['x-account-id'] = account_id;
|
delete use_api_cfg.headers['x-no-account-id'];
|
||||||
delete use_api_cfg.headers['x-no-account-id'];
|
|
||||||
// WARNING: Do NOT add account_id to params here, as it causes a 500 error on the legacy backend.
|
|
||||||
}
|
|
||||||
|
|
||||||
ae_promises.auth_key__send_email = await api.get_object({
|
ae_promises.auth_key__send_email = await api.get_object({
|
||||||
api_cfg: use_api_cfg,
|
api_cfg: use_api_cfg,
|
||||||
endpoint: email_auth_key_endpoint,
|
endpoint: `/v3/action/user/${user_id}/email_auth_key_url`,
|
||||||
params: params,
|
params: { root_url: base_url, key_param_name },
|
||||||
log_lvl: log_lvl
|
log_lvl
|
||||||
});
|
});
|
||||||
|
|
||||||
return ae_promises.auth_key__send_email;
|
return ae_promises.auth_key__send_email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up user based on email address provided
|
// Updated 2026-04-25 — migrated from GET /user/lookup_email to POST /v3/crud/user/search
|
||||||
// Updated 2025-04-08
|
|
||||||
export async function qry_ae_obj_li__user_email({
|
export async function qry_ae_obj_li__user_email({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
account_id,
|
account_id,
|
||||||
null_account_id = false,
|
|
||||||
email,
|
email,
|
||||||
params = {},
|
|
||||||
try_cache = true,
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
null_account_id?: boolean;
|
|
||||||
email: string;
|
email: string;
|
||||||
params?: key_val;
|
|
||||||
try_cache?: boolean;
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
@@ -567,56 +483,39 @@ export async function qry_ae_obj_li__user_email({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = '/user/lookup_email';
|
|
||||||
|
|
||||||
// Prepare API config with correct headers
|
|
||||||
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
const use_api_cfg = { ...api_cfg, headers: { ...api_cfg.headers } };
|
||||||
if (account_id) {
|
use_api_cfg.headers['x-account-id'] = account_id;
|
||||||
use_api_cfg.headers['x-account-id'] = account_id;
|
delete use_api_cfg.headers['x-no-account-id'];
|
||||||
delete use_api_cfg.headers['x-no-account-id'];
|
|
||||||
params['account_id'] = account_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
params['email'] = email; // Required
|
const results = await api
|
||||||
params['null_account_id'] = null_account_id || false;
|
.search_ae_obj({
|
||||||
|
|
||||||
ae_promises.qry__user_email = await api
|
|
||||||
.get_object({
|
|
||||||
api_cfg: use_api_cfg,
|
api_cfg: use_api_cfg,
|
||||||
endpoint: endpoint,
|
obj_type: 'user',
|
||||||
params: params,
|
search_query: { and: [{ field: 'email', op: 'eq', value: email }] },
|
||||||
log_lvl: log_lvl
|
log_lvl
|
||||||
})
|
|
||||||
.then(async function (user_obj_get_result) {
|
|
||||||
if (user_obj_get_result) {
|
|
||||||
return user_obj_get_result;
|
|
||||||
} else {
|
|
||||||
console.log('No results returned.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (error: any) {
|
.catch(function (error: any) {
|
||||||
console.log('No results returned or failed.', error);
|
console.log('qry_ae_obj_li__user_email failed:', error);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return ae_promises.qry__user_email;
|
// Return the first match to preserve the same interface callers expect
|
||||||
|
// (the old /user/lookup_email endpoint returned a single user object)
|
||||||
|
return results?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change user password
|
// Updated 2026-04-25 — migrated from PATCH /user/{id}/change_password to POST /v3/action
|
||||||
// Updated 2025-04-11
|
|
||||||
export async function auth_ae_obj__user_id_change_password({
|
export async function auth_ae_obj__user_id_change_password({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
account_id,
|
account_id,
|
||||||
user_id,
|
user_id,
|
||||||
password,
|
password,
|
||||||
params = {},
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: {
|
}: {
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
password: string;
|
password: string;
|
||||||
params?: key_val;
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
@@ -625,27 +524,19 @@ export async function auth_ae_obj__user_id_change_password({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `/user/${user_id}/change_password`;
|
|
||||||
|
|
||||||
params['user_id'] = user_id; // Required
|
|
||||||
ae_promises.change_password__user_id = await api
|
ae_promises.change_password__user_id = await api
|
||||||
.patch_object({
|
.post_object({
|
||||||
api_cfg: api_cfg,
|
api_cfg,
|
||||||
endpoint: endpoint,
|
endpoint: `/v3/action/user/${user_id}/change_password`,
|
||||||
params: params,
|
data: { new_password: password },
|
||||||
data: { password: password },
|
log_lvl
|
||||||
log_lvl: log_lvl
|
|
||||||
})
|
})
|
||||||
.then(async function (change_password_result) {
|
.then(function (result: any) {
|
||||||
if (change_password_result) {
|
return result ?? null;
|
||||||
return change_password_result;
|
|
||||||
} else {
|
|
||||||
console.log('No results returned.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (error: any) {
|
.catch(function (error: any) {
|
||||||
console.log('No results returned or failed.', error);
|
console.log('auth_ae_obj__user_id_change_password failed:', error);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return ae_promises.change_password__user_id;
|
return ae_promises.change_password__user_id;
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ async function load_ae_obj_id__site_domain({
|
|||||||
no_account_id = true;
|
no_account_id = true;
|
||||||
// api_cfg.headers['x_account_id'] = 'nothing here';
|
// api_cfg.headers['x_account_id'] = 'nothing here';
|
||||||
}
|
}
|
||||||
|
// LEGACY BOOTSTRAP SPECIAL CASE: this helper is effectively a remove
|
||||||
|
// candidate once all site-domain lookups use the cache-first/bootstrap
|
||||||
|
// path in ae_core__site.ts.
|
||||||
no_account_id = true;
|
no_account_id = true;
|
||||||
|
|
||||||
const params = {};
|
const params = {};
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export async function load_ae_obj_by_code__data_store({
|
|||||||
save_idb?: boolean;
|
save_idb?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
|
// TEMPORARY: this no-account fallback exists only until the backend
|
||||||
|
// can serve account-scoped defaults via JWT-backed access alone.
|
||||||
|
// Keep this path narrow and remove it when the backend no longer
|
||||||
|
// needs a transport-level scope drop for data_store.
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(`*** load_ae_obj_by_code__data_store() *** code=${code}`);
|
console.log(`*** load_ae_obj_by_code__data_store() *** code=${code}`);
|
||||||
|
|||||||
@@ -1,6 +1,53 @@
|
|||||||
import type { Dexie, Table } from 'dexie';
|
import type { Dexie, Table } from 'dexie';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks IDB table content versions and clears any tables whose version has
|
||||||
|
* changed since the last check.
|
||||||
|
*
|
||||||
|
* Version numbers live in IDB_CONTENT_VERSIONS (store_versions.ts). State is
|
||||||
|
* tracked in localStorage (ae_idb_ver__{module}__{table}) so each table is
|
||||||
|
* cleared exactly once per version bump, not on every page load.
|
||||||
|
*
|
||||||
|
* Call once at module init in each db_*.ts, after the singleton is created.
|
||||||
|
* The clear is intentionally fire-and-forget — the SWR pattern repopulates
|
||||||
|
* from the API naturally after a cache miss.
|
||||||
|
*
|
||||||
|
* A null stored version (never tracked) is treated as outdated so existing
|
||||||
|
* installs with stale cached data are cleaned up on first run.
|
||||||
|
*/
|
||||||
|
export async function check_and_clear_idb_tables({
|
||||||
|
db_instance,
|
||||||
|
module_name,
|
||||||
|
table_versions,
|
||||||
|
log_lvl = 0
|
||||||
|
}: {
|
||||||
|
db_instance: Dexie;
|
||||||
|
module_name: string;
|
||||||
|
table_versions: Record<string, number>;
|
||||||
|
log_lvl?: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
for (const [table_name, expected_version] of Object.entries(table_versions)) {
|
||||||
|
const ls_key = `ae_idb_ver__${module_name}__${table_name}`;
|
||||||
|
const stored_raw = localStorage.getItem(ls_key);
|
||||||
|
const stored_version = stored_raw !== null ? parseInt(stored_raw, 10) : null;
|
||||||
|
|
||||||
|
if (stored_version === expected_version) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db_instance.table(table_name).clear();
|
||||||
|
localStorage.setItem(ls_key, String(expected_version));
|
||||||
|
console.log(
|
||||||
|
`[IDB] "${module_name}.${table_name}" cleared — v${stored_version ?? 'new'} → v${expected_version}`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[IDB] Failed to clear "${module_name}.${table_name}":`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the primary key from an object using a prioritized list of possible key names.
|
* Extracts the primary key from an object using a prioritized list of possible key names.
|
||||||
* @param obj The object to extract the ID from.
|
* @param obj The object to extract the ID from.
|
||||||
|
|||||||
54
src/lib/ae_core/core__idb_sort.ts
Normal file
54
src/lib/ae_core/core__idb_sort.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* src/lib/ae_core/core__idb_sort.ts
|
||||||
|
*
|
||||||
|
* Shared utility for computing tmp_sort_* fields stored in Dexie.
|
||||||
|
* All fields are designed for ascending .sortBy() — no .reverse() needed.
|
||||||
|
*
|
||||||
|
* Encoding rules:
|
||||||
|
* priority — inverted boolean: true→'0', false→'1' so priority=true sorts first (ASC)
|
||||||
|
* sort — zero-padded integer string so "00000010" < "00000020" (correct numeric order)
|
||||||
|
* all other fields — appended as-is; ISO 8601 datetimes already sort correctly
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||||
|
* prefix: [obj.group ?? '0'], // fields before priority (optional)
|
||||||
|
* priority: obj.priority,
|
||||||
|
* sort: obj.sort,
|
||||||
|
* fields_1: [obj.start_datetime], // appended to base for tmp_sort_1
|
||||||
|
* fields_2: [obj.name], // appended after fields_1 for tmp_sort_2
|
||||||
|
* fields_3: [obj.updated_on], // appended after fields_2 for tmp_sort_3
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function build_tmp_sort({
|
||||||
|
prefix = [],
|
||||||
|
priority,
|
||||||
|
sort,
|
||||||
|
fields_1 = [],
|
||||||
|
fields_2 = [],
|
||||||
|
fields_3 = [],
|
||||||
|
pad_width = 8
|
||||||
|
}: {
|
||||||
|
prefix?: (string | null | undefined)[];
|
||||||
|
priority?: boolean | null;
|
||||||
|
sort?: number | string | null;
|
||||||
|
fields_1?: (string | null | undefined)[];
|
||||||
|
fields_2?: (string | null | undefined)[];
|
||||||
|
fields_3?: (string | null | undefined)[];
|
||||||
|
pad_width?: number;
|
||||||
|
}): { tmp_sort_1: string; tmp_sort_2: string; tmp_sort_3: string } {
|
||||||
|
const clean = (v: string | null | undefined): string => v ?? '';
|
||||||
|
|
||||||
|
const p = priority ? '0' : '1';
|
||||||
|
const s = String(Number(sort ?? 0)).padStart(pad_width, '0');
|
||||||
|
|
||||||
|
const parts_base = [...prefix.map(clean), p, s].join('_');
|
||||||
|
const parts_1 = fields_1.map(clean).filter(Boolean).join('_');
|
||||||
|
const parts_2 = fields_2.map(clean).filter(Boolean).join('_');
|
||||||
|
const parts_3 = fields_3.map(clean).filter(Boolean).join('_');
|
||||||
|
|
||||||
|
const tmp_sort_1 = [parts_base, parts_1].filter(Boolean).join('_');
|
||||||
|
const tmp_sort_2 = [tmp_sort_1, parts_2].filter(Boolean).join('_');
|
||||||
|
const tmp_sort_3 = [tmp_sort_2, parts_3].filter(Boolean).join('_');
|
||||||
|
|
||||||
|
return { tmp_sort_1, tmp_sort_2, tmp_sort_3 };
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
export interface Site_Domain {
|
|
||||||
id: string;
|
|
||||||
// id_random: string;
|
|
||||||
site_id: string;
|
|
||||||
site_id_random?: string;
|
|
||||||
|
|
||||||
fqdn: string;
|
|
||||||
access_key?: null | string;
|
|
||||||
required_referrer?: null | string;
|
|
||||||
valid_for?: null | number; // In hours
|
|
||||||
|
|
||||||
enable: null | boolean;
|
|
||||||
hide?: null | boolean;
|
|
||||||
priority?: null | boolean;
|
|
||||||
sort?: null | number;
|
|
||||||
group?: null | string;
|
|
||||||
notes?: null | string;
|
|
||||||
created_on: Date;
|
|
||||||
updated_on?: null | Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
import { api } from '$lib/api/api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a site_domain object by its Fully Qualified Domain Name (FQDN).
|
|
||||||
*
|
|
||||||
* @param api_cfg - The API configuration object.
|
|
||||||
* @param fqdn - The FQDN of the site domain to fetch.
|
|
||||||
* @param timeout - The request timeout in milliseconds.
|
|
||||||
* @param log_lvl - The logging level.
|
|
||||||
* @returns The site domain object or null if not found.
|
|
||||||
*/
|
|
||||||
export async function load_ae_obj_by_fqdn__site_domain({
|
|
||||||
api_cfg,
|
|
||||||
fqdn,
|
|
||||||
timeout = 7000,
|
|
||||||
log_lvl = 0
|
|
||||||
}: {
|
|
||||||
api_cfg: any;
|
|
||||||
fqdn: string;
|
|
||||||
timeout?: number;
|
|
||||||
log_lvl?: number;
|
|
||||||
}): Promise<any> {
|
|
||||||
if (log_lvl) {
|
|
||||||
console.log(
|
|
||||||
`*** load_ae_obj_by_fqdn__site_domain() *** api.base_url=${api_cfg.base_url}, fqdn=${fqdn}, timeout=${timeout}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const site_domain_obj = await api.get_ae_obj_id_crud({
|
|
||||||
api_cfg: api_cfg,
|
|
||||||
no_account_id: true, // This seems to be a special case for this endpoint
|
|
||||||
obj_type: 'site_domain',
|
|
||||||
obj_id: fqdn, // NOTE: This is the FQDN, not the ID.
|
|
||||||
use_alt_table: true,
|
|
||||||
use_alt_base: true,
|
|
||||||
params: params,
|
|
||||||
timeout: timeout,
|
|
||||||
log_lvl: log_lvl
|
|
||||||
});
|
|
||||||
|
|
||||||
if (site_domain_obj) {
|
|
||||||
return site_domain_obj;
|
|
||||||
} else {
|
|
||||||
console.log('No results returned.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('No results returned or failed.', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,23 +9,23 @@ import { Modal } from 'flowbite-svelte';
|
|||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
BotMessageSquare,
|
BotMessageSquare,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Globe,
|
||||||
|
Copy,
|
||||||
Loader,
|
Loader,
|
||||||
FileText,
|
|
||||||
Save,
|
Save,
|
||||||
FilePenLine,
|
FilePenLine,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Settings,
|
Settings,
|
||||||
RefreshCcw,
|
|
||||||
Globe,
|
|
||||||
Copy
|
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
import { ae_loc } from '$lib/stores/ae_stores';
|
||||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// Core Props
|
// Core Props
|
||||||
content: string; // The text to summarize/analyze
|
content: string | null | undefined; // The text to summarize/analyze
|
||||||
summary: string; // The result (bindable)
|
summary: string | null | undefined; // The result (bindable)
|
||||||
|
|
||||||
// Configuration (Bindable for global settings persistence)
|
// Configuration (Bindable for global settings persistence)
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -68,10 +68,21 @@ if (maxTokens === undefined) maxTokens = 512;
|
|||||||
if (temperature === undefined) temperature = 0.7;
|
if (temperature === undefined) temperature = 0.7;
|
||||||
|
|
||||||
// Internal State
|
// Internal State
|
||||||
let ae_promises: any = $state(null);
|
let ae_promises = $state<Promise<unknown> | null>(null);
|
||||||
let show_modal = $state(false);
|
let show_modal = $state(false);
|
||||||
let active_tab: 'result' | 'settings' = $state('result');
|
let active_tab: 'result' | 'settings' = $state('result');
|
||||||
let tmp_summary = $state('');
|
let tmp_summary = $state('');
|
||||||
|
let show_api_token = $state(false);
|
||||||
|
|
||||||
|
const panel_class = 'space-y-4 rounded-xl border border-surface-500/20 bg-surface-500/5 p-4 shadow-sm';
|
||||||
|
const panel_title_class = 'flex items-center gap-2 border-b border-surface-500/20 pb-2 text-lg font-bold';
|
||||||
|
const tab_button_base_class = 'btn btn-sm border transition-all duration-200';
|
||||||
|
const tab_button_active_class = 'border-surface-200-800 bg-surface-200-800 text-surface-950-50 shadow-sm';
|
||||||
|
const tab_button_inactive_class = 'border-transparent bg-surface-50-900/60 text-surface-600-400 hover:border-surface-200-800 hover:bg-surface-100-900 hover:text-surface-950-50';
|
||||||
|
|
||||||
|
function tab_button_class(is_active: boolean): string {
|
||||||
|
return `${tab_button_base_class} ${is_active ? tab_button_active_class : tab_button_inactive_class}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function generate_ai_result() {
|
async function generate_ai_result() {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
@@ -83,7 +94,9 @@ async function generate_ai_result() {
|
|||||||
|
|
||||||
// If no token is provided, trigger a "Demo Mode" placeholder after a fake delay
|
// If no token is provided, trigger a "Demo Mode" placeholder after a fake delay
|
||||||
if (!token || token === '') {
|
if (!token || token === '') {
|
||||||
console.log('AE_AITools: No token provided. Entering Demo Mode.');
|
if (log_lvl > 0) {
|
||||||
|
console.log('AE_AITools: No token provided. Entering Demo Mode.');
|
||||||
|
}
|
||||||
ae_promises = new Promise((resolve) => {
|
ae_promises = new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tmp_summary = `### AI Summary (DEMO MODE)\n\nThis is a placeholder summary because no API token was provided in the settings. \n\n**Original Content Length:** ${content.length} characters.\n\n**System Prompt:** ${systemPrompt}\n\n**Model:** ${model}`;
|
tmp_summary = `### AI Summary (DEMO MODE)\n\nThis is a placeholder summary because no API token was provided in the settings. \n\n**Original Content Length:** ${content.length} characters.\n\n**System Prompt:** ${systemPrompt}\n\n**Model:** ${model}`;
|
||||||
@@ -121,10 +134,13 @@ async function generate_ai_result() {
|
|||||||
tmp_summary = result;
|
tmp_summary = result;
|
||||||
show_modal = true;
|
show_modal = true;
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
console.error('AE_AITools: AI Error:', err);
|
if (log_lvl > 0) {
|
||||||
|
console.error('AE_AITools: AI Error:', err);
|
||||||
|
}
|
||||||
|
const err_msg = err instanceof Error ? err.message : String(err);
|
||||||
// Even on error, show the modal with the error message so the UI can be inspected
|
// Even on error, show the modal with the error message so the UI can be inspected
|
||||||
tmp_summary = `### AI Error\n\nFailed to connect to the AI service.\n\n**Error:** ${err.message}\n\nCheck your Settings tab for Base URL and Token configuration.`;
|
tmp_summary = `### AI Error\n\nFailed to connect to the AI service.\n\n**Error:** ${err_msg}\n\nCheck your Settings tab for Base URL and Token configuration.`;
|
||||||
show_modal = true;
|
show_modal = true;
|
||||||
ae_promises = Promise.resolve();
|
ae_promises = Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -141,15 +157,23 @@ function handle_save() {
|
|||||||
<!-- Trigger Button -->
|
<!-- Trigger Button -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={generate_ai_result}
|
onclick={() => {
|
||||||
|
if (summary) {
|
||||||
|
tmp_summary = summary;
|
||||||
|
active_tab = 'result';
|
||||||
|
show_modal = true;
|
||||||
|
} else {
|
||||||
|
generate_ai_result();
|
||||||
|
}
|
||||||
|
}}
|
||||||
class={buttonClass}
|
class={buttonClass}
|
||||||
title="Generate AI summary/analysis">
|
title={summary ? 'View existing AI summary' : 'Generate AI summary/analysis'}>
|
||||||
{#await ae_promises}
|
{#await ae_promises}
|
||||||
<Loader class="mr-1 inline-block animate-spin" size="1.2em" />
|
<Loader class="mr-1 inline-block animate-spin" size="1.2em" />
|
||||||
<span class="text-sm">Processing...</span>
|
<span class="text-sm">Processing...</span>
|
||||||
{:then}
|
{:then}
|
||||||
<BotMessageSquare class="mr-1 inline-block" size="1.2em" />
|
<BotMessageSquare class="mr-1 inline-block" size="1.2em" />
|
||||||
<span class="text-sm">Summarize</span>
|
<span class="text-sm hidden">Summarize</span>
|
||||||
{:catch}
|
{:catch}
|
||||||
<span class="text-sm text-red-500">Error</span>
|
<span class="text-sm text-red-500">Error</span>
|
||||||
{/await}
|
{/await}
|
||||||
@@ -162,9 +186,10 @@ function handle_save() {
|
|||||||
active_tab = 'settings';
|
active_tab = 'settings';
|
||||||
show_modal = true;
|
show_modal = true;
|
||||||
}}
|
}}
|
||||||
class="btn btn-sm variant-soft-surface shadow-md"
|
class="btn btn-sm preset-tonal-surface shadow-md"
|
||||||
title="AI Settings">
|
title="AI Settings">
|
||||||
<Settings size="1.2em" />
|
<Settings size="1.2em" />
|
||||||
|
<span class="text-sm hidden">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Unified AI Modal -->
|
<!-- Unified AI Modal -->
|
||||||
@@ -173,21 +198,17 @@ function handle_save() {
|
|||||||
title="Aether AI Assistant"
|
title="Aether AI Assistant"
|
||||||
bind:open={show_modal}
|
bind:open={show_modal}
|
||||||
size="lg"
|
size="lg"
|
||||||
class="bg-white dark:bg-gray-800">
|
class="relative mx-auto flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border border-surface-200-800 bg-surface-50-900 text-surface-950-50 shadow-xl">
|
||||||
<div class="space-y-4 p-2">
|
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-3">
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<div class="border-surface-500/20 flex gap-1 border-b pb-2">
|
<div class="bg-surface-500/10 sticky top-0 z-10 mx-auto flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm {active_tab === 'result'
|
class={tab_button_class(active_tab === 'result')}
|
||||||
? 'variant-filled-primary'
|
|
||||||
: 'variant-soft-surface'}"
|
|
||||||
onclick={() => (active_tab = 'result')}>
|
onclick={() => (active_tab = 'result')}>
|
||||||
<Bot size="1.1em" class="mr-1" /> Result
|
<Bot size="1.1em" class="mr-1" /> Result
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm {active_tab === 'settings'
|
class={tab_button_class(active_tab === 'settings')}
|
||||||
? 'variant-filled-secondary'
|
|
||||||
: 'variant-soft-surface'}"
|
|
||||||
onclick={() => (active_tab = 'settings')}>
|
onclick={() => (active_tab = 'settings')}>
|
||||||
<Settings size="1.1em" class="mr-1" /> Settings
|
<Settings size="1.1em" class="mr-1" /> Settings
|
||||||
</button>
|
</button>
|
||||||
@@ -197,12 +218,12 @@ function handle_save() {
|
|||||||
<div class="animate-in fade-in space-y-4 duration-200">
|
<div class="animate-in fade-in space-y-4 duration-200">
|
||||||
<div class="flex justify-start gap-2">
|
<div class="flex justify-start gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm variant-filled-success"
|
class="btn btn-sm preset-filled-success"
|
||||||
onclick={handle_save}>
|
onclick={handle_save}>
|
||||||
<Save size="1.1em" class="mr-1" /> Save Result
|
<Save size="1.1em" class="mr-1" /> Save Result
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm variant-ghost-primary"
|
class="btn btn-sm preset-tonal-primary"
|
||||||
onclick={generate_ai_result}>
|
onclick={generate_ai_result}>
|
||||||
<RotateCcw size="1.1em" class="mr-1" /> Re-run
|
<RotateCcw size="1.1em" class="mr-1" /> Re-run
|
||||||
</button>
|
</button>
|
||||||
@@ -219,15 +240,14 @@ function handle_save() {
|
|||||||
<div
|
<div
|
||||||
class="animate-in slide-in-from-left-4 space-y-6 duration-200">
|
class="animate-in slide-in-from-left-4 space-y-6 duration-200">
|
||||||
<!-- Connection Settings -->
|
<!-- Connection Settings -->
|
||||||
<div class="space-y-4">
|
<section class={panel_class}>
|
||||||
<h3
|
<h3 class={panel_title_class}>
|
||||||
class="text-surface-500 flex items-center gap-2 text-sm font-bold tracking-widest uppercase">
|
|
||||||
<Globe size="1.1em" /> API Connection
|
<Globe size="1.1em" /> API Connection
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{#if onSyncConfig}
|
{#if onSyncConfig}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm variant-soft-primary"
|
class="btn btn-sm preset-tonal-primary"
|
||||||
onclick={onSyncConfig}>
|
onclick={onSyncConfig}>
|
||||||
<Copy size="1.1em" class="mr-1" /> Sync Global
|
<Copy size="1.1em" class="mr-1" /> Sync Global
|
||||||
Defaults
|
Defaults
|
||||||
@@ -239,6 +259,10 @@ function handle_save() {
|
|||||||
<span>Base URL</span>
|
<span>Base URL</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
bind:value={baseUrl}
|
bind:value={baseUrl}
|
||||||
class="input input-sm" />
|
class="input input-sm" />
|
||||||
</label>
|
</label>
|
||||||
@@ -246,24 +270,54 @@ function handle_save() {
|
|||||||
<span>Model</span>
|
<span>Model</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
name="ai_model"
|
||||||
|
data-bwignore="true"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
bind:value={model}
|
bind:value={model}
|
||||||
class="input input-sm" />
|
class="input input-sm" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span>API Token</span>
|
<span>API Token</span>
|
||||||
<input
|
<div class="flex gap-2">
|
||||||
type="password"
|
<input
|
||||||
bind:value={token}
|
type={show_api_token ? 'text' : 'password'}
|
||||||
class="input input-sm font-mono" />
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
name="ai_api_token"
|
||||||
|
data-bwignore="true"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
bind:value={token}
|
||||||
|
class="input input-sm font-mono" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm preset-tonal-surface"
|
||||||
|
onclick={() =>
|
||||||
|
(show_api_token = !show_api_token)}
|
||||||
|
title={show_api_token
|
||||||
|
? 'Hide API Token'
|
||||||
|
: 'Show API Token'}>
|
||||||
|
{#if show_api_token}
|
||||||
|
<EyeOff size="1.1em" />
|
||||||
|
{:else}
|
||||||
|
<Eye size="1.1em" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Model Parameters -->
|
<!-- Model Parameters -->
|
||||||
<div
|
<section class={panel_class}>
|
||||||
class="border-surface-500/10 space-y-4 border-t pt-4">
|
<h3 class={panel_title_class}>
|
||||||
<h3
|
|
||||||
class="text-surface-500 flex items-center gap-2 text-sm font-bold tracking-widest uppercase">
|
|
||||||
<FilePenLine size="1.1em" /> Inference Parameters
|
<FilePenLine size="1.1em" /> Inference Parameters
|
||||||
</h3>
|
</h3>
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
@@ -289,10 +343,10 @@ function handle_save() {
|
|||||||
<span>System Prompt</span>
|
<span>System Prompt</span>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={systemPrompt}
|
bind:value={systemPrompt}
|
||||||
class="textarea h-24 font-mono text-xs"
|
class="textarea h-24 font-mono text-xs bg-surface-50-900"
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Siren,
|
Siren,
|
||||||
MessageSquareWarning,
|
FingerprintPattern,
|
||||||
Fingerprint,
|
|
||||||
Globe,
|
Globe,
|
||||||
BookHeart,
|
BookHeart,
|
||||||
BriefcaseBusiness,
|
BriefcaseBusiness,
|
||||||
@@ -15,10 +14,11 @@ import {
|
|||||||
Settings
|
Settings
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { ae_loc } from '$lib/stores/ae_stores';
|
import { ae_loc } from '$lib/stores/ae_stores';
|
||||||
|
import type { ae_JournalEntryDraft } from '$lib/types/ae_types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// The object containing the flags (bindable)
|
// The object containing the flags (bindable)
|
||||||
obj: any;
|
obj: ae_JournalEntryDraft;
|
||||||
|
|
||||||
// Visibility configuration (optional overrides)
|
// Visibility configuration (optional overrides)
|
||||||
show_labels?: boolean;
|
show_labels?: boolean;
|
||||||
@@ -49,9 +49,38 @@ let {
|
|||||||
container_class = 'flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10'
|
container_class = 'flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
function handle_toggle(prop: string) {
|
function emit_toggle(prop: string, value: boolean) {
|
||||||
obj[prop] = !obj[prop];
|
if (onToggle) onToggle(prop, value);
|
||||||
if (onToggle) onToggle(prop, obj[prop]);
|
}
|
||||||
|
|
||||||
|
function toggle_alert() {
|
||||||
|
obj.alert = !obj.alert;
|
||||||
|
emit_toggle('alert', !!obj.alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_private() {
|
||||||
|
obj.private = !obj.private;
|
||||||
|
emit_toggle('private', !!obj.private);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_public() {
|
||||||
|
obj.public = !obj.public;
|
||||||
|
emit_toggle('public', !!obj.public);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_personal() {
|
||||||
|
obj.personal = !obj.personal;
|
||||||
|
emit_toggle('personal', !!obj.personal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_professional() {
|
||||||
|
obj.professional = !obj.professional;
|
||||||
|
emit_toggle('professional', !!obj.professional);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_template() {
|
||||||
|
obj.template = !obj.template;
|
||||||
|
emit_toggle('template', !!obj.template);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -63,81 +92,69 @@ function handle_toggle(prop: string) {
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Alert Status -->
|
|
||||||
{#if !hide_alert}
|
{#if !hide_alert}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handle_toggle('alert')}
|
onclick={toggle_alert}
|
||||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||||
title="Toggle Alert Status">
|
title="Toggle alert status">
|
||||||
<Siren
|
<Siren size="1.2em" class={obj?.alert ? 'text-error-500' : 'opacity-40'} />
|
||||||
size="1.2em"
|
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Alert</span>
|
||||||
class={obj?.alert ? 'text-error-500' : 'opacity-40'} />
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Private / E2EE -->
|
|
||||||
{#if !hide_private}
|
{#if !hide_private}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handle_toggle('private')}
|
onclick={toggle_private}
|
||||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||||
title="Toggle Private/Encrypted">
|
title="Toggle private or encrypted visibility">
|
||||||
<Fingerprint
|
<FingerprintPattern size="1.2em" class={obj?.private ? 'text-success-500' : 'opacity-40'} />
|
||||||
size="1.2em"
|
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Private or Encrypt</span>
|
||||||
class={obj?.private ? 'text-success-500' : 'opacity-40'} />
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Public Visibility -->
|
|
||||||
{#if !hide_public}
|
{#if !hide_public}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handle_toggle('public')}
|
onclick={toggle_public}
|
||||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||||
title="Toggle Public Visibility">
|
title="Toggle public visibility">
|
||||||
<Globe
|
<Globe size="1.2em" class={obj?.public ? 'text-success-500' : 'opacity-40'} />
|
||||||
size="1.2em"
|
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Public</span>
|
||||||
class={obj?.public ? 'text-success-500' : 'opacity-40'} />
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Personal Scope -->
|
|
||||||
{#if !hide_personal}
|
{#if !hide_personal}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handle_toggle('personal')}
|
onclick={toggle_personal}
|
||||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||||
title="Toggle Personal Scope">
|
title="Toggle personal scope">
|
||||||
<BookHeart
|
<BookHeart size="1.2em" class={obj?.personal ? 'text-success-500' : 'opacity-40'} />
|
||||||
size="1.2em"
|
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Personal</span>
|
||||||
class={obj?.personal ? 'text-success-500' : 'opacity-40'} />
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Professional Scope -->
|
|
||||||
{#if !hide_professional}
|
{#if !hide_professional}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handle_toggle('professional')}
|
onclick={toggle_professional}
|
||||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||||
title="Toggle Professional Scope">
|
title="Toggle professional scope">
|
||||||
<BriefcaseBusiness
|
<BriefcaseBusiness size="1.2em" class={obj?.professional ? 'text-success-500' : 'opacity-40'} />
|
||||||
size="1.2em"
|
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Professional</span>
|
||||||
class={obj?.professional ? 'text-success-500' : 'opacity-40'} />
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Template Status -->
|
|
||||||
{#if !hide_template && $ae_loc.edit_mode}
|
{#if !hide_template && $ae_loc.edit_mode}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handle_toggle('template')}
|
onclick={toggle_template}
|
||||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||||
title="Toggle Template Mode">
|
title="Toggle template mode">
|
||||||
<NotepadTextDashed
|
<NotepadTextDashed size="1.2em" class={obj?.template ? 'text-success-500' : 'opacity-40'} />
|
||||||
size="1.2em"
|
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Template</span>
|
||||||
class={obj?.template ? 'text-success-500' : 'opacity-40'} />
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { key_val } from '$lib/stores/ae_stores';
|
import type { key_val } from '$lib/stores/ae_stores';
|
||||||
import { api } from '$lib/api/api';
|
import { api } from '$lib/api/api';
|
||||||
|
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
|
||||||
|
import type { PressMgmtRemoteCfg } from '$lib/stores/ae_events_stores__pres_mgmt_defaults';
|
||||||
|
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
|
||||||
|
import type { BadgesRemoteCfg } from '$lib/stores/ae_events_stores__badges_defaults';
|
||||||
|
|
||||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||||
import { db_events } from '$lib/ae_events/db_events';
|
import { db_events } from '$lib/ae_events/db_events';
|
||||||
@@ -783,7 +787,7 @@ export const properties_to_save = [
|
|||||||
'event_id',
|
'event_id',
|
||||||
'code',
|
'code',
|
||||||
'account_id',
|
'account_id',
|
||||||
'account_id_random',
|
// 'account_id_random',
|
||||||
'conference',
|
'conference',
|
||||||
'type',
|
'type',
|
||||||
'name',
|
'name',
|
||||||
@@ -833,6 +837,7 @@ export const properties_to_save = [
|
|||||||
'attend_url_passcode',
|
'attend_url_passcode',
|
||||||
'attend_phone',
|
'attend_phone',
|
||||||
'attend_phone_passcode',
|
'attend_phone_passcode',
|
||||||
|
'default_qry_str',
|
||||||
'tmp_sort_1',
|
'tmp_sort_1',
|
||||||
'tmp_sort_2',
|
'tmp_sort_2',
|
||||||
'file_count',
|
'file_count',
|
||||||
@@ -943,83 +948,165 @@ export async function process_ae_obj__event_props({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sync_config__event_pres_mgmt
|
||||||
|
*
|
||||||
|
* Syncs the server-side event pres_mgmt config (mod_pres_mgmt_json) into the
|
||||||
|
* local PersistedState store (pres_mgmt_loc.current).
|
||||||
|
*
|
||||||
|
* Called reactively in pres_mgmt/+page.svelte whenever the event object changes.
|
||||||
|
*
|
||||||
|
* Always-synced fields: labels, requirements, access links, file config.
|
||||||
|
* Lock-synced fields (only when lock_config=true): all visibility/hide__ toggles,
|
||||||
|
* launcher links, navigation limits. This prevents presenter laptops from
|
||||||
|
* drifting into different display configs across a conference.
|
||||||
|
*
|
||||||
|
* Canonical key convention (enforced here):
|
||||||
|
* hide__* → feature ON by default; true = turn off (default: false = visible)
|
||||||
|
* show__* → feature OFF by default; true = turn on (default: false = hidden)
|
||||||
|
*/
|
||||||
export function sync_config__event_pres_mgmt({
|
export function sync_config__event_pres_mgmt({
|
||||||
pres_mgmt_cfg_remote,
|
pres_mgmt_cfg_remote,
|
||||||
pres_mgmt_cfg_local,
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: {
|
}: {
|
||||||
pres_mgmt_cfg_remote: key_val;
|
pres_mgmt_cfg_remote: Partial<PressMgmtRemoteCfg>;
|
||||||
pres_mgmt_cfg_local: key_val;
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}) {
|
}) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(
|
console.log(
|
||||||
`*** sync_config__event_pres_mgmt() *** pres_mgmt_cfg_remote:`,
|
`*** sync_config__event_pres_mgmt() *** remote:`,
|
||||||
pres_mgmt_cfg_remote
|
pres_mgmt_cfg_remote
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pres_mgmt_cfg_local.label__person_external_id =
|
const loc = pres_mgmt_loc.current;
|
||||||
|
|
||||||
|
// --- Always sync: labels, requirements, opt-in features, file config ---
|
||||||
|
// These are safe to always apply — they don't override local display preferences.
|
||||||
|
loc.label__person_external_id =
|
||||||
pres_mgmt_cfg_remote?.label__person_external_id ?? 'External ID';
|
pres_mgmt_cfg_remote?.label__person_external_id ?? 'External ID';
|
||||||
pres_mgmt_cfg_local.label__presenter_external_id =
|
loc.label__presenter_external_id =
|
||||||
pres_mgmt_cfg_remote?.label__presenter_external_id ?? 'External ID';
|
pres_mgmt_cfg_remote?.label__presenter_external_id ?? 'External ID';
|
||||||
pres_mgmt_cfg_local.label__session_poc_type =
|
loc.label__session_poc_type =
|
||||||
pres_mgmt_cfg_remote?.label__session_poc_type ?? 'poc';
|
pres_mgmt_cfg_remote?.label__session_poc_type ?? 'poc';
|
||||||
pres_mgmt_cfg_local.label__session_poc_name =
|
loc.label__session_poc_name =
|
||||||
pres_mgmt_cfg_remote?.label__session_poc_name_short ?? 'POC';
|
|
||||||
pres_mgmt_cfg_local.label__session_poc_name =
|
|
||||||
pres_mgmt_cfg_remote?.label__session_poc_name ?? 'Point of Contact';
|
pres_mgmt_cfg_remote?.label__session_poc_name ?? 'Point of Contact';
|
||||||
pres_mgmt_cfg_local.hide__session_poc =
|
|
||||||
pres_mgmt_cfg_remote?.hide__session_poc ?? false;
|
loc.require__presenter_agree =
|
||||||
pres_mgmt_cfg_local.require__presenter_agree =
|
|
||||||
pres_mgmt_cfg_remote?.require__presenter_agree ?? false;
|
pres_mgmt_cfg_remote?.require__presenter_agree ?? false;
|
||||||
pres_mgmt_cfg_local.require__session_agree =
|
loc.require__session_agree =
|
||||||
pres_mgmt_cfg_remote?.require__session_agree ?? false;
|
pres_mgmt_cfg_remote?.require__session_agree ?? false;
|
||||||
pres_mgmt_cfg_local.show__copy_access_link =
|
|
||||||
|
loc.show__copy_access_link =
|
||||||
pres_mgmt_cfg_remote?.show__copy_access_link ?? false;
|
pres_mgmt_cfg_remote?.show__copy_access_link ?? false;
|
||||||
pres_mgmt_cfg_local.show__email_access_link =
|
loc.show__email_access_link =
|
||||||
pres_mgmt_cfg_remote?.show__email_access_link ?? false;
|
pres_mgmt_cfg_remote?.show__email_access_link ?? false;
|
||||||
pres_mgmt_cfg_local.file_purpose_option_kv =
|
|
||||||
|
loc.file_purpose_option_kv =
|
||||||
pres_mgmt_cfg_remote?.file_purpose_option_kv ?? null;
|
pres_mgmt_cfg_remote?.file_purpose_option_kv ?? null;
|
||||||
|
|
||||||
if (pres_mgmt_cfg_local.lock_config) {
|
loc.hide__report_kv = pres_mgmt_cfg_remote?.hide__report_kv ?? {};
|
||||||
console.log(`The config should be locked! Forcing the sync!`);
|
|
||||||
pres_mgmt_cfg_local.sync_local_config = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pres_mgmt_cfg_local?.sync_local_config) {
|
// --- Lock-synced: visibility, launcher, navigation, session/presenter fields ---
|
||||||
pres_mgmt_cfg_local.hide__location_code =
|
// When lock_config=true the remote config wins for ALL display flags, preventing
|
||||||
|
// local browser state from overriding the admin's event-level configuration.
|
||||||
|
if (pres_mgmt_cfg_remote?.lock_config) {
|
||||||
|
if (log_lvl) console.log(`sync_config__event_pres_mgmt: lock_config=true, forcing full sync`);
|
||||||
|
|
||||||
|
// Codes
|
||||||
|
loc.hide__location_code =
|
||||||
pres_mgmt_cfg_remote?.hide__location_code ?? false;
|
pres_mgmt_cfg_remote?.hide__location_code ?? false;
|
||||||
pres_mgmt_cfg_local.hide__presentation_code =
|
loc.hide__presentation_code =
|
||||||
pres_mgmt_cfg_remote?.hide__presentation_code ?? false;
|
pres_mgmt_cfg_remote?.hide__presentation_code ?? false;
|
||||||
pres_mgmt_cfg_local.hide__presentation_datetime =
|
loc.hide__presenter_code =
|
||||||
pres_mgmt_cfg_remote?.hide__presentation_datetime ?? false;
|
|
||||||
pres_mgmt_cfg_local.show_content__presentation_description =
|
|
||||||
pres_mgmt_cfg_remote?.show_content__presentation_description ??
|
|
||||||
false;
|
|
||||||
pres_mgmt_cfg_local.hide__presenter_code =
|
|
||||||
pres_mgmt_cfg_remote?.hide__presenter_code ?? false;
|
pres_mgmt_cfg_remote?.hide__presenter_code ?? false;
|
||||||
pres_mgmt_cfg_local.hide__presenter_biography =
|
loc.hide__session_code =
|
||||||
pres_mgmt_cfg_remote?.hide__presenter_biography ?? false;
|
|
||||||
pres_mgmt_cfg_local.hide__session_code =
|
|
||||||
pres_mgmt_cfg_remote?.hide__session_code ?? false;
|
pres_mgmt_cfg_remote?.hide__session_code ?? false;
|
||||||
pres_mgmt_cfg_local.hide__session_description =
|
|
||||||
|
// Session fields
|
||||||
|
loc.hide__session_description =
|
||||||
pres_mgmt_cfg_remote?.hide__session_description ?? false;
|
pres_mgmt_cfg_remote?.hide__session_description ?? false;
|
||||||
pres_mgmt_cfg_local.hide__session_location =
|
loc.hide__session_location =
|
||||||
pres_mgmt_cfg_remote?.hide__session_location ?? false;
|
pres_mgmt_cfg_remote?.hide__session_location ?? false;
|
||||||
pres_mgmt_cfg_local.hide__session_msg =
|
loc.hide__session_msg =
|
||||||
pres_mgmt_cfg_remote?.hide__session_msg ?? false;
|
pres_mgmt_cfg_remote?.hide__session_msg ?? false;
|
||||||
pres_mgmt_cfg_local.hide__session_poc_profile =
|
loc.hide__session_poc =
|
||||||
pres_mgmt_cfg_remote?.hide__session_poc_profile ?? false;
|
pres_mgmt_cfg_remote?.hide__session_poc ?? false;
|
||||||
pres_mgmt_cfg_local.hide__session_poc_biography =
|
loc.hide__session_poc_biography =
|
||||||
pres_mgmt_cfg_remote?.hide__session_poc_biography ?? false;
|
pres_mgmt_cfg_remote?.hide__session_poc_biography ?? false;
|
||||||
pres_mgmt_cfg_local.hide__session_poc_profile_pic =
|
loc.hide__session_poc_profile =
|
||||||
|
pres_mgmt_cfg_remote?.hide__session_poc_profile ?? false;
|
||||||
|
loc.hide__session_poc_profile_pic =
|
||||||
pres_mgmt_cfg_remote?.hide__session_poc_profile_pic ?? false;
|
pres_mgmt_cfg_remote?.hide__session_poc_profile_pic ?? false;
|
||||||
pres_mgmt_cfg_local.hide_launcher_link =
|
|
||||||
pres_mgmt_cfg_remote?.hide_launcher_link ?? false;
|
// Presenter fields
|
||||||
pres_mgmt_cfg_local.hide_launcher_link_legacy =
|
loc.hide__presenter_biography =
|
||||||
pres_mgmt_cfg_remote?.hide_launcher_link_legacy ?? false;
|
pres_mgmt_cfg_remote?.hide__presenter_biography ?? false;
|
||||||
|
|
||||||
|
// Presentation fields
|
||||||
|
loc.hide__presentation_datetime =
|
||||||
|
pres_mgmt_cfg_remote?.hide__presentation_datetime ?? false;
|
||||||
|
loc.hide__presentation_description =
|
||||||
|
pres_mgmt_cfg_remote?.hide__presentation_description ?? false;
|
||||||
|
|
||||||
|
// Launcher links (show__ in remote → invert to hide__ in local display state)
|
||||||
|
loc.hide__launcher_link =
|
||||||
|
!(pres_mgmt_cfg_remote?.show__launcher_link ?? false);
|
||||||
|
// Legacy Flask launcher is retired — always hide regardless of remote config
|
||||||
|
loc.hide__launcher_link_legacy = true;
|
||||||
|
|
||||||
|
// Navigation / UI constraints
|
||||||
|
loc.limit__navigation =
|
||||||
|
pres_mgmt_cfg_remote?.limit__navigation ?? false;
|
||||||
|
loc.limit__options =
|
||||||
|
pres_mgmt_cfg_remote?.limit__options ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sync_config__event_badges
|
||||||
|
*
|
||||||
|
* Mirror a subset of server-side `mod_badges_json` into the local persisted
|
||||||
|
* `badges_loc.current` store so the UI can read a fast local copy of
|
||||||
|
* authoritative feature flags (search mode, QR enable, mass-print, etc.).
|
||||||
|
*
|
||||||
|
* Called reactively from badge-related pages when the event object changes.
|
||||||
|
*/
|
||||||
|
export function sync_config__event_badges({
|
||||||
|
badges_cfg_remote,
|
||||||
|
log_lvl = 0
|
||||||
|
}: {
|
||||||
|
badges_cfg_remote: Partial<BadgesRemoteCfg>;
|
||||||
|
log_lvl?: number;
|
||||||
|
}) {
|
||||||
|
if (log_lvl) {
|
||||||
|
console.log(
|
||||||
|
`*** sync_config__event_badges() *** remote:`,
|
||||||
|
badges_cfg_remote
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pres_mgmt_cfg_local;
|
const loc = badges_loc.current;
|
||||||
|
|
||||||
|
// Always-sync: feature flags and simple values
|
||||||
|
loc.enable_mass_print = badges_cfg_remote?.enable_mass_print ?? true;
|
||||||
|
loc.enable_add_badge_btn = badges_cfg_remote?.enable_add_badge_btn ?? true;
|
||||||
|
loc.enable_upload_badge_li_btn = badges_cfg_remote?.enable_upload_badge_li_btn ?? true;
|
||||||
|
loc.enable_search_qr = badges_cfg_remote?.enable_search_qr ?? true;
|
||||||
|
loc.qr_type = badges_cfg_remote?.qr_type ?? null;
|
||||||
|
|
||||||
|
// Per-tier search constraints
|
||||||
|
loc.anon_search_result_limit = badges_cfg_remote?.anon_search_result_limit ?? 15;
|
||||||
|
loc.anon_search_min_chars = badges_cfg_remote?.anon_search_min_chars ?? 3;
|
||||||
|
loc.auth_search_result_limit = badges_cfg_remote?.auth_search_result_limit ?? 25;
|
||||||
|
loc.auth_search_min_chars = badges_cfg_remote?.auth_search_min_chars ?? 2;
|
||||||
|
loc.trusted_search_result_limit = badges_cfg_remote?.trusted_search_result_limit ?? 150;
|
||||||
|
loc.trusted_search_min_chars = badges_cfg_remote?.trusted_search_min_chars ?? 1;
|
||||||
|
|
||||||
|
// Passcodes and permissions (may be null)
|
||||||
|
loc.trusted_passcode = badges_cfg_remote?.trusted_passcode ?? null;
|
||||||
|
loc.administrator_passcode = badges_cfg_remote?.administrator_passcode ?? null;
|
||||||
|
loc.edit_permissions = badges_cfg_remote?.edit_permissions ?? null;
|
||||||
|
|
||||||
|
loc.remote_cfg_last_synced_on = new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ describe('create_ae_obj__event_badge', () => {
|
|||||||
expect(mockCreateNested).toHaveBeenCalled();
|
expect(mockCreateNested).toHaveBeenCalled();
|
||||||
expect(mockCreateNested).toHaveBeenCalledWith({
|
expect(mockCreateNested).toHaveBeenCalledWith({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
parent_type: 'event',
|
parent_type: 'event_person',
|
||||||
parent_id: event_id,
|
parent_id: event_id,
|
||||||
child_type: 'event_badge',
|
child_type: 'event_badge',
|
||||||
fields: data_kv,
|
fields: data_kv,
|
||||||
return_obj: true,
|
params: {},
|
||||||
log_lvl: 0
|
log_lvl: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ describe('update_ae_obj__event_badge', () => {
|
|||||||
child_type: 'event_badge',
|
child_type: 'event_badge',
|
||||||
child_id: 'eb999',
|
child_id: 'eb999',
|
||||||
fields: { full_name_override: 'Updated' },
|
fields: { full_name_override: 'Updated' },
|
||||||
return_obj: true,
|
params: {},
|
||||||
log_lvl: 0
|
log_lvl: 0
|
||||||
});
|
});
|
||||||
expect(res).toEqual(fakeResult);
|
expect(res).toEqual(fakeResult);
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export async function create_ae_obj__event_badge({
|
|||||||
|
|
||||||
const result = await api.create_nested_obj({
|
const result = await api.create_nested_obj({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
parent_type: 'event',
|
parent_type: 'event_person',
|
||||||
parent_id: event_id,
|
parent_id: event_id,
|
||||||
child_type: 'event_badge',
|
child_type: 'event_badge',
|
||||||
fields: data_kv,
|
fields: data_kv,
|
||||||
@@ -501,9 +501,10 @@ export async function search__event_badge({
|
|||||||
|
|
||||||
if (printed_status === 'printed') {
|
if (printed_status === 'printed') {
|
||||||
search_query.and.push({ field: 'print_count', op: 'gt', value: 0 });
|
search_query.and.push({ field: 'print_count', op: 'gt', value: 0 });
|
||||||
} else if (printed_status === 'not_printed') {
|
|
||||||
search_query.and.push({ field: 'print_count', op: 'eq', value: 0 });
|
|
||||||
}
|
}
|
||||||
|
// 'not_printed' intentionally omitted from the API query: unprinted badges have
|
||||||
|
// print_count = NULL (never set), and `eq 0` in SQL does not match NULL.
|
||||||
|
// The IDB fast path (print_count ?? 0) < 1 and visible_badge_obj_li handle this correctly.
|
||||||
|
|
||||||
if (enabled === 'enabled')
|
if (enabled === 'enabled')
|
||||||
search_query.and.push({ field: 'enable', op: 'eq', value: true });
|
search_query.and.push({ field: 'enable', op: 'eq', value: true });
|
||||||
@@ -632,6 +633,7 @@ export const properties_to_save = [
|
|||||||
'print_last_datetime',
|
'print_last_datetime',
|
||||||
'allow_tracking',
|
'allow_tracking',
|
||||||
'agree_to_tc',
|
'agree_to_tc',
|
||||||
|
'cfg_json',
|
||||||
'other_1_code',
|
'other_1_code',
|
||||||
'other_2_code',
|
'other_2_code',
|
||||||
'other_3_code',
|
'other_3_code',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const editable_fields__event_badge_template = [
|
|||||||
'logo_filename',
|
'logo_filename',
|
||||||
'logo_path',
|
'logo_path',
|
||||||
'header_path',
|
'header_path',
|
||||||
|
'background_image_path',
|
||||||
'secondary_header_path',
|
'secondary_header_path',
|
||||||
'footer_path',
|
'footer_path',
|
||||||
'header_row_1',
|
'header_row_1',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const properties_to_save = [
|
|||||||
'logo_filename',
|
'logo_filename',
|
||||||
'logo_path',
|
'logo_path',
|
||||||
'header_path',
|
'header_path',
|
||||||
|
'background_image_path',
|
||||||
'secondary_header_path',
|
'secondary_header_path',
|
||||||
'footer_path',
|
'footer_path',
|
||||||
'header_row_1',
|
'header_row_1',
|
||||||
@@ -36,6 +37,8 @@ export const properties_to_save = [
|
|||||||
'layout',
|
'layout',
|
||||||
'style_filename',
|
'style_filename',
|
||||||
'style_href',
|
'style_href',
|
||||||
|
'cfg_json',
|
||||||
|
'other_json',
|
||||||
'duplex',
|
'duplex',
|
||||||
'enable',
|
'enable',
|
||||||
'hide',
|
'hide',
|
||||||
|
|||||||
@@ -269,6 +269,50 @@ async function _refresh_file_li_background({
|
|||||||
properties_to_save,
|
properties_to_save,
|
||||||
log_lvl
|
log_lvl
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prune stale Dexie records that no longer exist on the server.
|
||||||
|
// WHY: bulkPut only upserts — it never removes records deleted on
|
||||||
|
// the server. Without this, deleted files remain visible in the UI
|
||||||
|
// indefinitely (the liveQuery sees them in Dexie forever).
|
||||||
|
//
|
||||||
|
// Scope guard: only delete records that WOULD have been included in
|
||||||
|
// this query. Records outside the query scope (e.g. a hidden file
|
||||||
|
// when hidden='not_hidden') are intentionally left in Dexie so
|
||||||
|
// that a later 'all' query can still find them.
|
||||||
|
// We prune on any valid API response, including an empty list —
|
||||||
|
// an empty array from the API is authoritative ("no files here").
|
||||||
|
const returned_ids = new Set(
|
||||||
|
processed.map((f: any) => f.id).filter(Boolean)
|
||||||
|
);
|
||||||
|
const dexie_all = await db_events.file
|
||||||
|
.where('for_id')
|
||||||
|
.equals(for_obj_id)
|
||||||
|
.toArray();
|
||||||
|
const stale_ids = dexie_all
|
||||||
|
.filter((f) => {
|
||||||
|
if (!f.id || returned_ids.has(f.id)) return false;
|
||||||
|
// Would this record have appeared in our query?
|
||||||
|
const is_enabled = !!f.enable;
|
||||||
|
const is_hidden = !!f.hide;
|
||||||
|
const within_enabled_scope =
|
||||||
|
enabled === 'all' ||
|
||||||
|
(enabled === 'enabled' && is_enabled) ||
|
||||||
|
(enabled === 'not_enabled' && !is_enabled);
|
||||||
|
const within_hidden_scope =
|
||||||
|
hidden === 'all' ||
|
||||||
|
(hidden === 'not_hidden' && !is_hidden) ||
|
||||||
|
(hidden === 'hidden' && is_hidden);
|
||||||
|
return within_enabled_scope && within_hidden_scope;
|
||||||
|
})
|
||||||
|
.map((f) => f.id as string);
|
||||||
|
if (stale_ids.length > 0) {
|
||||||
|
if (log_lvl)
|
||||||
|
console.log(
|
||||||
|
`🗑️ [DEBUG] Pruning ${stale_ids.length} stale file records from Dexie`,
|
||||||
|
stale_ids
|
||||||
|
);
|
||||||
|
await db_events.file.bulkDelete(stale_ids);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
@@ -315,7 +359,7 @@ export async function create_event_file_obj_from_hosted_file_async({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (return_obj) return result;
|
if (return_obj) return result;
|
||||||
return result?.event_file_id || result?.id || result?.event_file_id_random;
|
return result?.event_file_id || result?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delete_ae_obj_id__event_file({
|
export async function delete_ae_obj_id__event_file({
|
||||||
@@ -483,15 +527,11 @@ export const qry__event_file = search__event_file;
|
|||||||
export const properties_to_save = [
|
export const properties_to_save = [
|
||||||
'id',
|
'id',
|
||||||
'event_file_id',
|
'event_file_id',
|
||||||
// 'event_file_id_random', // DO NOT UNCOMMENT
|
|
||||||
'hosted_file_id',
|
'hosted_file_id',
|
||||||
// 'hosted_file_id_random', // DO NOT UNCOMMENT
|
|
||||||
'hash_sha256',
|
'hash_sha256',
|
||||||
'for_type',
|
'for_type',
|
||||||
'for_id',
|
'for_id',
|
||||||
// 'for_id_random', // DO NOT UNCOMMENT
|
|
||||||
'event_id',
|
'event_id',
|
||||||
// 'event_id_random', // DO NOT UNCOMMENT
|
|
||||||
'event_session_id',
|
'event_session_id',
|
||||||
'event_presentation_id',
|
'event_presentation_id',
|
||||||
'event_presenter_id',
|
'event_presenter_id',
|
||||||
@@ -554,22 +594,9 @@ async function _process_generic_props<T extends Record<string, any>>({
|
|||||||
const processed_obj_li: T[] = [];
|
const processed_obj_li: T[] = [];
|
||||||
for (const original_obj of obj_li) {
|
for (const original_obj of obj_li) {
|
||||||
let processed_obj = { ...original_obj };
|
let processed_obj = { ...original_obj };
|
||||||
for (const key in processed_obj) {
|
const base_id_key = `${obj_type}_id`;
|
||||||
if (key.endsWith('_random')) {
|
if (processed_obj[base_id_key])
|
||||||
const newKey = key.slice(0, -7);
|
(processed_obj as any).id = processed_obj[base_id_key];
|
||||||
// ONLY overwrite if the random variant has a valid value
|
|
||||||
if (
|
|
||||||
processed_obj[key] !== null &&
|
|
||||||
processed_obj[key] !== undefined &&
|
|
||||||
processed_obj[key] !== ''
|
|
||||||
) {
|
|
||||||
(processed_obj as any)[newKey] = processed_obj[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const random_id_key = `${obj_type}_id_random`;
|
|
||||||
if (processed_obj[random_id_key])
|
|
||||||
(processed_obj as any).id = processed_obj[random_id_key];
|
|
||||||
const group = processed_obj.group ?? '0';
|
const group = processed_obj.group ?? '0';
|
||||||
const priority = processed_obj.priority ? 1 : 0;
|
const priority = processed_obj.priority ? 1 : 0;
|
||||||
const sort = processed_obj.sort ?? '0';
|
const sort = processed_obj.sort ?? '0';
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ async function _handle_nested_loads(
|
|||||||
for_obj_type: 'event_location',
|
for_obj_type: 'event_location',
|
||||||
for_obj_id: current_location_id,
|
for_obj_id: current_location_id,
|
||||||
enabled: 'all',
|
enabled: 'all',
|
||||||
limit: 25,
|
hidden: 'all',
|
||||||
log_lvl
|
log_lvl
|
||||||
}).then((res) => (location_obj.event_file_li = res))
|
}).then((res) => (location_obj.event_file_li = res))
|
||||||
);
|
);
|
||||||
|
|||||||
42
src/lib/ae_events/ae_events__event_person.ts
Normal file
42
src/lib/ae_events/ae_events__event_person.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { key_val } from '$lib/stores/ae_stores';
|
||||||
|
import { api } from '$lib/api/api';
|
||||||
|
|
||||||
|
const ae_promises: key_val = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create_ae_obj__event_person
|
||||||
|
* Creates a new event_person record linked to an event.
|
||||||
|
* Used as the first step of manual one-off badge creation.
|
||||||
|
* The returned event_person_id is then passed to create_ae_obj__event_badge.
|
||||||
|
*/
|
||||||
|
export async function create_ae_obj__event_person({
|
||||||
|
api_cfg,
|
||||||
|
event_id,
|
||||||
|
data_kv,
|
||||||
|
params = {},
|
||||||
|
log_lvl = 0
|
||||||
|
}: {
|
||||||
|
api_cfg: any;
|
||||||
|
event_id: string;
|
||||||
|
data_kv: key_val;
|
||||||
|
params?: key_val;
|
||||||
|
log_lvl?: number;
|
||||||
|
}): Promise<any | null> {
|
||||||
|
if (log_lvl) {
|
||||||
|
console.log(
|
||||||
|
`*** create_ae_obj__event_person() *** event_id=${event_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ae_promises.create__event_person = await api.create_nested_obj({
|
||||||
|
api_cfg,
|
||||||
|
parent_type: 'event',
|
||||||
|
parent_id: event_id,
|
||||||
|
child_type: 'event_person',
|
||||||
|
fields: data_kv,
|
||||||
|
params,
|
||||||
|
log_lvl
|
||||||
|
});
|
||||||
|
|
||||||
|
return ae_promises.create__event_person;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { key_val } from '$lib/stores/ae_stores';
|
|||||||
import { api } from '$lib/api/api';
|
import { api } from '$lib/api/api';
|
||||||
|
|
||||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||||
|
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||||
import { db_events } from '$lib/ae_events/db_events';
|
import { db_events } from '$lib/ae_events/db_events';
|
||||||
import type { ae_EventPresentation } from '$lib/types/ae_types';
|
import type { ae_EventPresentation } from '$lib/types/ae_types';
|
||||||
|
|
||||||
@@ -181,6 +182,8 @@ async function _handle_nested_loads(
|
|||||||
for_obj_type: 'event_presentation',
|
for_obj_type: 'event_presentation',
|
||||||
for_obj_id: current_presentation_id,
|
for_obj_id: current_presentation_id,
|
||||||
enabled,
|
enabled,
|
||||||
|
// WHY: include hidden files so Manage Files UI can show/unhide them.
|
||||||
|
hidden: 'all',
|
||||||
limit: 25,
|
limit: 25,
|
||||||
try_cache,
|
try_cache,
|
||||||
log_lvl
|
log_lvl
|
||||||
@@ -678,6 +681,18 @@ export async function process_ae_obj__event_presentation_props({
|
|||||||
if (obj.event_session_id_random)
|
if (obj.event_session_id_random)
|
||||||
obj.event_session_id = obj.event_session_id_random;
|
obj.event_session_id = obj.event_session_id_random;
|
||||||
if (obj.event_id_random) obj.event_id = obj.event_id_random;
|
if (obj.event_id_random) obj.event_id = obj.event_id_random;
|
||||||
|
|
||||||
|
// Override generic tmp_sort_* with presentation-specific encoding via
|
||||||
|
// build_tmp_sort. Order: priority DESC → sort ASC → start_datetime ASC → code ASC → name ASC
|
||||||
|
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
|
||||||
|
prefix: [obj.group ?? '0'],
|
||||||
|
priority: obj.priority,
|
||||||
|
sort: obj.sort,
|
||||||
|
fields_1: [obj.start_datetime, obj.code],
|
||||||
|
fields_2: [obj.name]
|
||||||
|
});
|
||||||
|
obj.tmp_sort_1 = tmp_sort_1;
|
||||||
|
obj.tmp_sort_2 = tmp_sort_2;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ async function _refresh_presenter_id_background({
|
|||||||
for_obj_type: 'event_presenter',
|
for_obj_type: 'event_presenter',
|
||||||
for_obj_id: event_presenter_id,
|
for_obj_id: event_presenter_id,
|
||||||
enabled: 'all',
|
enabled: 'all',
|
||||||
|
// WHY: include hidden files so Manage Files UI can show/unhide them.
|
||||||
|
hidden: 'all',
|
||||||
limit: 25,
|
limit: 25,
|
||||||
try_cache: false,
|
try_cache: false,
|
||||||
log_lvl
|
log_lvl
|
||||||
@@ -284,6 +286,8 @@ async function _refresh_presenter_li_background({
|
|||||||
for_obj_type: 'event_presenter',
|
for_obj_type: 'event_presenter',
|
||||||
for_obj_id: p.id,
|
for_obj_id: p.id,
|
||||||
enabled: 'all',
|
enabled: 'all',
|
||||||
|
// WHY: include hidden files so Manage Files UI can show/unhide them.
|
||||||
|
hidden: 'all',
|
||||||
limit: 25,
|
limit: 25,
|
||||||
try_cache: false,
|
try_cache: false,
|
||||||
log_lvl: 0
|
log_lvl: 0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const editable_fields__event_session = [
|
|||||||
'external_id',
|
'external_id',
|
||||||
'code',
|
'code',
|
||||||
'type_code',
|
'type_code',
|
||||||
|
'event_location_id',
|
||||||
'poc_agree',
|
'poc_agree',
|
||||||
'poc_kv_json',
|
'poc_kv_json',
|
||||||
'name',
|
'name',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user