Compare commits
624 Commits
3038be0686
...
ae_app_3x_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03b3c84921 | ||
|
|
e89c982022 | ||
|
|
c6ef729c55 | ||
|
|
fd7ccd7ecc | ||
|
|
7831179970 | ||
|
|
75e7ca541a | ||
|
|
e6fb4b289f | ||
|
|
1e3f541a39 | ||
|
|
e966261324 | ||
|
|
67d2607da1 | ||
|
|
7192cbc0af | ||
|
|
a227c6aaa7 | ||
|
|
e05602b87b | ||
|
|
10f7f04fbc | ||
|
|
94bdeb9a26 | ||
|
|
080ad06a45 | ||
|
|
45f8bb5e58 | ||
|
|
3085d1dc63 | ||
|
|
7fc073053b | ||
|
|
582b43da34 | ||
|
|
98e31f1528 | ||
|
|
573d20e574 | ||
|
|
83c9b9fd4f | ||
|
|
27c775d816 | ||
|
|
5823f18161 | ||
|
|
94e4fad061 | ||
|
|
9a1ba02b59 | ||
|
|
05841350fe | ||
|
|
a5beff4aa8 | ||
|
|
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 | ||
|
|
929f08b656 | ||
|
|
48a39b16d5 | ||
|
|
ab294c2a0b | ||
|
|
1de563203d | ||
|
|
0091fe3ff6 | ||
|
|
0ad36a74b2 | ||
|
|
fd244720a7 | ||
|
|
362136e677 | ||
|
|
a5a806e256 | ||
|
|
613e43114c | ||
|
|
1c818e648b | ||
|
|
5cd1d3b7ad | ||
|
|
66f0efb507 | ||
|
|
a637343544 | ||
|
|
a8f3c29b9f | ||
|
|
23d25bf65a | ||
|
|
12a9472064 | ||
|
|
b74c6d0e9c | ||
|
|
e1338b1a72 | ||
|
|
6018a94499 | ||
|
|
6e67534454 | ||
|
|
693486bac9 | ||
|
|
6d1d1e2658 | ||
|
|
7f6e286b73 | ||
|
|
a3ed379b17 | ||
|
|
e9379be5a1 | ||
|
|
9a75243d9c | ||
|
|
94849137f0 | ||
|
|
512e5ef87c | ||
|
|
d27ec58fe9 | ||
|
|
42358efe7d | ||
|
|
8e61bd0ba1 | ||
|
|
0bc71391fc | ||
|
|
6e22639e6e | ||
|
|
a6f8ff709e | ||
|
|
dafe79b3c6 | ||
|
|
a4927d37bd | ||
|
|
f3ab1c1050 | ||
|
|
5bed167829 | ||
|
|
a14320d9ed | ||
|
|
de8a016bda | ||
|
|
1c8997bd4f | ||
|
|
fe23899479 | ||
|
|
6662e82f40 | ||
|
|
4586e809d7 | ||
|
|
334c3a21bc | ||
|
|
14c2635df4 | ||
|
|
9673cbefe3 | ||
|
|
9a43879535 | ||
|
|
2c570c82dc | ||
|
|
bf9aa9710c | ||
|
|
942b3ddf5f | ||
|
|
bb0365f1e8 | ||
|
|
2b747de9bd | ||
|
|
519f5b949c | ||
|
|
bf834aa165 | ||
|
|
0d960435f8 | ||
|
|
60461de9d9 | ||
|
|
da3b8dcf46 | ||
|
|
f628e7e3fc | ||
|
|
fdd8691e2e | ||
|
|
621a637b85 | ||
|
|
639e436854 | ||
|
|
81741919a8 | ||
|
|
7f17b3b9a1 | ||
|
|
ca29e9c1b8 | ||
|
|
af02e38528 | ||
|
|
07d21c29c8 | ||
|
|
ec5b09dfaa | ||
|
|
c1f96ba94e | ||
|
|
931df5581f | ||
|
|
5978c5e341 | ||
|
|
93bd8ba962 | ||
|
|
543dc3c300 | ||
|
|
dcfeb99024 | ||
|
|
8693989a69 | ||
|
|
9fc3ee0198 |
15
.ae_brief
15
.ae_brief
@@ -1,22 +1,15 @@
|
||||
# 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
|
||||
|
||||
## 🛠️ What I Just Did
|
||||
Addressed multiple Svelte compiler warnings:
|
||||
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.
|
||||
Implemented "Force Sync Location" feature. Optimized file download order with a 4-tier chronological sort (Global > Session > Presentation > Creation Date). Added UI button for onsite operators. Updated project documentation. Verified with npm run check.
|
||||
|
||||
## 🚧 Current Blockers
|
||||
None. Remaining svelte-check warnings (219) require more granular ID/for linking in complex forms.
|
||||
None.
|
||||
|
||||
## ➡️ Exact Next Steps
|
||||
1. Granular fix for remaining 68 label/ID associations in address/person forms.
|
||||
2. Systematic application of untrack() for remaining state-from-props warnings.
|
||||
3. Clean up unused TipTap CSS selectors identified by svelte-check.
|
||||
User to review changes. Ready for onsite testing/deployment.
|
||||
|
||||
---
|
||||
*Generated by ae_brief*
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
# Build artifacts and local state
|
||||
.svelte-kit/
|
||||
.vite/
|
||||
node_modules/
|
||||
build/
|
||||
.svelte-kit/
|
||||
dist/
|
||||
.cache/
|
||||
|
||||
# VCS and IDE
|
||||
.git/
|
||||
.env
|
||||
.env.*
|
||||
!.env.staging
|
||||
!.env.prod
|
||||
npm_deploy/
|
||||
test-results/
|
||||
test_results/
|
||||
documentation/
|
||||
backups/
|
||||
.gitignore
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
.directory
|
||||
|
||||
# Logs and temp files
|
||||
*.log
|
||||
*.bak
|
||||
.claude/
|
||||
.vscode/
|
||||
*.tgz
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Test output and dev-only dirs
|
||||
tests/
|
||||
test-results/
|
||||
test_results/
|
||||
coverage/
|
||||
documentation/
|
||||
backups/
|
||||
.claude/
|
||||
|
||||
# Deployment artifacts
|
||||
npm_deploy/
|
||||
package-lock.json.bak
|
||||
|
||||
# Env files: exclude all live secrets, allow only the per-environment files needed for Docker builds.
|
||||
# .env.local is workstation-only and must never enter a container image.
|
||||
.env
|
||||
.env.*
|
||||
!.env.dev
|
||||
!.env.test
|
||||
!.env.prod
|
||||
|
||||
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.
|
||||
# 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
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
@@ -13,16 +9,10 @@ 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
|
||||
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.example
|
||||
!.env.prod.default
|
||||
!.env.staging.default
|
||||
!.env.test.default
|
||||
!.env.dev.default
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
|
||||
10
.prettierrc
10
.prettierrc
@@ -4,6 +4,14 @@
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"bracketSameLine": true,
|
||||
|
||||
"svelteSortOrder": "options-scripts-markup-styles",
|
||||
"svelteIndentScriptAndStyle": false,
|
||||
"svelteAllowShorthand": true,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
11
CLAUDE.md
11
CLAUDE.md
@@ -31,7 +31,7 @@
|
||||
1. **Before starting:** Read `documentation/TODO__Agents.md` for active tasks
|
||||
2. **Before committing:** Run `npx svelte-check` — no exceptions
|
||||
3. **Commits:** Atomic — one component or fix per commit
|
||||
4. **Never delete files with `rm`** — move to `~/tmp/gemini_trash`
|
||||
4. **Never delete files with `rm`** — move to `~/tmp/agents_trash`
|
||||
5. **Backend coordination:** Use `ae_send_message` or flag changes clearly
|
||||
|
||||
---
|
||||
@@ -100,10 +100,9 @@ src/routes/
|
||||
|
||||
## Active Issues (check TODO__Agents.md for current state)
|
||||
|
||||
- Session/Presentation page cold-start bug (Events Pres Mgmt) — unresolved
|
||||
- Sev-1: `PUBLIC_AE_API_SECRET_KEY` audit
|
||||
- CRUD v2 retirement → V3 editor
|
||||
- 403/401 error boundaries ("Session Expired" UI)
|
||||
- Sev-1: `PUBLIC_AE_API_SECRET_KEY` audit — see TODO__Agents.md (assessed acceptable, 2026-03-11)
|
||||
- V3 CRUD migration — remaining legacy API wrappers in events/sponsorships/core (see `PROJECT__Use_AE_API_V3_CRUD_upgrade.md`)
|
||||
- Style Review Phase 3 — IDAA + Pres Mgmt card polish deferred post-April 2026 conference
|
||||
|
||||
---
|
||||
|
||||
@@ -117,4 +116,4 @@ src/routes/
|
||||
| `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` | Dexie + liveQuery patterns |
|
||||
| `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns |
|
||||
| `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` | Electron/Launcher |
|
||||
| `documentation/PROJECT__AE_Pres_Mgmt_Session_view_refactor_2026-02.md` | Session bug plan |
|
||||
| `documentation/PROJECT__AE_Events_Badges_Review_Print.md` | Badges — kiosk editing (Task 4.0 open) |
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@@ -9,18 +9,32 @@ RUN npm install
|
||||
# Copy the rest of the source code.
|
||||
COPY . .
|
||||
|
||||
# Build Argument to determine build environment (staging, prod, or production).
|
||||
ARG BUILD_MODE=staging
|
||||
# Build Argument to determine build environment (dev, test, prod).
|
||||
ARG BUILD_MODE=dev
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Sync the SvelteKit project to generate ./.svelte-kit/tsconfig.json
|
||||
RUN npx svelte-kit sync
|
||||
|
||||
# Perform the build based on the BUILD_MODE argument.
|
||||
# Each script uses vite --mode <name>, which reads .env.<name> directly — no cp hack needed.
|
||||
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
|
||||
npm run build:prod; \
|
||||
elif [ "$BUILD_MODE" = "test" ]; then \
|
||||
npm run build:test; \
|
||||
else \
|
||||
npm run build: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
|
||||
|
||||
# Stage 2: Final runtime image
|
||||
@@ -35,8 +49,8 @@ COPY --from=builder /app/package-lock.json .
|
||||
# Install only production dependencies.
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Copy the resulting .env.production file to .env.
|
||||
COPY --from=builder /app/.env.production .env
|
||||
# Copy the runtime env file (non-PUBLIC vars for the Node server).
|
||||
COPY --from=builder /app/.env.runtime .env
|
||||
|
||||
# SvelteKit (via adapter-node) defaults to port 3000.
|
||||
EXPOSE 3000
|
||||
|
||||
93
README.md
93
README.md
@@ -116,39 +116,79 @@ Built on the Events object.
|
||||
|
||||
Developer sandbox pages — not for production use.
|
||||
|
||||
- `/testing/ae_obj_field_editor_v3/` — V3 field editor playground
|
||||
- `/testing/data_store_v3/` — Data store V3 playground
|
||||
- `/testing/ae_obj_field_editor/` — V3 field editor playground
|
||||
- `/testing/data_store/` — Data store V3 playground
|
||||
- `/testing/editor_test/` — CodeMirror / TipTap editor tests
|
||||
- `/testing/hosted_files/` — File upload tests
|
||||
|
||||
# How to build and deploy SvelteKit:
|
||||
|
||||
The deployment is 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
|
||||
npm run deploy:staging
|
||||
```
|
||||
# Active development — Vite HMR, no Docker
|
||||
npm run dev
|
||||
|
||||
#### 2. Deploy to Production
|
||||
This builds the image using production flags (using `.env.prod`) and restarts the production container.
|
||||
```bash
|
||||
npm run deploy:prod
|
||||
# Build Vite output only (no Docker)
|
||||
npm run build:dev
|
||||
npm run build:test
|
||||
npm run build:prod
|
||||
|
||||
# Build Docker image and restart container locally
|
||||
npm run build:docker:dev
|
||||
npm run build:docker:test
|
||||
npm run build:docker:prod
|
||||
|
||||
# Deploy to remote server (SSH → linode.oneskyit.com → deploy.sh)
|
||||
npm run deploy:remote:test
|
||||
npm run deploy:remote:prod
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **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**:
|
||||
- `PUBLIC_` variables are baked into the image during the build step.
|
||||
- Private runtime variables are passed via the orchestration's `.env` file.
|
||||
- **Networking**: The frontend communicates with the backend via the high-speed internal Docker network (`http://ae_api:5005`).
|
||||
- `PUBLIC_` variables are baked into the image at build time via the `.env.<mode>` file.
|
||||
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
|
||||
- **Remote deploy**: `aether_container_env/deploy.sh` handles git pull + Docker build + restart on the server. Triggered via `npm run deploy:remote:*`.
|
||||
|
||||
### Client-Side Cache & IDB Version Management
|
||||
|
||||
The app uses Dexie (IndexedDB) as a local cache for API data (SWR pattern). To prevent
|
||||
stale cached records from persisting across deploys, two version-tracking systems exist
|
||||
in `src/lib/stores/store_versions.ts`:
|
||||
|
||||
**localStorage store versions (`AE_LOC_VERSION`, etc.)**
|
||||
Track the schema of persisted Svelte stores (`ae_loc`, `ae_events_loc`, etc.).
|
||||
Bump when a store's shape changes in a breaking way (field type change, required rename).
|
||||
The check runs synchronously at module import time, before any store hydrates.
|
||||
|
||||
**IDB content versions (`IDB_CONTENT_VERSIONS`)**
|
||||
Track the content shape of Dexie table rows — specifically what `properties_to_save`
|
||||
writes to each table. Bump when `properties_to_save` in an object file changes in a way
|
||||
that makes existing cached rows stale (fields added/removed/renamed, computed field behavior
|
||||
changed). The `check_and_clear_idb_table()` helper reads a localStorage key per table and
|
||||
clears the Dexie table on mismatch. Call it from the module's layout on mount.
|
||||
|
||||
**When to bump `IDB_CONTENT_VERSIONS`:**
|
||||
If you change `properties_to_save` in `ae_events__event.ts` (or any other object file),
|
||||
bump the matching entry here. Failure to do so has historically caused silent "no data"
|
||||
states that are extremely difficult to diagnose — stale rows pass silently, filter to zero,
|
||||
and the error looks identical to a genuinely empty result.
|
||||
|
||||
Currently wired: `events.event` (via `src/routes/idaa/(idaa)/+layout.svelte`).
|
||||
All other tables are defined but not yet wired — see the comment block in `store_versions.ts`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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).
|
||||
|
||||
- **`.env.staging`**: Used by `npm run deploy:staging`.
|
||||
- **`.env.prod`**: Used by `npm run deploy:prod`.
|
||||
- **`.env.dev`**: Used by `npm run build:docker:dev` and `npm run build:dev`.
|
||||
- **`.env.test`**: Used by `npm run build:docker:test` and `npm run build:test`.
|
||||
- **`.env.prod`**: Used by `npm run build:docker:prod` and `npm run build:prod`.
|
||||
- **`.env.local`**: Used during local development (`npm run dev`).
|
||||
|
||||
**Note:** Runtime variables (like private API keys or DB credentials) are managed in the deployment directory's `.env` file and passed to the containers via Docker Compose.
|
||||
@@ -248,16 +289,16 @@ npm run dev
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Deployment (Production/Staging)
|
||||
|
||||
To create a production or staging version and deploy it to the local containers:
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# For Staging/Dev
|
||||
npm run deploy:staging
|
||||
# Build Docker image locally and restart container
|
||||
npm run build:docker:dev
|
||||
npm run build:docker:prod
|
||||
|
||||
# For Production
|
||||
npm run deploy:prod
|
||||
# Deploy to remote server (linode.oneskyit.com)
|
||||
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.
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
"settings": {
|
||||
"cSpell.words": [
|
||||
"autofetch",
|
||||
"Axonius",
|
||||
"displayplacer",
|
||||
"elif",
|
||||
"filelist",
|
||||
"gsettings",
|
||||
"onsave"
|
||||
],
|
||||
"git.autofetch": true,
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"npx svelte-check": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
aether_container_env/Dockerfile.buildkit.example
Normal file
27
aether_container_env/Dockerfile.buildkit.example
Normal file
@@ -0,0 +1,27 @@
|
||||
## BuildKit-friendly multi-stage Dockerfile example for Aether frontend
|
||||
|
||||
# Stage 1: dependencies
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --prefer-offline
|
||||
|
||||
# Stage 2: build
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
# optionally reuse deps from previous stage
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
# If you want to use BuildKit cache mounts during local development, uncomment the next line
|
||||
# RUN --mount=type=cache,target=/root/.npm npm ci
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: runtime (static site served by nginx)
|
||||
FROM nginx:stable-alpine AS runtime
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
# Notes:
|
||||
# - Keep dependency installation separate from copying source to maximize cache hits when only application code changes.
|
||||
# - For backend images, follow the same pattern: install deps early, copy source later, and keep a small final runtime image.
|
||||
33
aether_container_env/ci_buildx_example.sh
Normal file
33
aether_container_env/ci_buildx_example.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Example CI script to build and push an image with buildx using registry cache.
|
||||
# This script is provider-agnostic and intended to be run inside CI where
|
||||
# Docker and buildx are available and authenticated against the registry.
|
||||
|
||||
REGISTRY=${REGISTRY:-ghcr.io/ORG/REPO}
|
||||
IMAGE_TAG=${IMAGE_TAG:-staging}
|
||||
CACHE_REF=${CACHE_REF:-${REGISTRY}:cache}
|
||||
|
||||
echo "Building ${REGISTRY}:${IMAGE_TAG} using registry cache ${CACHE_REF}"
|
||||
|
||||
docker buildx build \
|
||||
--push \
|
||||
--tag ${REGISTRY}:${IMAGE_TAG} \
|
||||
--cache-from type=registry,ref=${CACHE_REF} \
|
||||
--cache-to type=registry,ref=${CACHE_REF},mode=max \
|
||||
.
|
||||
|
||||
echo "Build complete. Image: ${REGISTRY}:${IMAGE_TAG}"
|
||||
|
||||
# Optional: instruct devs how to run locally with a local cache
|
||||
cat <<'EOF'
|
||||
Local test with BuildKit and local cache:
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--tag myapp:staging \
|
||||
--cache-to=type=local,dest=/tmp/docker-cache \
|
||||
--cache-from=type=local,src=/tmp/docker-cache .
|
||||
|
||||
Prune local builder cache older than 72 hours:
|
||||
docker builder prune --filter "until=72h" --force
|
||||
EOF
|
||||
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.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Project Architecture
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This document outlines the overall architecture and key technologies used in the Aether SvelteKit frontend project.
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -16,12 +18,20 @@ The Aether project is a Svelte and SvelteKit based application, utilizing Tailwi
|
||||
- Flowbite (Tailwind Components)
|
||||
- Custom Components (a growing library of `ae_comp__*` and `element_*` components)
|
||||
- **Text/Code Editors:**
|
||||
- CodeMirror 6.x (primary text and code editor, planned for rich text editing)
|
||||
- Edra (TipTap based Rich Text Editor)
|
||||
- **Note:** ShadEditor TipTap is present but marked for removal, with CodeMirror being the preferred solution for all text editing needs.
|
||||
- CodeMirror 6.x (`element_editor_codemirror.svelte`) — source/code editing, markdown
|
||||
- TipTap (`element_editor_tiptap.svelte`) — WYSIWYG rich-text for content fields (IDAA, Journals, Leads notes)
|
||||
- **Icons:** Lucide Icons (SVG Icons)
|
||||
- **Markdown Parsing:** `marked` library
|
||||
- **State Management:** Svelte stores, potentially with `liveQuery` from Dexie for reactive IndexedDB interactions.
|
||||
- **State Management:** Svelte 5 runes plus Dexie `liveQuery`. Events persisted state uses `runed` `PersistedState`; core and IDAA still contain legacy coarse-grained persisted stores pending migration.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -70,15 +80,15 @@ Used for client-side persistence of various application states and configuration
|
||||
|
||||
Used for more structured client-side data storage, often for caching and offline capabilities.
|
||||
|
||||
- `ae_core_db`: Core database instance.
|
||||
- `<module>`: Module-specific database instances.
|
||||
- `db_core`: Core database instance.
|
||||
- `db_<module>`: Module-specific database instances (for example, `db_events` and `db_journals`).
|
||||
- `<custom>`: Custom module-specific database instances (none currently defined).
|
||||
|
||||
## 5. Data Sorting
|
||||
|
||||
Standardized sorting orders are applied across various data lists.
|
||||
|
||||
- **Default/General:** `group > priority > 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`
|
||||
|
||||
## 6. Object Properties and Fields
|
||||
@@ -89,15 +99,15 @@ A set of standardized field names and types are used across Aether objects.
|
||||
|
||||
These fields are expected to be present in most Aether objects.
|
||||
|
||||
- `id`: Primary key for an object (internal use, often a UUID).
|
||||
- `id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
|
||||
- `<object_type>_id`: Canonical randomized string ID returned by V3 (for example, `person_id`). Use this for URLs, relationships, and Dexie indexed lookups.
|
||||
- `id`: Generic randomized string alias when returned by V3; do not assume it is a DB autonumber.
|
||||
- `id_random` / `<object_type>_id_random`: Legacy aliases. Do not introduce new usage.
|
||||
- `code`: Short, unique identifier.
|
||||
- `name`: Display name.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
- `hide`: Boolean for visibility.
|
||||
- `priority`: Numeric value for ordering.
|
||||
- `sort`: Numeric value for ordering.
|
||||
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||
- `sort`: Numeric value for ordering within a priority group.
|
||||
- `group`: Categorization string.
|
||||
- `notes`: General notes/comments.
|
||||
- `created_on`: Timestamp of creation.
|
||||
@@ -154,7 +164,7 @@ The Electron app (`aether_app_native_electron/`) exists **solely** to support th
|
||||
- Hardware telemetry for connected devices
|
||||
|
||||
**What Electron is NOT used for:**
|
||||
- Badge printing (browser works fine)
|
||||
- Badge printing (browser works well)
|
||||
- Any other Aether module
|
||||
- Any general-purpose Aether functionality
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Project Naming Conventions
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
## 1. General Principles
|
||||
|
||||
- **Clarity:** Names should clearly convey their purpose and meaning.
|
||||
@@ -11,9 +13,10 @@
|
||||
|
||||
- **Logic/Service Files:** `ae_<module>__<concept>.ts` (e.g., `ae_core__account.ts`, `ae_events__event.ts`)
|
||||
- **Database Definition Files:** `db_<module>.ts` (e.g., `db_core.ts`, `db_journals.ts`)
|
||||
- **Svelte Store Files:** `ae_<module>_stores.ts` (e.g., `ae_core_stores.ts`, `ae_journals_stores.ts`)
|
||||
- **Svelte Store Files:** Follow existing module names. Svelte 5 `PersistedState` files use a `.svelte.ts` suffix and are imported via the `.svelte` module path (for example, `ae_events_stores__badges.svelte.ts`).
|
||||
- **Svelte Components:**
|
||||
- **Module-specific components:** `ae_comp__<module>__<component_name>.svelte` (e.g., `ae_comp__events__event_card.svelte`)
|
||||
- **Route-level components:** `ae_comp__<component_name>.svelte`.
|
||||
- **Module-specific components:** `ae_<module>_comp__<component_name>.svelte` (for example, `ae_events_comp__session_list.svelte`).
|
||||
- **Generic/reusable components:** `element_<component_name>.svelte` (e.g., `element_input_file.svelte`, `element_qr_scanner_v2.svelte`)
|
||||
- **SvelteKit Routes:** Follow SvelteKit's standard routing conventions (e.g., `+page.svelte`, `+layout.svelte`, `[id]/+page.svelte`).
|
||||
- **CSS Files:** `ae-<module>-<purpose>.css` (e.g., `ae-c-idaa-light.css`, `ae-osit-default.css`)
|
||||
@@ -37,9 +40,9 @@
|
||||
|
||||
- **Singularity:** Use singular nouns for objects and properties (e.g., `example.id`, not `examples.id`).
|
||||
- **IDs:**
|
||||
- `id`: Primary key for an object (internal use, often a UUID).
|
||||
- `<object_type>_id`: Specific ID for an object (e.g., `person_id`).
|
||||
- `<object_type>_id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `<object_type>_id`: Canonical randomized string ID returned by V3 (for example, `person_id`).
|
||||
- `id`: Generic randomized string alias when V3 returns one; never assume it is an integer autonumber.
|
||||
- `<object_type>_id_random`: Legacy alias; do not introduce new usage.
|
||||
- `account_id`, `site_id`, `user_id`, etc.: Foreign keys.
|
||||
- **Common Properties:**
|
||||
- `code`: Short, unique identifier.
|
||||
@@ -47,8 +50,8 @@
|
||||
- `description`: Longer text description.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
- `hide`: Boolean for visibility.
|
||||
- `priority`: Numeric value for ordering.
|
||||
- `sort`: Numeric value for ordering.
|
||||
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||
- `sort`: Numeric value for ordering within a priority group.
|
||||
- `group`: Categorization string.
|
||||
- `notes`: General notes/comments.
|
||||
- `created_on`: Timestamp of creation.
|
||||
@@ -76,7 +79,7 @@
|
||||
|
||||
## 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`
|
||||
|
||||
## 10. Local Storage and IndexedDB Keys
|
||||
@@ -88,6 +91,6 @@
|
||||
- `<module>` (extended modules)
|
||||
- `<custom>` (custom modules)
|
||||
- **IndexedDB:**
|
||||
- `ae_core_db`
|
||||
- `<module>`
|
||||
- `db_core`
|
||||
- `db_<module>` (for example, `db_events`, `db_journals`)
|
||||
- `<custom>`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Aether — Permissions and Security
|
||||
|
||||
**Last updated:** 2026-02-27
|
||||
**Last Updated:** 2026-06-12
|
||||
**Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts`
|
||||
|
||||
---
|
||||
@@ -76,15 +76,30 @@ $ae_loc.adv_mode // boolean — advanced mode toggle
|
||||
| AE Username + Password | `trusted` and above | Staff with AE accounts |
|
||||
| Novi UUID | `authenticated` | IDAA members (Novi membership system) |
|
||||
|
||||
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`:
|
||||
```typescript
|
||||
site_access_code_kv: {
|
||||
administrator: null, // highest passcode tier
|
||||
trusted: null, // onsite staff passcode
|
||||
public: 'public1980', // example
|
||||
authenticated: 'auth1980'
|
||||
}
|
||||
```
|
||||
### Site Passcode Security Warning
|
||||
|
||||
The current frontend receives every site passcode in `access_code_kv_json`, copies the map into
|
||||
persisted `$ae_loc.site_access_code_kv`, and compares entered passcodes locally. Verbose logging
|
||||
can also expose the complete map. This is a known active security gap, not the target design.
|
||||
|
||||
Do not add new consumers of `site_access_code_kv`, log passcodes, or treat persisted
|
||||
`access_type` as durable proof of authentication. The target flow verifies passcodes through
|
||||
`/authenticate_passcode`, stores a signed JWT with a role-specific TTL, and removes passcodes from
|
||||
the public bootstrap response and client state.
|
||||
|
||||
See `documentation/PROJECT__AE_Site_Passcode_Security.md` for the active migration plan.
|
||||
|
||||
### `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.
|
||||
|
||||
---
|
||||
|
||||
@@ -113,6 +128,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.
|
||||
- A prior agent accidentally exposed IDAA BB data publicly — treat any IDAA exposure as Sev-1.
|
||||
|
||||
#### IDAA IndexedDB (IDB) Caching — Auth-Before-Cache Rule
|
||||
|
||||
**Root cause discovered 2026-04:** SvelteKit `+page.ts`/`+layout.ts` load functions run *before* layout `$effect` hooks and fire during link prefetch (hover). `if (browser)` guards do NOT prevent this — they only prevent SSR. This means API calls inside these files execute before Novi auth completes, writing private IDAA data to the user's IndexedDB even for unauthenticated sessions.
|
||||
|
||||
**The fix — established pattern for all IDAA routes:**
|
||||
|
||||
1. **Load/layout `.ts` files = thin shells.** Pass URL params only. No API calls. No `if (browser)` data fetching.
|
||||
2. **Data loading = `$effect` in `.svelte` files**, gated on:
|
||||
```svelte
|
||||
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
||||
```
|
||||
3. **Three IDB purge paths** in `(idaa)/+layout.svelte` (auth failure, anonymous no-UUID, Reset & Retry button) clear `db_posts`, `db_archives`, and `db_events` tables.
|
||||
|
||||
**Auth path matrix:**
|
||||
|
||||
| User type | `novi_verified` | `trusted_access` | Can load data? | Purge fires? |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Anonymous / unauthenticated | false | false | No | Yes (Case 1) |
|
||||
| Novi-verified IDAA member | true | false | Yes | No |
|
||||
| Manager / trusted access | false | true | Yes | No (Case 3 exemption) |
|
||||
|
||||
**Applied to routes (as of 2026-04-19):**
|
||||
- `idaa/bb/+page.svelte` — `$effect` gate added; `bb/+page.ts` stripped
|
||||
- `idaa/bb/[post_id]/+page.ts` — stripped; loading handled by trigger in `bb/+layout.svelte`
|
||||
- `idaa/archives/+page.svelte` — `$effect` gate added; `archives/+layout.ts` stripped
|
||||
- `idaa/archives/[archive_id]/+page.svelte` — `$effect` gate added; `[archive_id]/+page.ts` stripped
|
||||
- `idaa/recovery_meetings/+page.svelte` — `$effect` gate already present; `+layout.ts` stripped
|
||||
- `idaa/recovery_meetings/[event_id]/+page.svelte` — `$effect` gate added; `+page.ts` stripped
|
||||
|
||||
**When adding a new IDAA route:** never put API calls in `+page.ts`/`+layout.ts`. Always gate data fetching with the `$effect` pattern above.
|
||||
|
||||
### Journals
|
||||
- Private personal data. Always authenticated. Passcode/encryption features exist.
|
||||
- Never expose journal content publicly.
|
||||
@@ -123,6 +169,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.
|
||||
- Do not introduce new usages. Prefer `PUBLIC_AE_BOOTSTRAP_KEY` for unauthenticated lookups.
|
||||
|
||||
### JWT usage guidance
|
||||
- JWTs are the preferred proof of an established session. Keep them attached to authenticated flows instead of leaning on transport-level bypasses.
|
||||
- If a route or helper can work with a JWT and an account ID, it should not need `x-no-account-id`.
|
||||
- If a helper still needs the bypass today, document the reason and add a removal target.
|
||||
|
||||
### Email Display
|
||||
Non-trusted users must never see a full email address. Obscure using:
|
||||
```typescript
|
||||
@@ -139,6 +190,12 @@ This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if n
|
||||
|
||||
## Module-Specific Permission Patterns
|
||||
|
||||
### Journals — Entry Config Admin Actions
|
||||
- Entry configuration admin controls are gated to `trusted_access` and above.
|
||||
- `manager_access` and `administrator_access` see the Delete action, which performs a hard delete.
|
||||
- `trusted_access` users see Remove instead, which follows disable semantics rather than a hard delete.
|
||||
- The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; the template toggle belongs in Metadata, while visibility/audience flags remain separate.
|
||||
|
||||
### Events — Badges
|
||||
|
||||
| Scenario | Visibility | Print Action | Review Actions |
|
||||
|
||||
361
documentation/BOOTSTRAP__AI_Agent_Quickstart.md
Normal file
361
documentation/BOOTSTRAP__AI_Agent_Quickstart.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# Aether SvelteKit — AI Agent Bootstrap / Quickstart
|
||||
> **Doc Owner:** Frontend platform maintainers (OSIT) + active coding agents
|
||||
> **Review Trigger:** Update when module architecture, critical rules, or primary doc paths change.
|
||||
> **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 only: `x-aether-api-key` + `x-account-id` — **NOT** Bearer tokens |
|
||||
|
||||
---
|
||||
|
||||
## 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 has two kinds of persisted stores — know which you're reading:
|
||||
|
||||
**Svelte 4 `svelte-persisted-store` (coarse reactivity) — still used for:**
|
||||
- `$ae_loc`, `$ae_sess`, `$ae_api` (global app state)
|
||||
- `$idaa_loc`, `$idaa_sess` (IDAA module)
|
||||
|
||||
In Svelte 5 `$effect`, reading **any field** of these stores subscribes to the **entire store**.
|
||||
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.
|
||||
|
||||
**Svelte 5 `PersistedState` (fine-grained reactivity) — Events module stores:**
|
||||
- `badges_loc`, `leads_loc`, `pres_mgmt_loc`, `launcher_loc`, `events_auth_loc`
|
||||
|
||||
These use `runed`'s `PersistedState`. Access via `.current` (no `$` sigil):
|
||||
`badges_loc.current.field`. Writing one field only re-triggers effects that read that field.
|
||||
Import from the `.svelte` extension: `import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte'`.
|
||||
|
||||
For search pages using the coarse stores, 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
|
||||
|
||||
See `PROJECT__Stores_Svelte5_Migration.md` for migration status and the pattern to follow when migrating remaining stores.
|
||||
|
||||
### `{#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. Common Mistakes (Reference)
|
||||
|
||||
The full, curated mistake catalog now lives in
|
||||
`documentation/REFERENCE__Common_Agent_Mistakes.md`.
|
||||
|
||||
Read this section first, then open the reference doc when your task touches one of these areas:
|
||||
|
||||
1. **Security/Auth** — private route guards, account scoping, and pre-gate data load risks.
|
||||
2. **V3 API payloads** — object ID in URL, `data_kv` field-only PATCH payloads.
|
||||
3. **Dexie/IDB behavior** — `.get()` primary key trap, stale cache/version mismatches, broad result clipping.
|
||||
4. **Svelte 5 reactivity** — coarse-store `$effect` loops and `$`-sigil misuse on plain props.
|
||||
5. **Sorting and search correctness** — `tmp_sort_*` comparator direction and Dexie sorting caveats.
|
||||
6. **Network reliability** — retry classification in `api_*_object.ts` and timeout behavior.
|
||||
7. **JSON field safety** — `*_json` null reads/writes and wrapper serialization behavior.
|
||||
8. **Service worker rollout behavior** — stale-tab symptoms, activation expectations, and trade-offs.
|
||||
|
||||
The reference doc also includes a short archive list for older, less-relevant historical incidents.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| Documentation index | `documentation/README__Docs_Index.md` |
|
||||
| Dev workflow + commit rules | `documentation/GUIDE__Development.md` |
|
||||
| V3 API reference | `documentation/GUIDE__AE_API_V3_for_Frontend.md` |
|
||||
| WebSockets / real-time updates | `documentation/GUIDE__AE_API_V3_for_Frontend_websockets.md` |
|
||||
| Dexie / liveQuery patterns | `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` |
|
||||
| Common mistakes reference | `documentation/REFERENCE__Common_Agent_Mistakes.md` |
|
||||
| Svelte 5 patterns + pitfalls | `documentation/GEMINI__Svelte_and_Me.md` |
|
||||
| Permissions + auth levels | `documentation/AE__Permissions_and_Security.md` |
|
||||
| Electron / native launcher | `documentation/MODULE__AE_Events_Launcher_Native.md` |
|
||||
| Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.md` |
|
||||
| Journals module overview | `documentation/MODULE__AE_Journals.md` |
|
||||
| Journals settings map | `documentation/MODULE__AE_Journals_Config_Map.md` |
|
||||
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Leads.md` |
|
||||
| Presentation Management module | `documentation/MODULE__AE_Events_Presentation_Management.md` |
|
||||
| IDAA client architecture | `documentation/CLIENT__IDAA_and_customized_mods.md` |
|
||||
| IDAA Archives module | `documentation/MODULE__AE_IDAA_Archives.md` |
|
||||
| IDAA Bulletin Board module | `documentation/MODULE__AE_IDAA_Bulletin_Board.md` |
|
||||
| IDAA Recovery Meetings module | `documentation/MODULE__AE_IDAA_Recovery_Meetings.md` |
|
||||
| IDAA Video Conferences module | `documentation/MODULE__AE_IDAA_Video_Conferences.md` |
|
||||
| Naming conventions | `documentation/AE__Naming_Conventions.md` |
|
||||
@@ -3,7 +3,7 @@
|
||||
**Client:** International Doctors in Alcoholics Anonymous (IDAA)
|
||||
**Module Path:** `src/routes/idaa/`
|
||||
**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.
|
||||
|
||||
### 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
|
||||
@@ -85,8 +96,8 @@ src/routes/idaa/
|
||||
│ │ └── [event_id]/
|
||||
│ │ ├── +page.svelte # Meeting detail page — renders view OR edit based on session flag
|
||||
│ │ └── +page.ts
|
||||
│ └── video_conferences/ # Jitsi video conference integration
|
||||
└── jitsi_reports/ # External Jitsi reporting
|
||||
│ ├── video_conferences/ # Jitsi video conference integration
|
||||
│ └── jitsi_reports/ # Jitsi meeting activity log report (trusted_access only)
|
||||
```
|
||||
|
||||
> **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`
|
||||
> 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.
|
||||
|
||||
### 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)
|
||||
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`
|
||||
4. Sets `$idaa_loc.novi_verified = true` on success
|
||||
|
||||
A `novi_verifying` UI state prevents the "Access Denied" screen from flashing during the API round-trip.
|
||||
|
||||
**All or nothing:** If the Novi API key is not configured, 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:**
|
||||
```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".
|
||||
|
||||
- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The Aether backend handles 429 responses; the frontend receives a `429` and retries once after 10 seconds. The 12-hour TTL cache on successful verification (Redis server-side + `$idaa_loc` client-side) prevents repeated calls during normal use. A `503` (Novi unreachable) is auto-retried once after 3 seconds before surfacing an error to the user.
|
||||
|
||||
### Verification Flow (implementation)
|
||||
|
||||
1. The IDAA iframe loads Aether pages with a `?uuid=<uuid>&iframe=true` param.
|
||||
2. When the `uuid` param is present the IDAA layout 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
|
||||
// simplified
|
||||
fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
fetch(`${aether_api_url}/v3/action/idaa/novi_member/${uuid}`, {
|
||||
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.
|
||||
|
||||
@@ -169,9 +187,9 @@ fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
- **`jitsi_exclude_uuids`**: (optional) Array of Novi UUIDs to exclude from Jitsi Reports.
|
||||
This is the canonical staff/test filter. UUIDs are matched case-insensitively against
|
||||
`final_participants[].novi_uuid` when present. Example: `["uuid-1", "uuid-2"]`.
|
||||
|
||||
- **`jitsi_known_meetings`**: (optional) Array of meeting names / room names to keep in the report.
|
||||
When this list is non-empty, only matching `room_name` values are shown. Matching is
|
||||
case-insensitive.
|
||||
|
||||
- **Legacy fallback:** `jitsi_exclude_names` is still honored for older configs, but it should be
|
||||
migrated to UUIDs.
|
||||
|
||||
- **Email config values** (`noreply_email`, `noreply_name`, `admin_email`, `admin_name`): used by functions that send notification emails (BB posts, comments, recovery meetings).
|
||||
|
||||
### Stores / runtime fields set by verification
|
||||
@@ -201,12 +230,32 @@ These fields are read elsewhere in the IDAA UI to enable flows for verified user
|
||||
### Security notes and operational guidance
|
||||
|
||||
- The previous implementation leaked `email` and `full_name` via URL params — this was removed because those values are unauthenticated and can be spoofed.
|
||||
- The API key is sensitive — keep it only in site config and do not expose it in client-side code or public repositories.
|
||||
- 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 the layout parsing (display name/email normalization) and this documentation.
|
||||
- The API key is sensitive — keep it only in site `cfg_json` and do not expose it in client-side code or public repositories. The key is read and used exclusively by the Aether backend; it is never sent to the browser.
|
||||
- If Novi changes their customer API shape, update `app/methods/idaa_novi_verify_methods.py` in the backend (display name/email normalization) and this documentation.
|
||||
|
||||
If you need a compact checklist for re-creating this flow in another integration, ask and I will add a small runbook with exact request/response field mappings.
|
||||
|
||||
### ~~Planned: Server-Side Novi Verification~~ ✅ Implemented (2026-05-19)
|
||||
|
||||
**Problem solved:** The previous client-side Novi API call originated from the member's browser.
|
||||
Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering
|
||||
could block these calls and produce false "Access Denied" for legitimate members.
|
||||
|
||||
**Solution implemented:** A FastAPI endpoint proxies the Novi call server-to-server
|
||||
(Aether → Novi), with Redis caching. Members' browser IPs are no longer in the call path.
|
||||
|
||||
**Endpoint:** `GET /v3/action/idaa/novi_member/{uuid}`
|
||||
- Standard Aether auth headers (`x-aether-api-key`, `x-account-id`)
|
||||
- Server reads `novi_idaa_api_key` / `novi_api_root_url` from site `cfg_json`
|
||||
- Redis cache: `idaa:novi_member:{uuid}` — 4-hour TTL, only 200s cached
|
||||
- `404` results never cached (recently-joined members not incorrectly denied)
|
||||
|
||||
**Frontend:** `verify_novi_uuid()` in `(idaa)/+layout.svelte` now calls this endpoint with
|
||||
standard Aether headers. The `novi_idaa_api_key` is still checked for presence in
|
||||
`site_cfg_json` as a proxy for "is IDAA configured for this site" (server holds the key itself).
|
||||
|
||||
**Full API spec:** `GUIDE__AE_API_V3_for_Frontend.md` §12.
|
||||
|
||||
### Permission Levels (Ascending)
|
||||
| Level | Condition | Access |
|
||||
|---|---|---|
|
||||
@@ -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).
|
||||
|
||||
## Identity Linkage: The Novi UUID Rule (Triple Linkage)
|
||||
|
||||
**CRITICAL ARCHITECTURAL STANDARD:**
|
||||
All member-generated content in the IDAA module MUST be explicitly linked to the member's Novi UUID via the `external_person_id` field. This linkage is the primary mechanism for ownership, edit permissions, and auditing.
|
||||
|
||||
### 1. Mandatory at Creation
|
||||
Linkage MUST happen at the moment of initial object creation (POST). Shell records created without an `external_person_id` are considered orphaned and may be inaccessible to the creator.
|
||||
|
||||
### 2. Triple Linkage Scope
|
||||
The following objects require mandatory `external_person_id` linkage:
|
||||
- **Recovery Meetings** (`ae_Event`)
|
||||
- **Bulletin Board Posts** (`ae_Post`)
|
||||
- **Post Comments** (`ae_PostComment`)
|
||||
|
||||
### 3. Implementation Patterns
|
||||
- **Buttons:** Creation buttons (e.g., "Create New Meeting") must include `external_person_id: $idaa_loc.novi_uuid` in their initial `create_ae_obj` payload.
|
||||
- **Edit Forms:** Edit components must provide robust fallbacks to `$idaa_loc.novi_uuid` for new or incomplete records, ensuring identity is captured even if the initial creation call was narrow.
|
||||
- **Identity Sync:** Along with the UUID, `full_name` and `email` should also be synced from `$idaa_loc` to provide human-readable context in notifications and admin views.
|
||||
- **Race Condition Defense:** `$idaa_loc` may be briefly null on mount before the store hydrates from localStorage. Creation buttons and edit submit handlers must scavenge identity directly from `localStorage.getItem('ae_idaa_loc')` as a fallback when the store value is missing.
|
||||
|
||||
### 4. Staff Editing Rules (IDAA Trusted/Admin Staff)
|
||||
|
||||
IDAA staff have their own Novi UUID. When they edit member content, their identity must **not** overwrite the member's `external_person_id`, `full_name`, or `email`.
|
||||
|
||||
| Content Type | `external_person_id` for staff | `full_name` / `email` for staff |
|
||||
|---|---|---|
|
||||
| BB Post | **Readonly** (unless `administrator_access`) — member's UUID preserved | Same — rendered from existing record, not staff identity |
|
||||
| Post Comment | **Preserved** — form state initializes from existing record first | Same |
|
||||
| Recovery Meeting | **Intentionally editable** for trusted staff — staff can reassign meeting ownership | Contact 1 renders from existing `contact_li_json[0]` first; staff identity only fills if blank |
|
||||
|
||||
The fallback to `$idaa_loc.novi_uuid` (the current user's UUID) only fires when the record has **no** existing `external_person_id`. For any record properly created after the 2026-04-07 triple-linkage enforcement, this fallback should never be reached.
|
||||
|
||||
### 5. Recovery Meetings — Contact 1 Convention
|
||||
|
||||
In 99% of cases, **Contact 1 should be the same person linked via `external_person_id`** — the IDAA member who owns and runs the meeting. These are two separate fields:
|
||||
|
||||
- `external_person_id` — the ownership/identity link (Novi UUID). Determines who may edit the meeting.
|
||||
- `contact_li_json[0]` — the displayed contact info (name, email, phone). Shown to members searching for meetings.
|
||||
|
||||
They are expected to match but are set independently. Members unlock Contact 1 via confirm dialog if they need to list a different contact. Staff can edit both fields directly.
|
||||
|
||||
### Permission Upgrade Rule
|
||||
```
|
||||
// RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade.
|
||||
@@ -232,7 +322,12 @@ This ensures that OSIT staff with `super` or `manager` roles retain full access
|
||||
|
||||
### Access Gate (`(idaa)/+layout.svelte`)
|
||||
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
|
||||
- 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
|
||||
Members can filter meetings by:
|
||||
- **Fulltext search** — name, location
|
||||
- **Physical** — in-person meetings
|
||||
- **Virtual** — online meetings (Zoom, Google Meet, etc.)
|
||||
- **Meeting type** — specific meeting format categories
|
||||
- **Fulltext search** — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
|
||||
- **Virtual** — online meetings (Zoom, Jitsi, other)
|
||||
- **In-person** — physical location meetings
|
||||
- **Meeting type** — IDAA / Caduceus / Family Recovery
|
||||
- **My Meetings** — star toggle; shows only meetings the member has starred (favorites)
|
||||
|
||||
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
|
||||
|
||||
@@ -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`)
|
||||
|
||||
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
|
||||
|
||||
archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_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 }
|
||||
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj,
|
||||
qry__enabled, qry__hidden, qry__limit, qry__offset, qry__order_by, qry__order_by_li }
|
||||
recovery_meetings: {
|
||||
qry__enabled, qry__hidden, qry__limit, qry__offset,
|
||||
qry__fulltext_str, qry__physical, qry__virtual, qry__type,
|
||||
qry__order_by, qry__order_by_li,
|
||||
qry__favorites_only, // true = show only starred meetings (My Meetings filter)
|
||||
edit__event_obj // null or event_id string when edit form is open
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `idaa_sess` (sessionStorage — cleared on tab close)
|
||||
### `idaa_sess` (in-memory only — resets on page load)
|
||||
UI state per submodule:
|
||||
```typescript
|
||||
{
|
||||
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id, obj_changed }
|
||||
bb: { qry__status, show__modal_edit__post_id, show__modal_view__post_id, obj_changed }
|
||||
recovery_meetings: { qry__status, show__modal_edit, show__modal_view, attend_platform, obj_changed }
|
||||
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id,
|
||||
show__modal_edit__archive_content_id, show__modal_view__archive_content_id, obj_changed }
|
||||
bb: { qry__status, edit__post_obj, show__inline_edit__post_obj, show__modal_edit__post_id,
|
||||
show__modal_view__post_id, obj_changed }
|
||||
recovery_meetings: {
|
||||
qry__status, // null | 'loading' | 'done' | 'error'
|
||||
qry__fulltext_str, // session-only copy (separate from persisted loc copy)
|
||||
search_version, // incremented to trigger a new search cycle
|
||||
edit__event_obj, // null | event_id — controls edit form visibility
|
||||
show__modal_edit, show__modal_view,
|
||||
show__modal_edit__event_id, show__modal_view__event_id,
|
||||
attend_platform, // 'Zoom' | 'Jitsi' | null — platform selected in virtual attend section
|
||||
obj_changed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
- Class names `.field-input` and `.field-label` (used in the v2 edit form's scoped `<style>` block)
|
||||
also exist in `idaa.css`'s date picker — Svelte's scoped attribute selector wins, but be aware
|
||||
- In iframe widths near Tailwind `sm`, avoid hiding critical button labels behind breakpoint classes
|
||||
and do not depend on color-only active states; Bootstrap's `.active`/button styling can make the
|
||||
selected state nearly invisible unless the control uses an obvious fill/ring change plus
|
||||
`aria-pressed`
|
||||
|
||||
**Mitigation:** The iframe CSS conflicts existed before v2 and are not new. The v2 form uses the
|
||||
same Skeleton/Tailwind component classes as the rest of the app. Avoid using bare `<section>`,
|
||||
@@ -517,13 +839,16 @@ ae_loc.trusted_access = true;
|
||||
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 |
|
||||
|---|---|---|
|
||||
| Archives | ⚠️ Smoke only | `archive_content.test.ts` — no auth gate test |
|
||||
| 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 |
|
||||
| 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)
|
||||
- [Playwright Test README](../tests/README.md)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## IDAA Novi Groups and Moderators
|
||||
|
||||
### "IDAA Association Admins Group" = "409e91dc-f5a3-486c-a964-71b7d19e6841"
|
||||
|
||||
* Scott
|
||||
* Michelle
|
||||
* Brie
|
||||
|
||||
### "IDAA Couples Meeting" = "e9e162f0-3d03-4241-9682-340135ec3fb8"
|
||||
|
||||
* "Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
|
||||
* "Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
|
||||
* "Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
|
||||
* "Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
|
||||
* "Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
|
||||
* "Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"
|
||||
|
||||
### "Student/Resident Meeting Moderators" "d76d2c00-962d-40f6-a2e8-ed9c85594d96"
|
||||
|
||||
* "Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
|
||||
* "Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"
|
||||
|
||||
### "IDAA BIPOC Meeting" "873d3ad0-2605-4ccf-824c-638c16b2b9cf"
|
||||
|
||||
* "Paula Lynn Bailey-Walton" "68383ba2-0989-4860-9ea6-073f9698df67"
|
||||
* "Tasha Hudson" "03d5408c-3c13-4c3a-a93f-49871f9050b1"
|
||||
|
||||
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Current
|
||||
**Last Verified:** 2026-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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether API V3 Frontend Integration Guide (Svelte/TypeScript)
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints.
|
||||
|
||||
---
|
||||
@@ -19,8 +21,15 @@ Required for any non-public data (Journals, Badges, Users, etc.).
|
||||
* **Header:** `x-account-id: <account_id>`
|
||||
2. **Administrative Bypass**: For authorized scripts needing global access.
|
||||
* **Header:** `x-no-account-id: bypass`
|
||||
* **Scope:** Narrow escape hatch only. Keep it limited to allowlisted bootstrap/public/global-default paths and prefer `x-account-id` or JWT-backed requests everywhere else.
|
||||
3. **Token Access**: Provide a **JWT** in the query string.
|
||||
* **Query Param:** `?jwt=<token>`
|
||||
4. **Important Distinction:** A query parameter named `key` is **not** an account-context bypass signal.
|
||||
* `key` may be used by specific endpoints/business logic, but it must **not** cause the frontend to remove `x-account-id`.
|
||||
* Only explicit `x-no-account-id: bypass` should strip account context.
|
||||
|
||||
> [!NOTE]
|
||||
> The `x-no-account-id` path should continue to shrink over time. If you need a new use, document why `x-account-id` or JWT cannot cover it and mark the use as temporary unless it is a hard bootstrap/global-default requirement.
|
||||
|
||||
> [!CAUTION]
|
||||
> **UNSUPPORTED HEADERS:** The header `x-aether-api-token` is **NOT recognized** by the V3 API. If you send it, the backend will treat you as a guest and block access to private data.
|
||||
@@ -44,6 +53,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).
|
||||
* ** デザイン 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
|
||||
@@ -58,6 +98,26 @@ The primary way to retrieve data.
|
||||
* **Endpoint:** `POST /v3/crud/{obj_type}/search`
|
||||
* **Security:** Automatically filters results to only show records belonging to your `x-account-id`. If no account context is provided, it will return **0 records** for private objects.
|
||||
|
||||
#### 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
|
||||
Modify data in the system.
|
||||
* **Endpoints:**
|
||||
@@ -68,45 +128,167 @@ Modify data in the system.
|
||||
* **Header:** `x-ae-ignore-extra-fields: true`
|
||||
* **Behavior:** When set to `true`, the backend will automatically strip any fields from the payload that are not defined in the object's model before attempting to save to the database.
|
||||
|
||||
#### `*_json` field serialization — do NOT pre-stringify in route/component code
|
||||
|
||||
The frontend API wrappers (`src/lib/ae_api/api_post__crud_obj.ts` for V3, `src/lib/api/api.ts` for legacy CRUD) automatically serialize any field whose name ends in `_json` (e.g. `cfg_json`, `data_json`) before sending. They pretty-print with 2-space indent via an internal `serialize_json_field_pretty()` helper.
|
||||
|
||||
**Pass `*_json` fields as plain JS objects from routes and components.** The serialization layer handles the rest.
|
||||
|
||||
```ts
|
||||
// ✅ Correct — pass as plain object; V3 wrapper serializes it
|
||||
await update_ae_obj__site({ site_id, data_kv: { cfg_json: { jitsi_token_endpoint: url } } });
|
||||
|
||||
// ❌ Wrong — double-encodes the JSON string (the wrapper would stringify an already-stringified value)
|
||||
await update_ae_obj__site({ site_id, data_kv: { cfg_json: JSON.stringify({ jitsi_token_endpoint: url }) } });
|
||||
```
|
||||
|
||||
The V3 wrapper (`api_post__crud_obj.ts`) only serializes when `typeof value === 'object'`, so it will not double-encode a plain string. The legacy wrapper (`api.ts`) stringifies unconditionally, so pre-stringifying there **will** produce double-encoded JSON. In both cases, the right answer is to pass the raw object and let the layer handle it.
|
||||
|
||||
### D. ID Fields in Responses (Vision ID Convention)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **V3 responses always use random string IDs — never database integers.**
|
||||
|
||||
All V3 responses — `POST` create, `GET` single, `GET` list, search, and `PATCH` update — contain:
|
||||
|
||||
| Field | Type | Use |
|
||||
| :--- | :--- | :--- |
|
||||
| `{obj_type}_id` | `string` | **Primary public ID.** Use this for subsequent `PATCH` calls and UI routing. |
|
||||
| `{obj_type}_id_random` | `string` | Legacy alias. Same value as `{obj_type}_id`. Present for backward compat only. |
|
||||
|
||||
**Example — create then immediately PATCH:**
|
||||
```ts
|
||||
const created = await postArchiveContent(archiveId, payload);
|
||||
const newId = created.data.archive_content_id; // random string e.g. "xK9mP3qRtL2"
|
||||
|
||||
// Use it directly in the PATCH URL — no lookup needed
|
||||
await patchArchiveContent(newId, { name: 'Updated Name' });
|
||||
// PATCH /v3/crud/archive/{archive_id}/archive_content/{newId}
|
||||
```
|
||||
|
||||
> **Note on `_id_random` suffix:** The `{obj_type}_id_random` field is a legacy artifact from the pre-Vision model. Once you confirm `{obj_type}_id` is a random string (length 11–22), you do not need `_id_random` as a fallback. New code should only read `{obj_type}_id`.
|
||||
|
||||
---
|
||||
|
||||
## 4. V3 Uniform Lookup System
|
||||
|
||||
The V3 Lookup system provides a hierarchical, deduplicated interface for standardized tables (Countries, Timezones, etc.). It supports global defaults, account overrides, and site-specific whitelisting.
|
||||
The V3 Lookup system provides a hierarchical, deduplicated interface for standardized reference tables (Countries, Timezones, etc.). It supports global defaults, account-level overrides, and object-level overrides, with optional site-specific whitelisting.
|
||||
|
||||
### How the hierarchy works
|
||||
|
||||
Each lookup table (`lu_v3_country`, `lu_v3_time_zone`, etc.) can hold multiple rows for the same logical item at different scopes:
|
||||
|
||||
| Scope | `account_id` | `for_type` / `for_id` | Wins over |
|
||||
|---|---|---|---|
|
||||
| Global default | `NULL` | `NULL` / `NULL` | nothing |
|
||||
| Account override | set | `NULL` / `NULL` | Global default |
|
||||
| Object override | set | set | Account override + Global default |
|
||||
|
||||
The API uses `ROW_NUMBER() PARTITION BY group` to collapse all rows for the same item down to the single highest-priority winner before returning results. **`group` is the identity key** — it is what makes two rows "the same item competing for priority."
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **The `group` field is not a display label.** It is the deduplication key. Each lookup type uses a different natural key for `group`:
|
||||
>
|
||||
> | Lookup type | `group` value | Example |
|
||||
> |---|---|---|
|
||||
> | `country` | ISO alpha-2 code | `"US"`, `"CA"`, `"GB"` |
|
||||
> | `country_subdivision` | subdivision code | `"US-NY"`, `"CA-ON"` |
|
||||
> | `time_zone` | IANA timezone name | `"America/New_York"`, `"US/Eastern"` |
|
||||
>
|
||||
> For `time_zone`, `group` and `name` must always be identical — there is no concept of "override all US timezones as a group." Each timezone is its own identity.
|
||||
|
||||
### A. List Lookups
|
||||
Retrieve a ranked and filtered list of lookup items.
|
||||
|
||||
Retrieve the deduplicated, ranked list for a lookup type.
|
||||
|
||||
* **Endpoint:** `GET /v3/lookup/{lu_type}/list`
|
||||
* **Available Types:** `country`, `country_subdivision`, `time_zone`
|
||||
* **Parameters:**
|
||||
* `site_id` (Optional): Random ID of the site to apply a **Whitelist Policy**.
|
||||
* `only_priority` (Optional): Set to `true` to return only high-priority items (e.g., common time zones).
|
||||
* `for_type` / `for_id` (Optional): Context for object-specific overrides.
|
||||
* `include_disabled` (Optional): Set to `true` to see shadowed/disabled records.
|
||||
* `site_id` (Optional): Random ID of the site — applies a **Whitelist Policy** (see §C).
|
||||
* `only_priority` (Optional): `true` returns only `priority=1` items (e.g., common time zones).
|
||||
* `for_type` / `for_id` (Optional): Object context — activates object-level override matching.
|
||||
* `include_disabled` (Optional): `true` includes shadowed/disabled records (useful for admin views).
|
||||
|
||||
**Frontend keying:** Always key Svelte `{#each}` blocks on `group`, not `id` or `name`. `group` is guaranteed unique in the response. Keying on `id` will break if an account override wins (different `id`, same logical item).
|
||||
|
||||
### B. Resolve Identity
|
||||
Resolves a string (code, group, or name) to a single record.
|
||||
|
||||
Resolves a string to a single lookup record.
|
||||
|
||||
* **Endpoint:** `GET /v3/lookup/{lu_type}/resolve?q=VALUE`
|
||||
* **Usage:** Use this when you have an external code (e.g., ISO "US") and need the full Aether record.
|
||||
* **Usage:** Use when you have an external code (e.g., ISO `"US"`) and need the full Aether record. Scans `name`, `group`, and other identity fields.
|
||||
|
||||
### C. Site Whitelist Policy
|
||||
To limit lookups for a specific site, add a `lookup_policy` to the `site.cfg_json` field.
|
||||
**Schema:**
|
||||
|
||||
To restrict which lookup items appear for a specific site, add a `lookup_policy` to `site.cfg_json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"lookup_policy": {
|
||||
"country": ["US", "CA", "GB"],
|
||||
"time_zone": ["America/New_York"]
|
||||
"time_zone": ["America/New_York", "US/Eastern"]
|
||||
}
|
||||
}
|
||||
```
|
||||
*Note: Whitelist values must match the `group` field in the database.*
|
||||
|
||||
> **Whitelist values must match the `group` field** — i.e., the natural key for that type (ISO code for country, IANA name for time zone). Using a display name will silently return no results for that item.
|
||||
|
||||
### D. Adding and managing client overrides
|
||||
|
||||
When a client needs a customized label or wants to hide/reorder lookup items, create override records rather than modifying global defaults.
|
||||
|
||||
**Rules:**
|
||||
1. **Never modify global default rows** (`account_id = NULL`). Those are shared across all accounts. Any change there affects every client.
|
||||
2. **Set `group` to the exact same value as the global default row** for the item you are overriding. If `group` doesn't match, the override creates a new item instead of replacing the existing one.
|
||||
3. **Set `account_id`** to the client's account ID. Leave `for_type` / `for_id` null unless the override is specific to a single object (e.g., one site).
|
||||
|
||||
**Example — rename "US/Eastern" for one account:**
|
||||
|
||||
```sql
|
||||
INSERT INTO lu_v3_time_zone
|
||||
(account_id, name, name_override, `group`, enable, priority, sort)
|
||||
VALUES
|
||||
(42, 'US/Eastern', 'Eastern Time (Client Label)', 'US/Eastern', 1, 1, 50);
|
||||
```
|
||||
|
||||
The `name_override` field is the display label the frontend should prefer when set. `group = 'US/Eastern'` ensures this row competes with — and wins over — the global default in the `PARTITION BY group` deduplication.
|
||||
|
||||
**To disable an item for one account** (hide it from their dropdowns):
|
||||
|
||||
```sql
|
||||
INSERT INTO lu_v3_time_zone
|
||||
(account_id, name, `group`, enable)
|
||||
VALUES
|
||||
(42, 'US/Samoa', 'US/Samoa', 0);
|
||||
```
|
||||
|
||||
Setting `enable = 0` on an account-scoped row shadows the global default for that account only.
|
||||
|
||||
**To remove a client override** (revert to global default):
|
||||
|
||||
Simply delete the row where `account_id = <client>` and `group = '<item>'`. The global default row is unaffected and immediately resumes winning.
|
||||
|
||||
### E. Adding new global lookup items
|
||||
|
||||
When seeding new lookup data (e.g., adding timezones in bulk):
|
||||
|
||||
1. Set `group = name` for every row (for `time_zone`). This is a hard invariant — if `group` is set to a regional label like `"United States"` instead of the timezone name, the entire group collapses to a single winner and all but one entry disappear from the API response.
|
||||
2. Set `account_id = NULL` and `for_type = NULL` / `for_id = NULL` for global defaults.
|
||||
3. After seeding, verify with:
|
||||
```sql
|
||||
-- Should return 0 rows; any result means multiple items will collapse into one
|
||||
SELECT `group`, COUNT(*) AS cnt
|
||||
FROM lu_v3_time_zone
|
||||
WHERE account_id IS NULL
|
||||
GROUP BY `group`
|
||||
HAVING cnt > 1;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Event File Data Retrieval (Hosted Files)
|
||||
|
||||
Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File itself is a metadata record for binary content (files), which is accessed via separate Action endpoints (e.g., `/v3/action/hosted_file/download`). This API endpoint provides metadata about the associated hosted file. To retrieve this additional metadata:
|
||||
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}`
|
||||
* **Query Parameter:** Add `inc_hosted_file=true`
|
||||
@@ -121,6 +303,48 @@ Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file
|
||||
* `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.
|
||||
|
||||
### 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)
|
||||
@@ -141,22 +365,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`)
|
||||
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool)
|
||||
- Auth: standard V3 headers
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`. Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize. Returns 400 on failure.
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
|
||||
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize.
|
||||
- For longer-running clips you can schedule the job in the background by adding `?background=true`. When scheduled the API returns `202 Accepted` and the clip runs asynchronously on the server; check the returned `hosted_file` record later via the standard V3 `hosted_file` endpoints.
|
||||
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
|
||||
- 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.
|
||||
- Add `?background=true` to schedule the clip asynchronously — returns `202 Accepted` immediately; poll the `hosted_file` record for completion.
|
||||
- Returns 400 on synchronous failure; 202 when scheduled successfully.
|
||||
|
||||
Frontend guidance:
|
||||
|
||||
- Call these routes with the same `link_to_type` / `link_to_id` you plan to associate the resulting hosted_file with — the server resolves random IDs for you.
|
||||
- After a successful response, use the V3 `hosted_file` action endpoints (download/delete) to manage or retrieve the new file.
|
||||
- These endpoints run synchronously and can take time for large inputs; for heavy or batch workloads use a queued job pattern instead.
|
||||
- These endpoints may take time for large inputs. Prefer using `?background=true` to schedule work and receive a `202 Accepted` response for async processing. For heavy or batch workloads use a queued job pattern instead.
|
||||
- Prefer `?background=true` for large inputs to avoid request timeouts. 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.
|
||||
|
||||
@@ -224,10 +700,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:
|
||||
1. Verify `x-aether-api-key` is correct.
|
||||
2. Ensure you are sending `x-account-id` and NOT `x-aether-api-token`.
|
||||
3. Verify the record actually belongs to the account ID you are sending.
|
||||
4. Check if the object is marked `public_read: True` in the registry. (Posts and Archive Content allow guest access; Journals and Badges do not).
|
||||
5. Confirm the frontend is not treating `params.key` as an implicit bypass and stripping `x-account-id`.
|
||||
6. If list/search endpoints work but `GET /v3/crud/{obj_type}/{id}` still returns 403, this is likely endpoint-level policy (e.g., requires stronger auth like JWT) rather than a transport/header bug.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether API V3 WebSocket Integration Guide
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This guide explains how to implement real-time communication using the **Aether API V3 WebSocket** protocol. V3 introduces granular routing, strict message schemas, and improved multi-tenant isolation compared to previous versions.
|
||||
|
||||
---
|
||||
|
||||
244
documentation/GUIDE__AE_Events_Badges_Onsite.md
Normal file
244
documentation/GUIDE__AE_Events_Badges_Onsite.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# Aether Events — Onsite Badge Printing
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
Notes on setup, process, hardware, and browser behavior for onsite badge printing at events.
|
||||
|
||||
For cross-module onsite operations (SRR, launcher monitoring, exhibitor leads support), see:
|
||||
`documentation/GUIDE__AE_Events_Onsite_Runbook.md`.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aether badge printing uses the browser's native `window.print()` — no special software or print
|
||||
server needed. The badge render page (`/events/[event_id]/badges/print/[badge_id]`) outputs
|
||||
print-ready HTML/CSS, and the browser sends it directly to the connected printer via CUPS (Linux)
|
||||
or the OS print system (macOS/Windows).
|
||||
|
||||
Chrome (Chromium) is the recommended browser for onsite kiosk stations.
|
||||
Firefox is a solid alternative, especially for Save-to-PDF workflows.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Workflow — Onsite Kiosk
|
||||
|
||||
1. Open the event's badge printing page: `/events/[event_id]/badges`
|
||||
2. Search for the attendee (name, badge ID, or QR scan)
|
||||
3. Open the badge print page — review the rendered badge
|
||||
4. Click **Print Badge** in the controls panel (or use keyboard shortcut)
|
||||
5. In the browser print dialog:
|
||||
- Set Margins to **None** (Chrome) or leave defaults (Firefox)
|
||||
- Confirm paper/card size matches the stock loaded in the printer
|
||||
- Print
|
||||
6. `print_count` increments automatically on each print via the Print Badge button
|
||||
|
||||
For high-volume events, consider the **rapid QR scan** mode in the Leads module or using a
|
||||
dedicated kiosk session where the operator only handles physical card handoff.
|
||||
|
||||
---
|
||||
|
||||
## Browser Settings
|
||||
|
||||
### Chrome / Chromium (Recommended for kiosk use)
|
||||
|
||||
Chrome is recommended for onsite badge printing stations. Key print dialog settings:
|
||||
|
||||
| Setting | Correct value | Notes |
|
||||
|---|---|---|
|
||||
| Margins | **None** or **Minimum** | Default margins add URL/date headers — breaks badge centering |
|
||||
| Paper size | Match card stock (e.g. 3.5" × 5.5") | Zebra driver may override this automatically |
|
||||
| Background graphics | **On** | Required for colored header/footer stripe to print |
|
||||
| Pages | 1 | PVC single-sided — only front should print |
|
||||
|
||||
**Important:** Chrome ignores CSS `@page { size }` for Save to PDF — it defaults to letter/A4.
|
||||
For physical printer output, the printer driver controls paper size. This is expected behavior.
|
||||
|
||||
To lock Chrome settings for a kiosk, set Margins to "None" once and Chrome remembers per-printer.
|
||||
|
||||
### Firefox
|
||||
|
||||
Firefox honors CSS `@page { size }` which makes it ideal for PDF generation.
|
||||
For physical printing, Firefox generally "just works" without margin adjustments.
|
||||
|
||||
| Setting | Notes |
|
||||
|---|---|
|
||||
| Paper size | Can be set in dialog, but CSS `@page { size }` is honored |
|
||||
| Margins | Default is usually fine; remove headers/footers if they appear |
|
||||
| Background graphics | Enable for colored stripes and header images to print |
|
||||
|
||||
### General Notes
|
||||
|
||||
- **Background graphics must be enabled** in any browser — otherwise header images, footer
|
||||
color stripes, and tonal backgrounds will not print.
|
||||
- Private/incognito mode blocks PWA install prompts — use normal browser sessions for kiosk.
|
||||
- For highest reliability, set the kiosk machine to auto-login and open Chrome to the event URL.
|
||||
|
||||
---
|
||||
|
||||
## Linux / CUPS Setup
|
||||
|
||||
For Linux workstations and dedicated kiosk machines running Linux:
|
||||
|
||||
1. Install CUPS if not already present: `sudo pacman -S cups` (Arch) or equivalent
|
||||
2. Start the CUPS service: `sudo systemctl enable --now cups`
|
||||
3. Open the CUPS web UI: `http://localhost:631`
|
||||
4. Add the printer and install the appropriate driver (see per-printer sections below)
|
||||
5. Print a test page from CUPS to confirm card feed and quality
|
||||
6. In Chrome: select the CUPS printer name under Destination in the print dialog
|
||||
|
||||
On macOS and Windows, use the vendor-provided driver installer.
|
||||
|
||||
---
|
||||
|
||||
## Printers
|
||||
|
||||
---
|
||||
|
||||
### Zebra ZC10L — PVC Card Printer
|
||||
|
||||
**Card stock:** 3.5" × 5.5" PVC cards (CR80 extended)
|
||||
**Tested:** 2026-03-17 (rental test day, Arch Linux)
|
||||
**Status:** Working. Confirmed suitable for Axonius NYC (mid-April 2026).
|
||||
|
||||
#### Physical Setup
|
||||
|
||||
- Connect via USB (the ZC10L supports USB and Ethernet)
|
||||
- Load PVC card stock per the Zebra loading instructions — cards face-up, landscape
|
||||
- The ZC10L prints one side (single-sided dye-sub thermal); do not attempt duplex on PVC stock
|
||||
|
||||
#### Linux Driver
|
||||
|
||||
- Download the Zebra ZC10L CUPS driver from zebra.com (ZC Series Linux support)
|
||||
- Install the `.deb` or extract the PPD file and add to CUPS manually
|
||||
- In CUPS (`http://localhost:631`), add the printer and select the ZC10L PPD
|
||||
- Set default paper size to **3.5" × 5.5"** (or CR80 Extended if listed)
|
||||
- Print a blank test page from CUPS before using Chrome
|
||||
|
||||
> **Note:** Driver version tested: *(update here after confirming)*
|
||||
> CUPS printer name used: *(update here after setup)*
|
||||
|
||||
#### Chrome Print Settings (ZC10L)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Destination | Zebra ZC10L (CUPS name) |
|
||||
| Paper size | 3.5 × 5.5 in (or as set in CUPS) |
|
||||
| Margins | **None** |
|
||||
| Background graphics | On |
|
||||
| Pages | 1 (front only) |
|
||||
|
||||
#### CSS Layout
|
||||
|
||||
The ZC10L uses the `badge_3.5x5.5_pvc` layout. The PVC layout CSS is at:
|
||||
`src/routes/events/[event_id]/(badges)/badges/print/badge_layout_zebra_zc10l_pvc.css`
|
||||
|
||||
This layout hides `.badge_back` in `@media print` — only the front face prints.
|
||||
`@page { size: 3.5in 5.5in; margin: 0; }` is set in the CSS.
|
||||
|
||||
#### Known Behaviors / Watch-outs
|
||||
|
||||
- Chrome with **Default** margins: inserts URL/date headers, offsets badge — use **None**
|
||||
- Chrome with **None** or **Minimum** margins: correct output
|
||||
- Firefox: works correctly out of the box with this layout
|
||||
- Physical card alignment: if the badge appears offset on the card, a CSS margin tweak may
|
||||
be needed in the PVC layout file — note the offset and adjust `print_margin_cfg` once that
|
||||
field is wired to the UI
|
||||
- Font sizes: if name/affiliation text appears too small at physical scale, adjust via the
|
||||
font size controls (+ / −) in the print controls panel; note the preferred values for this
|
||||
event's template
|
||||
|
||||
#### Test Results (2026-03-19)
|
||||
|
||||
- Card feeds and prints without jam: ✅
|
||||
- Single-sided PVC confirmed (back does not print): ✅
|
||||
- Chrome margins None/Minimum: correct output ✅
|
||||
- Firefox: correct output ✅
|
||||
- QR code scannable from printed card: ✅
|
||||
- Print tracking (`print_count` increment): ✅
|
||||
- Font sizes / visual quality: tested; specific calibration pending client design direction
|
||||
- Driver version: *(record here)*
|
||||
- Physical offset needed: *(record if any)*
|
||||
|
||||
---
|
||||
|
||||
### Epson ColorWorks C3500 — Fan-Fold Label Printer
|
||||
|
||||
**Card stock:** 4" × 6" fan-fold paper label stock
|
||||
**Layout code:** `badge_4x6_fanfold`
|
||||
**Status:** Configured. First live use: Axonius Adapt DC — June 9, 2026.
|
||||
|
||||
The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
|
||||
not individual cards. Badges are separated along the perforation after printing.
|
||||
|
||||
#### Physical Setup
|
||||
|
||||
- Connect via USB or Ethernet
|
||||
- Load 4" × 6" fan-fold stock per Epson instructions
|
||||
- The C3500 is single-sided — only the front face prints. Back section is suppressed in CSS.
|
||||
- The badge has a lanyard hole punch: 5/8" × 1/8", centered, 1/4" from the top.
|
||||
Most fan-fold stock for badge use includes a pre-punched lanyard slot — verify stock matches.
|
||||
|
||||
#### Driver
|
||||
|
||||
- Epson ColorWorks C3500 CUPS driver available from epson.com (ColorWorks section)
|
||||
- On Linux/CUPS: install the provided PPD and add the printer at `http://localhost:631`
|
||||
- Set default paper size to **4" × 6"** in CUPS
|
||||
- Print a test page from CUPS before going live
|
||||
|
||||
#### Chrome Print Settings (C3500)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Destination | Epson C3500 (CUPS name) |
|
||||
| Paper size | 4 × 6 in (set in CUPS driver) |
|
||||
| Margins | **None** |
|
||||
| Background graphics | On |
|
||||
| Pages | 1 (single-sided) |
|
||||
|
||||
#### CSS Layout
|
||||
|
||||
The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
|
||||
`src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
|
||||
|
||||
Created 2026-05-15 for Axonius Adapt DC. Key specs:
|
||||
- `badge_front` 4" × 6", portrait orientation
|
||||
- `badge_header` max-height 1.5in
|
||||
- Lanyard hole: 5/8" × 1/8", centered, 1/4" from top
|
||||
- `@page { size: 4in 6in; margin: 0; }` set in the print page dynamically
|
||||
- `.badge_back` suppressed in `@media print` (single-sided)
|
||||
|
||||
#### Known Behaviors
|
||||
|
||||
- Same Chrome margin rules apply: **Margins → None** prevents URL/date header clipping
|
||||
- Firefox honors `@page { size: 4in 6in }` for PDF proofing — use it to verify layout
|
||||
- Fan-fold stock separates along the perforation — no cutting needed, but verify the
|
||||
perforation lands outside the badge content area
|
||||
|
||||
---
|
||||
|
||||
## Print Tracking
|
||||
|
||||
The badge print page tracks print counts per badge:
|
||||
|
||||
- `print_count` — increments on each **Print Badge** button click
|
||||
- `print_first_datetime` — timestamp of first print
|
||||
- An amber "Printed N×" chip appears in the print page header after the first print
|
||||
|
||||
The reprint shortcut (trusted access + edit mode) does **not** increment the count.
|
||||
Only the **Print Badge** button path increments the count. This is intentional — reprints
|
||||
for alignment or quality checks should not inflate the print count.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| White border around printed badge | Chrome Default margins | Change to None or Minimum |
|
||||
| URL / date printed at top or bottom | Chrome Default margins | Change to None |
|
||||
| Header image / stripe not printing | Background graphics disabled | Enable in print dialog |
|
||||
| Badge appears on wrong-size output | Paper size mismatch | Set correct size in CUPS and/or print dialog |
|
||||
| Card jams | Card stock misloaded | Re-seat cards per printer manual; check stock orientation |
|
||||
| Badge content clipped | Layout overflow | Check font size — use − control to reduce if needed |
|
||||
| Second blank card ejected | Duplex triggered on PVC | Confirm `.badge_back { display: none }` in print CSS for this layout |
|
||||
138
documentation/GUIDE__AE_Events_Onsite_Runbook.md
Normal file
138
documentation/GUIDE__AE_Events_Onsite_Runbook.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Guide — Aether Events: Onsite Runbook
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
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.
|
||||
|
||||
This runbook keeps badge guidance concise for onsite flow. For detailed printer/browser setup,
|
||||
driver notes, and troubleshooting matrix, use:
|
||||
`documentation/GUIDE__AE_Events_Badges_Onsite.md`.
|
||||
|
||||
### Kiosk Station Setup
|
||||
- **Browser:** Use **Chrome (Chromium)** for all kiosk stations.
|
||||
- **Settings:** Set Margins to **None**. Enable **Background Graphics**.
|
||||
- **Mode:** Use normal browser sessions (not Incognito) to allow PWA caching.
|
||||
|
||||
### Printer Reference: Zebra ZC10L (PVC)
|
||||
- **Stock:** 3.5" × 5.5" PVC cards.
|
||||
- **Orientation:** Cards face-up, landscape in the hopper.
|
||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||
- **Layout code:** `badge_3.5x5.5_pvc`
|
||||
|
||||
### Printer Reference: Epson ColorWorks C3500 (Fan-Fold)
|
||||
- **Stock:** 4" × 6" fan-fold paper label stock.
|
||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||
- **Layout code:** `badge_4x6_fanfold`
|
||||
- **Lanyard hole:** Pre-punched 5/8" × 1/8" slot at top center — verify stock matches.
|
||||
- **First live use:** Axonius Adapt DC, June 9, 2026.
|
||||
|
||||
### Printing Workflow
|
||||
1. **Search:** Find the attendee by name or QR scan in the Badges module.
|
||||
2. **Review:** Open the print page and confirm the layout looks correct.
|
||||
3. **Print:** Click **Print Badge**. `print_count` increments automatically.
|
||||
4. **Handoff:** Verify the card print quality before handing it to the attendee.
|
||||
|
||||
---
|
||||
|
||||
## Exhibitor Leads (Lead Retrieval)
|
||||
|
||||
Exhibitors use a PWA (Progressive Web App) to scan badges and capture leads.
|
||||
|
||||
### Exhibitor Support Workflow
|
||||
1. **Booth Lookup:** Help the exhibitor find their booth in the Leads landing page.
|
||||
2. **Sign-In:** Assist with the **Shared Passcode** or individual **Licensed User** login.
|
||||
3. **App Install:** Encourage them to "Add to Home Screen" (iOS) or click the Install button (Android/Chrome) for offline stability.
|
||||
4. **Scanning Demo:** Show them the **Rapid Scan** mode. Remind them that attendees must have `allow_tracking = true` on their record to be scanned.
|
||||
|
||||
### Managing Licenses
|
||||
- License counts are managed in the **Manage** tab (Admin or Shared Passcode only).
|
||||
- If an exhibitor needs more staff slots, update the `license_max` in the Exhibit record.
|
||||
|
||||
---
|
||||
|
||||
## Speaker Ready Room (SRR)
|
||||
... (rest of the file) ...
|
||||
The SRR is the central hub for content management and presenter support.
|
||||
|
||||
### SRR Practice Stations
|
||||
Stations mirror the session room setup exactly:
|
||||
- Same Mac laptop model and adapter/dongle configuration as the podiums.
|
||||
- Projector and screen (where possible).
|
||||
- Launcher running in **Native** mode — ensures verification matches the podium experience.
|
||||
|
||||
### Staffing Roles
|
||||
|
||||
| Role | Access Level | Typical Tasks |
|
||||
|---|---|---|
|
||||
| **OSIT Staff** | `trusted_access` | Manage devices, monitor via VNC, deep troubleshooting. |
|
||||
| **Client Staff** | `authenticated_access` | Upload files, view session lists, assist presenters. |
|
||||
| **Presenter** | `authenticated_access` | Self-upload via QR link (if enabled). |
|
||||
|
||||
### SRR Workflow — Day-of-Show
|
||||
1. **Check-in:** Staff looks up the presenter's session in Presentation Management.
|
||||
2. **Upload:** File is uploaded to the presenter/session record.
|
||||
3. **Verification:** Staff opens the file on a practice station to confirm rendering.
|
||||
4. **Launcher Sync:** File propagates to the podium. Use **Force Sync Location** in the Launcher config if immediate full-room caching is needed.
|
||||
5. **Proceed:** Presenter walks to the room; the podium kiosk already has the file cached.
|
||||
|
||||
---
|
||||
|
||||
## Onsite Operation (Managing Parallel Rooms)
|
||||
|
||||
### SRR Overview Page
|
||||
The Pres Mgmt overview (`/events/[id]/pres_mgmt`) is the "Command Center":
|
||||
- Monitor file status per session.
|
||||
- Filter by location and time block to stay ahead of active sessions.
|
||||
|
||||
### Per-Room Monitoring
|
||||
- Use **VNC or RustDesk** to monitor all podium screens in real time from the SRR.
|
||||
- Confirm "Native Sync" status chip in the bottom-left of the Launcher is green/idle before sessions start.
|
||||
|
||||
### Session Transitions
|
||||
- **Timing:** Ideally, sessions show/hide based on `datetime_start`.
|
||||
- **Manual Control:** In looser schedules, use Launcher controls to manually select the current session.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Show Checklist
|
||||
|
||||
### 1–2 Weeks Before
|
||||
- [ ] Event created with correct dates and timezone.
|
||||
- [ ] `mod_pres_mgmt_json` configured for client needs.
|
||||
- [ ] Locations (rooms) created and named.
|
||||
- [ ] Sessions created, assigned to locations, and timed.
|
||||
- [ ] Launcher devices (`event_device`) registered with correct codes.
|
||||
- [ ] Device-to-location assignments confirmed.
|
||||
|
||||
### Day Before (SRR Setup)
|
||||
- [ ] Mac laptops at podiums booted; Electron app running.
|
||||
- [ ] Each podium confirms it loaded the correct room's Launcher.
|
||||
- [ ] SRR practice stations confirmed (matching hardware).
|
||||
- [ ] Run **Force Sync Location** on all podiums to pre-cache all day-1 content.
|
||||
- [ ] VNC/RustDesk connections established to all podiums.
|
||||
|
||||
### Day of Show
|
||||
- [ ] Confirm all session times are accurate before the first block.
|
||||
- [ ] Monitor SRR queue and verify every file on a practice station.
|
||||
- [ ] Check VNC wall to ensure all podiums are online and synced.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Session not in Launcher | Datetime wrong or Location unassigned. | Verify session metadata in Pres Mgmt. |
|
||||
| File uploaded but missing | Polling lag or attached at wrong level. | Wait 30s; check if file is at Session vs Presenter level. |
|
||||
| File opens slowly | Not in native cache yet. | Check "Native Sync" chip; use Force Sync in config. |
|
||||
| File won't open | Corrupt upload or missing Mac codec. | Test on SRR station; convert or re-upload. |
|
||||
| Drifted schedule | Room timing shifted. | Use Launcher controls to manually select the active session. |
|
||||
| `lock_config` resets changes | Remote config is forced. | Edit the master `mod_pres_mgmt_json` in Event Settings. |
|
||||
| Move laptop to new room | Hardware reassignment. | Update `location_id` in `event_device` record; restart Electron. |
|
||||
@@ -1,8 +1,10 @@
|
||||
# Aether UI — Design System Style Guidelines
|
||||
> **Version:** 1.1 (2026-03-17)
|
||||
> **Version:** 1.2 (2026-03-20)
|
||||
> **Last Updated:** 2026-03-20
|
||||
> **Author:** One Sky IT / Scott Idem
|
||||
> **Scope:** All Aether SvelteKit frontend components
|
||||
> **Related:** `AE__UI_Component_Patterns.md`, `ae-firefly.css`, `documentation/AE__Components.md`
|
||||
> **Related:** `ae-firefly.css`, `documentation/MODULE__AE_Journals.md`, `documentation/MODULE__AE_Events_Presentation_Management.md`
|
||||
> **Historical implementation log:** `documentation/archive/PROJECT__AE_Style_Review_2026-03.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -34,6 +36,7 @@ To maintain codebase health and performance, all new development must adhere to
|
||||
- **Mandatory**: Use `preset-*` classes for interactive elements (e.g., `preset-tonal-primary`).
|
||||
- **Forbidden**: Legacy Skeleton v3 `variant-*` classes.
|
||||
- **Customization**: Use Tailwind 4 `@theme` blocks for project-wide overrides.
|
||||
- **URLs**: Skeleton for Svelte for LLMs docs: https://www.skeleton.dev/llms-svelte.txt
|
||||
|
||||
### 🔣 Lucide Icons
|
||||
- **Mandatory**: Use `@lucide/svelte` components (e.g., `<Calendar size="1em" />`).
|
||||
@@ -192,6 +195,7 @@ Always wrap in `{#if $lq__obj}{...}{:else}...skeleton...{/if}` — **never** sho
|
||||
| Form inputs | Visible `<label>` linked via `for` / `id`, or explicit `aria-label` |
|
||||
| Color-only information | Always pair color coding with icon or text — never color alone |
|
||||
| Minimum touch target | 44×44px effective hit area for all tap targets |
|
||||
| Button label + icon | All buttons should include **both a Lucide icon and text label**. Icon-only is acceptable for space-constrained toolbar/header actions (with `title` attribute); text-only is acceptable when layout is extremely tight. The icon+text combination aids non-English-native users who may not read the label fluently. |
|
||||
|
||||
---
|
||||
|
||||
@@ -241,3 +245,34 @@ $events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = result; // ← URL string
|
||||
- **`text-sm leading-relaxed`**: Standard for body-level descriptive text in cards.
|
||||
- **`tracking-wide uppercase`**: Use for section label/eyebrow text with `opacity-40`.
|
||||
- **`whitespace-pre-wrap`**: Required for any `<pre>` or `<p>` displaying user-entered multi-line text (preserves breaks without horizontal overflow).
|
||||
|
||||
---
|
||||
|
||||
## 12. Known Issues & Workarounds
|
||||
|
||||
### `btn` + `preset-filled-*` resolves to transparent inside `card` components
|
||||
|
||||
**Symptom:** A button using `btn preset-filled-primary` (or any `preset-filled-*`) inside a `card` div renders with `background-color: transparent`, making it invisible against the card surface.
|
||||
|
||||
**Root cause:** The Skeleton v4 `btn` class sets a transparent background via a CSS variable chain. When nested inside a `card` element, the `preset-filled-*` class fails to win the specificity battle and the button appears invisible. This affects both light and dark mode.
|
||||
|
||||
**Workaround:** Skip `btn` and `preset-filled-*` entirely for buttons inside `card` elements. Use direct Tailwind token classes instead:
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Correct — works reliably inside cards -->
|
||||
<button class="w-full rounded-xl py-5 font-bold flex items-center justify-center gap-2
|
||||
bg-primary-500 text-white hover:brightness-110 transition-all cursor-pointer">
|
||||
...
|
||||
</button>
|
||||
|
||||
<!-- Secondary / cancel button inside a card -->
|
||||
<button class="w-full rounded-lg py-3 text-sm font-medium flex items-center justify-center gap-2
|
||||
border border-surface-500/40 hover:bg-surface-200-800 transition-colors cursor-pointer opacity-70">
|
||||
...
|
||||
</button>
|
||||
|
||||
<!-- ❌ Broken inside card — do not use -->
|
||||
<button class="btn btn-xl preset-filled-primary">...</button>
|
||||
```
|
||||
|
||||
**Scope:** `btn` + `preset-*` classes work correctly on standalone buttons (e.g. page headers, nav bars). The issue is specific to the `card` component context. If we migrate away from Skeleton `card`/`btn`, this issue goes away.
|
||||
|
||||
@@ -1,39 +1,192 @@
|
||||
# Aether Development SOP (Frontend)
|
||||
> **Version:** 1.1 (2026-02-16)
|
||||
> **Version:** 1.2 (2026-03-17)
|
||||
> **Last Updated:** 2026-03-17
|
||||
> **Location:** documentation/GUIDE__Development.md
|
||||
|
||||
## 1. 🛡️ Verification (The "Test-First" Mandate)
|
||||
**Rule:** No code is to be committed unless it has passed local verification. Skipping this is a violation of the Aether Dev Protocol.
|
||||
## 1. Verification (The "Test-First" Mandate)
|
||||
**Rule:** No code is to be committed unless it has passed local verification.
|
||||
|
||||
### Required Checks
|
||||
1. **Svelte Integrity:** `npx svelte-check`
|
||||
- **Zero Tolerance Policy:** If a task introduces even a single svelte-check warning or error, it must not be merged. All warnings must be resolved before code review or merge. This prevents the "circle-running" and technical debt that results from ignoring warnings.
|
||||
- **Zero Tolerance:** If a task introduces even a single svelte-check warning or error, it must not be merged. Resolve all warnings before committing.
|
||||
2. **Type Safety:** Ensure interfaces in `src/lib/types/ae_types.ts` match backend schemas.
|
||||
3. **Reactivity Check:** Verify Svelte 5 runes (`$state`, `$derived`) are not creating race conditions with Dexie `liveQuery`.
|
||||
4. **Build Check:** For major changes, run `npm run build: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):
|
||||
```bash
|
||||
npx playwright test tests/event_badge_render.test.ts tests/event_badge_attendee_workflow.test.ts
|
||||
```
|
||||
Run the full suite with `npm run test:integration`. The badge tests (`event_badge_*.test.ts`) are the canonical integration test template.
|
||||
|
||||
## 2. 📝 Commit & Sync Policy
|
||||
## 2. Commit Policy
|
||||
- **Atomic Commits:** One component or one logic fix per commit. Do not batch unrelated changes.
|
||||
- **Verification Log:** Mention the verification steps taken in your work log (`ae_log_work`).
|
||||
- **Safety:** Use `~/tmp/gemini_trash` for removals; 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.
|
||||
|
||||
## 3. 🤝 The Handshake (Coordination)
|
||||
You are not working in a vacuum. You MUST coordinate with the Backend Agent.
|
||||
## 3. Coordination (The Handshake)
|
||||
You are not working in a vacuum. Coordinate with the Backend Agent via MCP tools.
|
||||
|
||||
### Mandatory Messaging Triggers
|
||||
- **Data Requirements:** When a UI feature requires a new field or endpoint.
|
||||
- **API Failures:** When a V3 endpoint returns unexpected data or 500s.
|
||||
- **Status:** IGNORE THIS FOR NOW: ~~Update your shared Journal in `~/agents_sync/aether/journals/` after significant milestones.~~
|
||||
- **Stuck in Loop or Insufficient Information: ** If you find yourself in a loop or lacking information, ask Scott and or use the `message` tool to ask for clarification or assistance from the Backend Agent.
|
||||
- **API Failures:** When a V3 endpoint returns unexpected data or errors.
|
||||
- **Blocked:** If stuck in a loop or lacking information, use `ae_send_message` to ask the Backend Agent, or flag for Scott.
|
||||
|
||||
**Tool:** Use the `message` tool to communicate with the Backend Agent or Scott's other agents.
|
||||
### Tools
|
||||
- `ae_send_message` / `ae_inbox` — agent-to-agent messaging
|
||||
- `ae_task_list` / `ae_task_add` / `ae_task_complete` — shared Kanban board
|
||||
- `ae_log_work` — log activity to daily journal
|
||||
|
||||
## 🧠 Continuity
|
||||
Before starting work:
|
||||
1. Read `~/agents_sync/README.md` to understand the fleet status and cross-agent tasks.
|
||||
2. Check `README.md` in the project root for technical specs.
|
||||
3. Review your local `documentation/TODO__Agents.md` for active tasks.
|
||||
4. Be sure to describe the plan before you start making code changes to one or more files.
|
||||
## 4. Continuity (Before Starting Work)
|
||||
1. Review `documentation/TODO__Agents.md` for active tasks.
|
||||
2. Check `~/agents_sync/README.md` for fleet status and cross-agent tasks.
|
||||
3. Describe your plan before making code changes across multiple files.
|
||||
|
||||
## 4. Aether UI/UX and API V3 Documentation
|
||||
* documentation/GUIDE__AE_API_V3_for_Frontend.md
|
||||
## 5. Key Documentation
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `documentation/TODO__Agents.md` | Active task list — read first |
|
||||
| `documentation/GUIDE__AE_API_V3_for_Frontend.md` | V3 API reference (authoritative) |
|
||||
| `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` | Dexie + liveQuery patterns |
|
||||
| `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns |
|
||||
| `documentation/AE__Architecture.md` | System architecture overview |
|
||||
| `documentation/AE__Naming_Conventions.md` | Naming rules |
|
||||
| `documentation/MODULE__AE_Events_Launcher_Native.md` | Electron/Launcher reference |
|
||||
| `tests/README.md` | Playwright test guide — shared helpers, hard-won lessons, demo IDs |
|
||||
|
||||
## 6. Inline Field Editing — `element_ae_obj_field_editor`
|
||||
|
||||
The standard component for single-field inline editing throughout the platform. Wraps a `PATCH /v3/crud/{obj_type}/{obj_id}` call behind a click-to-edit UI that respects `$ae_loc.edit_mode`.
|
||||
|
||||
```svelte
|
||||
import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte';
|
||||
```
|
||||
|
||||
### Basic usage — text field with custom display
|
||||
|
||||
Wrap the display content in the default snippet. The component renders it in view mode and swaps in the input on edit.
|
||||
|
||||
```svelte
|
||||
<Element_ae_obj_field_editor
|
||||
object_type={'event_session'}
|
||||
object_id={session.id}
|
||||
field_name={'name'}
|
||||
field_type={'text'}
|
||||
current_value={session.name}
|
||||
on_success={() => events_func.load_ae_obj_id__event_session({ api_cfg: $ae_api, event_session_id: session.id })}
|
||||
>
|
||||
<h1 class="text-2xl font-bold">{session.name}</h1>
|
||||
</Element_ae_obj_field_editor>
|
||||
```
|
||||
|
||||
### Field types
|
||||
|
||||
| `field_type` | Input rendered |
|
||||
| --- | --- |
|
||||
| `text` (default) | `<input type="text">` — Enter key saves |
|
||||
| `textarea` | `<textarea>` — use `textarea_rows` prop |
|
||||
| `select` | `<select>` — pass `select_options={{ value: 'Label' }}` |
|
||||
| `checkbox` | `<input type="checkbox">` — shows Enabled/Disabled |
|
||||
| `tiptap` | TipTap rich-text editor |
|
||||
| `date` | `<input type="date">` |
|
||||
| `datetime` | `<input type="datetime-local">` |
|
||||
| `number` | `<input type="number">` — Enter key saves |
|
||||
|
||||
### Select with nullable FK
|
||||
|
||||
```svelte
|
||||
<Element_ae_obj_field_editor
|
||||
object_type={'event_presenter'}
|
||||
object_id={presenter.event_presenter_id}
|
||||
field_name={'person_id'}
|
||||
field_type={'select'}
|
||||
current_value={presenter.person_id}
|
||||
select_options={$slct.person_obj_kv}
|
||||
allow_null={$ae_loc.administrator_access}
|
||||
on_success={() => events_func.load_ae_obj_id__event_presenter({ api_cfg: $ae_api, event_presenter_id: presenter.event_presenter_id })}
|
||||
>
|
||||
{presenter.person_id ?? 'Not linked'}
|
||||
</Element_ae_obj_field_editor>
|
||||
```
|
||||
|
||||
### Key props
|
||||
|
||||
| Prop | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `current_value` | — | Required. Bound with `$bindable` — liveQuery updates flow through automatically |
|
||||
| `allow_null` | `false` | Shows a "Set Null" button in edit mode |
|
||||
| `display_block` | `false` | Makes the wrapper `display: block` instead of `inline-block` |
|
||||
| `on_success` | — | Callback after successful PATCH — use to trigger SWR cache refresh |
|
||||
| `object_reload` | `true` | Triggers internal SWR reload after patch (in addition to `on_success`) |
|
||||
|
||||
### Behavior notes
|
||||
- The edit trigger button is `visibility: hidden` (not `display: none`) when `$ae_loc.edit_mode` is off — this preserves layout so the page doesn't shift when edit mode toggles.
|
||||
- Optimistic display: draft value is shown immediately after save; cleared once liveQuery confirms the update came back from the DB.
|
||||
- `on_success` should always call the relevant `load_ae_obj_id__*` function to keep Dexie in sync.
|
||||
|
||||
---
|
||||
|
||||
## 7. URL Parameters
|
||||
|
||||
URL params consumed by the app. Params are read by layouts and applied on mount.
|
||||
|
||||
### Global (active on all routes — read by `src/routes/+layout.svelte`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `iframe` | `true` / `false` | Enables iframe mode — hides the AE system bar for all users by default, suppresses sign-in/passcode UI |
|
||||
| `show_menu` | `true` | Override: show the AE system bar inside an iframe. Intended for admins/trusted users who need menu access while testing an embed. |
|
||||
| `hide_menu` | `true` | Explicitly hide the AE system bar outside of iframe mode (e.g. fullscreen kiosk pages). |
|
||||
| `theme` | theme name | Applies a theme on load, then removes param from URL (no history entry) |
|
||||
| `theme_mode` | `light` / `dark` | Applies theme mode on load, then removes param from URL |
|
||||
|
||||
### IDAA Module (`/idaa/` routes)
|
||||
|
||||
| Param | Values | Consumed by | Effect |
|
||||
| --- | --- | --- | --- |
|
||||
| `iframe` | `true` | `idaa/+layout.svelte` | Hides IDAA nav chrome |
|
||||
| `uuid` | Novi UUID | `idaa/(idaa)/+layout.svelte` | Sets Novi UUID → triggers member auth lookup |
|
||||
|
||||
### IDAA Video Conferences (`/idaa/video_conferences`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `uuid` | Novi UUID | Member identity for Jitsi JWT |
|
||||
| `key` | site key string | Site auth key |
|
||||
| `room` | room name | Jitsi room name |
|
||||
| `moderator` | `true` | Grants moderator role + JWT, enables lobby and activity logging |
|
||||
| `domain` | hostname | Jitsi server (default: `jitsi.dgrzone.com`) |
|
||||
| `start_muted` | `true` | Start audio muted |
|
||||
| `start_hidden` | `true` | Start video off |
|
||||
| `incoming_msg_sound` | `true` | Disable incoming message sound |
|
||||
| `participant_joined_sound` | `true` | Disable participant joined sound |
|
||||
| `participant_left_sound` | `true` | Disable participant left sound |
|
||||
| `reaction_sound` | `true` | Disable reaction sound |
|
||||
| `raise_hand_sound` | `true` | Disable raise hand sound |
|
||||
|
||||
### Events Launcher (`/events/[id]/launcher`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `session_id` | session ID | Pre-selects a session on load |
|
||||
| `iframe` | `true` | Iframe mode flag |
|
||||
| `launcher_menu` | show/hide | Show/hide launcher menu chrome |
|
||||
| `launcher_header` | show/hide | Show/hide launcher header |
|
||||
| `launcher_footer` | show/hide | Show/hide launcher footer |
|
||||
|
||||
### Events Sign-In (`/events/[id]/sign_in_out`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `person_id` | person ID | Pre-fill attendee |
|
||||
| `person_pass` | passphrase | Auto-authenticate attendee |
|
||||
| `presentation_id` | ID | Pre-select presentation |
|
||||
| `presenter_id` | ID | Pre-select presenter |
|
||||
| `session_id` | ID | Pre-select session |
|
||||
|
||||
### Badges (`/events/[id]/badges/print_list`)
|
||||
|
||||
| Param | Values | Effect |
|
||||
| --- | --- | --- |
|
||||
| `printed_status` | filter value | Filter badge list by print status |
|
||||
| `badge_type_code` | code string | Filter badge list by type |
|
||||
|
||||
32
documentation/GUIDE__Docker_CI_Cache_Policy.md
Normal file
32
documentation/GUIDE__Docker_CI_Cache_Policy.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Aether Docker CI Cache Policy
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
Purpose
|
||||
- Provide a straightforward policy to keep build caches useful but bounded.
|
||||
|
||||
Recommendations
|
||||
- Primary CI cache: **registry-based buildx cache** (preferred). Use a single cache ref (e.g. `ghcr.io/ORG/REPO:cache`) reused by CI builds.
|
||||
- Local dev cache: use `--cache-to type=local` for fast iteration but prune periodically.
|
||||
- Retention: keep registry cache for 30 days by default. Implement registry GC or lifecycle rule to delete older cache blobs.
|
||||
|
||||
Rotation strategy
|
||||
- Option A (simple): CI always writes to the same cache ref `:cache`. Periodically (monthly) run a job to `docker pull` and `docker image rm` older tags if you use date-based tagging.
|
||||
- Option B (date-tag): CI writes cache to `cache-YYYYMMDD` and a small scheduled job deletes tags older than 30 days.
|
||||
|
||||
Pruning commands (developer)
|
||||
- Remove local build cache older than 72 hours:
|
||||
```bash
|
||||
docker builder prune --filter "until=72h" --force
|
||||
```
|
||||
- Remove all builder cache (aggressive):
|
||||
```bash
|
||||
docker builder prune --all --force
|
||||
```
|
||||
|
||||
CI runner requirements
|
||||
- `docker` and `docker buildx` available in runner environment.
|
||||
- Registry credentials provided via CI secrets with permission to push/pull images.
|
||||
|
||||
Security & Secrets
|
||||
- Do not store registry credentials in repo. Use CI secret storage.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Stability Patterns for liveQuery + Svelte 5
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
Dexie's `liveQuery` works well with Svelte 5 runes, but the combination requires a few stable patterns so queries don't get recreated unintentionally and components render correctly on a "cold start" (empty IndexedDB).
|
||||
- Keep the observable instance stable: wrap `liveQuery` in a stable `$derived` so the observable isn't recreated on every render. Recreate the `liveQuery` only when explicit dependencies change (IDs, filters, or search keys).
|
||||
|
||||
@@ -25,13 +27,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.
|
||||
|
||||
### Bootstrap Race: Account-scoped Loads Before `account_id` Is Set (2026-06)
|
||||
|
||||
Account-scoped `liveQuery` triggers can fire before `+layout.svelte`'s bootstrap Sync Effect
|
||||
has propagated the real `account_id`. Two failure modes:
|
||||
|
||||
1. **IDB empty:** fetch runs with `account_id = null`. The `localStorage` scavenge in
|
||||
`api_get_object.ts` reads the stale value from a previous session — possibly a different
|
||||
account — and caches that wrong record into IDB.
|
||||
2. **IDB has a stale record:** `liveQuery` returns a cached record from a different account as
|
||||
a valid hit, so the trigger condition (`!entry`) is never true and the correct record is
|
||||
never fetched.
|
||||
|
||||
**Rule:** Gate any trigger `$effect` that loads account-scoped data on `$slct.account_id`,
|
||||
not `$ae_loc.account_id`. `$slct` is a plain writable store (not persisted), initialized to
|
||||
`null` and set _only_ by the bootstrap Sync Effect. `$ae_loc` is a persisted store that
|
||||
hydrates from `localStorage` before effects run and may carry a stale `account_id`.
|
||||
|
||||
Also treat a non-null, non-matching `account_id` in an IDB record as a cache miss:
|
||||
|
||||
```typescript
|
||||
$effect(() => {
|
||||
const account_id = $slct.account_id; // null until bootstrap Sync Effect runs
|
||||
const api_ready = !!$ae_api?.base_url;
|
||||
const entry = $lq__obj as SomeType | null | undefined;
|
||||
|
||||
if (!browser || !account_id || !api_ready) return;
|
||||
|
||||
// null account_id on a record = global/shared fallback — still a valid hit.
|
||||
const entry_is_stale_account =
|
||||
entry !== undefined && entry !== null &&
|
||||
entry.account_id !== null &&
|
||||
entry.account_id !== account_id;
|
||||
|
||||
if (!entry || entry_is_stale_account) {
|
||||
trigger = 'load...';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See `BOOTSTRAP__AI_Agent_Quickstart.md` → Section 7, entry 14 for the full incident writeup.
|
||||
|
||||
### Critical Discovery (2026-02-26): The "try_cache: false" Bug
|
||||
|
||||
**Symptom:** Nested data (e.g., Session → Presentations → Presenters) requires multiple manual refreshes to display on cold-start, even when using blocking loads.
|
||||
|
||||
**Root Cause:** Two interconnected issues in nested data loaders:
|
||||
1. **Disabled caching in nested loads**: Parent loads were passing `try_cache: false` to child loads, meaning presentations and presenters were fetched from API but **never written to IndexedDB**.
|
||||
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery *before* IndexedDB writes completed, causing race conditions.
|
||||
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery _before_ IndexedDB writes completed, causing race conditions.
|
||||
|
||||
**Example of the Bug:**
|
||||
```typescript
|
||||
@@ -89,14 +132,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.
|
||||
|
||||
## 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 broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows.
|
||||
|
||||
- Sessions / Presentations: The session page demonstrates several best practices:
|
||||
- Use `url_*` constants (derived from `data.params`) so the `liveQuery` closure captures a stable value instead of the reactive store directly.
|
||||
- Provide `initial_session_obj` from `+page.ts` as a first-draw fallback to child components.
|
||||
- Use `$derived.by(() => liveQuery(...))` for presentation lists so the observable instance is stable across renders and recreated only when `event_session_id` or `search` changes.
|
||||
|
||||
- Search pages with persisted filters or saved query text should keep the auto-search trigger in a page-level `$effect`, but the duplicate guard should live inside the actual search executor. That preserves the first page-load search while blocking repeated identical reruns from localStorage-backed rerenders. In practice:
|
||||
- derive a single `qry_key` from the search inputs
|
||||
- debounce in the `$effect`
|
||||
- compare `qry_key` against a `last_executed_key` inside `handle_search_refresh()`
|
||||
- keep transient loading flags and trigger counters in session state when the value is only used to force a refresh, not as a persisted preference
|
||||
|
||||
Example (presentation list pattern):
|
||||
```typescript
|
||||
let lq__event_presentation_obj_li = $derived(
|
||||
@@ -113,6 +256,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.
|
||||
- Verify that `+page.ts` either `await`s critical loads or returns `initial_*` payloads for first-render hydration.
|
||||
- Confirm that dependent store values (selected IDs) are assigned before components subscribe — use `untrack` to prevent extra reactive cycles.
|
||||
- If a search page stops auto-loading after a localStorage change, check whether the duplicate guard was placed in the `$effect` instead of the executor. Guarding too early can suppress the initial search; guard at execution time instead.
|
||||
- If a broad Dexie-backed list shows fewer rows than a narrower filter, look for a limit or revalidation step overwriting the local IDB result set. Broad views should stay unbounded unless the user is actually narrowing by text.
|
||||
- Ensure your `liveQuery` closures return quickly and do not throw; any exception inside the query can stop updates.
|
||||
- If a dependent query appears stale, temporarily add `await 0` in the upstream query or an explicit `Promise.resolve()` after the IDB write to force the microtask queue to flush during debugging.
|
||||
|
||||
@@ -233,6 +378,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.
|
||||
|
||||
## SvelteKit Layout Hierarchy: Security and Execution Order
|
||||
|
||||
Understanding _when_ SvelteKit code runs is critical for private-data modules like IDAA.
|
||||
|
||||
### Execution order on any navigation
|
||||
|
||||
```text
|
||||
1. +layout.ts / +page.ts ← run FIRST — before any component mounts
|
||||
also fired by SvelteKit link prefetch (on hover)
|
||||
2. Parent +layout.svelte mounts → its $effect blocks run
|
||||
3. Child +layout.svelte mounts → only if parent called {#render children?.()}
|
||||
4. +page.svelte mounts → only if every parent in the chain rendered children
|
||||
5. $effect blocks in all of the above run after mount
|
||||
```
|
||||
|
||||
### The auth-gate consequence
|
||||
|
||||
A `{:else if authenticated} {@render children?.()}` block in a `+layout.svelte`
|
||||
controls whether **everything below it** ever mounts. If the gate blocks rendering,
|
||||
no child layout or page component instantiates — their `$effect` blocks, event
|
||||
handlers, and liveQuery closures never run.
|
||||
|
||||
```svelte
|
||||
<!-- (idaa)/+layout.svelte -->
|
||||
{:else if $ae_loc.trusted_access || $idaa_loc.novi_verified}
|
||||
{@render children?.()} ← children only mount if this branch runs
|
||||
{:else}
|
||||
<p>Access Denied</p> ← children never mount; their $effects never run
|
||||
{/if}
|
||||
```
|
||||
|
||||
**`$effect` blocks inside a child component cannot bypass a parent layout auth gate.**
|
||||
They are already inside the gate. Adding redundant auth guards to `$effect` blocks
|
||||
that only run after a parent has already verified access is unnecessary — and misleads
|
||||
future readers into thinking the parent gate alone is not sufficient.
|
||||
|
||||
### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts`
|
||||
|
||||
Universal load functions run _before_ components mount and _before_ layout effects
|
||||
execute. They also fire during SvelteKit link prefetch — triggered by the user
|
||||
hovering a link, even if they never navigate. This makes them unsafe for private data:
|
||||
|
||||
```text
|
||||
User hovers an /idaa/ link →
|
||||
SvelteKit prefetch fires →
|
||||
+page.ts runs (no layout has mounted yet, no auth gate has run) →
|
||||
API call / IDB write happens for an unauthenticated user
|
||||
```
|
||||
|
||||
**Rule for private modules (IDAA, Journals):** `+page.ts` and `+layout.ts` files must
|
||||
not call any data load functions that write to IDB. Move all data loading to `$effect`
|
||||
blocks in the corresponding `+page.svelte`, gated inside the auth-checked layout render.
|
||||
The comments in every `+page.ts` under `src/routes/idaa/(idaa)/` explain this pattern.
|
||||
|
||||
### The `$effect` auth guards in IDAA `+page.svelte` files
|
||||
|
||||
These ARE still useful — but for a different reason than layout bypass:
|
||||
|
||||
```ts
|
||||
// In bb/+page.svelte
|
||||
$effect(() => {
|
||||
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
||||
posts_func.load_ae_obj_li__post(...)
|
||||
});
|
||||
```
|
||||
|
||||
Because `$ae_loc` is a Svelte 4 coarse-grained store, any unrelated write to it
|
||||
(iframe height, SWR reload) re-triggers this `$effect`. The guard prevents a spurious
|
||||
API call if `$idaa_loc.novi_verified` has been cleared between re-runs (e.g. TTL
|
||||
expiry mid-session). It is a reactivity guard, not a layout-bypass guard.
|
||||
|
||||
---
|
||||
|
||||
## Page Load Strategies (Avoiding the "Waterfall")
|
||||
|
||||
When loading data for a primary page view (e.g., viewing a specific Journal, Session, or Person), you must choose a synchronization strategy to ensure the UI renders correctly on the first load.
|
||||
@@ -280,13 +498,73 @@ If you must use non-blocking loads, you must pass the initial data to the compon
|
||||
{/if}
|
||||
```
|
||||
|
||||
## The `untrack()` Reactive-Tracking Trap
|
||||
|
||||
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you _need_ to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
|
||||
|
||||
### Symptom
|
||||
|
||||
An effect runs once, reads a store value inside `untrack()`, takes an early-exit path (e.g. "no API key → skip"), and never retries — even after the store value is updated by a background process.
|
||||
|
||||
### Real Example (IDAA Novi Verification Bug — 2026-03-25)
|
||||
|
||||
The IDAA layout verifies Novi UUIDs. `site_cfg_json` (which contains the Novi API key) was read **inside** `untrack()`:
|
||||
|
||||
```typescript
|
||||
// BUG: site_cfg_json read inside untrack → one-shot, never retries
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const uuid = data.url.searchParams.get('uuid'); // tracked ✓
|
||||
|
||||
untrack(() => {
|
||||
const site_cfg_json = $ae_loc.site_cfg_json; // ← NOT tracked ✗
|
||||
const api_key = site_cfg_json?.novi_idaa_api_key ?? null;
|
||||
if (!api_key) return; // exits silently on first load with stale cache
|
||||
verify_novi_uuid(uuid, api_key, ...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
On first load, the Dexie cache returned a stale `site_cfg_json` missing the API key. The effect exited early. The background refresh later updated `$ae_loc.site_cfg_json`, but because `site_cfg_json` was consumed inside `untrack()`, the effect never re-ran.
|
||||
|
||||
**Fix:** Move the dependency read **outside** `untrack()`:
|
||||
|
||||
```typescript
|
||||
// FIX: site_cfg_json tracked outside untrack → effect re-runs when it changes
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const uuid = data.url.searchParams.get('uuid'); // tracked ✓
|
||||
const site_cfg_json = $ae_loc.site_cfg_json; // tracked ✓ — effect re-runs on change
|
||||
|
||||
untrack(() => {
|
||||
// Guard: already verified for this UUID — don't repeat the round-trip
|
||||
if ($idaa_loc.novi_verified && $idaa_loc.novi_uuid === uuid) return;
|
||||
|
||||
const api_key = site_cfg_json?.novi_idaa_api_key ?? null;
|
||||
if (!api_key) return;
|
||||
verify_novi_uuid(uuid, api_key, ...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The guard inside `untrack()` is important: without it, every unrelated change to `$ae_loc` would re-trigger verification.
|
||||
|
||||
### Rule of Thumb
|
||||
|
||||
Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re-run if this value changes?"**
|
||||
|
||||
- If yes → read it **outside** `untrack()`, and add a guard inside to prevent redundant work.
|
||||
- If no → `untrack()` is correct.
|
||||
|
||||
---
|
||||
|
||||
## Svelte 5 Binding Pitfalls
|
||||
|
||||
### 1. `props_invalid_value` (The "Expression Binding" Error)
|
||||
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
|
||||
|
||||
**❌ Invalid Pattern (Causes Compile Error):**
|
||||
Attempting to normalize a value *inside* the binding will fail.
|
||||
Attempting to normalize a value _inside_ the binding will fail.
|
||||
```svelte
|
||||
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
||||
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Module Path:** `src/routes/events/[event_id]/(badges)/templates/`
|
||||
**API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts`
|
||||
**Database Table:** `event_badge_template`
|
||||
**Last Updated:** 2026-03-02
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
---
|
||||
|
||||
@@ -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 |
|
||||
| `notes` | str | Internal notes |
|
||||
|
||||
### New Field (pending backend addition)
|
||||
### Duplex / Single-Sided
|
||||
| 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.
|
||||
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.
|
||||
|
||||
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
|
||||
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
|
||||
`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
|
||||
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
|
||||
|
||||
Axonius events use `duplex = false` — single-sided printing only.
|
||||
|
||||
---
|
||||
|
||||
@@ -155,7 +157,7 @@ The print page (`print/+page.svelte`) or the badge view should conditionally add
|
||||
</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
|
||||
|
||||
@@ -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_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 |
|
||||
|
||||
Layout CSS files live in `src/lib/ae_events/badges/css/` and are imported by
|
||||
@@ -192,6 +194,128 @@ 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`.
|
||||
|
||||
This config applies to the onsite print controls. Remote review currently uses
|
||||
`event.mod_badges_json.edit_permissions` instead. Consolidating or defining precedence between
|
||||
these two permission sources is tracked in `documentation/TODO__Agents.md`.
|
||||
|
||||
---
|
||||
|
||||
## Template-Derived Features (component behavior)
|
||||
|
||||
### badge_type_list → badge type select
|
||||
@@ -218,12 +342,12 @@ 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
|
||||
in DB and may be needed:
|
||||
|
||||
- `style_href` — needed once external CSS is wired via `<svelte:head>`
|
||||
- `passcode` — not needed client-side
|
||||
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
||||
- `header_background`, `footer_background` — not needed (legacy)
|
||||
- `script_src` — do not add; this field should not be used
|
||||
- `duplex` — **add when backend adds the field**
|
||||
|
||||
`duplex` is already saved to IDB and drives single-sided rendering.
|
||||
|
||||
---
|
||||
|
||||
@@ -356,9 +480,12 @@ Firefox users can use "Save to PDF" directly — it just works.
|
||||
|
||||
## Pending / TODO
|
||||
|
||||
- [ ] Wire `style_href` via `<svelte:head>` in print page
|
||||
- [ ] Add `duplex` to `properties_to_save` once backend field exists
|
||||
- [ ] Add `duplex`-driven `print:hidden` to `badge_back` section in `ae_comp__badge_obj_view.svelte`
|
||||
- [ ] Make `layout` field drive actual card dimensions in the badge component (currently hard-coded)
|
||||
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte`
|
||||
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)
|
||||
- [x] Wire `style_href` via `<svelte:head>` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified)
|
||||
- [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified)
|
||||
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
|
||||
- [x] `badge_4x6_fanfold` layout CSS created (`badge_layout_epson_4x6_fanfold.css`), imported in badge component, `@page 4in 6in` wired in print page. (2026-05-15)
|
||||
- [x] Template form expanded — `layout`, `style_href`, `badge_type_list`, `duplex`, and all `cfg_json` keys now editable via the form. (2026-06-04)
|
||||
- [x] `cfg_json.header_margin_top`, `header_border_color`, `header_border_width`, `header_padding_bottom` added — header image position and bottom border are fully configurable without a code deploy. (2026-06-04)
|
||||
- [ ] Wire `badge_type_list` from the template into the badge search filter — currently the search form uses a hardcoded list. See `ae_comp__badge_search.svelte` TODO comment.
|
||||
- [ ] `badge_4x5_fanfold` layout CSS exists but is stale (not used in 2+ years) — review against actual hardware before next use.
|
||||
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1)
|
||||
|
||||
@@ -1,843 +1,150 @@
|
||||
# MODULE: Aether Events — Badges
|
||||
# Aether Events — Badges
|
||||
|
||||
**Module Path:** `src/routes/events/[event_id]/(badges)/badges/`
|
||||
**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)
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
## Data Model & Hierarchy
|
||||
|
||||
The Badges module manages event attendee badges with support for:
|
||||
- **External system imports** (iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others)
|
||||
- **Field override protection** to prevent staff/attendee edits from being overwritten by automated syncs
|
||||
- **Multi-tier access control** for field editing
|
||||
- **QR code generation** for badge scanning
|
||||
- **Print tracking** (count, first/last print datetime)
|
||||
- **Advanced search and filtering**
|
||||
- **HTML rendering** in display fields for rich text formatting
|
||||
- **Accessibility features** (text enlargement toggle)
|
||||
### Core Objects
|
||||
- **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
|
||||
- **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
|
||||
|
||||
### Relationships
|
||||
- **Badge → Event:** Many-to-one.
|
||||
- **Badge → Template:** Many-to-one (via `event_badge_template_id`).
|
||||
- **Badge → Person:** Optional link to core Aether Person record for unified profiles.
|
||||
|
||||
---
|
||||
|
||||
## Critical Design Pattern: Override Fields
|
||||
|
||||
### Purpose
|
||||
The `*_override` fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because:
|
||||
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
|
||||
The `*_override` fields pattern (established in 2018) protects data from being overwritten during scheduled cron syncs from external systems (iMIS, Novi, etc.). This ensures that staff corrections or attendee self-updates persist across multiple sync cycles.
|
||||
|
||||
### How It Works
|
||||
1. **Import:** External systems populate **REGULAR** fields only.
|
||||
2. **Display Logic:** The UI displays the `*_override` field if it has a value; otherwise, it falls back to the regular field.
|
||||
3. **HTML Rendering:** Certain display fields (Name, Title, Affiliations, Location) support HTML markup for rich text formatting (bold, italics, line breaks) on the physical badge.
|
||||
|
||||
**Import Behavior:**
|
||||
```
|
||||
External System → Aether API → Populates REGULAR fields only
|
||||
(never touches *_override fields)
|
||||
```
|
||||
### Standard Override Pairs
|
||||
|
||||
**Display Behavior:**
|
||||
```
|
||||
UI Display Logic:
|
||||
1. IF `*_override` field has value → USE IT (highest priority)
|
||||
2. ELSE IF regular field has value → USE IT (fallback)
|
||||
3. ELSE → Display placeholder/empty
|
||||
```
|
||||
|
||||
**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
|
||||
| Regular Field | Override Field | Editable By | HTML? |
|
||||
|---|---|---|---|
|
||||
| `full_name` | `full_name_override` | Staff, Attendee | ✅ |
|
||||
| `professional_title` | `professional_title_override` | Staff, Attendee | ✅ |
|
||||
| `affiliations` | `affiliations_override` | Staff, Attendee | ✅ |
|
||||
| `location` | `location_override` | Staff, Attendee | ✅ |
|
||||
| `email` | `email_override` | Staff Only | No |
|
||||
| `badge_type` | `badge_type_override` | Staff Only | No |
|
||||
|
||||
---
|
||||
|
||||
## External System Integration
|
||||
|
||||
### Supported Import Sources
|
||||
- **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
|
||||
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."
|
||||
|
||||
### Data Flow Direction
|
||||
```
|
||||
External Systems ─────────> Aether
|
||||
(READ ONLY) (WRITE + DISPLAY)
|
||||
```
|
||||
|
||||
**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
|
||||
```
|
||||
### Supported Sources
|
||||
- **iMIS**, **Novi AMS**, **Impexium** (Associations)
|
||||
- **Zoom**, **Cvent** (Registrations)
|
||||
- **Confex** (Abstracts/Presenters)
|
||||
- **Custom CSV/Excel**
|
||||
|
||||
---
|
||||
|
||||
## Access Control & Edit Permissions
|
||||
## Access Control & Permissions
|
||||
|
||||
### Access Levels (Ascending)
|
||||
1. **Anonymous** — No access to badges
|
||||
2. **Public** — View public event info only (no badge access)
|
||||
3. **Authenticated** — View own badge, limited self-edit
|
||||
4. **Trusted** — Search all badges, view all, edit own
|
||||
5. **Administrator** — Full CRUD, bulk operations, override any field
|
||||
6. **Manager** — All administrator + event configuration
|
||||
7. **Super** — All manager + cross-event operations
|
||||
| Level | Access |
|
||||
|---|---|
|
||||
| **Public kiosk** | View badge and perform the first print; cannot edit fields without authenticated access. |
|
||||
| **Authenticated** | Edit fields allowed by the active permission config. |
|
||||
| **Trusted** | Search all badges, view all, and correct names; reprint requires global Edit Mode. |
|
||||
| **Administrator** | Full CRUD, bulk operations, and override access. |
|
||||
| **Manager** | All Administrator capabilities plus Event/Template configuration. |
|
||||
|
||||
### Current Implementation (v3) — 2026-02-27
|
||||
### Attendee Self-Service (`/review`)
|
||||
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`).
|
||||
Editable fields come from `event.mod_badges_json.edit_permissions`, with module defaults as fallback.
|
||||
|
||||
#### Badge Search Results Visibility
|
||||
### Onsite Kiosk (`/print`)
|
||||
The print controls update the badge preview live. Authenticated field editing is controlled by the
|
||||
badge template's `cfg_json.controls_cfg` (`shown` and `auth_editable`). Trusted + global Edit Mode
|
||||
overrides the template config and exposes all controls. This differs from the review page's
|
||||
event-level permission source; consolidation is an active follow-up.
|
||||
|
||||
| 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"]
|
||||
}
|
||||
}
|
||||
```
|
||||
### Review-Link Email
|
||||
Email Link actions are placeholders and do not currently send mail. When delivery is implemented,
|
||||
it must use the imported `event_badge.email` address, never attendee-editable `email_override`.
|
||||
|
||||
---
|
||||
|
||||
## Search & Filter Capabilities
|
||||
|
||||
### Search Component
|
||||
**File:** `ae_comp__badge_search.svelte`
|
||||
- **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
|
||||
- **Multi-Word Logic:** Queries like "Scott Idem" are split and treated as `LIKE %Scott% AND LIKE %Idem%`.
|
||||
- **QR Scan Search:** Scanning an attendee's QR code (from a confirmation email or old badge) immediately jumps to their record.
|
||||
- **Advanced Filters (Trusted + Edit Mode):** Badge Type, Printed Status, Affiliations, Sort Order.
|
||||
|
||||
### Multi-Word Search Fix (2026-02-26)
|
||||
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
|
||||
### Visibility Filter (Trusted + Edit Mode)
|
||||
|
||||
### Available Filters
|
||||
Three-option select controlling which records are shown:
|
||||
|
||||
**Fulltext Search** (All Users)
|
||||
- Searches: `default_qry_str` database field
|
||||
- Includes: Name, email, external IDs
|
||||
- Type: `LIKE %query%` (case-insensitive)
|
||||
- Trigger: Enter key or 3+ characters typed
|
||||
| Option | Who can set it | Effect |
|
||||
| --- | --- | --- |
|
||||
| **Default** | Any | Hides hidden and disabled badges |
|
||||
| **Show Hidden** | Trusted | Shows hidden badges alongside normal ones |
|
||||
| **Show Disabled + Hidden** | Manager only | Shows all records regardless of enable/hide flags |
|
||||
|
||||
**Advanced Filters** (Trusted Access & Above)
|
||||
```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.
|
||||
### Result Limit Stepper (Edit Mode)
|
||||
|
||||
// Print Status Filter
|
||||
qry_printed_status: 'all' | 'printed' | 'not_printed'
|
||||
Controls the maximum number of results returned. Only visible in edit mode.
|
||||
|
||||
// Affiliations Search
|
||||
qry_affiliations: string // Separate filter for organization search
|
||||
| Access Level | Range | Step |
|
||||
| --- | --- | --- |
|
||||
| Below Trusted | Fixed 25 | — |
|
||||
| Trusted | 25 – 250 | 25 |
|
||||
| Manager+ | 25 – 2550 | 25 up to 250, then 100 |
|
||||
|
||||
// Sort Options
|
||||
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'
|
||||
```
|
||||
### Badge Type Filter — Known Limitation
|
||||
|
||||
### QR Scan Search
|
||||
- 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
|
||||
})
|
||||
```
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Badge Display Logic
|
||||
## Print Rendering and Tracking
|
||||
|
||||
### 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
|
||||
```
|
||||
- The canonical badge render uses binary-search text fitting for name, title, affiliations, and location.
|
||||
- Template `show_qr_front`/`show_qr_back` settings control QR placement.
|
||||
- Template `style_href` loads event-specific CSS on the print page.
|
||||
- Template `duplex = false` suppresses the badge back for single-sided stock.
|
||||
- Chromium PDF proofing requires margins set to None; physical printer paper size remains driver-controlled.
|
||||
|
||||
### Badge View Page
|
||||
**Route:** `/events/[event_id]/badges/[badge_id]`
|
||||
Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
|
||||
|
||||
**Components:**
|
||||
- `+page.svelte` — Container with LiveQuery for badge data
|
||||
- `ae_comp__badge_obj_view.svelte` — Full badge display + edit UI
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `print_count` | Increments on every "Print Badge" action. |
|
||||
| `print_first_datetime` | Timestamp of the very first print. |
|
||||
| `print_last_datetime` | Timestamp of the most recent print. |
|
||||
|
||||
**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
|
||||
> **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
|
||||
|
||||
---
|
||||
|
||||
## 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 Fields
|
||||
```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)
|
||||
The `handle_print_badge()` function in `ae_comp__badge_obj_view.svelte` increments the count and records timestamps:
|
||||
```typescript
|
||||
async function handle_print_badge() {
|
||||
const now = new Date().toISOString();
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
### IndexedDB Table: `badge`
|
||||
**File:** `src/lib/ae_events/db_events.ts` (Lines 841-852)
|
||||
|
||||
**Indexed Fields:**
|
||||
```typescript
|
||||
badge: `
|
||||
event_badge_id_random, event_badge_id, id,
|
||||
event_id, event_id_random,
|
||||
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_v3` 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_v3` 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_v3()` 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_v3()` 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)
|
||||
```
|
||||
## Route Map (Badges)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/badges` | Main search and attendee list. |
|
||||
| `/events/[id]/badges/templates` | Badge template management. |
|
||||
| `/events/[id]/badges/[id]/print` | The actual print-ready render page. |
|
||||
| `/events/[id]/badges/[id]/review` | Attendee-facing self-service form. |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md)
|
||||
- [Development Guide](./GUIDE__Development.md)
|
||||
- [Events Launcher Native Integration](./PROJECT__AE_Events_Launcher_Native_integration.md)
|
||||
- [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)
|
||||
👉 **[MODULE__AE_Events_Badge_Templates.md](./MODULE__AE_Events_Badge_Templates.md)** (Technical reference for layouts)
|
||||
👉 **[GUIDE__AE_Events_Badges_Onsite.md](./GUIDE__AE_Events_Badges_Onsite.md)** (Hardware & station setup)
|
||||
👉 **[GUIDE__AE_Events_Onsite_Runbook.md](./GUIDE__AE_Events_Onsite_Runbook.md)** (Onsite operational checklists)
|
||||
|
||||
81
documentation/MODULE__AE_Events_Launcher.md
Normal file
81
documentation/MODULE__AE_Events_Launcher.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Aether Events — Launcher (Podium Display)
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
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) |
|
||||
95
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
95
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Aether Events — Launcher Configuration Menu (Inventory)
|
||||
|
||||
> **Status:** Current Reference (v3.0)
|
||||
> **Last Updated:** 2026-06-12
|
||||
> **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).
|
||||
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.
|
||||
285
documentation/MODULE__AE_Events_Leads.md
Normal file
285
documentation/MODULE__AE_Events_Leads.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Aether Events — Exhibitor Leads Module (v3)
|
||||
|
||||
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
|
||||
**Last Updated:** 2026-06-12
|
||||
**Platform:** PWA only — mobile-first, offline-capable.
|
||||
**Target users:** Conference exhibitors scanning attendee badges at their booths.
|
||||
|
||||
### Recent Changes (2026-04-03)
|
||||
|
||||
- Migrated Leads persisted state to Svelte‑5 PersistedState: `leads_loc` now implemented at `src/lib/stores/ae_events_stores__leads.svelte.ts` and the store version constant `AE_LEADS_LOC_VERSION` added to `src/lib/stores/store_versions.ts`.
|
||||
- Payment UI adjustments: `ae_comp__exhibit_payment.svelte` now accepts a `leads_require_payment` prop and enforces the event-level `mod_exhibits_json.leads_require_payment` flag; a loading guard was added so the component waits for the exhibit record (Dexie `liveQuery`) before deciding which UI to show.
|
||||
- Tests: update `tests/_helpers/leads_helpers.ts` to seed `leads_loc` defaults and `__version` when needed to avoid localStorage wipe caused by store version checks.
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
The Exhibitor Leads module lets conference exhibitors capture and manage attendee leads directly
|
||||
from their booth. Exhibitors scan or search attendee badges and build a list of contacts they met.
|
||||
All data is cached locally (IndexedDB / Dexie.js) for spotty or offline venue Wi-Fi, with
|
||||
background SWR revalidation against the API when the network is available.
|
||||
|
||||
Key capabilities:
|
||||
|
||||
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
|
||||
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
|
||||
- **Lead detail** — custom question responses, notes (rich text), priority boolean flag, hide/unhide
|
||||
- **Export** — CSV/XLSX download of all leads for an exhibit
|
||||
- **License management** — assign staff accounts (email + passcode) per max license count
|
||||
- **Custom questions** — configurable per-exhibit follow-up questions (ratings, dropdowns, text)
|
||||
- **Offline-first** — IndexedDB cache survives network drops; syncs on reconnect
|
||||
- **PWA install** — Chrome/Android native install prompt; iOS Safari "Add to Home Screen" nudge
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
Three sign-in levels are supported within this module:
|
||||
|
||||
| Level | How to sign in | What they can do |
|
||||
|---|---|---|
|
||||
| **Aether Platform Auth** | Standard Aether login (manager/trusted access) | Full admin bypass; all exhibit data |
|
||||
| **Shared Exhibit Passcode** | Enter booth's `staff_passcode` | Manage licenses, view/add leads |
|
||||
| **Licensed User** | Email + individual passcode from `license_li_json` | Add and manage leads for this booth |
|
||||
|
||||
Auth state is persisted in `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-backed).
|
||||
|
||||
A booth only shows in the landing page search to non-admins if it is marked `priority = true` (i.e. paid).
|
||||
|
||||
### `allow_tracking` Opt-In
|
||||
|
||||
Attendees must have `allow_tracking = true` on their badge record to be added as a lead.
|
||||
Attendees without this flag are blocked at both the QR scanner and the manual search:
|
||||
- QR scan shows a "Tracking Blocked" warning card (`ShieldOff` icon)
|
||||
- Manual search shows an "Opt-Out" badge per result row; the "Add as Lead" button is suppressed
|
||||
|
||||
---
|
||||
|
||||
## Route Structure
|
||||
|
||||
```
|
||||
/events/[event_id]/leads/
|
||||
→ Exhibit search / landing page — find your booth
|
||||
|
||||
/events/[event_id]/leads/exhibit/[exhibit_id]/
|
||||
→ Main exhibitor view — all 4 tabs
|
||||
|
||||
/events/[event_id]/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/
|
||||
→ Lead detail view — edit notes, custom responses, flags
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Tabs
|
||||
|
||||
### Tab 1 — Start / Sign In
|
||||
|
||||
The only tab visible when not signed in as a licensed leads user.
|
||||
|
||||
- **Sign in with shared passcode** — grants booth management access (license management, passcode change)
|
||||
- **Sign in as licensed user** — grants lead capture access (email + passcode)
|
||||
- **PWA install prompt** — Chrome/Android native install button; iOS "Share → Add to Home Screen" instructions
|
||||
- **License list** — shown when signed in via shared passcode or Aether admin; add/edit/remove staff slots
|
||||
|
||||
### Tab 2 — Add Leads
|
||||
|
||||
Visible only when signed in (licensed user or Aether auth).
|
||||
|
||||
- **Text search** — search by name, email, affiliations, badge ID
|
||||
- **QR scan** — three modes (persisted per exhibit in `tab_scan_qualify`):
|
||||
- **Confirm** (`rapid`) — scan, then choose per badge: **Add & Scan Next** (resets after 2s) or **Add & View Lead** (navigates to detail)
|
||||
- **Auto** — no confirmation tap; adds immediately and auto-resets (high-throughput)
|
||||
- **Multi** — BarcodeDetector batch scan; up to 4 badges in one frame as a confirm grid
|
||||
- Previously-removed leads detected on scan — shown a "Previously Removed" card with **Restore & Scan Next** / **Restore & View Lead** buttons
|
||||
- Results show "Add as Lead" or "View Lead" depending on whether already captured
|
||||
- `external_person_id` and `group` resolved by auth type — see [Capture Identity](#capture-identity) below
|
||||
|
||||
### Tab 3 — Leads List
|
||||
|
||||
The main lead management view.
|
||||
|
||||
- **Search** — full-text across name, email, notes (local IDB fast path + API revalidation)
|
||||
- **Sort** — Newest first, Oldest first, Name A→Z, Name Z→A
|
||||
- **Filter by staff member** — "All Leads" or filter by individual licensed user
|
||||
- **Show/hide hidden records** — toggles `hide` filter on IDB and API results
|
||||
- **Export** — downloads CSV/XLSX for the exhibit (`leads_api_access` required)
|
||||
|
||||
### Tab 4 — Manage / Config
|
||||
|
||||
Exhibit configuration and app settings.
|
||||
|
||||
**Admin Tools** (manager_access only):
|
||||
- Payment status toggle (`priority` boolean field)
|
||||
- Max licenses, small/large device counts
|
||||
|
||||
**Booth Profile** (all signed-in users):
|
||||
- Exhibitor name, booth description (rich text)
|
||||
|
||||
**Access & Security**:
|
||||
- View/change shared staff passcode
|
||||
- Sign out button
|
||||
|
||||
**Lead Retrieval Config**:
|
||||
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` OR signed in via shared exhibit passcode)
|
||||
- Qualifiers & Questions — custom question config
|
||||
- Licenses & Billing — Stripe payment (only shown when `event.mod_exhibits_json.leads_require_payment = true`)
|
||||
|
||||
**App Settings**:
|
||||
- Auto-hide header/footer toggle
|
||||
- Show Extra Details toggle
|
||||
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
||||
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### `event_exhibit`
|
||||
One exhibitor's presence at an event.
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `event_exhibit_id` | Primary / URL-safe ID |
|
||||
| `name` | Exhibitor display name |
|
||||
| `code` | Booth number |
|
||||
| `staff_passcode` | Shared sign-in code |
|
||||
| `priority` | `1` = paid/active |
|
||||
| `license_max` | Max licensed staff slots |
|
||||
| `license_li_json` | Array of `{ full_name, email, passcode }` |
|
||||
| `leads_custom_questions_json` | Array of question definitions |
|
||||
| `leads_device_sm_qty` / `leads_device_lg_qty` | Device count tracking |
|
||||
|
||||
### `event_exhibit_tracking`
|
||||
One captured lead — links an exhibit to a badge.
|
||||
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `event_exhibit_tracking_id` | Primary key |
|
||||
| `event_exhibit_id` | Parent exhibit |
|
||||
| `event_badge_id` | Captured attendee's badge |
|
||||
| `external_person_id` | Capturing staff's email (from license) |
|
||||
| `exhibitor_notes` | Rich text notes (HTML via TipTap) |
|
||||
| `responses_json` | `{ [question_code]: { response: value } }` |
|
||||
| `priority` | Star/flag for high-priority leads |
|
||||
| `hide` | Soft-delete / hide from list |
|
||||
| Denormalized badge fields | `event_badge_full_name`, `event_badge_email`, `event_badge_affiliations`, `event_badge_professional_title` |
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
### Routes
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `leads/+page.svelte` | Exhibit search/landing |
|
||||
| `leads/exhibit/[exhibit_id]/+page.svelte` | Main exhibitor view — orchestrates all tabs |
|
||||
| `leads/exhibit/[exhibit_id]/+layout.svelte` / `+layout.ts` | Layout / data load |
|
||||
| `leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte` | Lead detail |
|
||||
|
||||
### Components
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `ae_tab__start.svelte` | Tab 1 — welcome, sign-in, license list |
|
||||
| `ae_tab__add.svelte` | Tab 2 — QR scan + text search toggle |
|
||||
| `ae_tab__manage.svelte` | Tab 4 — admin tools, booth config, app settings |
|
||||
| `ae_comp__exhibit_signin.svelte` | Sign-in UI (shared passcode + licensed user) |
|
||||
| `ae_comp__lead_qr_scanner.svelte` | QR scanner (rapid / qualify mode) |
|
||||
| `ae_comp__lead_manual_search.svelte` | Manual badge search + add |
|
||||
| `ae_comp__exhibit_tracking_search.svelte` | Lead list search/filter/sort bar |
|
||||
| `ae_comp__exhibit_tracking_obj_li.svelte` | Lead list item renderer |
|
||||
| `ae_comp__exhibit_license_list.svelte` | License slot manager |
|
||||
| `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor |
|
||||
| `ae_comp__exhibit_payment.svelte` | **STUB** — Stripe placeholder |
|
||||
| `ae_comp__exhibit_search.svelte` | Exhibit search on the landing page |
|
||||
| `lead/ae_comp__lead_detail_form.svelte` | Custom question response editor |
|
||||
|
||||
### Lib Functions
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/lib/ae_events/ae_events__exhibit.ts` | Exhibit load, search, create, update |
|
||||
| `src/lib/ae_events/ae_events__exhibit_tracking.ts` | Tracking load, search, create, update, export |
|
||||
|
||||
Both aggregated into `events_func` via `src/lib/ae_events/ae_events_functions.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Offline / PWA Notes
|
||||
|
||||
- All data is stored in `db_events` (Dexie.js) — `exhibit` and `exhibit_tracking` tables
|
||||
- SWR pattern: IDB cache returned immediately; background API fetch updates IDB and triggers UI refresh
|
||||
- Search: local IDB first pass (fast), then API revalidation via `search__exhibit_tracking`
|
||||
- `beforeinstallprompt` event captured at module load time (`src/lib/pwa/pwa_install.svelte.ts`)
|
||||
— fires within ~1 second of page load, before any Svelte `$effect` runs
|
||||
- iOS Safari: no native install prompt; shows "Share → Add to Home Screen" instructions instead
|
||||
|
||||
---
|
||||
|
||||
## Capture Identity
|
||||
|
||||
`external_person_id` and `group` on every `event_exhibit_tracking` record record who captured the lead. Resolved at capture time in all three lead capture components (single scanner, multi scanner, manual search):
|
||||
|
||||
| Auth type | `kv.type` | Value stored |
|
||||
| --- | --- | --- |
|
||||
| Licensed exhibit user | `'licensed'` | Their email address (`kv.key`) |
|
||||
| Shared exhibit passcode | `'shared'` | `'shared_passcode'` (label — raw passcode is NOT stored) |
|
||||
| Aether user (admin bypass, no kv) | `undefined` | `$ae_loc.access_type` — e.g. `'trusted'`, `'manager'`, `'super'` |
|
||||
|
||||
`kv` = `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-persisted exhibit sign-in state).
|
||||
|
||||
---
|
||||
|
||||
## Lead Soft-Delete / Re-enable
|
||||
|
||||
Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors:
|
||||
|
||||
- **Leads list** always filters out `enable = false` records (both IDB fast-path and API results) — no flash of removed records
|
||||
- **QR scanner**: if a previously-removed badge is scanned, the scanner detects it via `existing_leads_map` (IDB) or API fallback search (`search__exhibit_tracking` with `qry_badge_id` + `enabled: 'not_enabled'`) and shows the reenable card instead of an error
|
||||
- **Lead detail page**: "Remove Lead" button (two-click confirm in header) sets `enable = false` and navigates back. "Restore Lead" card appears at the bottom of the right sidebar when `enable` is falsy.
|
||||
- `search__exhibit_tracking` supports `qry_badge_id` param (added) and `enabled: 'not_enabled'` to find disabled records for a specific badge + exhibit combination
|
||||
|
||||
---
|
||||
|
||||
## Known Gaps
|
||||
|
||||
None currently. See TODO__Agents.md for remaining smoke test items.
|
||||
|
||||
## Implemented (previously listed as gaps)
|
||||
|
||||
### Payment / Stripe
|
||||
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
|
||||
confirmation card), Stripe not configured (admin hint), payment form with license tier selector.
|
||||
Visibility is event-wide: set `event.mod_exhibits_json.leads_require_payment = true` in the event
|
||||
settings JSON to enable. When `false` (default), both the header CreditCard button and the
|
||||
"Licenses & Billing" accordion in the Manage tab are hidden. The Stripe component itself is
|
||||
unchanged — gating is done in `+page.svelte` and `ae_tab__manage.svelte`.
|
||||
|
||||
### License Management — Shared Passcode Access
|
||||
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
|
||||
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
|
||||
|
||||
### "My Leads" filter for shared-passcode users
|
||||
Fixed. `external_person_id` is stored as the literal `'shared_passcode'` for shared users (not
|
||||
the raw passcode string). The `search_params` derived in `+page.svelte` now checks `kv.type ===
|
||||
'shared'` and resolves to `'shared_passcode'` instead of `kv.key`, so the "My Leads" filter
|
||||
correctly returns their captured records.
|
||||
|
||||
---
|
||||
|
||||
## OSIT Admin Notes
|
||||
|
||||
- Mark `priority = 1` on an exhibit to make it visible in public search and to enable lead capture
|
||||
- `license_max` controls how many licensed staff slots an exhibit can have
|
||||
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
|
||||
- Custom questions are stored per-exhibit in `leads_custom_questions_json` (not global)
|
||||
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id]/`
|
||||
|
||||
|
||||
## Old Files for Reference
|
||||
|
||||
@backups/legacy/events_leads_v2/exhibit/[slug]/+page.svelte
|
||||
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_manage.svelte
|
||||
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_payment.svelte
|
||||
141
documentation/MODULE__AE_Events_Presentation_Management.md
Normal file
141
documentation/MODULE__AE_Events_Presentation_Management.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Aether Events — Presentation Management
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
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` |
|
||||
39
documentation/MODULE__AE_IDAA_Archives.md
Normal file
39
documentation/MODULE__AE_IDAA_Archives.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Aether IDAA — Archives Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/archives/`
|
||||
**Underlying library:** `src/lib/ae_archives/`
|
||||
|
||||
IDAA Archives provides authenticated access to archival documents and media for the IDAA community.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- List, view, and edit archive records (permission-gated).
|
||||
- Upload and manage archive content files.
|
||||
- Render media/content viewers for archived assets.
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- All IDAA archive content is private.
|
||||
- Auth guard must remain enforced for all archive routes and child views.
|
||||
- Do not add pre-gate data loading in universal `+page.ts`/`+layout.ts` paths.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
- `/idaa/archives`
|
||||
- `/idaa/archives/[archive_id]`
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
39
documentation/MODULE__AE_IDAA_Bulletin_Board.md
Normal file
39
documentation/MODULE__AE_IDAA_Bulletin_Board.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Aether IDAA — Bulletin Board Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/bb/`
|
||||
**Underlying library:** `src/lib/ae_posts/`
|
||||
|
||||
The IDAA Bulletin Board (BB) is a private community posting and comment system for authenticated IDAA users.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Post list and post detail flows.
|
||||
- Comment create/edit workflows.
|
||||
- Priority/visibility and sort behavior aligned with IDAA privacy and moderation rules.
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- All BB routes are private/authenticated.
|
||||
- Do not weaken layout-level auth gating.
|
||||
- Treat any public exposure of BB data as Sev-1.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
- `/idaa/bb`
|
||||
- `/idaa/bb/[post_id]`
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
41
documentation/MODULE__AE_IDAA_Recovery_Meetings.md
Normal file
41
documentation/MODULE__AE_IDAA_Recovery_Meetings.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Aether IDAA — Recovery Meetings Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/recovery_meetings/`
|
||||
**Underlying library:** `src/lib/ae_events/` (repurposed)
|
||||
|
||||
Recovery Meetings adapts Events module primitives for IDAA meeting discovery, filtering, viewing, and trusted editing.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Meeting search/list/detail flows (IDB-first with API revalidation).
|
||||
- Filter and sort controls for member workflows.
|
||||
- Trusted/staff edit flows for meeting records.
|
||||
- Modal and direct-page detail/edit entry paths.
|
||||
|
||||
---
|
||||
|
||||
## Security and Data Integrity
|
||||
|
||||
- Module is private/authenticated.
|
||||
- Keep error state distinct from empty-result state.
|
||||
- When persisted object shape changes, update `IDB_CONTENT_VERSIONS` wiring/version.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
- `/idaa/recovery_meetings`
|
||||
- `/idaa/recovery_meetings/[event_id]`
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
35
documentation/MODULE__AE_IDAA_Video_Conferences.md
Normal file
35
documentation/MODULE__AE_IDAA_Video_Conferences.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Aether IDAA — Video Conferences Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/video_conferences/`
|
||||
|
||||
Video Conferences provides IDAA’s Jitsi meeting access experience within the IDAA private module.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Render conference links and meeting access UX.
|
||||
- Support breakout links from Novi iframe context.
|
||||
- Preserve required URL bootstrap parameters when generating breakout URLs.
|
||||
|
||||
---
|
||||
|
||||
## Breakout Link Requirement
|
||||
|
||||
When opening outside the iframe, ensure required keys (for example site key and Novi identity UUID) remain in the URL so users do not hit access denial flows.
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- Module is private/authenticated.
|
||||
- Avoid exposing conference route details publicly.
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
81
documentation/MODULE__AE_Journals.md
Normal file
81
documentation/MODULE__AE_Journals.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Aether Journals — Module Overview
|
||||
|
||||
**Status:** Active module
|
||||
**Last Updated:** 2026-06-12
|
||||
**Library:** `src/lib/ae_journals/`
|
||||
**Routes:** `src/routes/journals/`
|
||||
|
||||
This module manages private personal journals and journal entries with offline-first behavior and Svelte 5 runes patterns.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Journal and journal-entry CRUD via V3 API wrappers.
|
||||
- Dexie-backed local cache with liveQuery-driven UI updates.
|
||||
- Private/passcode-aware access behavior and client-side content encryption.
|
||||
- Quick Add, Append/Prepend, import/export, and entry auto-save workflows.
|
||||
- Tabbed module, journal, and entry configuration modals.
|
||||
|
||||
---
|
||||
|
||||
## Key Data Objects
|
||||
|
||||
- `journal`
|
||||
- `journal_entry`
|
||||
|
||||
Common fields and behavior follow Aether object conventions (`code`, `name`, `enable`, `hide`, `priority`, `sort`, `cfg_json`, `data_json`).
|
||||
|
||||
---
|
||||
|
||||
## Storage and Reactivity
|
||||
|
||||
- Local cache: Dexie tables in journals DB layer.
|
||||
- UI reactivity: Svelte 5 runes (`$state`, `$derived`, `$effect`) plus liveQuery wrappers (`lq__*`, `lqw__*`).
|
||||
- Persisted module settings: see config map.
|
||||
|
||||
Related config map:
|
||||
- `documentation/MODULE__AE_Journals_Config_Map.md`
|
||||
|
||||
---
|
||||
|
||||
## Implemented Entry Workflows
|
||||
|
||||
- Quick Add creates a plaintext note in a selected journal without opening the full editor.
|
||||
- Append/Prepend injects timestamped content into an existing entry.
|
||||
- Bulk import creates entries from parsed files; export supports centralized templates.
|
||||
- Entry edits support debounced auto-save when `journals_loc.entry.auto_save` is enabled.
|
||||
- Full entry saves encrypt `content` into `content_encrypted` when the entry's `private`
|
||||
flag is enabled; disabling `private` clears encrypted content/history fields.
|
||||
- The non-reactive `decrypt_journal_entry()` helper isolates decryption from Svelte effects.
|
||||
- Entry configuration exposes Actions, Metadata, Security, and JSON views. Trusted users
|
||||
can Remove (disable); managers and administrators can hard Delete.
|
||||
|
||||
## Current Security Limitations
|
||||
|
||||
- `passcode_hash` is editable but is not compared as secondary authentication before
|
||||
decryption. This remains an active task.
|
||||
- Quick Add explicitly creates entries with `private: false`; import creates plaintext
|
||||
content without setting encryption fields. These paths do not currently offer E2EE.
|
||||
- Successful decryption currently logs a short plaintext preview to the browser console.
|
||||
Removal is tracked as an active privacy fix.
|
||||
- Outbound email sharing is not implemented and requires a product/security decision
|
||||
because journal content is private.
|
||||
|
||||
---
|
||||
|
||||
## Access and Privacy
|
||||
|
||||
Journals contain private personal data. The Journals layout renders module content only when
|
||||
the user has `user_id`, `person_id`, and `trusted_access`. Treat all journal and journal-entry
|
||||
routes, API responses, decrypted state, logs, exports, and future sharing features as private.
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/archive/PROJECT__AE_UI_Journals_Module_Update_2026.md`
|
||||
- `documentation/TODO__Agents.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Journals: Configuration & Settings Map
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This document tracks all available settings across the three levels of the Journals module.
|
||||
|
||||
## 1. Module Level (Global)
|
||||
@@ -51,9 +53,23 @@ This document tracks all available settings across the three levels of the Journ
|
||||
| `sort` | integer | Manual sort order weight. | Manual (Done) |
|
||||
| `archive_on` | datetime | Scheduled date for automatic archiving. | Manual (Done) |
|
||||
| `private` | boolean | Trigger for E2EE (Encryption). | Manual (Done) |
|
||||
| `content_encrypted` | encrypted string | Encrypted entry content written during a full save when `private` is enabled. | Generated on save |
|
||||
| `history_encrypted` | encrypted string | Encrypted entry history when history encryption is available. | Generated on save |
|
||||
| `passcode_hash` | string | Entry-level secondary-auth field; comparison logic is not yet implemented. | Manual (Done) |
|
||||
| `alert` | boolean | Trigger for visual "Alert" state. | Manual (Done) |
|
||||
| `group` | string | Grouping key for the list view. | Manual (JSON only) |
|
||||
|
||||
## Encryption Behavior and Gaps
|
||||
|
||||
1. Full entry saves combine the journal `passcode` and `private_passcode` to encrypt
|
||||
plaintext content when the entry's `private` flag is enabled.
|
||||
2. Decryption prefers a passcode typed in the current session, then falls back to the
|
||||
journal `private_passcode`; the journal `passcode` is combined with that private key.
|
||||
3. `passcode_hash` secondary-auth comparison is pending and must not be described as enforced.
|
||||
4. Quick Add currently forces `private: false`, and bulk import creates plaintext entries
|
||||
without encryption fields. Use the full editor to enable encryption until those workflows
|
||||
are updated.
|
||||
|
||||
## 📐 Data Normalization Rules
|
||||
To prevent infinite reactivity loops and trivial save cycles, the following normalizations are applied before comparison:
|
||||
1. **Strings:** Trimmed and `null` treated as `""`.
|
||||
@@ -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.
|
||||
251
documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md
Normal file
251
documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Project: Pres Mgmt Config Cleanup & Config UI
|
||||
|
||||
**Status:** 🟡 ~70% Complete — Core Working, Cleanup Pass Needed
|
||||
**Priority:** Medium (core features functional; remaining work is deprecation + consistency)
|
||||
**Created:** 2026-04-02
|
||||
**Last Updated:** 2026-06-12 (regression audit)
|
||||
**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
|
||||
|
||||
- [x] **Step 1** — Define `PressMgmtRemoteCfg` TypeScript interface (new file or in `ae_events__event.ts`)
|
||||
- [x] **Step 2** — New `ae_events_stores__pres_mgmt.svelte.ts` with `PersistedState` ⚠️ **Missing:** version gate in `store_versions.ts`
|
||||
- [x] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys ⚠️ **Issue:** `show__launcher_link_legacy` hard-coded instead of synced from remote
|
||||
- [x] **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) **BLOCKING**
|
||||
- [x] **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 (blocked on Step 5)
|
||||
- [ ] **Step 8** — `npx svelte-check` clean; commit
|
||||
|
||||
### Regression Fixes Needed (2026-06-12 Audit)
|
||||
|
||||
- [ ] **Add `show__launcher_link_legacy` to `PressMgmtRemoteCfg`** or remove entirely if deprecated
|
||||
- Currently hard-coded to `true` in sync function (line 1054 `ae_events__event.ts`)
|
||||
- Can't be controlled via config UI
|
||||
- [ ] **Resolve `hide__launcher_link*` local/remote conflict**
|
||||
- Menu toggles ([ae_comp__events_menu_opts.svelte](../src/routes/events/ae_comp__events_menu_opts.svelte) lines 462-494) use `hide__launcher_link` for LOCAL UI state
|
||||
- Remote schema uses `show__launcher_link` (inverted)
|
||||
- Decision: Keep separate? Document clearly? Unify?
|
||||
- [ ] **Add `AE_PRES_MGMT_LOC_VERSION` to `store_versions.ts`** (Step 2 requirement)
|
||||
- [ ] **Clean `hide__launcher_link*` from defaults** if truly deprecated (lines 154-155, 333-334 in `pres_mgmt_defaults.ts`)
|
||||
|
||||
### 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.
|
||||
431
documentation/PROJECT__AE_Site_Passcode_Security.md
Normal file
431
documentation/PROJECT__AE_Site_Passcode_Security.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# PROJECT: Site Passcode Security — API-Verified Auth
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
**Last Verified Against Frontend Source:** 2026-06-12
|
||||
**Status:** Active security gap — frontend migration not started
|
||||
**Priority:** High — passcodes for trusted/administrator access currently remain in localStorage plaintext
|
||||
|
||||
The frontend still caches `access_code_kv_json`, compares passcodes locally, and can log the
|
||||
full passcode map when verbose logging is enabled. No frontend call to `/authenticate_passcode`
|
||||
or passcode-JWT expiry restoration exists. Backend implementation is documented as completed,
|
||||
but deployment must be confirmed in the backend repository/environment before frontend cutover.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
**Backend status note:** The fixes below were reported implemented and tested in the
|
||||
`aether_api_fastapi` repository. This frontend-only audit did not verify the backend source or
|
||||
deployment. Confirm that the deployed `/authenticate_passcode` uses explicit role priority,
|
||||
returns a complete passcode JWT with `auth_type: 'passcode'`, applies per-role TTLs, and validates
|
||||
passcode length before starting frontend cutover.
|
||||
|
||||
### 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 Implementation Status
|
||||
|
||||
Verified 2026-06-12:
|
||||
|
||||
- [ ] Confirm the corrected backend endpoint is deployed and reachable.
|
||||
- [ ] Replace local passcode comparison with API verification and JWT storage.
|
||||
- [ ] Add pending/error UI for passcode authentication.
|
||||
- [ ] Stop copying `access_code_kv_json` into frontend auth state.
|
||||
- [ ] Validate passcode JWT expiry during session restoration.
|
||||
- [ ] Remove `site_access_code_kv` from auth store defaults and types.
|
||||
- [ ] Remove any logging of passcode maps or entered passcodes.
|
||||
- [ ] Backend Phase 2: remove `access_code_kv_json` from the public bootstrap model.
|
||||
|
||||
## 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 |
|
||||
@@ -0,0 +1,88 @@
|
||||
# Project: Documentation Refresh and Archive Plan (2026)
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
**Goal:** Keep onboarding docs fast and current while preserving historical context in archive.
|
||||
|
||||
## 1) Naming Standard
|
||||
|
||||
Use one of these prefixes consistently:
|
||||
- `BOOTSTRAP__` for first-read onboarding.
|
||||
- `GUIDE__` for cross-module technical guides.
|
||||
- `MODULE__` for module-specific docs.
|
||||
- `PROJECT__` for active, time-bounded workstreams.
|
||||
- `PROPOSAL__` for design proposals not yet adopted.
|
||||
- `REFERENCE__` for evergreen troubleshooting/reference catalogs.
|
||||
|
||||
## 2) Module Coverage Baseline (Active)
|
||||
|
||||
Minimum one module doc per active module family:
|
||||
- Events Presentation Management -> `MODULE__AE_Events_Presentation_Management.md`
|
||||
- Events Launcher -> `MODULE__AE_Events_Launcher.md`
|
||||
- Events Launcher Native -> `MODULE__AE_Events_Launcher_Native.md`
|
||||
- Events Badges -> `MODULE__AE_Events_Badges.md`
|
||||
- Events Leads -> `MODULE__AE_Events_Leads.md`
|
||||
- Journals -> `MODULE__AE_Journals.md`
|
||||
- IDAA Archives -> `MODULE__AE_IDAA_Archives.md`
|
||||
- IDAA Bulletin Board -> `MODULE__AE_IDAA_Bulletin_Board.md`
|
||||
- IDAA Recovery Meetings -> `MODULE__AE_IDAA_Recovery_Meetings.md`
|
||||
- IDAA Video Conferences -> `MODULE__AE_IDAA_Video_Conferences.md`
|
||||
|
||||
## 3) Archive Strategy
|
||||
|
||||
Archive docs that are superseded, duplicate, or no longer operationally used.
|
||||
Do not delete historical context; move to `documentation/archive/` with clear names.
|
||||
|
||||
### Completed in this pass
|
||||
- Moved `MODULE__AE_Events_Launcher_Config_Menu_new.md` -> `archive/PROPOSAL__AE_Events_Launcher_Config_Menu_Unified_Vision_v3_1.md`.
|
||||
- Moved `PROPOSAL__IDAA_UI_UX_Roadmap_2026.md` -> `archive/PROPOSAL__IDAA_Recovery_Meetings_UI_UX_Roadmap_2026.md`.
|
||||
- Renamed `MODULE__AE Journals_config_map.md` -> `MODULE__AE_Journals_Config_Map.md`.
|
||||
- Renamed `PROJECT__AE_UI_Journals_module_update_2026.md` -> `PROJECT__AE_UI_Journals_Module_Update_2026.md`.
|
||||
- Renamed `AE_Docker_CI_cache_policy.md` -> `AE__Docker_CI_Cache_Policy.md`.
|
||||
- Archived `PROJECT__AE_Style_Review.md` -> `archive/PROJECT__AE_Style_Review_2026-03.md` (active style source is `GUIDE__AE_UI_Style_Guidelines.md`).
|
||||
- Clarified scope split: `GUIDE__AE_Events_Badges_Onsite.md` = deep badge-print reference, `GUIDE__AE_Events_Onsite_Runbook.md` = cross-module onsite operations runbook.
|
||||
- Validated all `documentation/*.md` references in active docs; no missing targets remain.
|
||||
- Added ownership and review-trigger metadata to the bootstrap, task list, and docs index.
|
||||
- Reviewed active project docs for archive eligibility. Object Field Editor and Site Passcode Security remain active and were added to the docs index.
|
||||
- Archived legacy API-object, component-inventory, data-structure, performance, and UI-pattern references that contradicted V3 IDs, Svelte 5, or current private-route execution rules.
|
||||
- Refreshed `AE__Architecture.md` and `AE__Naming_Conventions.md` as the active replacements.
|
||||
- Added `documentation/archive/README.md` to explain archive categories and restoration policy.
|
||||
- Renamed `AE__Docker_CI_Cache_Policy.md` -> `GUIDE__Docker_CI_Cache_Policy.md`.
|
||||
- Renamed `AE__UI_UX_future_ideas.md` -> `PROPOSAL__AE_UI_UX_Future_Ideas.md`.
|
||||
- Audited the Journals UI update against current source and archived
|
||||
`PROJECT__AE_UI_Journals_Module_Update_2026.md`; remaining security work was moved to
|
||||
the active task list and module documentation.
|
||||
- Audited the Badges review/print project against current source and archived
|
||||
`PROJECT__AE_Events_Badges_Review_Print.md`; email delivery and permission-source
|
||||
unification remain active follow-ups.
|
||||
- Audited Site Passcode Security against current source. It remains an active high-priority
|
||||
project because plaintext client storage and local passcode comparison are still present.
|
||||
- Audited V3 CRUD upgrade project against source (2026-06-12). All production code migrated
|
||||
to V3; legacy wrappers remain exported but unused. Archived `PROJECT__Use_AE_API_V3_CRUD_upgrade.md`.
|
||||
Optional cleanup task added to TODO for removing dead wrapper code.
|
||||
- Audited Field Editor V3 project against source (2026-06-12). Component complete with 50+ active
|
||||
usages, GUIDE documentation, and all core field types. Searchable dropdowns deferred as optional
|
||||
enhancement. Archived `PROJECT__AE_Object_Field_Editor_V3_upgrade.md`.
|
||||
- Audited Pres Mgmt Config Cleanup project (2026-06-12). Core infrastructure working (~70% complete)
|
||||
but regressions identified: `show__launcher_link_legacy` missing from schema, old settings form
|
||||
not removed, local/remote key conflicts. Updated project doc with regression fixes; keeping active.
|
||||
|
||||
### Next archive candidates (review + approve)
|
||||
- Older style-review snapshots once current style guide references are centralized.
|
||||
- Closed project docs where TODO/archive already capture final status.
|
||||
- Duplicate docs where one `MODULE__` file and one `PROJECT__` file now overlap heavily.
|
||||
|
||||
## 4) Review Cadence
|
||||
|
||||
Monthly lightweight review:
|
||||
1. Verify `BOOTSTRAP__AI_Agent_Quickstart.md` links still resolve and reflect active work.
|
||||
2. Verify each active module has one current `MODULE__` anchor doc.
|
||||
3. Move stale proposals and completed projects into archive.
|
||||
4. Update `REFERENCE__Common_Agent_Mistakes.md` keep/archive sections.
|
||||
|
||||
## 5) Immediate Follow-Up Tasks
|
||||
|
||||
1. Continue quarterly archive reviews for remaining stale `PROJECT__` docs; the Journals and Badges projects were archived on 2026-06-12, while Site Passcode Security remains active.
|
||||
2. Continue the broader permission-helper and IDAA authentication review; the Site Passcode section was source-verified on 2026-06-12.
|
||||
3. Review module docs against current routes and store names rather than relying only on filename/header freshness.
|
||||
4. Add a lightweight reusable link-check script if manual path validation becomes frequent.
|
||||
267
documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md
Normal file
267
documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# PROJECT: IDAA `idaa_loc` Migration to Svelte 5 `PersistedState`
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
## Objective
|
||||
Migrate IDAA persisted local state from legacy `svelte-persisted-store` (`$idaa_loc`) to Svelte 5 `PersistedState` (`idaa_loc.current`) without behavior regressions, auth leaks, or broken page flows.
|
||||
|
||||
Primary target store:
|
||||
- `src/lib/stores/ae_idaa_stores__idaa_loc.svelte.ts` ← new store (created, not yet wired in)
|
||||
|
||||
Legacy source currently used by routes:
|
||||
- `src/lib/stores/ae_idaa_stores.ts` ← remove `idaa_loc` export after migration
|
||||
|
||||
## Why This Matters
|
||||
- Removes coarse-grained reactivity side effects from legacy persisted store access.
|
||||
- Aligns IDAA with the completed Events store migration pattern.
|
||||
- Reduces risk of auth-state corruption from broad re-renders triggered by unrelated
|
||||
writes to the same store key.
|
||||
|
||||
## Scope
|
||||
In scope:
|
||||
- Replace all `idaa_loc` imports from `ae_idaa_stores.ts` with imports from
|
||||
`ae_idaa_stores__idaa_loc.svelte.ts`.
|
||||
- Replace all `$idaa_loc.*` reads/writes with `idaa_loc.current.*`.
|
||||
- Remove `idaa_loc` export and its `persisted()` definition from `ae_idaa_stores.ts`
|
||||
after all consumers are migrated.
|
||||
- Keep `store_versions.ts` wipe call for `ae_idaa_loc` — this cleans old data from
|
||||
browsers of returning users. Keep it for at least one year post-migration (same rule
|
||||
as `ae_events_loc`).
|
||||
|
||||
Out of scope:
|
||||
- Migrating `idaa_sess`, `idaa_slct`, `idaa_trig`, `idaa_prom` — in-memory writables,
|
||||
no coarse-reactivity problem.
|
||||
- Backend/API changes.
|
||||
- `ae_loc` migration (separate project).
|
||||
|
||||
## Consumer File Inventory
|
||||
|
||||
### Files requiring import + `$idaa_loc` → `idaa_loc.current` changes (29 files)
|
||||
|
||||
#### IDAA module root layouts (auth-critical — do these first and last)
|
||||
|
||||
| File | `$idaa_loc` hits | Notes |
|
||||
| --- | --- | --- |
|
||||
| `src/routes/idaa/+layout.svelte` | 3 | Top-level IDAA layout; `/idaa/clear-caches` lives outside this |
|
||||
| `src/routes/idaa/(idaa)/+layout.svelte` | 40 | Most complex. Novi verification loop, auth escalation, admin/trusted list writes |
|
||||
|
||||
#### Bulletin Board (BB)
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/bb/+layout.svelte` | 3 |
|
||||
| `src/routes/idaa/(idaa)/bb/+page.svelte` | 11 |
|
||||
| `src/routes/idaa/(idaa)/bb/[post_id]/+page.svelte` | 6 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_li.svelte` | 4 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte` | 11 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_view.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte` | 7 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte` | 18 |
|
||||
|
||||
#### Archives
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/archives/+layout.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/archives/+page.svelte` | 6 |
|
||||
| `src/routes/idaa/(idaa)/archives/ae_idaa_comp__media_player.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte` | 11 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_obj_id_edit.svelte` | 4 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_obj_id_view.svelte` | 16 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_content_obj_id_edit.svelte` | 4 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_content_obj_li.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__modal_media_player.svelte` | low |
|
||||
|
||||
#### Recovery Meetings
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/+layout.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` | 37 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte` | 10 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte` | 41 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte` | 8 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte` | 12 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_view.svelte` | low |
|
||||
|
||||
#### Other IDAA
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/jitsi_reports/+page.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/video_conferences/+page.svelte` | 12 |
|
||||
|
||||
### Files that reference `ae_idaa_loc` as a raw string only — NO changes needed
|
||||
- `src/routes/events/+layout.svelte` — `localStorage.removeItem('ae_idaa_loc')` (sign-out)
|
||||
- `src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_local_actions.svelte` — same
|
||||
- `src/lib/stores/store_versions.ts` — `_check_and_wipe('ae_idaa_loc', ...)` — keep as-is
|
||||
|
||||
## Safety Rules
|
||||
1. Do this as one atomic migration — no split old/new `idaa_loc` consumers in the same session.
|
||||
2. Do not loosen any IDAA auth gating — IDAA is private data, always authenticated.
|
||||
3. Do not move IDAA private data loads into `+page.ts` / `+layout.ts` where prefetch can run.
|
||||
4. Keep route behavior and permissions exactly as current production behavior.
|
||||
5. The `(idaa)/+layout.svelte` auth gate is the most sensitive file. Review it by hand after
|
||||
the mechanical pass — do not rely solely on `svelte-check` to catch auth logic regressions.
|
||||
|
||||
## Key Syntax Changes
|
||||
|
||||
| Before | After |
|
||||
| --- | --- |
|
||||
| `import { idaa_loc } from '$lib/stores/ae_idaa_stores'` | `import { idaa_loc } from '$lib/stores/ae_idaa_stores__idaa_loc.svelte'` |
|
||||
| `$idaa_loc.novi_uuid` | `idaa_loc.current.novi_uuid` |
|
||||
| `$idaa_loc.bb.qry__hidden = 'not_hidden'` | `idaa_loc.current.bb.qry__hidden = 'not_hidden'` |
|
||||
| `$idaa_loc.recovery_meetings.qry__favorites_only` | `idaa_loc.current.recovery_meetings.qry__favorites_only` |
|
||||
|
||||
Notes:
|
||||
- Keep other imports from `ae_idaa_stores` (`idaa_sess`, `idaa_slct`, `idaa_trig`, `idaa_prom`)
|
||||
unchanged — they stay in that file.
|
||||
- No `$` sigil — access via `.current` property, not Svelte store subscription.
|
||||
|
||||
## Execution Plan
|
||||
|
||||
### Phase 0: Baseline Checkpoint
|
||||
- Ensure clean compile baseline: `npx svelte-check` → 0/0.
|
||||
- Confirm new store file committed: `ae_idaa_stores__idaa_loc.svelte.ts` ✅ (done 2026-06-11)
|
||||
|
||||
Exit criteria:
|
||||
- `svelte-check` 0 errors / 0 warnings.
|
||||
|
||||
### Phase 1: Store Consumer Conversion (Mechanical)
|
||||
|
||||
Recommended order — least to most auth-critical, so layouts are done fresh at the end:
|
||||
|
||||
#### 1a. Sub-module components (BB, Archives, Recovery Meetings)
|
||||
|
||||
All `ae_idaa_comp__*` component files — pure consumers, no auth logic.
|
||||
|
||||
#### 1b. Sub-module pages and layouts (BB, Archives, Recovery Meetings)
|
||||
|
||||
Page and layout files for the three sub-modules. Sub-layouts (`bb/+layout.svelte`,
|
||||
`archives/+layout.svelte`, `recovery_meetings/+layout.svelte`) check auth indirectly
|
||||
but don't own the Novi verification loop.
|
||||
|
||||
#### 1c. Jitsi reports + video conferences
|
||||
|
||||
Low-hit, isolated pages.
|
||||
|
||||
#### 1d. `src/routes/idaa/+layout.svelte` (top-level)
|
||||
|
||||
Reads `$idaa_loc` for display only; writes happen in the (idaa) inner layout.
|
||||
|
||||
#### 1e. `src/routes/idaa/(idaa)/+layout.svelte` (innermost — do last)
|
||||
|
||||
Most complex file (40 hits). Owns the Novi verification loop and all auth escalation writes.
|
||||
The `$idaa_loc.bb.qry__hidden`, `$idaa_loc.novi_admin_li`, etc. writes all live here.
|
||||
Review by hand after mechanical pass.
|
||||
|
||||
For each file:
|
||||
1. Update import — change `idaa_loc` source, keep `idaa_sess` / `idaa_slct` / etc. from `ae_idaa_stores`.
|
||||
2. Replace `$idaa_loc.` → `idaa_loc.current.` (global find-replace within file).
|
||||
3. No other logic changes — mechanical pass only.
|
||||
|
||||
Exit criteria:
|
||||
- No remaining `$idaa_loc.` usages in `src/routes/idaa/`.
|
||||
- No `idaa_loc` imports pointing to `ae_idaa_stores` in route files.
|
||||
|
||||
### Phase 2: Store File Cleanup
|
||||
After all consumers are migrated:
|
||||
- Remove `idaa_loc` export and `persisted('ae_idaa_loc', idaa_local_data_struct)` from
|
||||
`ae_idaa_stores.ts`.
|
||||
- Remove `AE_IDAA_LOC_VERSION` import from `ae_idaa_stores.ts` (no longer needed there).
|
||||
- Keep `store_versions.ts` unchanged — the `_check_and_wipe` call stays to clean old data.
|
||||
|
||||
### Phase 3: Critical Auth Layout Validation
|
||||
Manually review after conversion:
|
||||
- `src/routes/idaa/(idaa)/+layout.svelte` — Novi verification `$effect`, auth escalation, sign-out
|
||||
- `src/routes/idaa/+layout.svelte` — top-level auth gate template
|
||||
- `src/routes/idaa/(idaa)/bb/+layout.svelte`
|
||||
- `src/routes/idaa/(idaa)/archives/+layout.svelte`
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+layout.svelte`
|
||||
|
||||
Checks:
|
||||
- Auth gate still blocks unauthenticated users on all sub-routes.
|
||||
- `novi_verified`, `novi_uuid`, trusted/admin flag writes work correctly.
|
||||
- No duplicate or skipped verification loops.
|
||||
- `bb.qry__hidden`, `bb.qry__enabled` reset after Novi verification still fires.
|
||||
|
||||
Exit criteria:
|
||||
- Auth flow matches current production behavior exactly.
|
||||
|
||||
### Phase 4: Compile + Search Guards
|
||||
Run:
|
||||
```bash
|
||||
npx svelte-check
|
||||
grep -rn '\$idaa_loc\.' src/
|
||||
grep -rn "from '\$lib/stores/ae_idaa_stores'" src/routes/idaa/
|
||||
```
|
||||
|
||||
Exit criteria:
|
||||
- `svelte-check`: 0 errors / 0 warnings.
|
||||
- No `$idaa_loc.` references remaining in source.
|
||||
- No `idaa_loc` imports from `ae_idaa_stores` in route files.
|
||||
|
||||
### Phase 5: Test File Updates
|
||||
The IDAA Novi auth test (`tests/idaa_novi_auth.test.ts`) seeds `ae_idaa_loc` via
|
||||
`addInitScript`. After migration:
|
||||
- The seeded structure remains valid (same key, same shape).
|
||||
- Remove any `ver:` field from the seed if present — `PersistedState` stores don't use it.
|
||||
- Verify the full nested structure is still seeded (the `bb`, `archives`, `recovery_meetings`
|
||||
objects must be present — see the "Seed the Full ae_idaa_loc Structure" lesson in `tests/README.md`).
|
||||
|
||||
### Phase 6: Runtime Smoke Test
|
||||
Test flows:
|
||||
1. Direct navigation to `/idaa/` — auth gate behavior correct.
|
||||
2. Bulletin Board: list, post view, post edit, comment.
|
||||
3. Archives: list, archive detail, media player.
|
||||
4. Recovery Meetings: list, search/filter, favorites toggle, edit form.
|
||||
5. Video Conferences page loads.
|
||||
6. Jitsi Reports page loads.
|
||||
7. Cache clear page (`/idaa/clear-caches`) still clears state and posts message to parent.
|
||||
8. Sign-out clears `ae_idaa_loc` (the localStorage key name is unchanged, so this works
|
||||
automatically for any caller using `localStorage.removeItem('ae_idaa_loc')`).
|
||||
|
||||
Exit criteria:
|
||||
- No regressions in primary user paths.
|
||||
|
||||
## Risk Register
|
||||
|
||||
### R1: Split-brain state
|
||||
**Risk:** Mixed old/new `idaa_loc` consumers in the same session can lead to inconsistent state.
|
||||
**Mitigation:** Convert all consumers in one agent pass before committing. Do not commit partial migrations.
|
||||
|
||||
### R2: Auth regression in `(idaa)/+layout.svelte`
|
||||
**Risk:** The Novi verification loop is complex (40 `$idaa_loc` hits). A subtle change to
|
||||
`$effect` dependency tracking between old and new store access could break or skip verification.
|
||||
**Mitigation:** Review this file by hand after the mechanical pass. Test auth flow explicitly.
|
||||
|
||||
### R3: Nested object merge gap
|
||||
**Risk:** The `deserialize` function does a shallow spread at the top level only:
|
||||
`{ ...idaa_loc_defaults, ...JSON.parse(raw) }`. If a new field is added inside `bb`,
|
||||
`archives`, or `recovery_meetings` after a user has stored data, that field will get
|
||||
`undefined` rather than its default.
|
||||
**Mitigation:** This is the same accepted trade-off as the events sub-stores. If a new
|
||||
nested field is added in the future, add a migration step or accept that the old stored
|
||||
value takes over wholesale.
|
||||
|
||||
### R4: Mechanical typo / missed reference
|
||||
**Risk:** High replacement count (29 files, ~300+ hits) introduces missed `$idaa_loc.` references.
|
||||
**Mitigation:** Run the grep guard in Phase 4 before declaring done.
|
||||
|
||||
## Rollback Plan
|
||||
If issues are found after migration:
|
||||
1. `git revert` the migration commit(s).
|
||||
2. Re-run `npx svelte-check`.
|
||||
3. Re-attempt using smaller batches per sub-module (BB only, then Archives, then Recovery Meetings).
|
||||
|
||||
## Deliverables
|
||||
- All 29 consumer files converted from `$idaa_loc.*` → `idaa_loc.current.*`.
|
||||
- `idaa_loc` export removed from `ae_idaa_stores.ts`.
|
||||
- Passing compile check (0/0).
|
||||
- Smoke-tested all IDAA sub-modules.
|
||||
|
||||
## Definition of Done
|
||||
- Full `idaa_loc` migration complete.
|
||||
- No auth/privacy regressions.
|
||||
- `svelte-check` 0/0.
|
||||
- IDAA smoke-tested (auth gate, BB, Archives, Recovery Meetings, cache clear).
|
||||
122
documentation/PROJECT__Stores_Svelte5_Migration.md
Normal file
122
documentation/PROJECT__Stores_Svelte5_Migration.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Project: Svelte 4 Store → Svelte 5 State Migration
|
||||
|
||||
**Status:** Events module — COMPLETE. Core / IDAA — In Progress (field cleanup done, PersistedState pending).
|
||||
**Priority:** High
|
||||
**Created:** 2026-03-30
|
||||
**Last Updated:** 2026-06-11
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
All core Aether stores were built with `svelte-persisted-store` (Svelte 4 contract). This provides
|
||||
coarse reactivity: any write to any field notifies *all* subscribers and re-serializes the entire
|
||||
object. For large stores like `ae_loc` and `ae_events_loc`, this caused unnecessary re-renders and
|
||||
was the root cause of the IDAA "Access Denied" corruption bug (a bootstrap write to `ae_loc` would
|
||||
overwrite `authenticated_access` if a persisted value was slightly different, corrupting IDAA
|
||||
member state stored in the same key).
|
||||
|
||||
The migration target: replace all `persisted()` stores with `runed`'s `PersistedState`, which uses
|
||||
Svelte 5 fine-grained reactivity — a write to one field only triggers effects that read that field.
|
||||
|
||||
---
|
||||
|
||||
## Completed: Events Module (2026-06-11)
|
||||
|
||||
All `ae_events_stores` sub-modules have been promoted to their own `PersistedState` stores and
|
||||
`events_loc` (the old `persisted()` store) has been **fully retired**.
|
||||
|
||||
| Store | File | localStorage key | Status |
|
||||
|---|---|---|---|
|
||||
| `badges_loc` | `ae_events_stores__badges.svelte.ts` | `ae_badges_loc` | ✅ Done (2026-04-02) |
|
||||
| `leads_loc` | `ae_events_stores__leads.svelte.ts` | `ae_leads_loc` | ✅ Done (2026-04-03) |
|
||||
| `pres_mgmt_loc` | `ae_events_stores__pres_mgmt.svelte.ts` | `ae_pres_mgmt_loc` | ✅ Done (2026-04-03) |
|
||||
| `launcher_loc` | `ae_events_stores__launcher.svelte.ts` | `ae_launcher_loc` | ✅ Done (2026-06-11) |
|
||||
| `events_auth_loc` | `ae_events_stores__auth.svelte.ts` | `ae_events_auth_loc` | ✅ Done (2026-06-11) |
|
||||
| `events_loc` | *(retired)* | `ae_events_loc` | ✅ Store removed (2026-06-11) |
|
||||
|
||||
`ae_events_stores.ts` now only exports `events_sess` (in-memory writable, no migration needed),
|
||||
`events_slct`, `events_trig`, `events_trig_kv`, `events_trigger`, and the `EVENTS_MODULE_TITLE`
|
||||
constant. The file no longer imports `svelte-persisted-store`.
|
||||
|
||||
`store_versions.ts` still calls `_check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION)` to clean
|
||||
old data out of users' browsers — this is intentional and should be kept for at least one year.
|
||||
|
||||
---
|
||||
|
||||
## In Progress / Remaining: `ae_stores.ts` and `ae_idaa_stores.ts`
|
||||
|
||||
Both stores had their unused default properties pruned (2026-06-11), reducing migration scope:
|
||||
|
||||
**`ae_loc`** (in `ae_stores.ts`) — still `persisted('ae_loc', ...)`:
|
||||
- Remaining fields: auth/identity, theme, permissions, ui config, file upload tracking, query prefs
|
||||
- This is the highest-impact remaining migration — used in nearly every route
|
||||
- Root cause of IDAA "Access Denied" bug (coarse write during bootstrap stomps on permission fields)
|
||||
|
||||
**`idaa_loc`** (in `ae_idaa_stores.ts`) — still `persisted('ae_idaa_loc', ...)`:
|
||||
- Remaining fields: `novi_uuid/verified/ts`, `novi_admin_li/trusted_li`, `archives/bb/recovery_meetings` sub-objects
|
||||
- Fields pruned (2026-06-11): `ds`, `idaa_cfg_json`, top-level `qry__*`, `novi_*_base_url`, `novi_rate_limited_until`
|
||||
|
||||
---
|
||||
|
||||
## Migration Pattern (Established)
|
||||
|
||||
Each sub-store follows this pattern, using the `badges` store as the canonical reference:
|
||||
|
||||
### 1. Defaults file (`*_defaults.ts`)
|
||||
Define the shape and defaults as a plain TypeScript object (and interface if complex):
|
||||
|
||||
```ts
|
||||
export interface BadgesLocState { ... }
|
||||
export const badges_loc_defaults: BadgesLocState = { ... };
|
||||
```
|
||||
|
||||
### 2. Store file (`*.svelte.ts`)
|
||||
```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, {
|
||||
serializer: {
|
||||
serialize: JSON.stringify,
|
||||
// Merge with defaults so new fields added after first session get their defaults.
|
||||
deserialize: (raw: string) => ({ ...badges_loc_defaults, ...JSON.parse(raw) })
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Consumer syntax
|
||||
```ts
|
||||
// Import (note .svelte extension, not .svelte.ts):
|
||||
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
|
||||
|
||||
// Read:
|
||||
badges_loc.current.fulltext_search_qry_str
|
||||
|
||||
// Write (fine-grained — only triggers effects that read this field):
|
||||
badges_loc.current.fulltext_search_qry_str = 'hello';
|
||||
|
||||
// Bulk reset:
|
||||
badges_loc.current = { ...badges_loc_defaults };
|
||||
```
|
||||
|
||||
### Key differences from old `svelte-persisted-store` pattern:
|
||||
| | Old (`persisted()`) | New (`PersistedState`) |
|
||||
|---|---|---|
|
||||
| Import | `import { events_loc } from '...ae_events_stores'` | `import { badges_loc } from '...ae_events_stores__badges.svelte'` |
|
||||
| Read | `$events_loc.badges.field` | `badges_loc.current.field` |
|
||||
| Write | `$events_loc.badges.field = x` | `badges_loc.current.field = x` |
|
||||
| Reactivity | Coarse — entire store notified | Fine-grained — only affected fields |
|
||||
| In `$effect` | Subscribes to entire store | Only subscribes to fields you read |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **`idaa_loc` → PersistedState** — Highest priority for IDAA stability. Promotes `novi_uuid/verified`,
|
||||
`archives`, `bb`, `recovery_meetings` sub-objects to their own stores following the same pattern.
|
||||
Primary benefit: eliminates the IDAA "Access Denied" corruption from `ae_loc` bootstrap writes.
|
||||
|
||||
2. **`ae_loc` → PersistedState** — Largest scope (~every route in the app). Defer until after
|
||||
`idaa_loc` is done. Consider extracting `auth_loc` (the identity/permission fields) as the
|
||||
first sub-store since those are the fields implicated in the IDAA corruption bug.
|
||||
570
documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md
Normal file
570
documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md
Normal file
@@ -0,0 +1,570 @@
|
||||
# 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.
|
||||
> **Created:** 2026-05-17
|
||||
> **Last Updated:** 2026-05-18
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
68
documentation/README__Docs_Index.md
Normal file
68
documentation/README__Docs_Index.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Aether SvelteKit — Documentation Index
|
||||
|
||||
**Doc Owner:** Frontend platform maintainers (OSIT)
|
||||
**Review Trigger:** Update whenever a documentation file is added, renamed, archived, or restored.
|
||||
|
||||
Use this file as the routing map for project documentation.
|
||||
|
||||
## 1) First Read
|
||||
|
||||
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
|
||||
- `documentation/TODO__Agents.md`
|
||||
|
||||
## 2) Core Guides
|
||||
|
||||
- `documentation/GUIDE__Development.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend_websockets.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/GUIDE__AE_UI_Style_Guidelines.md`
|
||||
- `documentation/GUIDE__Docker_CI_Cache_Policy.md`
|
||||
|
||||
## 3) Safety and Reference
|
||||
|
||||
- `documentation/AE__Architecture.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
- `documentation/AE__Naming_Conventions.md`
|
||||
|
||||
## 4) Module Docs (Active)
|
||||
|
||||
### Events
|
||||
- `documentation/MODULE__AE_Events_Presentation_Management.md`
|
||||
- `documentation/MODULE__AE_Events_Launcher.md`
|
||||
- `documentation/MODULE__AE_Events_Launcher_Native.md`
|
||||
- `documentation/MODULE__AE_Events_Launcher_Config_Menu.md`
|
||||
- `documentation/MODULE__AE_Events_Badges.md`
|
||||
- `documentation/MODULE__AE_Events_Badge_Templates.md`
|
||||
- `documentation/MODULE__AE_Events_Leads.md`
|
||||
|
||||
### Journals
|
||||
- `documentation/MODULE__AE_Journals.md`
|
||||
- `documentation/MODULE__AE_Journals_Config_Map.md`
|
||||
|
||||
### IDAA
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/MODULE__AE_IDAA_Archives.md`
|
||||
- `documentation/MODULE__AE_IDAA_Bulletin_Board.md`
|
||||
- `documentation/MODULE__AE_IDAA_Recovery_Meetings.md`
|
||||
- `documentation/MODULE__AE_IDAA_Video_Conferences.md`
|
||||
|
||||
## 5) Active Projects
|
||||
|
||||
- `documentation/PROJECT__Documentation_Refresh_and_Archive_Plan_2026.md`
|
||||
- `documentation/PROJECT__Stores_Svelte5_Migration.md`
|
||||
- `documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md`
|
||||
- `documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`
|
||||
- `documentation/PROJECT__AE_Site_Passcode_Security.md`
|
||||
|
||||
## 6) Active Proposals
|
||||
|
||||
- `documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md`
|
||||
|
||||
## 7) Archive
|
||||
|
||||
- `documentation/archive/README.md`
|
||||
|
||||
Archive contains completed historical project notes and superseded proposals.
|
||||
Legacy API-object, component-inventory, data-structure, performance, and UI-pattern snapshots are archived because they describe pre-V3 or pre-runes behavior. Use the active V3, Dexie, style, architecture, and module docs instead.
|
||||
171
documentation/REFERENCE__Common_Agent_Mistakes.md
Normal file
171
documentation/REFERENCE__Common_Agent_Mistakes.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Aether SvelteKit — Common Agent Mistakes (Reference)
|
||||
|
||||
This is the detailed mistake catalog referenced by `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
Use it as a troubleshooting and prevention guide, not as first-pass onboarding.
|
||||
|
||||
Review policy: balanced curation.
|
||||
- Keep: high-impact or recurring mistakes.
|
||||
- Archive: one-off or lower-signal historical incidents.
|
||||
|
||||
---
|
||||
|
||||
## Active Mistakes (Curated)
|
||||
|
||||
### 1) IDAA content exposed publicly
|
||||
**Impact:** Sev-1 privacy breach.
|
||||
|
||||
**What happened:** A route guard was removed on an IDAA BB route.
|
||||
|
||||
**Rule:** All `/idaa/` content is private. Never remove auth guards on IDAA routes.
|
||||
|
||||
**Verify:** Confirm route/layout auth checks still gate all IDAA pages before data load.
|
||||
|
||||
### 2) Object ID included in PATCH body (`data_kv`)
|
||||
**Impact:** 400 errors (`Unknown column in SET`).
|
||||
|
||||
**What happened:** Object ID (for URL path) was also sent in `data_kv`.
|
||||
|
||||
**Rule:** Keep object ID in the URL only; `data_kv` contains changed fields only.
|
||||
|
||||
**Verify:** In `update_ae_obj__*` calls, confirm `data_kv` excludes `*_id` fields.
|
||||
|
||||
### 3) Coarse-store `$effect` reactivity loops
|
||||
**Impact:** repeated fetches, duplicate side effects, noisy state churn.
|
||||
|
||||
**What happened:** `$effect` read fields from `svelte-persisted-store` stores (`$ae_loc`, `$idaa_loc`), subscribing to entire store.
|
||||
|
||||
**Rule:** Be minimal about coarse-store reads in `$effect`; move transient triggers to session state and keep duplicate guards in executor paths.
|
||||
|
||||
**Verify:** Check effect dependencies and ensure unrelated writes do not retrigger critical effects.
|
||||
|
||||
### 4) Dexie `.get()` used with string object ID
|
||||
**Impact:** false misses (`undefined`) despite cached data.
|
||||
|
||||
**What happened:** `.get()` queried primary key `id`, but V3 records rely on object string IDs (e.g. `person_id`).
|
||||
|
||||
**Rule:** Use `.where('<obj_id>').equals(value).first()` for object lookups.
|
||||
|
||||
**Verify:** Search for `.get(` against object IDs and replace with indexed `where` query.
|
||||
|
||||
### 5) Misunderstanding `$effect` and auth gates
|
||||
**Impact:** security confusion and wrong mitigations.
|
||||
|
||||
**What happened:** Child-component `$effect` blocks were treated as possible bypass vectors.
|
||||
|
||||
**Rule:** Child effects cannot bypass parent layout auth gates; pre-gate risk is in universal `+page.ts` / `+layout.ts` loads and prefetch.
|
||||
|
||||
**Verify:** Keep private-module data loads out of universal load files unless explicitly authenticated.
|
||||
|
||||
### 6) Using query `key` like `x-no-account-id: bypass`
|
||||
**Impact:** dropped `x-account-id`, unexpected 403 on scoped requests.
|
||||
|
||||
**What happened:** `key` was treated as bypass intent and account context was stripped.
|
||||
|
||||
**Rule:** `key` is business data, not bypass intent. Only explicit `x-no-account-id: bypass` drops account context.
|
||||
|
||||
**Verify:** Check request headers for account-scoped calls and preserve `x-account-id` unless bypass is explicitly required.
|
||||
|
||||
### 7) Pre-stringifying `*_json` before API wrappers
|
||||
**Impact:** double encoding risk and inconsistent payloads.
|
||||
|
||||
**What happened:** Callers stringified JSON fields manually before wrapper serialization.
|
||||
|
||||
**Rule:** Pass plain objects for `*_json` fields; wrappers handle serialization.
|
||||
|
||||
**Verify:** Remove `JSON.stringify(...)` around `cfg_json`, `data_json`, and related fields before wrapper calls.
|
||||
|
||||
### 8) Broad Dexie result windows silently clipped
|
||||
**Impact:** “All” views show fewer rows than narrower filters.
|
||||
|
||||
**What happened:** page limits or API revalidation replaced local broad result sets.
|
||||
|
||||
**Rule:** For empty text search, local IDB result set should drive visible results; server refresh updates cache without shrinking display.
|
||||
|
||||
**Verify:** Compare empty-search “All” view vs narrower filter counts and inspect revalidation merge behavior.
|
||||
|
||||
### 9) Missing `IDB_CONTENT_VERSIONS` bump after `properties_to_save` changes
|
||||
**Impact:** long-lived stale cache bugs (e.g. IDAA “no meetings found”).
|
||||
|
||||
**What happened:** shape persisted in IDB changed, but table content version was not bumped.
|
||||
|
||||
**Rule:** When persisted object shape/behavior changes, bump matching `IDB_CONTENT_VERSIONS` entry in `src/lib/stores/store_versions.ts`.
|
||||
|
||||
**Verify:** For relevant object changes, confirm both version increment and table-wiring path are in the same change set.
|
||||
|
||||
### 10) API retry loop broken by returning transient errors
|
||||
**Impact:** no retries on common WiFi/network blips.
|
||||
|
||||
**What happened:** transient error paths returned `false` instead of throwing, bypassing retry loop.
|
||||
|
||||
**Rule:** Keep retry classification strict:
|
||||
- transient `TypeError` and timeout aborts -> `throw`
|
||||
- navigation abort -> `return false`
|
||||
- deterministic 4xx -> `return false`
|
||||
- 5xx -> `throw`
|
||||
|
||||
**Verify:** Simulate transient network failure and confirm retry/backoff attempts occur.
|
||||
|
||||
### 11) Account-scoped trigger fired before bootstrap account is ready
|
||||
**Impact:** wrong-account API fetch and cached cross-account data.
|
||||
|
||||
**What happened:** trigger effects ran before account bootstrap settled, reading stale context.
|
||||
|
||||
**Rule:** Gate account-scoped load triggers on `$slct.account_id` readiness, not persisted `$ae_loc.account_id`.
|
||||
|
||||
**Verify:** Ensure trigger effects require browser + account_id + api readiness before fetch trigger assignment.
|
||||
|
||||
### 12) `tmp_sort_*` comparator direction inverted
|
||||
**Impact:** priority ordering reversed.
|
||||
|
||||
**What happened:** descending comparator used with `build_tmp_sort` encoding (which expects ascending).
|
||||
|
||||
**Rule:** Use ascending compare for `build_tmp_sort` outputs, with documented exceptions for legacy encodings.
|
||||
|
||||
**Verify:** Confirm comparator direction per module encoding and avoid `collection.reverse().sortBy(...)` assumptions.
|
||||
|
||||
### 13) `$` sigil used on plain prop values
|
||||
**Impact:** runtime `store_invalid_shape` errors.
|
||||
|
||||
**What happened:** child component treated normal prop values as Svelte stores.
|
||||
|
||||
**Rule:** In Svelte 5, props passed as values are plain values. Do not use `$prop` unless prop is an actual store.
|
||||
|
||||
**Verify:** In migrated components, replace `$lq__...` prop reads with plain `lq__...` prop access.
|
||||
|
||||
### 14) Null JSON blob fields not guarded
|
||||
**Impact:** read/write crashes (`cannot access property of null`).
|
||||
|
||||
**What happened:** `*_json` / `*_kv_json` DB columns were null before first write.
|
||||
|
||||
**Rule:** Use optional chaining for reads and `?? {}` initialization before object spread writes.
|
||||
|
||||
**Verify:** Audit reads/writes for `cfg_json`, `data_json`, `poc_kv_json`, and similar nullable blob fields.
|
||||
|
||||
### 15) Service worker stale-tab behavior misunderstood
|
||||
**Impact:** users run old code longer than expected, “can’t reproduce” bug reports.
|
||||
|
||||
**What happened:** deployment assumptions ignored SW activation lifecycle.
|
||||
|
||||
**Rule:** Keep SW activation behavior explicit (`skipWaiting`, `clients.claim`) and evaluate trade-offs for session-heavy flows.
|
||||
|
||||
**Verify:** After deploy, validate that long-lived tabs pick up new SW behavior as intended.
|
||||
|
||||
---
|
||||
|
||||
## Archived Historical Items (Pruned)
|
||||
|
||||
These are retained as project memory but removed from the active mistake list because they are lower-signal or one-off.
|
||||
|
||||
1. **Bad `.d.ts` module declaration masking errors** (important incident, low recurrence recently).
|
||||
2. **Launcher `file_purpose == 'admin'` filtering gap** (specific historical enum-audit miss).
|
||||
3. **Deleting files with `rm`** (still a workflow rule, now maintained in bootstrap File Safety section rather than as a mistake entry).
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
|
||||
- `documentation/TODO__Agents.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
- `documentation/PROJECT__Stores_Svelte5_Migration.md`
|
||||
@@ -1,134 +1,197 @@
|
||||
# Frontend Agent Task List
|
||||
> **Doc Owner:** Active frontend implementation team (human + agent)
|
||||
> **Review Trigger:** Update when work starts, completes, changes priority, or moves to an archive.
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** <20> Stable — ongoing development.
|
||||
> **Status:** Stable — ongoing development.
|
||||
> **Scope:** Active/open work only. Completed detail lives in archive files.
|
||||
|
||||
## 🔴 LCI October — Pres Mgmt Restoration (in progress 2026-06-12)
|
||||
|
||||
These features regressed over the last 6 months and must be working before the LCI conference.
|
||||
Reference commit for original working implementation: `bb993a102`.
|
||||
|
||||
### Session POC (Champion/Moderator) — `session_view.svelte`
|
||||
|
||||
**Root cause of visible bugs:** The POC section is placed *below* the session hero card as a
|
||||
separate disconnected block. In the original it was part of a structured `<ul>` with the session
|
||||
name, code, datetime, location, and description all together. The current layout looks and feels
|
||||
wrong to users.
|
||||
|
||||
- [x] **[Pres Mgmt] POC section — move inside session hero card** (2026-06-12)
|
||||
Restructured hero card as a `<ul>` with datetime, room, and POC as rows inside the card.
|
||||
Session name and code are now always visible (not just in edit_mode — that was a bug).
|
||||
|
||||
- [x] **[Pres Mgmt] POC assignment — "Select Person" flow broken** (2026-06-12)
|
||||
Gated the select editor on `person_options_loaded` (`Object.keys($slct.person_obj_kv).length > 0`).
|
||||
"Select Person" button renders as "Reload Person" after list is loaded.
|
||||
|
||||
- [x] **[Pres Mgmt] Email Session POC sign-in link — UI missing** (2026-06-12)
|
||||
Restored email button in POC row with `sending/sent/error` state feedback.
|
||||
Shown when `require__session_agree && show__email_access_link && poc_person_primary_email`.
|
||||
|
||||
- [x] **[Pres Mgmt] Copy Session POC access link — UI missing from session view** (2026-06-12)
|
||||
Restored inline `MyClipboard` copy button in POC row for trusted staff.
|
||||
Shown when `show__copy_access_link && trusted_access && poc_sign_in_url`.
|
||||
|
||||
### Presenter Sign-In
|
||||
|
||||
- [ ] **[Pres Mgmt] Presenter email sign-in link routes to wrong page**
|
||||
`email_sign_in__event_presenter()` builds a URL to `/presenter/[id]?person_id=...&person_pass=...`.
|
||||
The URL param parser (`sign_in_out.svelte`) is only mounted on the *session* page menu, not the
|
||||
presenter page. A presenter clicking their email link lands on their page with no auth granted.
|
||||
Fix: mount `Sign_in_out` in `presenter_page_menu.svelte` (same way session menu does it), or
|
||||
change the email link to route to the session page (which already has the parser) and include
|
||||
the presenter/presentation IDs as params — which is how it worked originally.
|
||||
|
||||
- [ ] **[Pres Mgmt] Presenter agreement not enforced before file upload**
|
||||
`require__presenter_agree` is stored and displayed but the upload components are gated on
|
||||
`auth__kv.presenter[id]` only, not on `presenter.agree`. A presenter who signs in but has not
|
||||
agreed can still upload. The original blocked the upload section until `agree === true`.
|
||||
|
||||
### Session POC Sign-In
|
||||
|
||||
- [ ] **[Pres Mgmt] `session_page_menu.svelte` sign-in prop still wrong**
|
||||
`event_session_id` prop passed to `Sign_in_out` was just changed from `event_id` to
|
||||
`event_session_id` — verify this is actually `$lq__event_session_obj?.event_session_id`
|
||||
(the real session ID string) not the URL param `url_session_id`. The sign-in component
|
||||
uses this value to set `auth__kv.session[event_session_id]`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 🚧 Upcoming High Priority
|
||||
|
||||
### [Stores] Refactor — Phase 2c (deferred)
|
||||
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
|
||||
- [ ] **[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.
|
||||
|
||||
- [ ] **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.
|
||||
- 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`
|
||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||
**Setup/Registration:** June 8 | **Show:** June 9
|
||||
|
||||
### [Launcher] Active features (identified 2026-03-06)
|
||||
- [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
|
||||
wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
|
||||
|
||||
- [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)
|
||||
### Badges follow-ups
|
||||
|
||||
- [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)
|
||||
- [ ] **[Badges] Implement review-link email delivery** — current Email Link actions only show
|
||||
placeholder alerts. Send to `event_badge.email`, never the attendee-editable `email_override`.
|
||||
- [ ] **[Badges] Unify review and kiosk edit permissions** — remote review reads
|
||||
`event.mod_badges_json.edit_permissions`; print controls read template `cfg_json.controls_cfg`.
|
||||
Define precedence or consolidate them so both flows enforce one documented policy.
|
||||
- [ ] **[Badges] Use template badge types in search filter** — replace the hardcoded badge-type
|
||||
list in `ae_comp__badge_search.svelte` with the active template's `badge_type_list`.
|
||||
|
||||
### [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
|
||||
- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
|
||||
File: `ae_comp__badge_print_controls.svelte`.
|
||||
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||
|
||||
### [Badges] Zebra ZC10L Hardware Testing — ~week of 2026-03-16
|
||||
Scott is renting a Zebra ZC10L for one day to do real-world badge printing tests before
|
||||
Axonius (mid-April). See `documentation/PROJECT__AE_Events_Zebra_Hardware_Test_Day.md`
|
||||
for the full checklist and prep plan.
|
||||
- [ ] **[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`.
|
||||
|
||||
**Pre-test work (do before printer arrives):**
|
||||
- [ ] **Remove debug outlines** from `print/+page.svelte` print CSS (lime/blue/red/orange/purple/cyan)
|
||||
- [ ] **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.
|
||||
- [ ] **Wire `style_href`** via `<svelte:head>` in `print/+page.svelte` — needed so client-specific
|
||||
CSS loads from the template's `style_href` field. Currently tracked as pending in MODULE doc.
|
||||
- [ ] **Confirm `duplex=0` hides badge back in print** — `duplex` field not yet on backend;
|
||||
verify badge_back is suppressed for single-sided PVC. May need a manual print CSS rule until
|
||||
the backend field lands and `properties_to_save` is updated.
|
||||
- [ ] **Set up test event + PVC template** in dev DB with `layout: badge_3.5x5.5_pvc`,
|
||||
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)
|
||||
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`.
|
||||
## 🚧 High Priority Workstreams
|
||||
|
||||
**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 (rapid vs. qualify mode) + manual badge search; duplicate 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)
|
||||
### [Security] Site Passcode JWT Migration
|
||||
|
||||
**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.
|
||||
- [ ] **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.
|
||||
- [ ] **[Security] Verify `/authenticate_passcode` deployment** — confirm explicit role priority,
|
||||
complete role flags, `auth_type: 'passcode'`, per-role TTLs, and minimum length validation.
|
||||
- [ ] **[Security] Replace local passcode comparison** — migrate
|
||||
`e_app_access_type.svelte` to server verification, JWT storage, and pending/error UI.
|
||||
- [ ] **[Security] Remove client-side passcode delivery/storage** — stop caching
|
||||
`access_code_kv_json`, remove `site_access_code_kv` from auth state, and remove passcode logging.
|
||||
- [ ] **[Security] Enforce passcode JWT expiry on restore** — expired passcode sessions must
|
||||
return to anonymous without affecting user-login JWT handling.
|
||||
|
||||
### [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)
|
||||
Reference: `documentation/PROJECT__AE_Site_Passcode_Security.md`.
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration
|
||||
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
|
||||
`PersistedState` (from `runed`) for fine-grained updates. See `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
|
||||
### [General]
|
||||
- [x] **Temp Cleanup:** `cleanup_tmp_files` wired in `launcher_background_sync.svelte`; called at launcher startup. Confirmed working. (2026-03-11)
|
||||
- **`window.print()` for badge print button:** Wire the existing `handle_print_badge()` to trigger `window.print()`. Browser print works well across Chrome/Chromium/Firefox — no Electron needed.
|
||||
- **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.)
|
||||
- [x] **Events module — COMPLETE (2026-06-11):** `events_loc` fully retired. All 5 sub-stores
|
||||
(`badges_loc`, `leads_loc`, `pres_mgmt_loc`, `launcher_loc`, `events_auth_loc`) are on
|
||||
`PersistedState`. Unused fields also pruned from `ae_stores.ts` and `ae_idaa_stores.ts`.
|
||||
- [ ] **`idaa_loc` → PersistedState** — Highest remaining priority. Root cause of the IDAA
|
||||
"Access Denied" corruption bug (`ae_loc` bootstrap writes stomp on `authenticated_access`).
|
||||
Promote `novi_*` identity fields and `archives/bb/recovery_meetings` sub-objects.
|
||||
- [ ] **`ae_loc` → PersistedState** — Largest scope. Extract `auth_loc` sub-store first
|
||||
(the identity/permission fields are what get corrupted). Defer full migration until after `idaa_loc`.
|
||||
- [ ] **Non-persisted writables** (`ae_sess`, `slct`, etc.) — Low priority; no coarse-reactivity problem.
|
||||
|
||||
## ✅ 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)
|
||||
### [Data Layer] IDB sorting + content version rollout
|
||||
Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
|
||||
|
||||
**⚠️ Exception:** `ae_events__event.ts` and `ae_events__event_session.ts` use **legacy encoding**
|
||||
(`priority ? 1 : 0`, priority=true→`'1'`). Their sort comparators must remain **descending**
|
||||
until the modules are migrated to `build_tmp_sort`. `ae_events__event_presentation.ts` already
|
||||
uses `build_tmp_sort` (overrides generic encoding in its `specific_processor`). See
|
||||
`CLIENT__IDAA_and_customized_mods.md` → "Sort Encoding" for full table.
|
||||
|
||||
- [ ] **[IDB Sort] Migrate `ae_events__event.ts` to `build_tmp_sort`** — requires bumping
|
||||
`IDB_CONTENT_VERSIONS.events.event` (currently v3) and switching all event sort comparators
|
||||
to ascending. Check all pages that sort events before doing this.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_session`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_presenter`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_location`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_core__person` + `ae_core__account`** after sort behavior review.
|
||||
- [ ] **[IDB Version] Roll out to `db_events.ts`** (session, presenter, badge, etc.).
|
||||
- [ ] **[IDB Version] Roll out to `db_core.ts`** (site_domain, person, user).
|
||||
|
||||
### [Journals] Journal Entry Config follow-ups
|
||||
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
|
||||
- [ ] **[Journals] Quick Add/import encryption behavior** — both creation paths currently
|
||||
create plaintext entries; define the intended privacy UX and add encryption support before
|
||||
claiming that these paths honor entry E2EE.
|
||||
- [ ] **[Journals] Remove decrypted-content console preview** —
|
||||
`ae_journals_decryption.ts` logs the first 30 plaintext characters after successful decryption.
|
||||
Never log private journal content.
|
||||
- [ ] **[Journals] Confirm outbound email-sharing requirement** — the archived UI project listed
|
||||
this as unfinished, but no implementation exists. Confirm product/security requirements before
|
||||
creating an email workflow for private journal content.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 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
|
||||
|
||||
- [ ] **[Cleanup] Remove unused legacy API wrappers** — `create_ae_obj_crud()`,
|
||||
`get_ae_obj_id_crud()`, and `update_ae_obj_id_crud()` are still exported from `api.ts` but
|
||||
no longer called anywhere in production code. V3 migration is 100% complete. Safe to delete:
|
||||
definitions in `api.ts` (lines 109-260), `src/lib/ae_api/api_get__crud_obj_id.ts`, unused
|
||||
wrapper in `ae_core_functions.ts` (`get_site_domain_obj_from_fqdn`, `update_ae_obj_id_crud`).
|
||||
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
|
||||
override currently uses a localStorage workaround (`launcher_loc.current.file_display_overrides`)
|
||||
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
|
||||
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
|
||||
backend field (restoring global/cross-device persistence). Frontend code is in
|
||||
`launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||
- [x] **[DevOps] Service worker `skipWaiting` + `clients.claim`** — Root cause of "users see
|
||||
old code / can't reproduce in dev testing": the SW sat in waiting state until all tabs closed.
|
||||
IDAA members leave idaa.org open all day. Fixed 2026-06-03: both calls added to
|
||||
`src/service-worker.js`. See mistake #16 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
- [ ] **[DevOps] Nginx proxy buffer tuning** — Buffer settings copied from PHP guide; not
|
||||
optimal for Node.js. `proxy_busy_buffers_size` technically exceeds safe limit. Re-examine
|
||||
when enabling compression (now re-enabled) stabilizes.
|
||||
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed (archived)
|
||||
See the full completed history in:
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-06.md](documentation/archive/TODO__Agents__ARCHIVE_2026-06.md)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Project: AE Docker + CI BuildKit Implementation
|
||||
|
||||
**Status:** Proposed
|
||||
|
||||
**Goal:** Make Docker image builds for Aether cache-friendly using BuildKit/buildx and CI registry caching, while keeping local developer caches small and manageable.
|
||||
|
||||
Summary
|
||||
- Implement a BuildKit-friendly multi-stage `Dockerfile` pattern for frontend and API images.
|
||||
- Add CI `buildx` examples that push/read registry-based cache to avoid local disk bloat.
|
||||
- Provide cache retention/rotation guidance and developer commands for safe pruning.
|
||||
|
||||
Scope
|
||||
- Repository areas: `aether_container_env/`, root `Dockerfile` (if present), and CI pipeline definitions (Gitea/Drone or other).
|
||||
- Non-goal: full CI pipeline migration to a new provider. This work provides CI snippets and a PR-ready set of files for your CI team.
|
||||
|
||||
Deliverables (this PR)
|
||||
- `documentation/PROJECT__AE_Docker_CI_BuildKit_implement.md` (this file)
|
||||
- `aether_container_env/Dockerfile.buildkit.example` — BuildKit-friendly multi-stage Dockerfile example.
|
||||
- `aether_container_env/ci_buildx_example.sh` — standalone CI script examples (registry cache + local cache usage).
|
||||
- `documentation/GUIDE__Docker_CI_Cache_Policy.md` — cache rotation and prune guidance.
|
||||
|
||||
Tasks (implementation checklist)
|
||||
- [ ] Review existing `Dockerfile`(s) under `aether_container_env/` and repository root.
|
||||
- [ ] Replace/extend Dockerfile with multi-stage BuildKit-friendly layout (use example as guide).
|
||||
- [ ] Ensure `.dockerignore` (already added) excludes large build artifacts.
|
||||
- [ ] Add CI step using `docker buildx build` with `--cache-from` and `--cache-to` pointed at a registry cache.
|
||||
- [ ] Add a scheduled job or registry lifecycle rule to delete old cache images (30 days default).
|
||||
- [ ] Document required CI secrets and permissions (registry write/read) for the operations team.
|
||||
- [ ] Run verification builds (dev local with BuildKit; CI runs with cache) and record timings.
|
||||
|
||||
Verification
|
||||
- Local dev: `DOCKER_BUILDKIT=1` build with `--cache-to`/`--cache-from` shows cache hits on second run and faster build time.
|
||||
- CI: subsequent CI runs log `cache hit` from `buildx` and total build time reduced vs baseline.
|
||||
- Confirm registry contains `cache` image tags and that rotation job/prune removes old entries.
|
||||
|
||||
Notes about Gitea/CI
|
||||
- Gitea does not include native Actions like GitHub; teams typically use Drone CI, Tekton, or a self-hosted runner that can execute the `docker`/`buildx` CLI.
|
||||
- The provided `ci_buildx_example.sh` is intentionally provider-agnostic — pasteable into Drone, Jenkins, GitLab CI, or any shell-capable runner.
|
||||
|
||||
Risks & Mitigations
|
||||
- Risk: Unbounded registry cache growth. Mitigation: enforce retention policy and rotation job; prefer a single `cache` tag reused by CI.
|
||||
- Risk: Developers unfamiliar with BuildKit. Mitigation: examples show simple `DOCKER_BUILDKIT=1` usage and local cache prune commands.
|
||||
|
||||
Next steps for the container team
|
||||
1. Review examples in `aether_container_env/` and adapt the Dockerfile to your runtime constraints (ssl certs, env injection, secrets).
|
||||
2. Add a CI job using the `ci_buildx_example.sh` snippet; configure registry credentials as secrets.
|
||||
3. Add a scheduled job to rotate/delete old cache images or configure registry lifecycle rules.
|
||||
4. Run a before/after benchmark of `time npm run build:prod` inside the build stage to quantify improvement.
|
||||
|
||||
Files included in this PR for reference:
|
||||
- `aether_container_env/Dockerfile.buildkit.example`
|
||||
- `aether_container_env/ci_buildx_example.sh`
|
||||
- `documentation/GUIDE__Docker_CI_Cache_Policy.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.
|
||||
@@ -1,11 +1,17 @@
|
||||
# PROJECT: AE Events Badges — Review Form & Print Font Controls
|
||||
# Archived Project: AE Events Badges — Review Form & Print Font Controls
|
||||
|
||||
**Created:** 2026-02-27
|
||||
**Last Updated:** 2026-03-12
|
||||
**Completed and Archived:** 2026-06-12
|
||||
**Last Verified Against Source:** 2026-06-12
|
||||
**Branch:** `ae_app_3x_llm`
|
||||
**Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026
|
||||
**Owner:** Scott Idem / One Sky IT
|
||||
**Status:** ✅ TASK 1 COMPLETE | ✅ TASK 2 COMPLETE | ✅ TASK 3 COMPLETE | ⏳ TASK 4 NEXT UP
|
||||
**Status:** Complete — review form, kiosk controls, auto-scaling, QR rendering, layouts, and print tracking are implemented.
|
||||
|
||||
The original project scope is complete and this document is retained as implementation history.
|
||||
Current behavior is documented in `documentation/MODULE__AE_Events_Badges.md` and
|
||||
`documentation/MODULE__AE_Events_Badge_Templates.md`. Remaining email-delivery and permission-config
|
||||
unification work is tracked in `documentation/TODO__Agents.md`. Planning statements later in this
|
||||
archived document describe the state at the time they were written and are not current instructions.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,7 +37,7 @@
|
||||
- Once satisfied, staff prints the badge.
|
||||
- The key differentiator vs the review form: **the live badge render** shows exactly how
|
||||
the badge will print. Attendees and staff can see changes immediately.
|
||||
- Component: `ae_comp__badge_obj_view.svelte` / `ae_comp__badge_obj_view_v2.svelte`
|
||||
- Component: `ae_comp__badge_obj_view.svelte`
|
||||
- Route: `/events/[event_id]/badges/[badge_id]/print/`
|
||||
|
||||
### Permission Model — Same Logic, Both Flows
|
||||
@@ -44,33 +50,25 @@ Both flows should respect the same permission model:
|
||||
- Permissions are configured per-event in `event.mod_badges_json.edit_permissions`.
|
||||
Hardcoded defaults are used until that config is implemented.
|
||||
|
||||
**Current gap (TASK 4):** The print page edit button is currently gated to trusted_access only.
|
||||
It needs to be accessible to attendees at the kiosk (with appropriate field-level gating),
|
||||
matching the permission model already implemented in `ae_comp__badge_review_form.svelte`.
|
||||
**Task 4 outcome:** The print controls now implement field-level editing. Authenticated users
|
||||
can edit template-approved fields, trusted staff can correct names, and trusted staff in global
|
||||
Edit Mode can edit all fields. First printing is available at public kiosk access; reprinting
|
||||
requires trusted access plus Edit Mode. Remote review uses event-level `edit_permissions`, while
|
||||
the print controls currently use template-level `controls_cfg`; unification is tracked separately.
|
||||
|
||||
---
|
||||
|
||||
## Next Up for Badges (TASK 4)
|
||||
## Task 4 Outcomes
|
||||
|
||||
### 0. Kiosk Editing — Print Page Permission Model Alignment
|
||||
**This is the most important gap before the first live event.**
|
||||
### 0. Kiosk Editing — Complete
|
||||
|
||||
Currently the print page edit button is staff-only (trusted_access gate). At the kiosk,
|
||||
attendees need to be able to edit their own fields (same attendee-level permissions as the
|
||||
review form), with staff-only fields gated appropriately.
|
||||
`ae_comp__badge_print_controls.svelte` provides the inline controls and live preview. Its default
|
||||
authenticated fields are title, affiliations, location, lead tracking, and pronouns; template
|
||||
`controls_cfg` can narrow the fields shown and editable. Email delivery remains a placeholder;
|
||||
when implemented it must send to `event_badge.email`, never `email_override`.
|
||||
|
||||
Work needed:
|
||||
- Wire the same `can_edit_fields` / `can_edit(field)` permission logic into the print page
|
||||
that `ae_comp__badge_review_form.svelte` already uses.
|
||||
- The edit panel on the print page should show attendee-editable fields to all authenticated
|
||||
users, and staff-only fields to trusted_access+.
|
||||
- The badge render (v1 or v2) should update live as the attendee edits fields.
|
||||
- Consider whether the print page needs its own inline edit panel (sidebar or overlay)
|
||||
or whether it should share/reuse the review form component alongside the badge render.
|
||||
- **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`.
|
||||
|
||||
### 1. Auto-Scaling Badge Text (v2) — In Progress
|
||||
`ae_comp__badge_obj_view_v2.svelte` using `element_fit_text.svelte` (binary search auto-scale).
|
||||
### 1. Auto-Scaling Badge Text — Complete
|
||||
`ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale).
|
||||
Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button.
|
||||
Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges.
|
||||
|
||||
@@ -105,22 +103,27 @@ badge data, gated by `allow_tracking` on the badge.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ⏳ TASK 4.0: Kiosk Editing — NOT STARTED (2026-03-12)
|
||||
Print page edit access needs to be opened to attendee-level permissions, not just trusted_access.
|
||||
The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte`
|
||||
should be the reference. See Design Intent section above.
|
||||
### ✅ TASK 4.0: Kiosk Editing — COMPLETE (verified 2026-06-12)
|
||||
The print controls implement authenticated field editing, trusted name correction, trusted + Edit
|
||||
Mode full editing, and live preview. The print path uses template `controls_cfg`; the review path
|
||||
uses event `mod_badges_json.edit_permissions`. Aligning those configuration sources is a follow-up,
|
||||
not a blocker to the completed kiosk controls.
|
||||
|
||||
**Note (2026-03-18):** `style_href` and `duplex` are both fully implemented and verified in code —
|
||||
the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badge render gates
|
||||
`show_badge_back` on it. `style_href` loads via `<svelte:head>` in `print/+page.svelte`.
|
||||
|
||||
### ✅ TASK 4.1: Auto-Scaling Badge Text v2 — COMPLETE (2026-03-12)
|
||||
**Files created/updated:**
|
||||
- `src/lib/elements/action_fit_text.ts` — Svelte action
|
||||
- `src/lib/elements/element_fit_text.svelte` — Component wrapper
|
||||
- `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render (canonical)
|
||||
- `src/routes/events/.../ae_comp__badge_obj_view.svelte` — V2 badge render (canonical)
|
||||
Debug blocks gated behind `$ae_loc.edit_mode` (hidden in production).
|
||||
- `print/+page.svelte` — Always uses v2 now. v1/v2 toggle removed. Header redesigned for kiosk UX.
|
||||
- `ae_comp__badge_print_controls.svelte` — Identity card at top, pronouns moved to attendee section,
|
||||
"Staff adjustments" divider before badge_type field.
|
||||
- `print_list/+page.svelte` — Updated to import v2.
|
||||
- `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/gemini_trash/**
|
||||
- `ae_comp__badge_obj_view.svelte` (v1) — **Moved to ~/tmp/agents_trash/**
|
||||
|
||||
**Kiosk UX improvements (2026-03-12):**
|
||||
- Print page header: cleaner, shows name + "Ready"/"Printed N×" status chip, event name.
|
||||
@@ -40,3 +40,6 @@
|
||||
|
||||
---
|
||||
*Prepared by: Gemini CLI (March 17, 2026)*
|
||||
|
||||
---
|
||||
*Archival note (2026-03-20): `element_modal_v1.svelte` (referenced in §2 as "new standard modal") was subsequently retired — it had zero active importers. Modal usage in the codebase relies on Flowbite `<Modal>` component. See `AE__UI_Component_Patterns.md` §11.*
|
||||
@@ -1,8 +1,10 @@
|
||||
# Project Plan: Aether AE Obj Field Editor v3 (Consolidated)
|
||||
|
||||
> **Status:** 🔵 Active / Testing & Stabilization
|
||||
> **Date:** February 13, 2026
|
||||
> **Target Component:** `src/lib/elements/element_ae_obj_field_editor_v3.svelte`
|
||||
> **Status:** ✅ **Completed and archived 2026-06-12**
|
||||
> **Completion:** V3 component deployed and documented. Legacy V1/V2 removed. Searchable dropdowns deferred as optional enhancement.
|
||||
> **Created:** 2026-02-13
|
||||
> **Last Updated:** 2026-06-12
|
||||
> **Target Component:** `src/lib/elements/element_ae_obj_field_editor.svelte`
|
||||
> **Replaces:** `element_ae_crud.svelte` and `element_ae_crud_v2.svelte`
|
||||
|
||||
## 1. Overview
|
||||
@@ -30,15 +32,17 @@ Consolidate the legacy CRUD components into a single, high-performance "Aether O
|
||||
- [x] Add a "Save" loading state with Lucide's `LoaderCircle` spinner.
|
||||
- [x] Implement a clear "Cancel" path that restores the original value.
|
||||
|
||||
### Phase 3: Field Type Parity (IN PROGRESS)
|
||||
### Phase 3: Field Type Parity (COMPLETE)
|
||||
- [x] Support `text`, `textarea`, `select`, `tiptap`, and `checkbox`.
|
||||
- [ ] Add `datetime` support using native browser pickers.
|
||||
- [ ] Implement searchable dropdowns for the `select` type.
|
||||
- [x] Add `datetime` support using native browser pickers — `date` and `datetime-local` inputs implemented.
|
||||
- [x] Add `number` field type support.
|
||||
- [ ] *(Optional)* Implement searchable dropdowns for the `select` type — deferred as UX enhancement; basic select works for all current use cases.
|
||||
|
||||
### Phase 4: Migration & Cleanup
|
||||
- [x] Create a playground route for V3 verification (`/testing/ae_obj_field_editor_v3`).
|
||||
- [ ] Deprecate and eventually remove `v1` and `v2` files.
|
||||
- [ ] Update `GUIDE__Development.md` with the new usage patterns.
|
||||
### Phase 4: Migration & Cleanup (COMPLETE)
|
||||
- [x] Create a playground route for V3 verification (`/testing/ae_obj_field_editor`).
|
||||
- [x] Deprecate and remove `v1` and `v2` files — `element_ae_crud.svelte` and `element_ae_crud_v2.svelte` removed 2026-03-20.
|
||||
- [x] Update `GUIDE__Development.md` with the new usage patterns — documented at lines 57-109.
|
||||
- [x] Production deployment — 50+ active usages across session views, person views, locations, presentations, and leads.
|
||||
|
||||
## ⚠️ Security & Reliability Stabilization (NEW)
|
||||
- [x] **Account Context:** Fixed 403 errors by unifying API helpers to the `/v3/crud/` standard.
|
||||
@@ -1,20 +1,26 @@
|
||||
# Aether Journals UI Update (2026)
|
||||
# Archived Project: Aether Journals UI Update (2026)
|
||||
|
||||
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Style pass complete)
|
||||
> **Last Updated:** 2026-03-06
|
||||
> **Status:** Completed and archived 2026-06-12
|
||||
> **Last Verified Against Source:** 2026-06-12
|
||||
> **Primary Agent:** Frontend SvelteKit Agent
|
||||
|
||||
The UI modernization scope is complete: V3 CRUD, Quick Add, Append/Prepend,
|
||||
import/export, auto-save, configuration modals, decryption isolation, and the
|
||||
Journals style pass are implemented. Unfinished security and product follow-ups
|
||||
were transferred to `documentation/TODO__Agents.md`; current operational behavior
|
||||
and limitations live in `documentation/MODULE__AE_Journals.md`.
|
||||
|
||||
## 1. Project Overview
|
||||
This document outlines the modernization of the Journals module UI in the SvelteKit frontend (`aether_app_sveltekit`). The primary goals are to fully leverage the generic V3 API architecture and introduce high-velocity productivity features for journal management.
|
||||
|
||||
**Context:** The backend transition to the generic `api_crud_v3` router is complete. Custom legacy routers have been removed. The frontend must now fully align with this pattern and provide a frictionless user experience.
|
||||
**Context:** The backend transition to the generic `api_crud` router is complete. Custom legacy routers have been removed. The frontend must now fully align with this pattern and provide a frictionless user experience.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Objectives
|
||||
|
||||
### 🎯 Primary Goals
|
||||
1. **V3 API Verification:** Ensure all CRUD operations utilize the generic `api_crud_v3` endpoints (Verified).
|
||||
1. **V3 API Verification:** Ensure all CRUD operations utilize the generic `api_crud` endpoints (Verified).
|
||||
2. **Quick Add UI:** Implement a specialized interface for rapid, friction-free entry creation.
|
||||
3. **Append/Prepend UI:** Allow users to quickly add text to the beginning or end of existing entries without full edit mode.
|
||||
4. **Interop & Portability:** Robust import/export logic for Markdown/HTML (Nextcloud Notes compatibility).
|
||||
@@ -25,14 +31,14 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
||||
## 3. Technical Architecture
|
||||
|
||||
### Backend (Completed)
|
||||
* **Router:** `api_crud_v3` (Generic)
|
||||
* **Router:** `api_crud` (Generic)
|
||||
* **Definitions:** `app/ae_obj_types_def.py` -> `app/object_definitions/journals.py`
|
||||
* **Endpoints:** `/v3/crud/journal/...` and `/v3/crud/journal_entry/...`
|
||||
|
||||
### Frontend (In Progress)
|
||||
### Frontend (Completed UI modernization scope)
|
||||
* **State Management:** `src/lib/ae_journals/ae_journals_stores.ts`
|
||||
* **Local Storage:** Dexie.js (`db_journals`)
|
||||
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj_v3`
|
||||
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj`
|
||||
* **Export Engine:** Centralized templates in `src/lib/ae_journals/ae_journals_export_templates.ts`.
|
||||
|
||||
---
|
||||
@@ -68,18 +74,22 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
||||
- [x] Implement Bulk Export/Import system.
|
||||
- [x] Establish centralized Export Template engine.
|
||||
|
||||
### Phase 4: Polish & Security (ACTIVE)
|
||||
### Phase 4: Polish & Security (UI scope complete; security follow-ups transferred)
|
||||
- [x] Implement Auto-Save toggle and visual status indicators.
|
||||
- [x] Extract decryption workflow to non-reactive helper.
|
||||
- [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
|
||||
- [x] **Journal Entry Config cleanup:** Summary now lives in Metadata; Alert lives in its own Alerts & Messaging section; Privacy Flags is visibility-only; Admin controls are split out and gated to trusted-access and above.
|
||||
- [x] **Shared Flags widget:** `AE_Object_Flags` now shows visible button text and hover titles instead of icon-only controls.
|
||||
- [x] **Modal sizing:** Entry config modal now expands to viewport height instead of stopping at a fixed 60vh body cap.
|
||||
- [x] **Delete/Remove behavior:** Entry config Admin section now uses the real delete helper. Managers/admins see Delete (hard delete); trusted access sees Remove (disable semantics).
|
||||
- [x] **RESOLVED:** Decryption workflow stability (Fixed via dependency isolation).
|
||||
- [x] **Style Standardization (2026-03-06):** Full Skeleton v4 `preset-*` class pass across all 17 journal components. See style token table in Lessons Learned below.
|
||||
- [x] **Dark mode fixes:** Entry content hover, journal view section/description background and text colors.
|
||||
- [x] **Modal close button:** All 3 config modals use `dismissable={false}` + explicit `<X>` button in header snippet for correct right-aligned placement.
|
||||
- [x] **Global select padding:** Added `padding-inline: 0.5rem` to `@layer base` in `app.css` (safe — utility `px-*` classes override it where intentional).
|
||||
- [ ] Solidify E2EE passcode system for Journals and Entries.
|
||||
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
||||
- [ ] Integrate Outbound Email sharing.
|
||||
- [ ] Solidify E2EE passcode system for Journals and Entries. See active task list.
|
||||
- [ ] Audit encryption flow for Quick Added and Imported entries. See active task list.
|
||||
- [ ] Integrate Outbound Email sharing. Deferred pending product confirmation.
|
||||
|
||||
---
|
||||
|
||||
@@ -100,6 +110,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.
|
||||
* **Tabs:** Center-aligned `btn btn-sm` with `preset-filled-primary` (active) / `preset-tonal-surface` (inactive).
|
||||
* **Icons:** Every tab and primary action should have a Lucide icon for better scannability.
|
||||
* **Button titles:** Any button that uses icon+text or icon-only must include a descriptive `title` for hover clarity.
|
||||
* **Explicit Persistence:** Follow "Edit working copy → Save Changes" pattern to prevent accidental store/API churn.
|
||||
|
||||
#### Skeleton v4 Style Token Reference (Journals = canonical example)
|
||||
@@ -111,6 +122,7 @@ We have established a unified design language for configuration interfaces and a
|
||||
| Success (confirmed save) | `btn preset-filled-success` |
|
||||
| Warning (caution action) | `btn preset-tonal-warning hover:preset-filled-warning-500` |
|
||||
| Error / danger (delete, force reset) | `btn preset-tonal-error hover:preset-filled-error-500` |
|
||||
| Warning action (remove/disable) | `btn preset-tonal-warning hover:preset-filled-warning-500` |
|
||||
| Active tab | `preset-filled-primary` |
|
||||
| Inactive tab | `preset-tonal-surface` |
|
||||
| Icon button | `btn-icon btn-icon-sm preset-tonal-surface` |
|
||||
@@ -143,6 +155,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 Fix:** Implement a manual `deep_copy` helper or selective property assignment when syncing "Original" vs "Temporary" state. This ensures `orig_entry_obj` is a plain JS object, making the `has_unsaved_changes` check stable.
|
||||
|
||||
### 5. Journal Entry Config Layout Notes
|
||||
The Entry Config modal now follows a stricter section grammar:
|
||||
* `Metadata` contains category, tags, summary, archive date, and template.
|
||||
* `Status & Security` contains enabled/hidden/priority/sort.
|
||||
* `Visibility & Audience` contains only visibility/audience toggles.
|
||||
* `Alerts & Messaging` contains alert flag + alert message.
|
||||
* `Admin` is gated to trusted access and above, and is the only place for notes plus delete/remove actions.
|
||||
|
||||
### 3. Concurrency Locking (`is_processing`)
|
||||
* **The Problem:** Decryption (Async) and Auto-Save (Debounced Async) can fire nearly simultaneously.
|
||||
* **The Fix:** Use a simple `is_processing` boolean flag. If any async workflow is active, block others from starting and prevent the `has_unsaved_changes` derived rune from reporting `true`.
|
||||
@@ -1,7 +1,8 @@
|
||||
# Project: CRUD V3 Final Migration
|
||||
|
||||
> **Status:** Active / In Progress
|
||||
> **Last Updated:** 2026-01-20
|
||||
> **Status:** ✅ **Completed and archived 2026-06-12**
|
||||
> **Completion:** All production code migrated to V3. Legacy wrappers remain defined but unused.
|
||||
> **Last Updated:** 2026-06-12
|
||||
> **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 +22,23 @@ While the **Journals** and **Identity (User/Account)** modules have been success
|
||||
The following files have been identified as using legacy CRUD wrappers.
|
||||
|
||||
### 🔴 High Priority: Events Module
|
||||
The entire `ae_events` library is heavily dependent on legacy `v2` list and `v1` CRUD wrappers.
|
||||
|
||||
- [ ] `src/lib/ae_events/ae_events__event_session.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_presenter.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_presentation.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_location.ts`
|
||||
- [ ] `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__event_session.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_presenter.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_presentation.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_location.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_badge_template.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_device.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28)
|
||||
- [ ] `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
|
||||
Legacy patterns persisting in core logic and config modules.
|
||||
|
||||
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
||||
- [ ] `src/lib/ae_core/core__hosted_files.ts` (Uses `get_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__site.ts` (Uses `get_ae_obj_id_crud`)
|
||||
- [x] `src/lib/ae_core/core__hosted_files.ts` (Migrated 2026-01-20)
|
||||
- [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26; bootstrap path uses V3 `search_ae_obj` by `fqdn`)
|
||||
- [x] `src/lib/ae_core/core__site_domain.ts` (Retired 2026-06-02; helper removed after bootstrap migration to `core__site.ts`)
|
||||
- [ ] `src/lib/ae_core/ae_core_functions.ts` (STILL USES `get_ae_obj_id_crud` / `update_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__country_subdivisions.ts`
|
||||
- [ ] `src/lib/ae_core/core__time_zones.ts`
|
||||
- [ ] `src/lib/ae_core/core__countries.ts`
|
||||
@@ -48,9 +49,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`)
|
||||
- [x] `src/lib/elements/element_data_store_v2.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/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`)
|
||||
|
||||
---
|
||||
|
||||
@@ -60,26 +62,26 @@ For each file listed above, follow this standard refactoring pattern:
|
||||
|
||||
1. **Imports:**
|
||||
* Remove imports of `create_ae_obj_crud`, `update_ae_obj_id_crud`, etc.
|
||||
* Import V3 helpers: `get_ae_obj_v3`, `create_ae_obj_v3`, `update_ae_obj_v3`, `delete_ae_obj_v3`, `search_ae_obj_v3`.
|
||||
* Import V3 helpers: `get_ae_obj`, `create_ae_obj`, `update_ae_obj`, `delete_ae_obj`, `search_ae_obj`.
|
||||
|
||||
2. **Pattern Replacement:**
|
||||
|
||||
* **Get (Single):**
|
||||
* *Old:* `get_ae_obj_id_crud({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
* *New:* `get_ae_obj_v3({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
* *New:* `get_ae_obj({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
|
||||
* **Get (List):**
|
||||
* *Old:* `get_ae_obj_li_for_obj_id_crud_v2(...)`
|
||||
* *New:* `get_ae_obj_li_v3(...)` or `search_ae_obj_v3(...)` if complex filtering is needed.
|
||||
* *New:* `get_ae_obj_li(...)` or `search_ae_obj(...)` if complex filtering is needed.
|
||||
|
||||
* **Update:**
|
||||
* *Old:* `update_ae_obj_id_crud({ ..., fields: { name: 'New Name' } })`
|
||||
* *New:* `update_ae_obj_v3({ ..., data: { name: 'New Name' } })`
|
||||
* *New:* `update_ae_obj({ ..., data: { name: 'New Name' } })`
|
||||
* *Note:* Ensure payload whitelisting is applied! V3 will 400 Error on unknown columns.
|
||||
|
||||
* **Create:**
|
||||
* *Old:* `create_ae_obj_crud({ ..., fields: { ... } })`
|
||||
* *New:* `create_ae_obj_v3({ ..., data: { ... } })`
|
||||
* *New:* `create_ae_obj({ ..., data: { ... } })`
|
||||
|
||||
3. **Verification:**
|
||||
* Verify the module still loads data (check Network tab for `/v3/` requests).
|
||||
@@ -106,7 +108,7 @@ V3 returns detailed error metadata in the `meta.details` object.
|
||||
**Symptom:** Providing a string ID in a search body that the backend maps to an integer can result in **Zero Results** if the underlying view expects a string.
|
||||
|
||||
**Final Solution (Body + Header Injection):**
|
||||
1. **Body:** Inject the raw field name (e.g. `account_id_random`) into the `search_query.and` array to bypass automatic backend mapping.
|
||||
1. **Body:** Inject the raw field name (e.g. `account_id`) into the `search_query.and` array to bypass automatic backend mapping.
|
||||
2. **Headers:** Pass `headers: { 'x-account-id': ... }` manually to provide context for Auth validation.
|
||||
3. **Isolation (IDAA):** Due to specific bugs in the IDAA module, it has been temporarily isolated to a legacy V2 search function (`qry_ae_obj_li__event_v2`) using `default_qry_str` for text searching, while the main module continues to use the V3 implementation.
|
||||
|
||||
@@ -114,7 +116,7 @@ V3 returns detailed error metadata in the `meta.details` object.
|
||||
|
||||
## 6. Final Cleanup
|
||||
Once all checkboxes above are completed:
|
||||
1. [ ] Remove legacy exports from `src/lib/api/api.ts`.
|
||||
2. [ ] Delete `src/lib/ae_api/api_get__crud_obj_li_v1.ts`.
|
||||
3. [ ] Delete `src/lib/ae_api/api_get__crud_obj_li_v2.ts`.
|
||||
4. [ ] Delete `src/lib/ae_api/api_get__crud_obj_id.ts` (Legacy version).
|
||||
1. [x] Remove legacy exports from `src/lib/api/api.ts`.
|
||||
2. [x] Delete `src/lib/ae_api/api_get__crud_obj_li_v1.ts`.
|
||||
3. [x] Delete `src/lib/ae_api/api_get__crud_obj_li_v2.ts`.
|
||||
4. [x] Delete `src/lib/ae_api/api_get__crud_obj_id.ts` (Legacy version).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
34
documentation/archive/README.md
Normal file
34
documentation/archive/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Documentation Archive Index
|
||||
|
||||
This directory preserves completed projects, superseded proposals, historical task logs, and legacy technical references.
|
||||
|
||||
Archived files are not authoritative for current implementation. Start with `documentation/README__Docs_Index.md` and the active module/guides directory.
|
||||
|
||||
## Categories
|
||||
|
||||
### Completed or Superseded Projects
|
||||
|
||||
Files prefixed with `PROJECT__` document completed implementation phases or superseded project plans.
|
||||
|
||||
### Historical Proposals
|
||||
|
||||
Files prefixed with `PROPOSAL__` preserve design ideas that are not the current implementation source.
|
||||
|
||||
### Task History
|
||||
|
||||
Files prefixed with `TODO__Agents__ARCHIVE_` contain completed task history by month.
|
||||
|
||||
### Legacy References
|
||||
|
||||
Files prefixed with `REFERENCE__Legacy_` are retained for historical context but contain pre-V3, pre-runes, or otherwise superseded guidance.
|
||||
|
||||
Do not copy implementation patterns from legacy references without validating them against current source and active guides.
|
||||
|
||||
## Restore Policy
|
||||
|
||||
Move an archived doc back to the active documentation root only when:
|
||||
|
||||
1. Its subject is active again.
|
||||
2. Its content has been reviewed against current source.
|
||||
3. Legacy paths, IDs, stores, and API conventions have been updated.
|
||||
4. It is added to `documentation/README__Docs_Index.md`.
|
||||
@@ -575,7 +575,6 @@ This document provides a reference for the data structures of the core Aether AP
|
||||
- `orders_info`: `Optional[dict]`
|
||||
- `order_list`: `Optional[list]`
|
||||
- `order_cart`: `Optional[dict]`
|
||||
- `order_cart_v3`: `Optional[dict]`
|
||||
- `organization`: `Optional[Union[Organization_Base, None]]`
|
||||
- `post_list`: `Optional[list]`
|
||||
- `user`: `Optional[Union[User_Base, None]]`
|
||||
@@ -1212,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`
|
||||
|
||||
**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`: `Optional[int]`
|
||||
- `journal_id_random`: `Optional[str]`
|
||||
@@ -40,9 +40,9 @@ These are reusable components that provide common functionalities across differe
|
||||
- **`copy_btn`**: A button to copy content to the clipboard.
|
||||
- Properties: `clipboard`, `bind:value`, `btn_text`, `btn_html`.
|
||||
- **`txt_editor`**: A basic text area editor.
|
||||
- **`md_editor`**: Markdown editor.
|
||||
- Uses CodeMirror (planned for rich text editing).
|
||||
- **Note:** ShadEditor TipTap is present but marked for removal. ShadCN components are also being phased out in favor of CodeMirror for text editing.
|
||||
- **`md_editor`**: Markdown/rich-text editing handled by two active components:
|
||||
- `element_editor_codemirror.svelte` — CodeMirror 6, used for source/code editing
|
||||
- `element_editor_tiptap.svelte` — TipTap (WYSIWYG), used for rich-text content fields
|
||||
- **`html_editor`**: HTML editor.
|
||||
- **`media_player`**: Component for playing media files.
|
||||
- Properties: `hosted_file`, `archive_content`, `media_player`.
|
||||
@@ -62,13 +62,11 @@ These are reusable components that provide common functionalities across differe
|
||||
- Bindings: `bind:trigger`, `bind:show_spinner`, `bind:show_percent`.
|
||||
- Status: `started`, `downloading`, `finished`.
|
||||
- **`data_store`**: Component for interacting with data stores.
|
||||
- **`ae_crud`**: Generic CRUD (Create, Read, Update, Delete) component.
|
||||
- **Note:** Needs simplification.
|
||||
- Properties: `obj`, `prop`, `current_value`.
|
||||
- Bindings: `bind:value`, `bind:trigger`, `inner fragment`.
|
||||
- **`ae_obj_prop_val`**: A wrapper for a function to manage object property values.
|
||||
- Bindings: `bind:obj_type`, `bind:obj_id`, `bind:obj_prop`, `bind:obj_value`, `bind:obj_new_value`, `bind:trigger`, `bind:show_spinner`, `bind:show_percent`.
|
||||
- Status: `status`, `result`.
|
||||
- **`element_ae_obj_field_editor`**: Standard single-field inline editor. Replaces retired `ae_crud` v1/v2 components.
|
||||
- Props: `object_type`, `object_id`, `field_name`, `field_type`, `current_value`
|
||||
- Field types: `text`, `textarea`, `select`, `tiptap`, `checkbox`, `date`, `datetime`, `number`
|
||||
- Callbacks: `on_success`, `on_error`
|
||||
- Respects `$ae_loc.edit_mode` — edit trigger hidden when edit mode is off.
|
||||
- **`sql_qry`**: Component for executing SQL queries.
|
||||
- **`obj_tbl`**: Object SQL results table or similar.
|
||||
- **`qr_scanner`**: Component for scanning QR codes.
|
||||
@@ -91,13 +89,13 @@ A standardized menu for interacting with objects.
|
||||
- **Actions:** `create`, `view`, `edit`, `update`, `hide`, `disable`, `delete`, `alert` (message), `archive` (not yet ready).
|
||||
- **Future Actions:** `copy`, `import`.
|
||||
- **Sort Options:**
|
||||
- `[default]`: `group > priority > sort (ASC/DESC) > alert > name`
|
||||
- `[sort_updated]`: `group > priority > sort (ASC/DESC) > alert > updated_on > created_on`
|
||||
- `[priority_updated]`: `group > priority > updated_on (ASC/DESC) > created_on`
|
||||
- `[priority_name]`: `group > priority > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||
- `[name]`: `priority > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||
- `[created_on]`: `priority > created_on (ASC/DESC)`
|
||||
- `[updated_on]`: `priority > updated_on (ASC/DESC) > created_on`
|
||||
- `[default]`: `group > priority (flag) > sort (ASC/DESC) > alert > name`
|
||||
- `[sort_updated]`: `group > priority (flag) > sort (ASC/DESC) > alert > updated_on > created_on`
|
||||
- `[priority_updated]`: `group > priority (flag) > updated_on (ASC/DESC) > created_on`
|
||||
- `[priority_name]`: `group > priority (flag) > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||
- `[name]`: `priority (flag) > name (ASC/DESC) > sort > alert > updated_on > created_on`
|
||||
- `[created_on]`: `priority (flag) > created_on (ASC/DESC)`
|
||||
- `[updated_on]`: `priority (flag) > updated_on (ASC/DESC) > created_on`
|
||||
|
||||
## 2. Pop-ups
|
||||
|
||||
@@ -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.
|
||||
|
||||
- `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).
|
||||
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
|
||||
- `code`: Short, unique identifier.
|
||||
- `name`: Display name.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
- `hide`: Boolean for visibility.
|
||||
- `priority`: Numeric value for ordering.
|
||||
- `sort`: Numeric value for ordering.
|
||||
- `priority`: Boolean/tinyint(1) ordering flag used by the object model.
|
||||
- `sort`: Numeric value for ordering within a priority group.
|
||||
- `group`: Categorization string.
|
||||
- `notes`: General notes/comments.
|
||||
- `created_on`: Timestamp of creation.
|
||||
- `updated_on`: Timestamp of last update.
|
||||
|
||||
### 1.2. 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.
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
|
||||
@@ -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_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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
- **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`
|
||||
|
||||
## 3. Data Storage Mechanisms
|
||||
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/archive/PROJECT__AE_Style_Review_2026-03.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)
|
||||
@@ -32,7 +32,9 @@ export default tseslint.config(
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'warn'
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
// No base path configured — this rule is not applicable to this project
|
||||
'svelte/no-navigation-without-resolve': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
503
package-lock.json
generated
503
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.12.08",
|
||||
"version": "3.00.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.12.08",
|
||||
"version": "3.00.20",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
@@ -26,16 +26,15 @@
|
||||
"@lucide/svelte": "^0.*.0",
|
||||
"@popperjs/core": "^2.11.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dexie": "^4.0.0",
|
||||
"flowbite-svelte": "^1.28.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"lucide-svelte": "^0.*.0",
|
||||
"marked": "^17.0.0",
|
||||
"openai": "^6.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"shadcn-svelte": "^1.0.11",
|
||||
"runed": "^0.37.1",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"typescript-eslint": "^8.47.0"
|
||||
},
|
||||
@@ -56,7 +55,6 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||
"@typescript-eslint/parser": "^8.47.0",
|
||||
"bits-ui": "^2.14.3",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
@@ -1785,7 +1783,7 @@
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
@@ -2329,7 +2327,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
@@ -2381,7 +2379,7 @@
|
||||
"version": "2.53.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz",
|
||||
"integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -2423,7 +2421,7 @@
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
@@ -2444,7 +2442,7 @@
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
||||
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"obug": "^2.1.0"
|
||||
@@ -2792,7 +2790,7 @@
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
@@ -3929,23 +3927,6 @@
|
||||
"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": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -3964,31 +3945,6 @@
|
||||
"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": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
@@ -4001,19 +3957,6 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4126,27 +4069,6 @@
|
||||
"devOptional": true,
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
@@ -4164,7 +4086,7 @@
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -4274,20 +4196,10 @@
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -4334,20 +4246,6 @@
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -4367,24 +4265,6 @@
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
@@ -4392,33 +4272,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
@@ -4970,42 +4823,6 @@
|
||||
"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": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@@ -5038,43 +4855,6 @@
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -5100,18 +4880,6 @@
|
||||
"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": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -5127,33 +4895,6 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -5349,7 +5090,7 @@
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -5689,20 +5430,10 @@
|
||||
"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": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
@@ -5729,36 +5460,6 @@
|
||||
"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": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
@@ -5864,7 +5565,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -5907,17 +5608,11 @@
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
@@ -6161,6 +5856,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-load-config/node_modules/yaml": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-safe-parser": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
|
||||
@@ -6242,7 +5947,6 @@
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
@@ -6258,13 +5962,91 @@
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
|
||||
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0",
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz",
|
||||
"integrity": "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-hermes": "*",
|
||||
"@prettier/plugin-oxc": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"@zackad/prettier-plugin-twig": "*",
|
||||
"prettier": "^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-multiline-arrays": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
"prettier-plugin-svelte": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-hermes": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-oxc": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@zackad/prettier-plugin-twig": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-multiline-arrays": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
|
||||
@@ -6272,12 +6054,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -6407,10 +6183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/runed": {
|
||||
"version": "0.35.1",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz",
|
||||
"integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==",
|
||||
"dev": true,
|
||||
"version": "0.37.1",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.37.1.tgz",
|
||||
"integrity": "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
"https://github.com/sponsors/tglide"
|
||||
@@ -6423,11 +6198,15 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.21.0",
|
||||
"svelte": "^5.7.0"
|
||||
"svelte": "^5.7.0",
|
||||
"zod": "^4.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@sveltejs/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6858,23 +6637,9 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -6907,7 +6672,7 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
@@ -7184,27 +6949,6 @@
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
@@ -7252,13 +6996,6 @@
|
||||
"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": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||
@@ -7364,7 +7101,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -7570,7 +7307,7 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz",
|
||||
"integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"tests/deps/*",
|
||||
@@ -7737,16 +7474,6 @@
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
|
||||
20
package.json
20
package.json
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.00.04",
|
||||
"version": "3.00.30",
|
||||
"description": "One Sky IT's Aether App created with Svelte, SvelteKit, Tailwind CSS, Lucide, Font Awesome, and Skeleton UI. -Scott Idem",
|
||||
"homepage": "https://oneskyit.com/",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:prod": "cp .env.prod .env.production && vite build",
|
||||
"build:staging": "cp .env.staging .env.production && vite build",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:prod": "vite build --mode prod",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -17,9 +18,10 @@
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"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",
|
||||
"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"
|
||||
"build:docker:dev": "docker compose -f ../aether_container_env/docker-compose.yml build ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
|
||||
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down",
|
||||
"deploy:remote:test": "ssh linode.oneskyit.com 'bash /srv/env/test_aether/deploy.sh test'",
|
||||
"deploy:remote:prod": "ssh linode.oneskyit.com 'bash /srv/env/prod_aether/deploy.sh prod'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -38,7 +40,6 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||
"@typescript-eslint/parser": "^8.47.0",
|
||||
"bits-ui": "^2.14.3",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
@@ -104,16 +105,15 @@
|
||||
"@lucide/svelte": "^0.*.0",
|
||||
"@popperjs/core": "^2.11.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dexie": "^4.0.0",
|
||||
"flowbite-svelte": "^1.28.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"lucide-svelte": "^0.*.0",
|
||||
"marked": "^17.0.0",
|
||||
"openai": "^6.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"shadcn-svelte": "^1.0.11",
|
||||
"runed": "^0.37.1",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"typescript-eslint": "^8.47.0"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ const config: PlaywrightTestConfig = {
|
||||
use: {
|
||||
baseURL: 'http://demo.localhost:5173',
|
||||
// baseURL: 'https://dev-demo.oneskyit.com',
|
||||
trace: 'on-first-retry'
|
||||
trace: 'on-first-retry',
|
||||
// Arch Linux: Playwright's downloaded Chromium requires Ubuntu system libs (libicu74 etc.)
|
||||
// that don't exist on Arch. Use the system Chromium package instead.
|
||||
launchOptions: {
|
||||
executablePath: '/usr/bin/chromium',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ FA_TO_LUCIDE = {
|
||||
'fa-times': 'X',
|
||||
'fa-exclamation-triangle': 'TriangleAlert',
|
||||
'fa-check': 'Check',
|
||||
'fa-check-circle': 'CheckCircle',
|
||||
'fa-check-circle': 'CircleCheck',
|
||||
'fa-plus': 'Plus',
|
||||
'fa-minus': 'Minus',
|
||||
'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);
|
||||
}
|
||||
@@ -63,50 +63,50 @@ html[data-theme='AE_Firefly_Indigo'] {
|
||||
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
html[data-theme='AE_Firefly_Indigo'] {
|
||||
--color-primary-50: oklch(95.5% 0.040 270deg);
|
||||
--color-primary-50: oklch(95.5% 0.04 270deg);
|
||||
--color-primary-100: oklch(89.5% 0.072 270deg);
|
||||
--color-primary-200: oklch(82.5% 0.108 269deg);
|
||||
--color-primary-300: oklch(74.5% 0.135 268deg);
|
||||
--color-primary-400: oklch(65.0% 0.155 267deg);
|
||||
--color-primary-500: oklch(50.5% 0.160 266deg);
|
||||
--color-primary-400: oklch(65% 0.155 267deg);
|
||||
--color-primary-500: oklch(50.5% 0.16 266deg);
|
||||
--color-primary-600: oklch(43.5% 0.152 265deg);
|
||||
--color-primary-700: oklch(37.0% 0.138 264deg);
|
||||
--color-primary-800: oklch(30.0% 0.120 263deg);
|
||||
--color-primary-900: oklch(23.0% 0.100 262deg);
|
||||
--color-primary-950: oklch(15.5% 0.080 261deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-700: oklch(37% 0.138 264deg);
|
||||
--color-primary-800: oklch(30% 0.12 263deg);
|
||||
--color-primary-900: oklch(23% 0.1 262deg);
|
||||
--color-primary-950: oklch(15.5% 0.08 261deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(96.5% 0.032 297deg);
|
||||
--color-secondary-50: oklch(96.5% 0.032 297deg);
|
||||
--color-secondary-100: oklch(91.5% 0.058 295deg);
|
||||
--color-secondary-200: oklch(85.5% 0.090 293deg);
|
||||
--color-secondary-200: oklch(85.5% 0.09 293deg);
|
||||
--color-secondary-300: oklch(78.5% 0.115 292deg);
|
||||
--color-secondary-400: oklch(70.0% 0.132 291deg);
|
||||
--color-secondary-500: oklch(60.0% 0.140 290deg);
|
||||
--color-secondary-400: oklch(70% 0.132 291deg);
|
||||
--color-secondary-500: oklch(60% 0.14 290deg);
|
||||
--color-secondary-600: oklch(52.5% 0.135 289deg);
|
||||
--color-secondary-700: oklch(45.0% 0.126 288deg);
|
||||
--color-secondary-700: oklch(45% 0.126 288deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 286deg);
|
||||
--color-secondary-900: oklch(30.0% 0.094 284deg);
|
||||
--color-secondary-950: oklch(22.0% 0.076 282deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-900: oklch(30% 0.094 284deg);
|
||||
--color-secondary-950: oklch(22% 0.076 282deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(96.5% 0.022 348deg);
|
||||
--color-tertiary-100: oklch(91.0% 0.042 346deg);
|
||||
--color-tertiary-50: oklch(96.5% 0.022 348deg);
|
||||
--color-tertiary-100: oklch(91% 0.042 346deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.068 344deg);
|
||||
--color-tertiary-300: oklch(76.5% 0.095 343deg);
|
||||
--color-tertiary-400: oklch(68.0% 0.118 342deg);
|
||||
--color-tertiary-400: oklch(68% 0.118 342deg);
|
||||
--color-tertiary-500: oklch(57.5% 0.128 341deg);
|
||||
--color-tertiary-600: oklch(50.0% 0.122 340deg);
|
||||
--color-tertiary-700: oklch(43.0% 0.112 339deg);
|
||||
--color-tertiary-600: oklch(50% 0.122 340deg);
|
||||
--color-tertiary-700: oklch(43% 0.112 339deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.098 338deg);
|
||||
--color-tertiary-900: oklch(28.0% 0.080 337deg);
|
||||
--color-tertiary-900: oklch(28% 0.08 337deg);
|
||||
--color-tertiary-950: oklch(20.5% 0.062 336deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -114,51 +114,51 @@ html[data-theme='AE_Firefly_Indigo'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99.0% 0.003 270deg);
|
||||
--color-surface-50: oklch(99% 0.003 270deg);
|
||||
--color-surface-100: oklch(96.5% 0.006 268deg);
|
||||
--color-surface-200: oklch(92.5% 0.010 266deg);
|
||||
--color-surface-300: oklch(87.0% 0.014 265deg);
|
||||
--color-surface-200: oklch(92.5% 0.01 266deg);
|
||||
--color-surface-300: oklch(87% 0.014 265deg);
|
||||
--color-surface-400: oklch(78.5% 0.018 265deg);
|
||||
--color-surface-500: oklch(66.5% 0.020 267deg);
|
||||
--color-surface-500: oklch(66.5% 0.02 267deg);
|
||||
--color-surface-600: oklch(54.5% 0.022 269deg);
|
||||
--color-surface-700: oklch(42.5% 0.024 270deg);
|
||||
--color-surface-800: oklch(31.0% 0.026 272deg);
|
||||
--color-surface-900: oklch(20.5% 0.030 274deg);
|
||||
--color-surface-950: oklch(13.0% 0.034 276deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-800: oklch(31% 0.026 272deg);
|
||||
--color-surface-900: oklch(20.5% 0.03 274deg);
|
||||
--color-surface-950: oklch(13% 0.034 276deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
@@ -182,20 +182,20 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* maintaining sufficient contrast at mid-range shades.
|
||||
* At 500 (L≈50%): sufficient contrast with primary-50 text (≥4:1).
|
||||
* =================================================================== */
|
||||
--color-primary-50: oklch(95.5% 0.040 270deg);
|
||||
--color-primary-50: oklch(95.5% 0.04 270deg);
|
||||
--color-primary-100: oklch(89.5% 0.072 270deg);
|
||||
--color-primary-200: oklch(82.5% 0.108 269deg);
|
||||
--color-primary-300: oklch(74.5% 0.135 268deg);
|
||||
--color-primary-400: oklch(65.0% 0.155 267deg);
|
||||
--color-primary-500: oklch(50.5% 0.160 266deg);
|
||||
--color-primary-400: oklch(65% 0.155 267deg);
|
||||
--color-primary-500: oklch(50.5% 0.16 266deg);
|
||||
--color-primary-600: oklch(43.5% 0.152 265deg);
|
||||
--color-primary-700: oklch(37.0% 0.138 264deg);
|
||||
--color-primary-800: oklch(30.0% 0.120 263deg);
|
||||
--color-primary-900: oklch(23.0% 0.100 262deg);
|
||||
--color-primary-950: oklch(15.5% 0.080 261deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-700: oklch(37% 0.138 264deg);
|
||||
--color-primary-800: oklch(30% 0.12 263deg);
|
||||
--color-primary-900: oklch(23% 0.1 262deg);
|
||||
--color-primary-950: oklch(15.5% 0.08 261deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-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);
|
||||
@@ -214,20 +214,20 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* remaining clearly distinct from the primary.
|
||||
* Used for secondary actions, badges, and soft highlights.
|
||||
* =================================================================== */
|
||||
--color-secondary-50: oklch(96.5% 0.032 297deg);
|
||||
--color-secondary-50: oklch(96.5% 0.032 297deg);
|
||||
--color-secondary-100: oklch(91.5% 0.058 295deg);
|
||||
--color-secondary-200: oklch(85.5% 0.090 293deg);
|
||||
--color-secondary-200: oklch(85.5% 0.09 293deg);
|
||||
--color-secondary-300: oklch(78.5% 0.115 292deg);
|
||||
--color-secondary-400: oklch(70.0% 0.132 291deg);
|
||||
--color-secondary-500: oklch(60.0% 0.140 290deg);
|
||||
--color-secondary-400: oklch(70% 0.132 291deg);
|
||||
--color-secondary-500: oklch(60% 0.14 290deg);
|
||||
--color-secondary-600: oklch(52.5% 0.135 289deg);
|
||||
--color-secondary-700: oklch(45.0% 0.126 288deg);
|
||||
--color-secondary-700: oklch(45% 0.126 288deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 286deg);
|
||||
--color-secondary-900: oklch(30.0% 0.094 284deg);
|
||||
--color-secondary-950: oklch(22.0% 0.076 282deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-900: oklch(30% 0.094 284deg);
|
||||
--color-secondary-950: oklch(22% 0.076 282deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-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);
|
||||
@@ -247,20 +247,20 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* breaking against a deep indigo sky.
|
||||
* Used for location chips, warm accents, tertiary elements.
|
||||
* =================================================================== */
|
||||
--color-tertiary-50: oklch(96.5% 0.022 348deg);
|
||||
--color-tertiary-100: oklch(91.0% 0.042 346deg);
|
||||
--color-tertiary-50: oklch(96.5% 0.022 348deg);
|
||||
--color-tertiary-100: oklch(91% 0.042 346deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.068 344deg);
|
||||
--color-tertiary-300: oklch(76.5% 0.095 343deg);
|
||||
--color-tertiary-400: oklch(68.0% 0.118 342deg);
|
||||
--color-tertiary-400: oklch(68% 0.118 342deg);
|
||||
--color-tertiary-500: oklch(57.5% 0.128 341deg);
|
||||
--color-tertiary-600: oklch(50.0% 0.122 340deg);
|
||||
--color-tertiary-700: oklch(43.0% 0.112 339deg);
|
||||
--color-tertiary-600: oklch(50% 0.122 340deg);
|
||||
--color-tertiary-700: oklch(43% 0.112 339deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.098 338deg);
|
||||
--color-tertiary-900: oklch(28.0% 0.080 337deg);
|
||||
--color-tertiary-900: oklch(28% 0.08 337deg);
|
||||
--color-tertiary-950: oklch(20.5% 0.062 336deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-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-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);
|
||||
@@ -277,8 +277,8 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* Hue: ~152°. Consistent with AE_Firefly for recognizable semantic
|
||||
* color meaning across OSIT themes.
|
||||
* =================================================================== */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -286,11 +286,11 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--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-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);
|
||||
@@ -306,20 +306,20 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* WARNING — Amber Orange
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--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-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);
|
||||
@@ -335,20 +335,20 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* ERROR — Soft Coral/Rose
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-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-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);
|
||||
@@ -370,20 +370,20 @@ html.dark[data-theme='AE_Firefly_Indigo'] {
|
||||
* 50 → body-bg light: near-white with ImperceptibleISTIC purple cast
|
||||
* 950 → body-bg dark: deep midnight with indigo depth
|
||||
* =================================================================== */
|
||||
--color-surface-50: oklch(99.0% 0.003 270deg);
|
||||
--color-surface-50: oklch(99% 0.003 270deg);
|
||||
--color-surface-100: oklch(96.5% 0.006 268deg);
|
||||
--color-surface-200: oklch(92.5% 0.010 266deg);
|
||||
--color-surface-300: oklch(87.0% 0.014 265deg);
|
||||
--color-surface-200: oklch(92.5% 0.01 266deg);
|
||||
--color-surface-300: oklch(87% 0.014 265deg);
|
||||
--color-surface-400: oklch(78.5% 0.018 265deg);
|
||||
--color-surface-500: oklch(66.5% 0.020 267deg);
|
||||
--color-surface-500: oklch(66.5% 0.02 267deg);
|
||||
--color-surface-600: oklch(54.5% 0.022 269deg);
|
||||
--color-surface-700: oklch(42.5% 0.024 270deg);
|
||||
--color-surface-800: oklch(31.0% 0.026 272deg);
|
||||
--color-surface-900: oklch(20.5% 0.030 274deg);
|
||||
--color-surface-950: oklch(13.0% 0.034 276deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-800: oklch(31% 0.026 272deg);
|
||||
--color-surface-900: oklch(20.5% 0.03 274deg);
|
||||
--color-surface-950: oklch(13% 0.034 276deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-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);
|
||||
|
||||
@@ -63,50 +63,50 @@ html[data-theme='AE_Firefly_Rainbow'] {
|
||||
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
html[data-theme='AE_Firefly_Rainbow'] {
|
||||
--color-primary-50: oklch(97.0% 0.020 15deg);
|
||||
--color-primary-100: oklch(92.0% 0.048 14deg);
|
||||
--color-primary-200: oklch(86.0% 0.085 13deg);
|
||||
--color-primary-300: oklch(79.0% 0.125 13deg);
|
||||
--color-primary-400: oklch(71.0% 0.160 13deg);
|
||||
--color-primary-500: oklch(60.0% 0.190 14deg);
|
||||
--color-primary-50: oklch(97% 0.02 15deg);
|
||||
--color-primary-100: oklch(92% 0.048 14deg);
|
||||
--color-primary-200: oklch(86% 0.085 13deg);
|
||||
--color-primary-300: oklch(79% 0.125 13deg);
|
||||
--color-primary-400: oklch(71% 0.16 13deg);
|
||||
--color-primary-500: oklch(60% 0.19 14deg);
|
||||
--color-primary-600: oklch(52.5% 0.178 15deg);
|
||||
--color-primary-700: oklch(45.0% 0.162 16deg);
|
||||
--color-primary-700: oklch(45% 0.162 16deg);
|
||||
--color-primary-800: oklch(37.5% 0.142 17deg);
|
||||
--color-primary-900: oklch(30.0% 0.118 18deg);
|
||||
--color-primary-900: oklch(30% 0.118 18deg);
|
||||
--color-primary-950: oklch(22.5% 0.092 19deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(97.0% 0.040 152deg);
|
||||
--color-secondary-50: oklch(97% 0.04 152deg);
|
||||
--color-secondary-100: oklch(92.5% 0.072 150deg);
|
||||
--color-secondary-200: oklch(87.0% 0.105 149deg);
|
||||
--color-secondary-300: oklch(81.0% 0.132 148deg);
|
||||
--color-secondary-200: oklch(87% 0.105 149deg);
|
||||
--color-secondary-300: oklch(81% 0.132 148deg);
|
||||
--color-secondary-400: oklch(74.5% 0.152 148deg);
|
||||
--color-secondary-500: oklch(62.0% 0.160 148deg);
|
||||
--color-secondary-500: oklch(62% 0.16 148deg);
|
||||
--color-secondary-600: oklch(53.5% 0.148 148deg);
|
||||
--color-secondary-700: oklch(45.5% 0.132 147deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 146deg);
|
||||
--color-secondary-900: oklch(29.5% 0.090 145deg);
|
||||
--color-secondary-900: oklch(29.5% 0.09 145deg);
|
||||
--color-secondary-950: oklch(21.5% 0.068 144deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(96.5% 0.030 299deg);
|
||||
--color-tertiary-100: oklch(91.0% 0.058 297deg);
|
||||
--color-tertiary-50: oklch(96.5% 0.03 299deg);
|
||||
--color-tertiary-100: oklch(91% 0.058 297deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.092 296deg);
|
||||
--color-tertiary-300: oklch(77.0% 0.122 295deg);
|
||||
--color-tertiary-300: oklch(77% 0.122 295deg);
|
||||
--color-tertiary-400: oklch(68.5% 0.148 295deg);
|
||||
--color-tertiary-500: oklch(57.0% 0.158 295deg);
|
||||
--color-tertiary-600: oklch(49.5% 0.150 294deg);
|
||||
--color-tertiary-500: oklch(57% 0.158 295deg);
|
||||
--color-tertiary-600: oklch(49.5% 0.15 294deg);
|
||||
--color-tertiary-700: oklch(42.5% 0.138 293deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.122 292deg);
|
||||
--color-tertiary-900: oklch(28.5% 0.102 291deg);
|
||||
--color-tertiary-950: oklch(21.0% 0.080 290deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-950: oklch(21% 0.08 290deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -114,51 +114,51 @@ html[data-theme='AE_Firefly_Rainbow'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99.2% 0.004 75deg);
|
||||
--color-surface-100: oklch(97.0% 0.007 72deg);
|
||||
--color-surface-200: oklch(93.5% 0.010 70deg);
|
||||
--color-surface-50: oklch(99.2% 0.004 75deg);
|
||||
--color-surface-100: oklch(97% 0.007 72deg);
|
||||
--color-surface-200: oklch(93.5% 0.01 70deg);
|
||||
--color-surface-300: oklch(88.5% 0.013 68deg);
|
||||
--color-surface-400: oklch(81.5% 0.016 66deg);
|
||||
--color-surface-500: oklch(70.5% 0.018 64deg);
|
||||
--color-surface-600: oklch(59.0% 0.018 62deg);
|
||||
--color-surface-600: oklch(59% 0.018 62deg);
|
||||
--color-surface-700: oklch(47.5% 0.018 58deg);
|
||||
--color-surface-800: oklch(35.5% 0.020 55deg);
|
||||
--color-surface-900: oklch(24.5% 0.020 52deg);
|
||||
--color-surface-800: oklch(35.5% 0.02 55deg);
|
||||
--color-surface-900: oklch(24.5% 0.02 52deg);
|
||||
--color-surface-950: oklch(15.5% 0.022 48deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
@@ -180,20 +180,20 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* Kept within sRGB gamut across the full ramp.
|
||||
* At 500 (L≈60%): sufficient contrast with primary-50 text (≥4:1).
|
||||
* =================================================================== */
|
||||
--color-primary-50: oklch(97.0% 0.020 15deg);
|
||||
--color-primary-100: oklch(92.0% 0.048 14deg);
|
||||
--color-primary-200: oklch(86.0% 0.085 13deg);
|
||||
--color-primary-300: oklch(79.0% 0.125 13deg);
|
||||
--color-primary-400: oklch(71.0% 0.160 13deg);
|
||||
--color-primary-500: oklch(60.0% 0.190 14deg);
|
||||
--color-primary-50: oklch(97% 0.02 15deg);
|
||||
--color-primary-100: oklch(92% 0.048 14deg);
|
||||
--color-primary-200: oklch(86% 0.085 13deg);
|
||||
--color-primary-300: oklch(79% 0.125 13deg);
|
||||
--color-primary-400: oklch(71% 0.16 13deg);
|
||||
--color-primary-500: oklch(60% 0.19 14deg);
|
||||
--color-primary-600: oklch(52.5% 0.178 15deg);
|
||||
--color-primary-700: oklch(45.0% 0.162 16deg);
|
||||
--color-primary-700: oklch(45% 0.162 16deg);
|
||||
--color-primary-800: oklch(37.5% 0.142 17deg);
|
||||
--color-primary-900: oklch(30.0% 0.118 18deg);
|
||||
--color-primary-900: oklch(30% 0.118 18deg);
|
||||
--color-primary-950: oklch(22.5% 0.092 19deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-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-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);
|
||||
@@ -212,20 +212,20 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* it bridges the warm red primary and the cool violet tertiary.
|
||||
* Used for secondary actions, success-adjacent highlights, badges.
|
||||
* =================================================================== */
|
||||
--color-secondary-50: oklch(97.0% 0.040 152deg);
|
||||
--color-secondary-50: oklch(97% 0.04 152deg);
|
||||
--color-secondary-100: oklch(92.5% 0.072 150deg);
|
||||
--color-secondary-200: oklch(87.0% 0.105 149deg);
|
||||
--color-secondary-300: oklch(81.0% 0.132 148deg);
|
||||
--color-secondary-200: oklch(87% 0.105 149deg);
|
||||
--color-secondary-300: oklch(81% 0.132 148deg);
|
||||
--color-secondary-400: oklch(74.5% 0.152 148deg);
|
||||
--color-secondary-500: oklch(62.0% 0.160 148deg);
|
||||
--color-secondary-500: oklch(62% 0.16 148deg);
|
||||
--color-secondary-600: oklch(53.5% 0.148 148deg);
|
||||
--color-secondary-700: oklch(45.5% 0.132 147deg);
|
||||
--color-secondary-800: oklch(37.5% 0.112 146deg);
|
||||
--color-secondary-900: oklch(29.5% 0.090 145deg);
|
||||
--color-secondary-900: oklch(29.5% 0.09 145deg);
|
||||
--color-secondary-950: oklch(21.5% 0.068 144deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-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-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);
|
||||
@@ -244,20 +244,20 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* brand color slots. Creates striking contrast with the warm primary.
|
||||
* Used for location chips, deep accents, tertiary elements.
|
||||
* =================================================================== */
|
||||
--color-tertiary-50: oklch(96.5% 0.030 299deg);
|
||||
--color-tertiary-100: oklch(91.0% 0.058 297deg);
|
||||
--color-tertiary-50: oklch(96.5% 0.03 299deg);
|
||||
--color-tertiary-100: oklch(91% 0.058 297deg);
|
||||
--color-tertiary-200: oklch(84.5% 0.092 296deg);
|
||||
--color-tertiary-300: oklch(77.0% 0.122 295deg);
|
||||
--color-tertiary-300: oklch(77% 0.122 295deg);
|
||||
--color-tertiary-400: oklch(68.5% 0.148 295deg);
|
||||
--color-tertiary-500: oklch(57.0% 0.158 295deg);
|
||||
--color-tertiary-600: oklch(49.5% 0.150 294deg);
|
||||
--color-tertiary-500: oklch(57% 0.158 295deg);
|
||||
--color-tertiary-600: oklch(49.5% 0.15 294deg);
|
||||
--color-tertiary-700: oklch(42.5% 0.138 293deg);
|
||||
--color-tertiary-800: oklch(35.5% 0.122 292deg);
|
||||
--color-tertiary-900: oklch(28.5% 0.102 291deg);
|
||||
--color-tertiary-950: oklch(21.0% 0.080 290deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-950: oklch(21% 0.08 290deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-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);
|
||||
@@ -274,8 +274,8 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* Hue: ~152°. Consistent with AE_Firefly for recognizable semantic
|
||||
* color meaning across OSIT themes.
|
||||
* =================================================================== */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -283,11 +283,11 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--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-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);
|
||||
@@ -303,20 +303,20 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* WARNING — Amber Orange
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--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-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);
|
||||
@@ -332,20 +332,20 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* ERROR — Soft Coral/Rose
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-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-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);
|
||||
@@ -366,20 +366,20 @@ html.dark[data-theme='AE_Firefly_Rainbow'] {
|
||||
* 50 → body-bg light: warm near-white, like morning paper
|
||||
* 950 → body-bg dark: deep warm charcoal, like a dim theatre
|
||||
* =================================================================== */
|
||||
--color-surface-50: oklch(99.2% 0.004 75deg);
|
||||
--color-surface-100: oklch(97.0% 0.007 72deg);
|
||||
--color-surface-200: oklch(93.5% 0.010 70deg);
|
||||
--color-surface-50: oklch(99.2% 0.004 75deg);
|
||||
--color-surface-100: oklch(97% 0.007 72deg);
|
||||
--color-surface-200: oklch(93.5% 0.01 70deg);
|
||||
--color-surface-300: oklch(88.5% 0.013 68deg);
|
||||
--color-surface-400: oklch(81.5% 0.016 66deg);
|
||||
--color-surface-500: oklch(70.5% 0.018 64deg);
|
||||
--color-surface-600: oklch(59.0% 0.018 62deg);
|
||||
--color-surface-600: oklch(59% 0.018 62deg);
|
||||
--color-surface-700: oklch(47.5% 0.018 58deg);
|
||||
--color-surface-800: oklch(35.5% 0.020 55deg);
|
||||
--color-surface-900: oklch(24.5% 0.020 52deg);
|
||||
--color-surface-800: oklch(35.5% 0.02 55deg);
|
||||
--color-surface-900: oklch(24.5% 0.02 52deg);
|
||||
--color-surface-950: oklch(15.5% 0.022 48deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-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-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);
|
||||
|
||||
@@ -60,50 +60,50 @@ html[data-theme='AE_Firefly_SteelBlue'] {
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.875rem;
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
--color-primary-50: oklch(96.5% 0.022 214deg);
|
||||
--color-primary-100: oklch(91.0% 0.045 213deg);
|
||||
--color-primary-50: oklch(96.5% 0.022 214deg);
|
||||
--color-primary-100: oklch(91% 0.045 213deg);
|
||||
--color-primary-200: oklch(84.5% 0.072 212deg);
|
||||
--color-primary-300: oklch(76.5% 0.097 212deg);
|
||||
--color-primary-400: oklch(67.0% 0.115 213deg);
|
||||
--color-primary-500: oklch(56.0% 0.115 214deg);
|
||||
--color-primary-600: oklch(49.0% 0.112 214deg);
|
||||
--color-primary-400: oklch(67% 0.115 213deg);
|
||||
--color-primary-500: oklch(56% 0.115 214deg);
|
||||
--color-primary-600: oklch(49% 0.112 214deg);
|
||||
--color-primary-700: oklch(41.5% 0.105 213deg);
|
||||
--color-primary-800: oklch(34.0% 0.095 212deg);
|
||||
--color-primary-900: oklch(26.5% 0.080 211deg);
|
||||
--color-primary-800: oklch(34% 0.095 212deg);
|
||||
--color-primary-900: oklch(26.5% 0.08 211deg);
|
||||
--color-primary-950: oklch(18.5% 0.065 210deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(97.5% 0.055 56deg);
|
||||
--color-secondary-100: oklch(93.5% 0.090 55deg);
|
||||
--color-secondary-200: oklch(89.5% 0.120 54deg);
|
||||
--color-secondary-50: oklch(97.5% 0.055 56deg);
|
||||
--color-secondary-100: oklch(93.5% 0.09 55deg);
|
||||
--color-secondary-200: oklch(89.5% 0.12 54deg);
|
||||
--color-secondary-300: oklch(85.5% 0.148 53deg);
|
||||
--color-secondary-400: oklch(81.5% 0.162 52deg);
|
||||
--color-secondary-500: oklch(76.5% 0.162 51deg);
|
||||
--color-secondary-600: oklch(68.5% 0.152 50deg);
|
||||
--color-secondary-700: oklch(60.5% 0.138 49deg);
|
||||
--color-secondary-800: oklch(52.0% 0.122 48deg);
|
||||
--color-secondary-800: oklch(52% 0.122 48deg);
|
||||
--color-secondary-900: oklch(43.5% 0.102 47deg);
|
||||
--color-secondary-950: oklch(35.0% 0.084 46deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-950: oklch(35% 0.084 46deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(95.5% 0.025 232deg);
|
||||
--color-tertiary-50: oklch(95.5% 0.025 232deg);
|
||||
--color-tertiary-100: oklch(89.5% 0.048 231deg);
|
||||
--color-tertiary-200: oklch(82.5% 0.072 230deg);
|
||||
--color-tertiary-300: oklch(74.5% 0.095 229deg);
|
||||
--color-tertiary-400: oklch(65.5% 0.120 229deg);
|
||||
--color-tertiary-400: oklch(65.5% 0.12 229deg);
|
||||
--color-tertiary-500: oklch(54.5% 0.135 230deg);
|
||||
--color-tertiary-600: oklch(47.0% 0.132 230deg);
|
||||
--color-tertiary-600: oklch(47% 0.132 230deg);
|
||||
--color-tertiary-700: oklch(39.5% 0.122 229deg);
|
||||
--color-tertiary-800: oklch(32.0% 0.108 228deg);
|
||||
--color-tertiary-900: oklch(25.0% 0.090 227deg);
|
||||
--color-tertiary-800: oklch(32% 0.108 228deg);
|
||||
--color-tertiary-900: oklch(25% 0.09 227deg);
|
||||
--color-tertiary-950: oklch(17.5% 0.072 226deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -111,51 +111,51 @@ html[data-theme='AE_Firefly_SteelBlue'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99.0% 0.004 220deg);
|
||||
--color-surface-50: oklch(99% 0.004 220deg);
|
||||
--color-surface-100: oklch(96.5% 0.008 218deg);
|
||||
--color-surface-200: oklch(92.5% 0.012 217deg);
|
||||
--color-surface-300: oklch(87.0% 0.016 216deg);
|
||||
--color-surface-400: oklch(78.5% 0.020 215deg);
|
||||
--color-surface-300: oklch(87% 0.016 216deg);
|
||||
--color-surface-400: oklch(78.5% 0.02 215deg);
|
||||
--color-surface-500: oklch(66.5% 0.022 217deg);
|
||||
--color-surface-600: oklch(54.5% 0.025 220deg);
|
||||
--color-surface-700: oklch(42.5% 0.028 223deg);
|
||||
--color-surface-800: oklch(31.0% 0.032 226deg);
|
||||
--color-surface-800: oklch(31% 0.032 226deg);
|
||||
--color-surface-900: oklch(20.5% 0.035 228deg);
|
||||
--color-surface-950: oklch(13.0% 0.040 232deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-950: oklch(13% 0.04 232deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
@@ -175,20 +175,20 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* Approx: #4682B4 (CSS SteelBlue) sits at oklch(56%, 0.113, 214°).
|
||||
* At 500 (L≈56%): sufficient contrast with primary-50 text (≥4:1).
|
||||
* =================================================================== */
|
||||
--color-primary-50: oklch(96.5% 0.022 214deg);
|
||||
--color-primary-100: oklch(91.0% 0.045 213deg);
|
||||
--color-primary-50: oklch(96.5% 0.022 214deg);
|
||||
--color-primary-100: oklch(91% 0.045 213deg);
|
||||
--color-primary-200: oklch(84.5% 0.072 212deg);
|
||||
--color-primary-300: oklch(76.5% 0.097 212deg);
|
||||
--color-primary-400: oklch(67.0% 0.115 213deg);
|
||||
--color-primary-500: oklch(56.0% 0.115 214deg);
|
||||
--color-primary-600: oklch(49.0% 0.112 214deg);
|
||||
--color-primary-400: oklch(67% 0.115 213deg);
|
||||
--color-primary-500: oklch(56% 0.115 214deg);
|
||||
--color-primary-600: oklch(49% 0.112 214deg);
|
||||
--color-primary-700: oklch(41.5% 0.105 213deg);
|
||||
--color-primary-800: oklch(34.0% 0.095 212deg);
|
||||
--color-primary-900: oklch(26.5% 0.080 211deg);
|
||||
--color-primary-800: oklch(34% 0.095 212deg);
|
||||
--color-primary-900: oklch(26.5% 0.08 211deg);
|
||||
--color-primary-950: oklch(18.5% 0.065 210deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-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-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);
|
||||
@@ -206,20 +206,20 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* of steel blue. The classic "metal on metal" contrast pairing —
|
||||
* used for secondary actions, badges, and call-to-action highlights.
|
||||
* =================================================================== */
|
||||
--color-secondary-50: oklch(97.5% 0.055 56deg);
|
||||
--color-secondary-100: oklch(93.5% 0.090 55deg);
|
||||
--color-secondary-200: oklch(89.5% 0.120 54deg);
|
||||
--color-secondary-50: oklch(97.5% 0.055 56deg);
|
||||
--color-secondary-100: oklch(93.5% 0.09 55deg);
|
||||
--color-secondary-200: oklch(89.5% 0.12 54deg);
|
||||
--color-secondary-300: oklch(85.5% 0.148 53deg);
|
||||
--color-secondary-400: oklch(81.5% 0.162 52deg);
|
||||
--color-secondary-500: oklch(76.5% 0.162 51deg);
|
||||
--color-secondary-600: oklch(68.5% 0.152 50deg);
|
||||
--color-secondary-700: oklch(60.5% 0.138 49deg);
|
||||
--color-secondary-800: oklch(52.0% 0.122 48deg);
|
||||
--color-secondary-800: oklch(52% 0.122 48deg);
|
||||
--color-secondary-900: oklch(43.5% 0.102 47deg);
|
||||
--color-secondary-950: oklch(35.0% 0.084 46deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-950: oklch(35% 0.084 46deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-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);
|
||||
@@ -237,20 +237,20 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* like the heavy cobalt-blue depths under polished chrome.
|
||||
* Used for accents, location chips, and depth elements.
|
||||
* =================================================================== */
|
||||
--color-tertiary-50: oklch(95.5% 0.025 232deg);
|
||||
--color-tertiary-50: oklch(95.5% 0.025 232deg);
|
||||
--color-tertiary-100: oklch(89.5% 0.048 231deg);
|
||||
--color-tertiary-200: oklch(82.5% 0.072 230deg);
|
||||
--color-tertiary-300: oklch(74.5% 0.095 229deg);
|
||||
--color-tertiary-400: oklch(65.5% 0.120 229deg);
|
||||
--color-tertiary-400: oklch(65.5% 0.12 229deg);
|
||||
--color-tertiary-500: oklch(54.5% 0.135 230deg);
|
||||
--color-tertiary-600: oklch(47.0% 0.132 230deg);
|
||||
--color-tertiary-600: oklch(47% 0.132 230deg);
|
||||
--color-tertiary-700: oklch(39.5% 0.122 229deg);
|
||||
--color-tertiary-800: oklch(32.0% 0.108 228deg);
|
||||
--color-tertiary-900: oklch(25.0% 0.090 227deg);
|
||||
--color-tertiary-800: oklch(32% 0.108 228deg);
|
||||
--color-tertiary-900: oklch(25% 0.09 227deg);
|
||||
--color-tertiary-950: oklch(17.5% 0.072 226deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-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-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);
|
||||
@@ -267,8 +267,8 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* Hue: ~152°. Consistent with AE_Firefly for recognizable semantic
|
||||
* color meaning across OSIT themes.
|
||||
* =================================================================== */
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -276,11 +276,11 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--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-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);
|
||||
@@ -296,20 +296,20 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* WARNING — Amber Orange
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--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-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);
|
||||
@@ -325,20 +325,20 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* ERROR — Soft Coral/Rose
|
||||
* Consistent with AE_Firefly for recognizable semantic meaning.
|
||||
* =================================================================== */
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-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-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);
|
||||
@@ -359,20 +359,20 @@ html.dark[data-theme='AE_Firefly_SteelBlue'] {
|
||||
* 50 → body-bg light: brilliant near-white with a chrome whisper
|
||||
* 950 → body-bg dark: deep gunmetal with subtle cool-blue depth
|
||||
* =================================================================== */
|
||||
--color-surface-50: oklch(99.0% 0.004 220deg);
|
||||
--color-surface-50: oklch(99% 0.004 220deg);
|
||||
--color-surface-100: oklch(96.5% 0.008 218deg);
|
||||
--color-surface-200: oklch(92.5% 0.012 217deg);
|
||||
--color-surface-300: oklch(87.0% 0.016 216deg);
|
||||
--color-surface-400: oklch(78.5% 0.020 215deg);
|
||||
--color-surface-300: oklch(87% 0.016 216deg);
|
||||
--color-surface-400: oklch(78.5% 0.02 215deg);
|
||||
--color-surface-500: oklch(66.5% 0.022 217deg);
|
||||
--color-surface-600: oklch(54.5% 0.025 220deg);
|
||||
--color-surface-700: oklch(42.5% 0.028 223deg);
|
||||
--color-surface-800: oklch(31.0% 0.032 226deg);
|
||||
--color-surface-800: oklch(31% 0.032 226deg);
|
||||
--color-surface-900: oklch(20.5% 0.035 228deg);
|
||||
--color-surface-950: oklch(13.0% 0.040 232deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-950: oklch(13% 0.04 232deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-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);
|
||||
|
||||
@@ -63,50 +63,50 @@ html[data-theme='AE_Firefly'] {
|
||||
|
||||
/* --- Color ramps (light mode) copied from dark block so both modes have full ramps --- */
|
||||
html[data-theme='AE_Firefly'] {
|
||||
--color-primary-50: oklch(96.5% 0.025 192deg);
|
||||
--color-primary-100: oklch(91.0% 0.050 190deg);
|
||||
--color-primary-50: oklch(96.5% 0.025 192deg);
|
||||
--color-primary-100: oklch(91% 0.05 190deg);
|
||||
--color-primary-200: oklch(84.5% 0.078 188deg);
|
||||
--color-primary-300: oklch(76.5% 0.105 186deg);
|
||||
--color-primary-400: oklch(67.5% 0.125 185deg);
|
||||
--color-primary-500: oklch(50.5% 0.130 184deg);
|
||||
--color-primary-600: oklch(44.0% 0.125 183deg);
|
||||
--color-primary-500: oklch(50.5% 0.13 184deg);
|
||||
--color-primary-600: oklch(44% 0.125 183deg);
|
||||
--color-primary-700: oklch(37.5% 0.115 182deg);
|
||||
--color-primary-800: oklch(30.5% 0.105 181deg);
|
||||
--color-primary-900: oklch(23.5% 0.090 180deg);
|
||||
--color-primary-950: oklch(16.0% 0.075 179deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-900: oklch(23.5% 0.09 180deg);
|
||||
--color-primary-950: oklch(16% 0.075 179deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
|
||||
--color-secondary-50: oklch(97.5% 0.060 102deg);
|
||||
--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% 0.170 93deg);
|
||||
--color-secondary-500: oklch(76.0% 0.170 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.160 87deg);
|
||||
--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% 0.130 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.110 81deg);
|
||||
--color-secondary-950: oklch(35.0% 0.090 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-800: oklch(52% 0.13 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.11 81deg);
|
||||
--color-secondary-950: oklch(35% 0.09 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89.0% 0.068 281deg);
|
||||
--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% 0.132 277deg);
|
||||
--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% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-950: oklch(20% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -114,51 +114,51 @@ html[data-theme='AE_Firefly'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-900: oklch(43.2% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.2% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-900: oklch(45% 0.105 61deg);
|
||||
--color-warning-950: oklch(37% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97.0% 0.006 217deg);
|
||||
--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% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.020 222deg);
|
||||
--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-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
}
|
||||
|
||||
@@ -174,20 +174,20 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
* PRIMARY — Luminescent Firefly Teal
|
||||
* ...existing code...
|
||||
*/
|
||||
--color-primary-50: oklch(96.5% 0.025 192deg);
|
||||
--color-primary-100: oklch(91.0% 0.050 190deg);
|
||||
--color-primary-50: oklch(96.5% 0.025 192deg);
|
||||
--color-primary-100: oklch(91% 0.05 190deg);
|
||||
--color-primary-200: oklch(84.5% 0.078 188deg);
|
||||
--color-primary-300: oklch(76.5% 0.105 186deg);
|
||||
--color-primary-400: oklch(67.5% 0.125 185deg);
|
||||
--color-primary-500: oklch(50.5% 0.130 184deg);
|
||||
--color-primary-600: oklch(44.0% 0.125 183deg);
|
||||
--color-primary-500: oklch(50.5% 0.13 184deg);
|
||||
--color-primary-600: oklch(44% 0.125 183deg);
|
||||
--color-primary-700: oklch(37.5% 0.115 182deg);
|
||||
--color-primary-800: oklch(30.5% 0.105 181deg);
|
||||
--color-primary-900: oklch(23.5% 0.090 180deg);
|
||||
--color-primary-950: oklch(16.0% 0.075 179deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-900: oklch(23.5% 0.09 180deg);
|
||||
--color-primary-950: oklch(16% 0.075 179deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-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);
|
||||
@@ -200,20 +200,20 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* ...existing code for secondary, tertiary, success, warning, error, surface... */
|
||||
--color-secondary-50: oklch(97.5% 0.060 102deg);
|
||||
--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% 0.170 93deg);
|
||||
--color-secondary-500: oklch(76.0% 0.170 90deg);
|
||||
--color-secondary-600: oklch(68.5% 0.160 87deg);
|
||||
--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% 0.130 83deg);
|
||||
--color-secondary-900: oklch(43.5% 0.110 81deg);
|
||||
--color-secondary-950: oklch(35.0% 0.090 79deg);
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--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-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);
|
||||
@@ -225,20 +225,20 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
--color-tertiary-50: oklch(95.5% 0.042 283deg);
|
||||
--color-tertiary-100: oklch(89.0% 0.068 281deg);
|
||||
--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% 0.132 277deg);
|
||||
--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% 0.082 271deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--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-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);
|
||||
@@ -250,8 +250,8 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
--color-success-50: oklch(95.77% 0.05 152.69deg);
|
||||
--color-success-100: oklch(91.59% 0.06 152.00deg);
|
||||
--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);
|
||||
@@ -259,11 +259,11 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--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.20% 0.06 151.12deg);
|
||||
--color-success-950: oklch(34.20% 0.04 151.44deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--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-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);
|
||||
@@ -275,20 +275,20 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
--color-warning-50: oklch(97.5% 0.065 78deg);
|
||||
--color-warning-100: oklch(93.5% 0.090 75deg);
|
||||
--color-warning-200: oklch(89.5% 0.120 73deg);
|
||||
--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.160 67deg);
|
||||
--color-warning-500: oklch(77.0% 0.165 65deg);
|
||||
--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.140 63deg);
|
||||
--color-warning-700: oklch(61.5% 0.14 63deg);
|
||||
--color-warning-800: oklch(53.5% 0.125 62deg);
|
||||
--color-warning-900: oklch(45.0% 0.105 61deg);
|
||||
--color-warning-950: oklch(37.0% 0.088 60deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--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-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);
|
||||
@@ -300,20 +300,20 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
--color-error-50: oklch(95.0% 0.040 18deg);
|
||||
--color-error-100: oklch(88.0% 0.070 20deg);
|
||||
--color-error-200: oklch(80.0% 0.105 21deg);
|
||||
--color-error-300: oklch(72.0% 0.140 22deg);
|
||||
--color-error-400: oklch(64.5% 0.170 23deg);
|
||||
--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% 0.128 28deg);
|
||||
--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-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-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);
|
||||
@@ -325,20 +325,20 @@ html.dark[data-theme='AE_Firefly'] {
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
--color-surface-50: oklch(99.2% 0.003 220deg);
|
||||
--color-surface-100: oklch(97.0% 0.006 217deg);
|
||||
--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% 0.018 218deg);
|
||||
--color-surface-700: oklch(47.5% 0.020 222deg);
|
||||
--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-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-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);
|
||||
|
||||
116
src/app.css
116
src/app.css
@@ -10,12 +10,15 @@
|
||||
/* Sync native browser control rendering (select dropdowns, scrollbars, etc.)
|
||||
with the app's dark/light mode toggle. Without this, native controls follow
|
||||
the OS theme rather than the app's .dark/.light class on <html>. */
|
||||
html.dark { color-scheme: dark; }
|
||||
html.light { color-scheme: light; }
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
html.light {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@import '@skeletonlabs/skeleton';
|
||||
|
||||
|
||||
/* Register Preset Themes */
|
||||
/* @import '@skeletonlabs/skeleton/themes/{theme-name}'; */
|
||||
@import '@skeletonlabs/skeleton/themes/cerberus';
|
||||
@@ -38,6 +41,8 @@ html.light { color-scheme: light; }
|
||||
@import './ae-firefly-steelblue.css';
|
||||
@import './ae-firefly-indigo.css';
|
||||
@import './ae-firefly-rainbow.css';
|
||||
@import './ae-firefly-axonius.css';
|
||||
@import './ae-firefly-bgh.css';
|
||||
|
||||
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';
|
||||
|
||||
@@ -154,13 +159,20 @@ html.light { color-scheme: light; }
|
||||
.dark .input:not([type='checkbox']):not([type='radio']):not([type='range']),
|
||||
.dark .select,
|
||||
.dark .textarea {
|
||||
color: rgb(243 244 246); /* gray-100 */
|
||||
color: rgb(243 244 246); /* gray-100 */
|
||||
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 .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 */
|
||||
.dark .select option {
|
||||
@@ -198,8 +210,12 @@ body {
|
||||
/* Font size accessibility modes — cycled via the font size button in the sys menu.
|
||||
Applied as a class on <html> by the layout DOM effect.
|
||||
The 'default' mode has no class (browser default, typically 16px). */
|
||||
html.font-size-larger { font-size: 112.5%; } /* ~18px base */
|
||||
html.font-size-smaller { font-size: 87.5%; } /* ~14px base */
|
||||
html.font-size-larger {
|
||||
font-size: 112.5%;
|
||||
} /* ~18px base */
|
||||
html.font-size-smaller {
|
||||
font-size: 87.5%;
|
||||
} /* ~14px base */
|
||||
|
||||
html.super_access #appShell {
|
||||
background-color: hsla(0, 100%, 50%, 0.5);
|
||||
@@ -224,12 +240,14 @@ html.trusted_access #appShell {
|
||||
font-display: swap;
|
||||
} */
|
||||
|
||||
/* modern theme */
|
||||
@font-face {
|
||||
/* modern theme — @font-face commented out 2026-05-19: Quicksand is declared but no
|
||||
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';
|
||||
src: url('/fonts/Quicksand.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
} */
|
||||
|
||||
/* :root [data-theme='modern'] { */
|
||||
/* --theme-rounded-base: 20px;
|
||||
@@ -356,51 +374,51 @@ html.trusted_access #appShell {
|
||||
/* @apply preset-tonal-primary border border-primary-500 transition-all; */
|
||||
}
|
||||
.ae_btn_secondary {
|
||||
@apply preset-tonal-secondary border border-secondary-500 transition-all;
|
||||
@apply preset-tonal-secondary border-secondary-500 border transition-all;
|
||||
/* hover:preset-filled-secondary-500 */
|
||||
}
|
||||
.ae_btn_tertiary {
|
||||
@apply preset-tonal-tertiary border border-tertiary-500 transition-all;
|
||||
@apply preset-tonal-tertiary border-tertiary-500 border transition-all;
|
||||
}
|
||||
.ae_btn_success {
|
||||
@apply preset-tonal-success border border-success-500 transition-all;
|
||||
@apply preset-tonal-success border-success-500 border transition-all;
|
||||
}
|
||||
.ae_btn_warning {
|
||||
@apply preset-tonal-warning border border-warning-500 text-warning-950-50 transition-all;
|
||||
@apply preset-tonal-warning border-warning-500 text-warning-950-50 border transition-all;
|
||||
}
|
||||
.ae_btn_error {
|
||||
@apply preset-tonal-error border border-error-500 transition-all;
|
||||
@apply preset-tonal-error border-error-500 border transition-all;
|
||||
}
|
||||
.ae_btn_surface {
|
||||
@apply preset-tonal-surface border border-surface-500 transition-all;
|
||||
@apply preset-tonal-surface border-surface-500 border transition-all;
|
||||
}
|
||||
/* Buttons customized for Aether using Skeleton Tailwind preset classes */
|
||||
.ae_btn_info {
|
||||
@apply border text-cyan-950 dark:text-cyan-50 bg-cyan-50 dark:bg-cyan-950 border-cyan-100 dark:border-cyan-900 hover:bg-cyan-200 hover:dark:bg-cyan-800 transition-all;
|
||||
@apply border border-cyan-100 bg-cyan-50 text-cyan-950 transition-all hover:bg-cyan-200 dark:border-cyan-900 dark:bg-cyan-950 dark:text-cyan-50 hover:dark:bg-cyan-800;
|
||||
}
|
||||
|
||||
/* Buttons are for filled and outlined presets */
|
||||
.ae_btn_secondary_filled {
|
||||
@apply preset-filled-secondary-200-800 border border-secondary-500 transition-all;
|
||||
@apply preset-filled-secondary-200-800 border-secondary-500 border transition-all;
|
||||
/* hover:preset-filled-secondary-500 */
|
||||
}
|
||||
.ae_btn_secondary_outlined {
|
||||
@apply preset-outlined-secondary-200-800 hover:preset-filled-secondary-400-600 text-secondary-950-50 transition-all;
|
||||
}
|
||||
.ae_btn_success_filled {
|
||||
@apply preset-filled-success-200-800 border border-success-500 transition-all;
|
||||
@apply preset-filled-success-200-800 border-success-500 border transition-all;
|
||||
}
|
||||
.ae_btn_success_outlined {
|
||||
@apply preset-outlined-success-200-800 hover:preset-filled-success-400-600 text-success-950-50 transition-all;
|
||||
}
|
||||
.ae_btn_warning_filled {
|
||||
@apply preset-filled-warning-200-800 border border-warning-500 transition-all;
|
||||
@apply preset-filled-warning-200-800 border-warning-500 border transition-all;
|
||||
}
|
||||
.ae_btn_warning_outlined {
|
||||
@apply preset-outlined-warning-200-800 hover:preset-filled-warning-400-600 text-warning-950-50 transition-all;
|
||||
}
|
||||
.ae_btn_surface_filled {
|
||||
@apply preset-filled-surface-200-800 border border-surface-500 transition-all;
|
||||
@apply preset-filled-surface-200-800 border-surface-500 border transition-all;
|
||||
}
|
||||
.ae_btn_surface_outlined {
|
||||
@apply preset-outlined-surface-200-800 hover:preset-filled-surface-400-600 text-surface-950-50 transition-all;
|
||||
@@ -409,10 +427,10 @@ html.trusted_access #appShell {
|
||||
@apply preset-outlined-error-200-800 hover:preset-filled-error-400-600 text-error-950-50 transition-all;
|
||||
}
|
||||
.ae_btn_info_filled {
|
||||
@apply border text-cyan-950 dark:text-cyan-50 bg-cyan-200 dark:bg-cyan-800 border-cyan-200 dark:border-cyan-800 transition-all;
|
||||
@apply border border-cyan-200 bg-cyan-200 text-cyan-950 transition-all dark:border-cyan-800 dark:bg-cyan-800 dark:text-cyan-50;
|
||||
}
|
||||
.ae_btn_info_outlined {
|
||||
@apply border text-cyan-950 dark:text-cyan-50 bg-cyan-50 dark:bg-cyan-950 border-cyan-200 dark:border-cyan-800 transition-all;
|
||||
@apply border border-cyan-200 bg-cyan-50 text-cyan-950 transition-all dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-50;
|
||||
}
|
||||
|
||||
/* Containers customized for Aether using Skeleton Tailwind preset classes */
|
||||
@@ -436,7 +454,7 @@ html.trusted_access #appShell {
|
||||
.ae_module_header {
|
||||
/* LCI request 3a5997 */
|
||||
/* bg-gray-300 */
|
||||
@apply preset-tonal-surface rounded-md flex flex-col md:flex-row flex-wrap gap-0.25 items-center justify-between w-full max-w-7xl p-1 px-2;
|
||||
@apply preset-tonal-surface flex w-full max-w-7xl flex-col flex-wrap items-center justify-between gap-0.25 rounded-md p-1 px-2 md:flex-row;
|
||||
}
|
||||
|
||||
[data-theme='AE_c_LCI'] .ae_module_header {
|
||||
@@ -453,32 +471,18 @@ html.trusted_access #appShell {
|
||||
@apply container;
|
||||
}
|
||||
.ae_container_module_menu {
|
||||
@apply w-full max-w-7xl flex flex-col items-center justify-center gap-1 p-1
|
||||
border rounded-md border-gray-200 dark:border-gray-800 hover:bg-gray-100 dark:hover:bg-gray-900 transition-all duration-700 hover:duration-300;
|
||||
@apply flex w-full max-w-7xl flex-col items-center justify-center gap-1 rounded-md border border-gray-200 p-1 transition-all duration-700 hover:bg-gray-100 hover:duration-300 dark:border-gray-800 dark:hover:bg-gray-900;
|
||||
}
|
||||
.ae_container_module_options {
|
||||
@apply text-cyan-950 dark:text-cyan-50
|
||||
bg-cyan-50 dark:bg-cyan-950 hover:bg-cyan-100 dark:hover:bg-cyan-900
|
||||
border border-cyan-200 dark:border-cyan-800 hover:border-cyan-400 dark:hover:border-cyan-600
|
||||
rounded-md
|
||||
flex flex-row flex-wrap items-center justify-around
|
||||
w-full max-w-full
|
||||
p-2
|
||||
transition-all;
|
||||
@apply flex w-full max-w-full flex-row flex-wrap items-center justify-around rounded-md border border-cyan-200 bg-cyan-50 p-2 text-cyan-950 transition-all hover:border-cyan-400 hover:bg-cyan-100 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-50 dark:hover:border-cyan-600 dark:hover:bg-cyan-900;
|
||||
}
|
||||
.ae_container_module_help {
|
||||
@apply text-yellow-950 dark:text-yellow-50
|
||||
bg-yellow-50 dark:bg-yellow-950 hover:bg-yellow-100 dark:hover:bg-yellow-900
|
||||
border border-yellow-200 dark:border-yellow-800 hover:border-yellow-400 dark:hover:border-yellow-600
|
||||
rounded-md
|
||||
w-lg max-w-full
|
||||
p-2
|
||||
transition-all;
|
||||
@apply w-lg max-w-full rounded-md border border-yellow-200 bg-yellow-50 p-2 text-yellow-950 transition-all hover:border-yellow-400 hover:bg-yellow-100 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-50 dark:hover:border-yellow-600 dark:hover:bg-yellow-900;
|
||||
/* bg-yellow-100 border border-yellow-400 p-2 rounded-md max-w-xl */
|
||||
}
|
||||
|
||||
.ae_container_actions {
|
||||
@apply container preset-tonal-success border border-success-500 rounded-md flex flex-row items-center my-2 p-2;
|
||||
@apply preset-tonal-success border-success-500 container my-2 flex flex-row items-center rounded-md border p-2;
|
||||
}
|
||||
.ae_container_results {
|
||||
@apply container;
|
||||
@@ -500,23 +504,11 @@ html.trusted_access #appShell {
|
||||
@apply container;
|
||||
}
|
||||
.ae_container_help {
|
||||
@apply text-yellow-950 dark:text-yellow-50
|
||||
bg-yellow-50 dark:bg-yellow-950 hover:bg-yellow-100 dark:hover:bg-yellow-900
|
||||
border border-yellow-200 dark:border-yellow-800 hover:border-yellow-400 dark:hover:border-yellow-600
|
||||
rounded-md
|
||||
max-w-full
|
||||
p-2
|
||||
transition-all;
|
||||
@apply max-w-full rounded-md border border-yellow-200 bg-yellow-50 p-2 text-yellow-950 transition-all hover:border-yellow-400 hover:bg-yellow-100 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-50 dark:hover:border-yellow-600 dark:hover:bg-yellow-900;
|
||||
/* bg-yellow-100 border border-yellow-400 p-2 rounded-md max-w-xl */
|
||||
}
|
||||
.ae_container_info {
|
||||
@apply text-cyan-950 dark:text-cyan-50
|
||||
bg-cyan-50 dark:bg-cyan-950 hover:bg-cyan-100 dark:hover:bg-cyan-900
|
||||
border border-cyan-200 dark:border-cyan-800 hover:border-cyan-400 dark:hover:border-cyan-600
|
||||
rounded-md
|
||||
max-w-full
|
||||
p-2
|
||||
transition-all;
|
||||
@apply max-w-full rounded-md border border-cyan-200 bg-cyan-50 p-2 text-cyan-950 transition-all hover:border-cyan-400 hover:bg-cyan-100 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-50 dark:hover:border-cyan-600 dark:hover:bg-cyan-900;
|
||||
}
|
||||
.ae_container_msg {
|
||||
@apply container;
|
||||
@@ -933,8 +925,14 @@ img.qr_code:focus {
|
||||
/* BEGIN: Overrides and fixes specific to Novi and IDAA */
|
||||
.iframe .novi_btn {
|
||||
border-radius: 60px;
|
||||
/* border-color: hsla(0, 0%, 50%, .5); */
|
||||
/* border-color: hsla(0, 0%, 0%, .15); */
|
||||
/* Bootstrap v3 (.btn) sets border:1px solid transparent and wins over
|
||||
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 {
|
||||
|
||||
14
src/app.d.ts
vendored
14
src/app.d.ts
vendored
@@ -14,7 +14,7 @@ declare global {
|
||||
namespace App {
|
||||
interface Platform {}
|
||||
}
|
||||
|
||||
|
||||
interface Window {
|
||||
native_app: any;
|
||||
}
|
||||
@@ -22,3 +22,15 @@ declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var native_app: any;
|
||||
}
|
||||
|
||||
// Stripe Buy Button web component — needed so Svelte templates accept the element without TS errors.
|
||||
declare module 'svelte/elements' {
|
||||
interface IntrinsicElements {
|
||||
'stripe-buy-button': {
|
||||
'buy-button-id': string;
|
||||
'publishable-key': string;
|
||||
'client-reference-id'?: string;
|
||||
[attr: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
85
src/app.html
85
src/app.html
@@ -3,33 +3,106 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const hostname = window.location.hostname;
|
||||
const is_local_dev =
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '[::1]' ||
|
||||
hostname.endsWith('.localhost');
|
||||
|
||||
if (!is_local_dev || !('serviceWorker' in navigator)) return;
|
||||
|
||||
// Prevent the app bootstrap from re-registering a worker on localhost.
|
||||
// The browser can otherwise keep routing fetches through a stale SW
|
||||
// during iterative iframe testing, which is exactly the noise we want to avoid.
|
||||
try {
|
||||
Object.defineProperty(navigator.serviceWorker, 'register', {
|
||||
configurable: true,
|
||||
value: async () => ({})
|
||||
});
|
||||
} catch {
|
||||
// If the property is not writable in this browser, the unregister
|
||||
// pass below still removes any existing worker registration.
|
||||
}
|
||||
|
||||
// Local iframe testing should not keep an older worker alive, because
|
||||
// Chromium can continue to route fetches through the stale worker until
|
||||
// it is explicitly unregistered.
|
||||
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||
for (const registration of registrations) {
|
||||
registration.unregister().catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Clear any stale runtime caches as well; local testing should always
|
||||
// rebuild from the current source rather than reusing old worker output.
|
||||
if ('caches' in window) {
|
||||
caches.keys().then((cache_keys) => {
|
||||
for (const cache_key of cache_keys) {
|
||||
caches.delete(cache_key).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<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.googleapis.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
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%
|
||||
</head>
|
||||
<!-- h-full w-full overflow-auto -->
|
||||
<!-- overflow-x-scroll -->
|
||||
<body data-sveltekit-preload-data="hover" class="h-full w-full">
|
||||
<div id="ae_loader" aria-hidden="true"></div>
|
||||
<div style="display: contents" class="">%sveltekit.body%</div>
|
||||
</body>
|
||||
</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';
|
||||
|
||||
/**
|
||||
@@ -11,7 +13,7 @@ export const delete_object = async function delete_object({
|
||||
headers = {},
|
||||
params = {},
|
||||
data = {},
|
||||
timeout = 60000,
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
log_lvl = 0,
|
||||
retry_count = 5
|
||||
@@ -43,7 +45,9 @@ export const delete_object = async function delete_object({
|
||||
// Construct the URL with query parameters
|
||||
const url = new URL(endpoint, api_cfg['base_url']);
|
||||
if (params) {
|
||||
Object.keys(params).forEach((key) => url.searchParams.append(key, params[key]));
|
||||
Object.keys(params).forEach((key) =>
|
||||
url.searchParams.append(key, params[key])
|
||||
);
|
||||
}
|
||||
|
||||
// Clean and merge headers without mutating the original api_cfg
|
||||
@@ -70,8 +74,13 @@ export const delete_object = async function delete_object({
|
||||
}
|
||||
|
||||
// Auto-inject Authorization header if JWT is present but header is missing
|
||||
const jwt = headers_cleaned['jwt'] || headers_cleaned['JWT'] || api_cfg['jwt'];
|
||||
if (jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization']) {
|
||||
const jwt =
|
||||
headers_cleaned['jwt'] || headers_cleaned['JWT'] || api_cfg['jwt'];
|
||||
if (
|
||||
jwt &&
|
||||
!headers_cleaned['Authorization'] &&
|
||||
!headers_cleaned['authorization']
|
||||
) {
|
||||
headers_cleaned['Authorization'] = `Bearer ${jwt}`;
|
||||
}
|
||||
|
||||
@@ -90,29 +99,77 @@ export const delete_object = async function delete_object({
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
// Keep timeout handle at attempt scope so catch can always clear it.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.error(`API DELETE request timed out after ${timeout}ms.`);
|
||||
// AbortError alone is ambiguous. Track helper-timeout aborts so
|
||||
// caller/navigation aborts can still fail fast with no retry.
|
||||
let did_timeout_abort = false;
|
||||
timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.error(
|
||||
`API DELETE request timed out after ${timeout}ms.`
|
||||
);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'DELETE',
|
||||
headers: headers_cleaned,
|
||||
body: Object.keys(data).length > 0 ? JSON.stringify(data) : undefined,
|
||||
body:
|
||||
Object.keys(data).length > 0
|
||||
? JSON.stringify(data)
|
||||
: undefined,
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
const response = await fetch_method(url.toString(), fetchOptions).catch(function (
|
||||
error: any
|
||||
) {
|
||||
const response = await fetch_method(
|
||||
url.toString(),
|
||||
fetchOptions
|
||||
).catch(function (error: any) {
|
||||
if (
|
||||
error?.name === 'AbortError' ||
|
||||
error?.name === 'TypeError' ||
|
||||
error?.message?.includes('aborted')
|
||||
) {
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'API DELETE: Request aborted or browser-terminated.',
|
||||
error
|
||||
);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'API DELETE Object *fetch* request was aborted or failed in an unexpected way.',
|
||||
error
|
||||
);
|
||||
return error;
|
||||
});
|
||||
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) {
|
||||
throw new Error(
|
||||
@@ -121,7 +178,9 @@ export const delete_object = async function delete_object({
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Response: status=${response.status} attempt=${attempt}`);
|
||||
console.log(
|
||||
`Response: status=${response.status} attempt=${attempt}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -129,15 +188,37 @@ export const delete_object = async function delete_object({
|
||||
console.warn('404 Not Found. Returning null.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorBody = await response.text();
|
||||
console.error(`HTTP error! status: ${response.status}`, errorBody);
|
||||
|
||||
if (response.status >= 400 && response.status < 404) {
|
||||
const errorBody = await response.text();
|
||||
console.error(
|
||||
`HTTP error! status: ${response.status}`,
|
||||
errorBody
|
||||
);
|
||||
|
||||
// Fail fast on client/auth/validation failures.
|
||||
if (
|
||||
response.status === 400 ||
|
||||
response.status === 401 ||
|
||||
response.status === 403 ||
|
||||
response.status === 422
|
||||
) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(
|
||||
`AUTH DIAGNOSTICS (DELETE): Headers sent for ${endpoint}:`,
|
||||
{
|
||||
has_api_key: !!headers_cleaned['x-aether-api-key'],
|
||||
has_account_id: !!headers_cleaned['x-account-id']
|
||||
}
|
||||
);
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status} - ${errorBody}`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
@@ -148,8 +229,14 @@ export const delete_object = async function delete_object({
|
||||
|
||||
// Return the response data or metadata
|
||||
// Robustly handle V3 response envelopes
|
||||
return return_meta ? json : (json.data !== undefined ? json.data : json);
|
||||
return return_meta
|
||||
? json
|
||||
: json.data !== undefined
|
||||
? json.data
|
||||
: json;
|
||||
} catch (error) {
|
||||
// Ensure per-attempt timeout is always cleared on failure.
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
console.error(`API DELETE error on attempt ${attempt}:`, error);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
@@ -157,9 +244,12 @@ export const delete_object = async function delete_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy.
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(
|
||||
`API DELETE: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
|
||||
);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,7 +44,9 @@ export async function get_ae_obj_id_crud({
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** get_ae_obj_id_crud() *** Type: ${obj_type} ID: ${obj_id}`);
|
||||
console.log(
|
||||
`*** get_ae_obj_id_crud() *** Type: ${obj_type} ID: ${obj_id}`
|
||||
);
|
||||
}
|
||||
|
||||
// V3 Standard: Unified endpoint for all objects
|
||||
@@ -77,7 +79,10 @@ export async function get_ae_obj_id_crud({
|
||||
log_lvl: log_lvl,
|
||||
return_meta: return_meta
|
||||
}).catch(function (error: any) {
|
||||
console.error(`API GET CRUD object ID request failed for ${obj_type}/${obj_id}`, error);
|
||||
console.error(
|
||||
`API GET CRUD object ID request failed for ${obj_type}/${obj_id}`,
|
||||
error
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -86,4 +91,4 @@ export async function get_ae_obj_id_crud({
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface GetAeObjV3Params {
|
||||
/**
|
||||
* Get a single object by ID (V3)
|
||||
*/
|
||||
export async function get_ae_obj_v3({
|
||||
export async function get_ae_obj({
|
||||
api_cfg,
|
||||
obj_type,
|
||||
obj_id,
|
||||
@@ -27,7 +27,7 @@ export async function get_ae_obj_v3({
|
||||
const query_params: key_val = { view, ...params };
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('*** get_ae_obj_v3 ***');
|
||||
console.log('*** get_ae_obj ***');
|
||||
console.log('Endpoint:', endpoint);
|
||||
console.log('Params:', query_params);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ interface GetNestedAeObjV3Params {
|
||||
/**
|
||||
* Get a single nested object by ID (V3)
|
||||
*/
|
||||
export async function get_nested_ae_obj_v3({
|
||||
export async function get_nested_ae_obj({
|
||||
api_cfg,
|
||||
parent_type,
|
||||
parent_id,
|
||||
@@ -71,7 +71,7 @@ export async function get_nested_ae_obj_v3({
|
||||
const query_params: key_val = { view, ...params };
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('*** get_nested_ae_obj_v3 ***');
|
||||
console.log('*** get_nested_ae_obj ***');
|
||||
console.log('Endpoint:', endpoint);
|
||||
console.log('Params:', query_params);
|
||||
}
|
||||
@@ -95,14 +95,17 @@ interface GetAeObjLiV3Params {
|
||||
view?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order_by_li?: Record<string, 'ASC' | 'DESC'> | Record<string, 'ASC' | 'DESC'>[] | null;
|
||||
order_by_li?:
|
||||
| Record<string, 'ASC' | 'DESC'>
|
||||
| Record<string, 'ASC' | 'DESC'>[]
|
||||
| null;
|
||||
delay_ms?: number;
|
||||
params?: key_val;
|
||||
headers?: key_val;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
export async function get_ae_obj_li_v3({
|
||||
export async function get_ae_obj_li({
|
||||
api_cfg,
|
||||
obj_type,
|
||||
for_obj_type,
|
||||
@@ -137,7 +140,7 @@ export async function get_ae_obj_li_v3({
|
||||
if (delay_ms > 0) query_params['delay_ms'] = delay_ms;
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('*** get_ae_obj_li_v3 ***');
|
||||
console.log('*** get_ae_obj_li ***');
|
||||
console.log('Endpoint:', endpoint);
|
||||
console.log('Params:', query_params);
|
||||
console.log('Headers:', headers);
|
||||
@@ -162,12 +165,15 @@ interface GetNestedObjLiV3Params {
|
||||
view?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order_by_li?: Record<string, 'ASC' | 'DESC'> | Record<string, 'ASC' | 'DESC'>[] | null;
|
||||
order_by_li?:
|
||||
| Record<string, 'ASC' | 'DESC'>
|
||||
| Record<string, 'ASC' | 'DESC'>[]
|
||||
| null;
|
||||
delay_ms?: number;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
export async function get_nested_obj_li_v3({
|
||||
export async function get_nested_obj_li({
|
||||
api_cfg,
|
||||
parent_type,
|
||||
parent_id,
|
||||
@@ -195,7 +201,7 @@ export async function get_nested_obj_li_v3({
|
||||
if (delay_ms > 0) params['delay_ms'] = delay_ms;
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('*** get_nested_obj_li_v3 ***');
|
||||
console.log('*** get_nested_obj_li ***');
|
||||
console.log('Endpoint:', endpoint);
|
||||
console.log('Params:', params);
|
||||
}
|
||||
@@ -206,4 +212,4 @@ export async function get_nested_obj_li_v3({
|
||||
params,
|
||||
log_lvl
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { get_object } from './api_get_object';
|
||||
|
||||
// The lookup "obj_type" should broken out into a separate function. - 2024-08-07
|
||||
// Updated 2023-11-15
|
||||
export async function get_ae_obj_li_for_obj_id_crud({
|
||||
api_cfg,
|
||||
obj_type,
|
||||
for_obj_type,
|
||||
for_obj_id, // NOTE: Changed 2023-12-06 to no longer required
|
||||
use_alt_table = false,
|
||||
use_alt_base = false,
|
||||
// inc = {},
|
||||
enabled = 'enabled',
|
||||
hidden = 'not_hidden',
|
||||
order_by_li = null,
|
||||
limit = 999999,
|
||||
offset = 0,
|
||||
// key,
|
||||
// jwt = null,
|
||||
headers = {},
|
||||
params_json = null, // NOTE: This is a JSON object that needs to be safely converted to a string for the params. This is used for the API endpoint. Example: { "fulltext_search": { "default_qry_str": "Search string for default", "address_default_qry_str": "Search string for address", "contact_1_default_qry_str": "Search string for contact_1" } }
|
||||
// json_obj = null, // NOTE: This is a JSON object that needs to be safely converted to a string for the params. This is used for the search endpoint.
|
||||
params = {},
|
||||
return_meta = false,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
api_cfg: any;
|
||||
obj_type: string;
|
||||
for_obj_type: null | string;
|
||||
for_obj_id?: string;
|
||||
use_alt_table?: boolean;
|
||||
use_alt_base?: boolean;
|
||||
// inc?: key_val
|
||||
enabled?: 'enabled' | 'all' | 'not_enabled' | undefined;
|
||||
hidden?: 'hidden' | 'all' | 'not_hidden' | undefined;
|
||||
order_by_li?: any;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
// key: string,
|
||||
// jwt?: string,
|
||||
headers?: any;
|
||||
params_json?: any;
|
||||
// json_obj?: any,
|
||||
params?: key_val;
|
||||
return_meta?: boolean;
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** get_ae_obj_li_for_obj_id_crud() *** [${obj_type}]`);
|
||||
}
|
||||
|
||||
// data = {};
|
||||
// data['super_key'] = key;
|
||||
// data['jwt'] = jwt;
|
||||
// NOTE: The key and or JWT should be in the header of the DELETE, GET, PATCH, POST
|
||||
|
||||
// const endpoint = `/crud/${obj_type}/list`;
|
||||
|
||||
let endpoint = '';
|
||||
if (obj_type == 'account') {
|
||||
endpoint = `/crud/account/list`;
|
||||
} else if (obj_type == 'address') {
|
||||
endpoint = `/crud/address/list`;
|
||||
} else if (obj_type == 'archive') {
|
||||
endpoint = `/crud/archive/list`;
|
||||
} else if (obj_type == 'archive_content') {
|
||||
endpoint = `/crud/archive/content/list`;
|
||||
} else if (obj_type == 'contact') {
|
||||
endpoint = `/crud/contact/list`;
|
||||
} else if (obj_type == 'data_store') {
|
||||
endpoint = `/crud/data_store/list`;
|
||||
} else if (obj_type == 'event') {
|
||||
endpoint = `/crud/event/list`;
|
||||
} else if (obj_type == 'event_abstract') {
|
||||
endpoint = `/crud/event/abstract/list`;
|
||||
} else if (obj_type == 'event_badge') {
|
||||
endpoint = `/crud/event/badge/list`;
|
||||
} else if (obj_type == 'event_device') {
|
||||
endpoint = `/crud/event/device/list`;
|
||||
} else if (obj_type == 'event_exhibit') {
|
||||
endpoint = `/crud/event/exhibit/list`;
|
||||
} else if (obj_type == 'event_exhibit_tracking') {
|
||||
endpoint = `/crud/event/exhibit/tracking/list`;
|
||||
} else if (obj_type == 'event_file') {
|
||||
endpoint = `/crud/event/file/list`;
|
||||
} else if (obj_type == 'event_location') {
|
||||
endpoint = `/crud/event/location/list`;
|
||||
} else if (obj_type == 'event_person') {
|
||||
endpoint = `/crud/event/person/list`;
|
||||
} else if (obj_type == 'event_presentation') {
|
||||
endpoint = `/crud/event/presentation/list`;
|
||||
} else if (obj_type == 'event_presenter') {
|
||||
endpoint = `/crud/event/presenter/list`;
|
||||
} else if (obj_type == 'event_session') {
|
||||
endpoint = `/crud/event/session/list`;
|
||||
} else if (obj_type == 'event_track') {
|
||||
endpoint = `/crud/event/track/list`;
|
||||
} else if (obj_type == 'grant') {
|
||||
endpoint = `/crud/grant/list`;
|
||||
} else if (obj_type == 'hosted_file') {
|
||||
endpoint = `/crud/hosted_file/list`;
|
||||
} else if (obj_type == 'journal') {
|
||||
endpoint = `/crud/journal/list`;
|
||||
} else if (obj_type == 'journal_entry') {
|
||||
endpoint = `/crud/journal/entry/list`;
|
||||
} else if (obj_type == 'order') {
|
||||
endpoint = `/crud/order/list`;
|
||||
} else if (obj_type == 'order_line') {
|
||||
endpoint = `/crud/order/line/list`;
|
||||
} else if (obj_type == 'page') {
|
||||
endpoint = `/crud/page/list`;
|
||||
} else if (obj_type == 'person') {
|
||||
endpoint = `/crud/person/list`;
|
||||
} else if (obj_type == 'post') {
|
||||
endpoint = `/crud/post/list`;
|
||||
} else if (obj_type == 'post_comment') {
|
||||
endpoint = `/crud/post/comment/list`;
|
||||
} else if (obj_type == 'site') {
|
||||
endpoint = `/crud/site/list`;
|
||||
} else if (obj_type == 'sponsorship_cfg') {
|
||||
endpoint = `/crud/sponsorship/cfg/list`;
|
||||
} else if (obj_type == 'sponsorship') {
|
||||
endpoint = `/crud/sponsorship/list`;
|
||||
// } else if (obj_type == 'user') {
|
||||
// endpoint = `/crud/user/list`;
|
||||
} else if (obj_type == 'lu' && for_obj_type == 'country_subdivision') {
|
||||
endpoint = `/crud/lu/country_subdivision/list`;
|
||||
for_obj_type = null;
|
||||
} else if (obj_type == 'lu' && for_obj_type == 'country') {
|
||||
endpoint = `/crud/lu/country/list`;
|
||||
for_obj_type = null;
|
||||
} else if (obj_type == 'lu' && for_obj_type == 'time_zone') {
|
||||
endpoint = `/crud/lu/time_zone/list`;
|
||||
for_obj_type = null;
|
||||
} else {
|
||||
console.log(`Unknown object type: ${obj_type}`);
|
||||
return false;
|
||||
}
|
||||
if (log_lvl) {
|
||||
console.log('Endpoint:', endpoint);
|
||||
}
|
||||
|
||||
if (for_obj_type) {
|
||||
params['for_obj_type'] = for_obj_type;
|
||||
}
|
||||
if (for_obj_id) {
|
||||
params['for_obj_id'] = for_obj_id;
|
||||
}
|
||||
|
||||
params['use_alt_table'] = use_alt_table;
|
||||
params['use_alt_base'] = use_alt_base;
|
||||
|
||||
/* Need to deal with inc params here */
|
||||
|
||||
const allowed_enabled_list = ['all', 'enabled', 'not_enabled'];
|
||||
if (allowed_enabled_list.includes(enabled)) {
|
||||
params['enabled'] = enabled;
|
||||
}
|
||||
|
||||
const allowed_hidden_list = ['all', 'hidden', 'not_hidden'];
|
||||
if (allowed_hidden_list.includes(hidden)) {
|
||||
params['hidden'] = hidden;
|
||||
}
|
||||
|
||||
// NOTE: The order_by_li variable is in the "headers" because if is a the URL GET params do not handle multiple values very well. Maybe base64 encore in the future or something? Reminder that GET requests should not have a body (no JSON).
|
||||
// NOTE: The order_by_li should be a key value pair of the property/DB field to sort and how to sort (ASC or DESC)
|
||||
if (order_by_li) {
|
||||
if (log_lvl) {
|
||||
console.log('Order By:', order_by_li);
|
||||
}
|
||||
headers['order_by_li'] = order_by_li;
|
||||
}
|
||||
|
||||
if (limit >= 0) {
|
||||
params['limit'] = limit;
|
||||
}
|
||||
|
||||
if (offset >= 0) {
|
||||
params['offset'] = offset;
|
||||
}
|
||||
|
||||
if (params_json) {
|
||||
// NOTE: This is a JSON object that needs to be safely converted to a string for the params. This is used for the search endpoint.
|
||||
// Max characters for a GET request is 2083. This is a limitation of the browser (Microsoft IE and Edge).
|
||||
if (log_lvl) {
|
||||
console.log('JSON Object:', params_json);
|
||||
console.log(JSON.stringify(params_json));
|
||||
}
|
||||
// NOTE: "jp" stands for "JSON Params"
|
||||
params['jp'] = encodeURIComponent(JSON.stringify(params_json));
|
||||
if (params['jp'].length > 2083) {
|
||||
console.log(
|
||||
`The JSON object is too large to be used as a GET parameter. The overall max URL length is 2083 characters. Please use the POST endpoint instead. Length = ${params['jp'].length} [THIS DOES NOT EXIST YET]`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if (json_obj) {
|
||||
// // NOTE: This is a JSON object that needs to be safely converted to a string for the params. This is used for the search endpoint.
|
||||
// // Max characters for a GET request is 2083. This is a limitation of the browser (Microsoft IE and Edge).
|
||||
// console.log('JSON Object:', json_obj);
|
||||
// params['json_str'] = encodeURIComponent(JSON.stringify(json_obj));
|
||||
// if (params['json_str'].length > 2083) {
|
||||
// console.log(`The JSON object is too large to be used as a GET parameter. The overall max URL length is 2083 characters. Please use the POST endpoint instead. Length = ${params['json_str'].length} [THIS DOES NOT EXIST YET]`);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('Params:', params);
|
||||
}
|
||||
|
||||
const object_li_get_promise = await get_object({
|
||||
api_cfg: api_cfg,
|
||||
endpoint: endpoint,
|
||||
headers: headers,
|
||||
params: params,
|
||||
return_meta: return_meta,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(object_li_get_promise);
|
||||
}
|
||||
|
||||
return object_li_get_promise;
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { get_object } from './api_get_object';
|
||||
|
||||
// Refactored 2025-11-13 to use a lookup map for endpoints.
|
||||
const objTypeToEndpointMap: Record<string, string> = {
|
||||
account: '/crud/account/list',
|
||||
address: '/crud/address/list',
|
||||
archive: '/crud/archive/list',
|
||||
archive_content: '/crud/archive/content/list',
|
||||
activity_log: '/crud/activity_log/list',
|
||||
contact: '/crud/contact/list',
|
||||
data_store: '/crud/data_store/list',
|
||||
event: '/crud/event/list',
|
||||
event_abstract: '/crud/event/abstract/list',
|
||||
event_badge: '/crud/event/badge/list',
|
||||
event_badge_template: '/crud/event/badge/template/list',
|
||||
event_device: '/crud/event/device/list',
|
||||
event_exhibit: '/crud/event/exhibit/list',
|
||||
event_exhibit_tracking: '/crud/event/exhibit/tracking/list',
|
||||
event_file: '/crud/event/file/list',
|
||||
event_location: '/crud/event/location/list',
|
||||
event_person: '/crud/event/person/list',
|
||||
event_presentation: '/crud/event/presentation/list',
|
||||
event_presenter: '/crud/event/presenter/list',
|
||||
event_session: '/crud/event/session/list',
|
||||
event_track: '/crud/event/track/list',
|
||||
grant: '/crud/grant/list',
|
||||
hosted_file: '/crud/hosted_file/list',
|
||||
journal: '/crud/journal/list',
|
||||
journal_entry: '/crud/journal/entry/list',
|
||||
order: '/crud/order/list',
|
||||
order_line: '/crud/order/line/list',
|
||||
page: '/crud/page/list',
|
||||
person: '/crud/person/list',
|
||||
post: '/crud/post/list',
|
||||
post_comment: '/crud/post/comment/list',
|
||||
site: '/crud/site/list',
|
||||
sponsorship_cfg: '/crud/sponsorship/cfg/list',
|
||||
sponsorship: '/crud/sponsorship/list',
|
||||
// user: '/crud/user/list',
|
||||
'lu-country_subdivision': '/crud/lu/country_subdivision/list',
|
||||
'lu-country': '/crud/lu/country/list',
|
||||
'lu-time_zone': '/crud/lu/time_zone/list'
|
||||
};
|
||||
|
||||
function getEndpointForObjType(obj_type: string, for_obj_type?: string): string {
|
||||
if (obj_type === 'lu' && for_obj_type) {
|
||||
const key = `lu-${for_obj_type}`;
|
||||
const endpoint = objTypeToEndpointMap[key];
|
||||
if (endpoint) return endpoint;
|
||||
}
|
||||
|
||||
const endpoint = objTypeToEndpointMap[obj_type];
|
||||
if (endpoint) return endpoint;
|
||||
|
||||
throw new Error(`Unknown object type: ${obj_type}`);
|
||||
}
|
||||
|
||||
type OrderBy = { [key: string]: 'ASC' | 'DESC' };
|
||||
|
||||
interface GetAeObjLiForObjIdCrudV2Params {
|
||||
api_cfg: any; // Consider defining a specific type for api_cfg
|
||||
obj_type: string;
|
||||
for_obj_type: string;
|
||||
for_obj_id?: string;
|
||||
use_alt_tbl?: boolean | string;
|
||||
use_alt_mdl?: boolean | string;
|
||||
use_alt_exp?: boolean | string;
|
||||
inc?: key_val;
|
||||
enabled?: 'all' | 'enabled' | 'not_enabled';
|
||||
hidden?: 'all' | 'hidden' | 'not_hidden';
|
||||
order_by_li?: OrderBy | OrderBy[] | null;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
headers?: Record<string, string>;
|
||||
params_json?: any;
|
||||
params?: key_val;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
export async function get_ae_obj_li_for_obj_id_crud_v2({
|
||||
api_cfg,
|
||||
obj_type,
|
||||
for_obj_type,
|
||||
for_obj_id,
|
||||
use_alt_tbl = false,
|
||||
use_alt_mdl = false,
|
||||
use_alt_exp = false,
|
||||
enabled = 'enabled',
|
||||
hidden = 'not_hidden',
|
||||
order_by_li = null,
|
||||
limit = 999999,
|
||||
offset = 0,
|
||||
headers = {},
|
||||
params_json = null,
|
||||
params = {},
|
||||
log_lvl = 0
|
||||
}: GetAeObjLiForObjIdCrudV2Params) {
|
||||
if (log_lvl) {
|
||||
console.log('*** get_ae_obj_li_for_obj_id_crud_v2() ***');
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = `/v2${getEndpointForObjType(obj_type, for_obj_type)}`;
|
||||
if (log_lvl) {
|
||||
console.log('Endpoint:', endpoint);
|
||||
}
|
||||
|
||||
// We need to remove a few parameters from the params object that are not allowed.
|
||||
delete params['qry__enabled'];
|
||||
delete params['qry__hidden'];
|
||||
delete params['qry__limit'];
|
||||
delete params['qry__offset'];
|
||||
|
||||
if (for_obj_type) params['for_obj_type'] = for_obj_type;
|
||||
if (for_obj_id) params['for_obj_id'] = for_obj_id;
|
||||
|
||||
if (use_alt_tbl === true) params['tbl_alt'] = 'alt';
|
||||
if (use_alt_mdl === true) params['mdl_alt'] = 'alt';
|
||||
if (use_alt_exp === true) params['exp_alt'] = 'alt';
|
||||
|
||||
const allowed_enabled_list = ['all', 'enabled', 'not_enabled'];
|
||||
if (allowed_enabled_list.includes(enabled)) {
|
||||
params['enabled'] = enabled;
|
||||
}
|
||||
|
||||
const allowed_hidden_list = ['all', 'hidden', 'not_hidden'];
|
||||
if (allowed_hidden_list.includes(hidden)) {
|
||||
params['hidden'] = hidden;
|
||||
}
|
||||
|
||||
// NOTE: The order_by_li variable is in the "headers" because URL GET params do not handle complex objects very well.
|
||||
if (order_by_li) {
|
||||
headers['order_by_li'] = JSON.stringify(order_by_li);
|
||||
}
|
||||
|
||||
if (limit > 0) params['limit'] = limit;
|
||||
if (offset > 0) params['offset'] = offset;
|
||||
|
||||
if (params_json) {
|
||||
// NOTE: "jp" stands for "JSON Params". This is a JSON object that needs to be safely converted to a string for the params.
|
||||
// Max characters for a GET request is ~2000. This is a limitation of the browser.
|
||||
const json_params_str = encodeURIComponent(JSON.stringify(params_json));
|
||||
if (json_params_str.length > 2083) {
|
||||
// Using console.error instead of throwing an error to avoid crashing the app for a known limitation.
|
||||
console.error(
|
||||
`The JSON object is too large to be used as a GET parameter. Max length is 2083 characters. Length = ${json_params_str.length}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
params['jp'] = json_params_str;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('Params:', params);
|
||||
}
|
||||
|
||||
const object_li_get_promise = await get_object({
|
||||
api_cfg: api_cfg,
|
||||
endpoint: endpoint,
|
||||
headers: headers,
|
||||
params: params,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(object_li_get_promise);
|
||||
}
|
||||
|
||||
return object_li_get_promise;
|
||||
} catch (error) {
|
||||
console.error('Error in get_ae_obj_li_for_obj_id_crud_v2:', error);
|
||||
return false; // Or handle the error as appropriate
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ interface GetDataStoreV3Params {
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
export async function get_data_store_v3({
|
||||
export async function get_data_store({
|
||||
api_cfg,
|
||||
code,
|
||||
for_type = null,
|
||||
@@ -24,7 +26,9 @@ export async function get_data_store_v3({
|
||||
log_lvl = 0
|
||||
}: GetDataStoreV3Params): Promise<any> {
|
||||
if (log_lvl) {
|
||||
console.log(`*** get_data_store_v3() *** code=${code} no_account_id=${no_account_id}`);
|
||||
console.log(
|
||||
`*** get_data_store() *** code=${code} no_account_id=${no_account_id}`
|
||||
);
|
||||
}
|
||||
|
||||
const endpoint = `/v3/data_store/code/${code}`;
|
||||
@@ -34,8 +38,10 @@ export async function get_data_store_v3({
|
||||
|
||||
const headers: key_val = {};
|
||||
if (no_account_id) {
|
||||
// This token allows bypassing the mandatory account_id requirement for global defaults
|
||||
headers['x-no-account-id-token'] = 'Nothing to See Here';
|
||||
// TEMPORARY: keep this narrow global-default escape hatch until the
|
||||
// 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({
|
||||
@@ -5,10 +5,10 @@ import type { key_val } from '$lib/stores/ae_stores';
|
||||
* Get a list of lookup objects (V3)
|
||||
* Standardized lookup data like countries, timezones, and subdivisions.
|
||||
* Updated 2026-02-20
|
||||
*
|
||||
*
|
||||
* Endpoint: GET /v3/lookup/{lu_type}/list
|
||||
*/
|
||||
export async function get_ae_lookup_li_v3({
|
||||
export async function get_ae_lookup_li({
|
||||
api_cfg,
|
||||
lu_type,
|
||||
site_id,
|
||||
@@ -16,6 +16,9 @@ export async function get_ae_lookup_li_v3({
|
||||
for_id,
|
||||
include_disabled = false,
|
||||
only_priority = false,
|
||||
order_by_li = null,
|
||||
limit = null,
|
||||
offset = null,
|
||||
params = {},
|
||||
headers = {},
|
||||
log_lvl = 0
|
||||
@@ -27,28 +30,34 @@ export async function get_ae_lookup_li_v3({
|
||||
for_id?: string;
|
||||
include_disabled?: boolean;
|
||||
only_priority?: boolean;
|
||||
order_by_li?: Record<string, 'ASC' | 'DESC'> | null;
|
||||
limit?: number | null;
|
||||
offset?: number | null;
|
||||
params?: key_val;
|
||||
headers?: Record<string, string>;
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** get_ae_lookup_li_v3() *** lu_type=${lu_type}`);
|
||||
console.log(`*** get_ae_lookup_li() *** lu_type=${lu_type}`);
|
||||
}
|
||||
|
||||
const endpoint = `/v3/lookup/${lu_type}/list`;
|
||||
|
||||
|
||||
// Build query params
|
||||
if (site_id) params['site_id'] = site_id;
|
||||
if (for_type) params['for_type'] = for_type;
|
||||
if (for_id) params['for_id'] = for_id;
|
||||
if (include_disabled) params['include_disabled'] = true;
|
||||
if (only_priority) params['only_priority'] = true;
|
||||
if (order_by_li) params['order_by_li'] = JSON.stringify(order_by_li);
|
||||
if (limit != null) params['limit'] = limit;
|
||||
if (offset != null) params['offset'] = offset;
|
||||
|
||||
// Lookup data is often global; ensure account context is handled if needed,
|
||||
// Lookup data is often global; ensure account context is handled if needed,
|
||||
// but GUIDE says it uses site Whitelist Policy.
|
||||
// If no account_id is present in api_cfg, we might need 'x-no-account-id'
|
||||
// If no account_id is present in api_cfg, we might need 'x-no-account-id'
|
||||
// for some lookups if they are public.
|
||||
|
||||
|
||||
return await get_object({
|
||||
api_cfg,
|
||||
endpoint,
|
||||
@@ -14,7 +14,7 @@ export const get_object = async function get_object({
|
||||
headers = {},
|
||||
params = {},
|
||||
data = {},
|
||||
timeout = 90000,
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
return_blob = false,
|
||||
filename = '',
|
||||
@@ -41,7 +41,9 @@ export const get_object = async function get_object({
|
||||
retry_count?: number;
|
||||
}) {
|
||||
if (log_lvl) {
|
||||
console.log(`*** get_object() *** Endpoint: ${endpoint} AE Task ID: ${task_id}`);
|
||||
console.log(
|
||||
`*** get_object() *** Endpoint: ${endpoint} AE Task ID: ${task_id}`
|
||||
);
|
||||
console.log('Params:', params);
|
||||
if (log_lvl > 1) {
|
||||
console.log('Data:', data);
|
||||
@@ -55,7 +57,10 @@ export const get_object = async function get_object({
|
||||
|
||||
// FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
||||
if (log_lvl) console.log('get_object: Browser is offline. Failing fast to allow cache fallback.');
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
'get_object: Browser is offline. Failing fast to allow cache fallback.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,10 +69,9 @@ export const get_object = async function get_object({
|
||||
}
|
||||
|
||||
const url = new URL(endpoint, api_cfg['base_url']);
|
||||
Object.keys(params).forEach((key) => url.searchParams.append(key, params[key]));
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
Object.keys(params).forEach((key) =>
|
||||
url.searchParams.append(key, params[key])
|
||||
);
|
||||
|
||||
// Clean and merge headers without mutating the original api_cfg
|
||||
const headers_cleaned: key_val = {};
|
||||
@@ -96,14 +100,18 @@ export const get_object = async function get_object({
|
||||
}
|
||||
|
||||
// Handle "Bootstrap Paradox" for unauthenticated requests
|
||||
const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
|
||||
const is_valid_bypass = bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
params['key'] ||
|
||||
bypass_val === 'direct-download';
|
||||
const bypass_val =
|
||||
merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
|
||||
const is_valid_bypass =
|
||||
bypass_val === 'bypass' ||
|
||||
bypass_val === 'Nothing to See Here' ||
|
||||
bypass_val === 'direct-download';
|
||||
|
||||
if (is_valid_bypass) {
|
||||
if (log_lvl > 1) console.log('api_get_object: Valid bypass detected. Stripping account ID context.');
|
||||
if (log_lvl > 1)
|
||||
console.log(
|
||||
'api_get_object: Valid bypass detected. Stripping account ID context.'
|
||||
);
|
||||
delete merged_headers['x-account-id'];
|
||||
delete merged_headers['x_account_id'];
|
||||
} else {
|
||||
@@ -126,11 +134,12 @@ export const get_object = async function get_object({
|
||||
}
|
||||
|
||||
// Auto-inject Authorization header if JWT is present but header is missing
|
||||
let jwt = headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
api_cfg['headers']?.['JWT'];
|
||||
let jwt =
|
||||
headers_cleaned['jwt'] ||
|
||||
headers_cleaned['JWT'] ||
|
||||
api_cfg['jwt'] ||
|
||||
api_cfg['headers']?.['jwt'] ||
|
||||
api_cfg['headers']?.['JWT'];
|
||||
|
||||
// Final Fallback: Direct check of primary ae_loc key
|
||||
if (!jwt && typeof localStorage !== 'undefined') {
|
||||
@@ -145,7 +154,11 @@ export const get_object = async function get_object({
|
||||
}
|
||||
}
|
||||
|
||||
if (jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization']) {
|
||||
if (
|
||||
jwt &&
|
||||
!headers_cleaned['Authorization'] &&
|
||||
!headers_cleaned['authorization']
|
||||
) {
|
||||
headers_cleaned['Authorization'] = `Bearer ${jwt}`;
|
||||
}
|
||||
|
||||
@@ -153,10 +166,17 @@ export const get_object = async function get_object({
|
||||
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 = {
|
||||
method: 'GET',
|
||||
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) {
|
||||
@@ -174,18 +194,43 @@ export const get_object = async function get_object({
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
// FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
||||
if (log_lvl) console.log(`get_object: Browser is offline (attempt ${attempt}). Failing fast to allow cache fallback.`);
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`get_object: Browser is offline (attempt ${attempt}). Failing fast to allow cache fallback.`
|
||||
);
|
||||
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 {
|
||||
const response = await fetch_method(url.toString(), fetchOptions).catch(function (
|
||||
error: any
|
||||
) {
|
||||
const response = await fetch_method(
|
||||
url.toString(),
|
||||
{ ...fetchOptions, signal: controller.signal }
|
||||
).catch(function (error: any) {
|
||||
// SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
|
||||
if (error.name === 'AbortError' || error.message?.includes('aborted') || error.name === 'TypeError') {
|
||||
if (
|
||||
error.name === 'AbortError' ||
|
||||
error.message?.includes('aborted') ||
|
||||
error.name === 'TypeError'
|
||||
) {
|
||||
if (log_lvl > 1) {
|
||||
console.log('API GET: Request was aborted or terminated by browser. This is expected during navigation.', error);
|
||||
console.log(
|
||||
'API GET: Request was aborted or terminated by browser. This is expected during navigation.',
|
||||
error
|
||||
);
|
||||
}
|
||||
return error; // Return error to be handled below
|
||||
}
|
||||
@@ -198,13 +243,36 @@ export const get_object = async function get_object({
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Check if we should stop due to abort or network failure
|
||||
if (response instanceof Error || (response && (response.name === 'TypeError' || response.name === 'AbortError'))) {
|
||||
// If it was an explicit abort, definitely stop
|
||||
if (response.name === 'AbortError') return false;
|
||||
// Check if we should stop due to abort or network failure.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'TypeError' ||
|
||||
response.name === 'AbortError'))
|
||||
) {
|
||||
// AbortError can be either timeout or intentional abort.
|
||||
// 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) console.log('API GET Object: Detected NetworkError or TypeError. Failing fast.');
|
||||
return false;
|
||||
// TypeError = transient network failure (ERR_NETWORK_CHANGED,
|
||||
// ERR_NETWORK_IO_SUSPENDED, hotel/conference WiFi blip, etc.).
|
||||
// IMPORTANT: throw here so the retry loop's catch block handles it with
|
||||
// backoff. Returning false would bypass retries entirely.
|
||||
//
|
||||
// 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) {
|
||||
@@ -223,6 +291,14 @@ export const get_object = async function get_object({
|
||||
console.log(
|
||||
`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) {
|
||||
console.log('Response:', response);
|
||||
@@ -231,24 +307,45 @@ export const get_object = async function get_object({
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
if (log_lvl) {
|
||||
console.log('The response was a 404 not found "error". Returning null.');
|
||||
console.log(
|
||||
'The response was a 404 not found "error". Returning null.'
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
|
||||
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
|
||||
if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`);
|
||||
if (
|
||||
response.status === 400 ||
|
||||
response.status === 401 ||
|
||||
response.status === 403 ||
|
||||
response.status === 422
|
||||
) {
|
||||
if (log_lvl)
|
||||
console.error(
|
||||
`API Client Failure (${response.status}). Failing fast.`
|
||||
);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(`AUTH DIAGNOSTICS: Headers sent for ${endpoint}:`, {
|
||||
has_auth: !!headers_cleaned['Authorization'],
|
||||
has_api_key: !!headers_cleaned['x-aether-api-key'],
|
||||
has_account_id: !!headers_cleaned['x-account-id'],
|
||||
jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING'
|
||||
});
|
||||
console.warn(
|
||||
`AUTH DIAGNOSTICS: Headers sent for ${endpoint}:`,
|
||||
{
|
||||
has_auth: !!headers_cleaned['Authorization'],
|
||||
has_api_key:
|
||||
!!headers_cleaned['x-aether-api-key'],
|
||||
has_account_id:
|
||||
!!headers_cleaned['x-account-id'],
|
||||
jwt_preview: jwt
|
||||
? `${jwt.slice(0, 8)}...`
|
||||
: 'MISSING'
|
||||
}
|
||||
);
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
if (browser)
|
||||
ae_auth_error.set({
|
||||
type: 'expired',
|
||||
ts: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Structured Error Handling (V3): Attempt to get rich error metadata
|
||||
@@ -259,7 +356,11 @@ export const get_object = async function get_object({
|
||||
// Not JSON
|
||||
}
|
||||
|
||||
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
'The response was not ok. Structured Error Check:',
|
||||
error_json
|
||||
);
|
||||
|
||||
if (error_json?.meta?.details) {
|
||||
return error_json;
|
||||
@@ -273,7 +374,10 @@ export const get_object = async function get_object({
|
||||
status_code: response.status,
|
||||
details: {
|
||||
category: 'validation',
|
||||
message: typeof error_json.detail === 'string' ? error_json.detail : JSON.stringify(error_json.detail),
|
||||
message:
|
||||
typeof error_json.detail === 'string'
|
||||
? error_json.detail
|
||||
: JSON.stringify(error_json.detail),
|
||||
raw: error_json.detail
|
||||
}
|
||||
}
|
||||
@@ -307,7 +411,9 @@ export const get_object = async function get_object({
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
const percent_completed = Math.round((receivedLength * 100) / contentLength);
|
||||
const percent_completed = Math.round(
|
||||
(receivedLength * 100) / contentLength
|
||||
);
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'GET Blob Progress:',
|
||||
@@ -359,17 +465,25 @@ export const get_object = async function get_object({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`API GET object request *fetch* error on attempt ${attempt}:`, error);
|
||||
// Ensure the per-attempt timeout timer is always cancelled on failure.
|
||||
clearTimeout(timeoutId);
|
||||
console.log(
|
||||
`API GET object request *fetch* error on attempt ${attempt}:`,
|
||||
error
|
||||
);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
console.log('Max retry attempts reached. Returning false.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log retry information
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying. Without a delay, rapid retries on a flaky
|
||||
// connection accomplish nothing and add noise. Caps at 8s so later
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user