Compare commits

...

239 Commits

Author SHA1 Message Date
gitea-actions
74924ea06f Improve resource loader error reporting and thumbnail uploads
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-08-24 10:14:46 +01:00
gitea-actions
4653a2caae Make Resource Loader server-backed and drop local metadata cache
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-08-20 14:19:00 +01:00
gitea-actions
7a60d33610 Auto commit
All checks were successful
Build Org Website / build (push) Successful in 40s
2026-08-20 14:03:52 +01:00
gitea-actions
0b4a68b81b Auto commit
All checks were successful
Build Org Website / build (push) Successful in 39s
2026-08-20 11:33:47 +01:00
gitea-actions
65885dbb3a updates
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-08-20 11:32:08 +01:00
gitea-actions
8dbeca7570 Refine resource loader status grouping and PDF import handling
All checks were successful
Build Org Website / build (push) Successful in 39s
2026-08-19 15:36:00 +01:00
gitea-actions
b8bb1944dd Add Resource Loader SPA and JS test harness
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-08-19 15:03:41 +01:00
gitea-actions
75af3e1f0e Improve search button accessibility and refresh site metadata
All checks were successful
Build Org Website / build (push) Successful in 32s
2026-08-19 12:03:35 +01:00
gitea-actions
d12edea8de Remove obsolete generated assets and dead code
All checks were successful
Build Org Website / build (push) Successful in 43s
2026-08-19 10:53:30 +01:00
gitea-actions
2ee6575539 updates
All checks were successful
Build Org Website / build (push) Successful in 1m4s
2026-08-19 10:45:57 +01:00
gitea-actions
08516fda14 Serve Two Rivers modules with production MIME type
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-08-11 13:40:14 +01:00
gitea-actions
262d943443 Fix Two Rivers production age gate
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-07-31 14:15:42 +01:00
gitea-actions
4659edc658 Add standalone export for Two Rivers at Moonrise
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-07-30 23:37:18 +01:00
gitea-actions
563d4b63cc Tune Princess Lima v2 boss balance and Sun Crystal aid
All checks were successful
Build Org Website / build (push) Successful in 37s
2026-07-30 15:36:59 +01:00
gitea-actions
6066f486de Refine Princess Lima for v2 presentation
All checks were successful
Build Org Website / build (push) Successful in 1m2s
2026-07-30 15:00:45 +01:00
gitea-actions
a6502b168d Polish Princess Lima map data and interaction feedback
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-30 13:58:25 +01:00
gitea-actions
15b626ee0d Rebuild Princess Lima maps around the new visual atlas
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-30 12:48:17 +01:00
gitea-actions
a0c78929b3 Refactor platform UI and data flow across core pages
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-07-30 12:22:44 +01:00
gitea-actions
fe8acac707 Refactor org web platform and remove legacy code
All checks were successful
Build Org Website / build (push) Successful in 39s
2026-07-30 11:29:20 +01:00
gitea-actions
dc1ca5c00e Refine archive world spawn and interaction handling
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-30 10:22:23 +01:00
gitea-actions
2f125f32ae Refine Archive World navigation and gate progression
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-30 10:02:58 +01:00
gitea-actions
c06af38d80 Harden archive-world setup and refresh site links
All checks were successful
Build Org Website / build (push) Successful in 39s
2026-07-30 09:08:12 +01:00
gitea-actions
5b59f5745c Refactor org platform navigation and content flows
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-29 21:42:31 +01:00
gitea-actions
7afc554f5d Refactor org web content and simplify shared UI
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-07-29 15:17:17 +01:00
gitea-actions
db90e9f452 21
Some checks failed
Build Org Website / build (push) Failing after 2m44s
2026-07-21 12:08:21 +01:00
gitea-actions
8dd05d4d03 catchups
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-15 18:53:38 +01:00
gitea-actions
67ab0d2ddb updates
All checks were successful
Build Org Website / build (push) Successful in 34s
2026-07-15 18:36:00 +01:00
gitea-actions
9d758f5254 Update hidden details [skip actions] 2026-07-15 01:00:35 +01:00
gitea-actions
4df4cd1055 update json
All checks were successful
Build Org Website / build (push) Successful in 36s
2026-07-14 12:38:11 +01:00
gitea-actions
2fb88916d5 docs: centralize repository documentation
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-07-13 00:02:22 +01:00
gitea-actions
e82fdf1230 12
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-07-12 23:49:58 +01:00
gitea-actions
f4b388d70b Update hidden details [skip actions] 2026-07-12 23:13:37 +01:00
gitea-actions
90c8321ba3 Update hidden details [skip actions] 2026-07-09 17:27:09 +01:00
gitea-actions
4c9294490e testing 3
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-07-09 17:21:44 +01:00
gitea-actions
0d56f36a26 testing 2
All checks were successful
Build Org Website / build (push) Successful in 30s
2026-07-09 17:16:50 +01:00
gitea-actions
275a955a5f testing
All checks were successful
Build Org Website / build (push) Successful in 34s
2026-07-09 17:06:35 +01:00
gitea-actions
af79b439ce testing first memory x2
All checks were successful
Build Org Website / build (push) Successful in 33s
2026-07-09 16:50:48 +01:00
gitea-actions
17e02977d6 testing first memory x2
All checks were successful
Build Org Website / build (push) Successful in 36s
2026-07-09 16:49:49 +01:00
gitea-actions
9b259ac173 testing first memory
All checks were successful
Build Org Website / build (push) Successful in 38s
2026-07-09 16:46:11 +01:00
gitea-actions
2bb816b2ae change mode and fix build
All checks were successful
Build Org Website / build (push) Successful in 30s
2026-07-08 22:42:05 +01:00
gitea-actions
76a708e8fc Update hidden details [skip actions] 2026-07-08 22:38:19 +01:00
1883097397 Merge pull request 'zq/personalise' (#1) from zq/personalise into main
Some checks failed
Build Org Website / build (push) Failing after 29s
Reviewed-on: #1
2026-07-08 22:31:38 +01:00
gitea-actions
0db2151f92 removing author and cleaning js / css 2026-07-08 22:28:43 +01:00
gitea-actions
d0610a5907 removing stale rnote files 2026-07-08 22:17:11 +01:00
gitea-actions
53df5b46a3 seperating play 2026-07-08 22:14:41 +01:00
gitea-actions
b7f08ba46d seperating build 2026-07-08 22:09:29 +01:00
gitea-actions
832eb1a913 cleaning up 2026-07-08 21:50:06 +01:00
gitea-actions
a980d6393e cleaning
Some checks failed
Build Org Website / build (push) Failing after 47s
2026-07-08 21:38:53 +01:00
gitea-actions
1b8c74a204 updates for org web
Some checks failed
Build Org Website / build (push) Failing after 42s
2026-06-17 20:52:07 +01:00
gitea-actions
cd62a01ec5 updates
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-06-04 14:13:45 +01:00
gitea-actions
d63dc6d2cf 02/06 cleanup
All checks were successful
Build Org Website / build (push) Successful in 55s
2026-06-02 09:55:52 +01:00
gitea-actions
2213fe8a0c 02/06
All checks were successful
Build Org Website / build (push) Successful in 1m6s
2026-06-02 09:48:08 +01:00
gitea-actions
4965341e1e mermaid js 2
All checks were successful
Build Org Website / build (push) Successful in 41s
2026-05-18 15:22:36 +01:00
gitea-actions
5cccf8a822 mermaid js
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-18 15:10:47 +01:00
gitea-actions
d08965126f 18/05
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-18 14:59:07 +01:00
gitea-actions
e6a8f7b1e9 new game 3
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-16 21:41:42 +01:00
gitea-actions
e436fe532c new game 2
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-16 21:29:12 +01:00
gitea-actions
9de58a438a new game
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-16 21:24:03 +01:00
gitea-actions
3dfbf6fd5e 16/05
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-16 21:16:45 +01:00
gitea-actions
75648ff77b Update hidden details [skip actions] 2026-05-16 01:00:46 +01:00
gitea-actions
14d35af78e search redesing
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-15 16:09:25 +01:00
gitea-actions
2488f07808 json
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-15 16:00:24 +01:00
gitea-actions
87926dc7cd Update hidden details [skip actions] 2026-05-15 01:00:06 +01:00
gitea-actions
0392a0fb05 memory observatory redesign
All checks were successful
Build Org Website / build (push) Successful in 41s
2026-05-14 16:45:28 +01:00
gitea-actions
c435a7eb78 search key nav fixes 4
All checks were successful
Build Org Website / build (push) Successful in 43s
2026-05-14 14:39:13 +01:00
gitea-actions
314ca414b3 search key nav fixes 3
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-14 14:28:04 +01:00
gitea-actions
41d5f8820e search key nav fixes 2
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-14 14:20:07 +01:00
gitea-actions
a45015fae8 search key nav fixes
All checks were successful
Build Org Website / build (push) Successful in 43s
2026-05-14 14:14:40 +01:00
gitea-actions
cc958b74af Update hidden details [skip actions] 2026-05-14 12:12:30 +01:00
gitea-actions
d271f03e4d changes to pipeline 2
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-14 12:09:15 +01:00
gitea-actions
cb33ea3c25 Update hidden details [skip actions] 2026-05-14 12:06:59 +01:00
4619213e22 temp change 3
All checks were successful
Build Org Website / build (push) Successful in 41s
2026-05-14 12:05:34 +01:00
7c3d79354e temp change 2
All checks were successful
Build Org Website / build (push) Successful in 1m2s
2026-05-14 12:00:24 +01:00
d20236832b temp change
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-14 11:55:49 +01:00
c182ad18e5 changing tag
All checks were successful
Build Org Website / build (push) Successful in 1m2s
2026-05-14 11:54:15 +01:00
608bfe904e changes to pipeline
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-14 11:53:38 +01:00
269ee61321 new blog
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 23:32:08 +01:00
c9cf182138 search improvements 4
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 23:26:54 +01:00
b84f5d2691 search improvements 3
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-13 23:24:24 +01:00
1b796ce91e search improvements 2
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 23:19:40 +01:00
9eb6e9ccbb search improvements
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 23:14:04 +01:00
ea8a64505b json 2
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 23:04:30 +01:00
43351539bf json
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-13 23:01:52 +01:00
966acb8e15 update memories
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 22:42:40 +01:00
fd20eea287 update jsons
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-13 16:53:49 +01:00
0874b544e0 json update 3
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-13 16:33:18 +01:00
3215d19915 json update 2
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-13 16:27:33 +01:00
a6feebd664 json update
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-13 16:16:54 +01:00
3446f1f6ff check
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-13 16:00:50 +01:00
d8927c17ba removal of authoring service
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-13 15:47:57 +01:00
8feaef21f1 13-05 x3
All checks were successful
Build Org Website / build (push) Successful in 53s
2026-05-13 15:18:42 +01:00
ce05a3b059 13-05 x2
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-13 15:12:17 +01:00
9270f1e6ad stories
All checks were successful
Build Org Website / build (push) Successful in 53s
2026-05-13 14:45:49 +01:00
d69912ec0b 13-05 x1
All checks were successful
Build Org Website / build (push) Successful in 56s
2026-05-13 14:32:34 +01:00
6c8afc2be9 12-05
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-12 16:39:34 +01:00
b9aff29cfd check 2
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-11 23:35:50 +01:00
bb60827782 chekc
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-11 23:31:49 +01:00
dba6a3d733 clean
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-11 16:35:40 +01:00
1278027484 avatar support next to messages x2
All checks were successful
Build Org Website / build (push) Successful in 56s
2026-05-11 16:30:31 +01:00
92093b9351 avatar support next to messages
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-11 16:21:21 +01:00
ad8cd407e2 adding avatars
All checks were successful
Build Org Website / build (push) Successful in 1m1s
2026-05-11 16:13:49 +01:00
669eafadd3 hotfix 3
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-10 23:08:29 +01:00
c865db47da hotfix 2
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 22:50:24 +01:00
8328d43650 recent comments x3
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-10 22:45:46 +01:00
0d9b3cfbd2 recent comments x2
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-10 22:39:44 +01:00
b9cb71a310 recent comments
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-10 22:32:41 +01:00
8f8a5dddf9 hotfix
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 22:09:45 +01:00
fa1f9d010a docs
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-10 19:50:57 +01:00
cb24fa6bd6 docs
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 19:48:06 +01:00
791a4a4d27 remove americanisation 2
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 18:47:42 +01:00
dacc1b9e90 remove americanisation
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 18:46:19 +01:00
cfe2943548 remove sync conflict
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-10 18:43:54 +01:00
9bcc4507d1 clean x2
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-10 18:43:26 +01:00
d9d2454de4 lets see
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 18:42:20 +01:00
2b3dcb45ee clean
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-10 18:41:56 +01:00
4f6d206d4e lets see
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 18:28:30 +01:00
7b3a60f937 json
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 18:25:01 +01:00
6c478207d3 gitignore update
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 18:23:02 +01:00
ba12affd72 typos
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 17:21:09 +01:00
b38b0d3270 persistence
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 17:17:42 +01:00
1566b2c595 more changes
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 17:04:40 +01:00
f267933239 trying to fix clusters
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 16:43:26 +01:00
527bc66b05 rename
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-10 16:29:25 +01:00
52a0160198 updating hidden
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 16:07:51 +01:00
5dc0b93416 updating hidden service x2
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-10 15:56:02 +01:00
771a73029e updating hidden service
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-10 15:43:22 +01:00
0ef179385d adding a hidden authoring service support
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 15:20:34 +01:00
909cde2465 fixing the spelling
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 15:12:45 +01:00
2e3efb201b fixing the hidden js
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-10 15:00:06 +01:00
4c7be9373a uncapitalise lima
All checks were successful
Build Org Website / build (push) Successful in 55s
2026-05-10 14:52:50 +01:00
c67501e446 major character changes
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-10 00:25:36 +01:00
3531636805 change name
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-10 00:04:35 +01:00
83f18a26f0 WOAHHHH
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-10 00:01:54 +01:00
4519cfe804 quirks
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-09 23:44:17 +01:00
fc3962ee44 rpg fleshing out x3
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-09 23:27:49 +01:00
be254d661d rpg fleshing out x2
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 23:03:42 +01:00
083ade12eb rpg fleshing out
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 22:52:16 +01:00
a2bc77805c rpg initial commit
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-09 22:38:51 +01:00
6f29244850 details instructions updates
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-09 20:20:01 +01:00
36ae6062c2 details instructions
Some checks failed
Build Org Website / build (push) Failing after 44s
2026-05-09 20:18:25 +01:00
3f8b7c8342 removing test
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 20:06:58 +01:00
063350b4bc authoring service now gets localstorage
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-09 20:05:32 +01:00
901ee02929 clean 2
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-09 19:59:51 +01:00
90af31d02a clean
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 19:58:16 +01:00
d002554036 clean 2026-05-09 19:58:16 +01:00
e806c865e5 cleanup 2 2026-05-09 19:58:16 +01:00
473bc19d25 fix dash
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-09 19:18:48 +01:00
e0f0e3a0ee cleanup
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-09 19:12:40 +01:00
67424af48b Adding pen icon
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 19:03:43 +01:00
63bd4d5fd4 commenting the db-year bar label
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 18:47:42 +01:00
110f39303d fixing stale runs 2
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 18:44:44 +01:00
2b5a23b566 fixing stale runs
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-09 18:33:35 +01:00
eea34a1c68 updating dark academia stylings 2
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-09 18:16:35 +01:00
bd368b5ccd updating dark academia stylings
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-09 18:08:58 +01:00
e2e4f1467f testing the service
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-09 18:00:14 +01:00
39ee0639b9 using zone colour
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 17:40:30 +01:00
14d931431e z
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-09 17:30:57 +01:00
e7d386b2b2 Adding author link
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-09 17:19:46 +01:00
e22dfc5a12 fix authoring service
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-09 17:13:59 +01:00
e45e05ea40 uhhh
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-09 16:41:52 +01:00
3cd053988c confused
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 16:37:08 +01:00
9623935ed0 changes
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 16:33:36 +01:00
63f260866e authoring service changes
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 16:25:31 +01:00
ca82878edf notes update
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-09 15:07:29 +01:00
fb3a474738 clean 2
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-09 01:27:09 +01:00
dababb86fc clean
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-09 01:23:14 +01:00
52a764664e revert
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-09 01:18:20 +01:00
ae7505d651 Revert "auth"
This reverts commit a18debbf21.
2026-05-09 01:18:08 +01:00
a76b559a0e Revert "hehe"
This reverts commit 138193ce13.
2026-05-09 01:18:08 +01:00
70b428fce7 Revert "hehe 2"
This reverts commit 66848674e9.
2026-05-09 01:18:08 +01:00
1789fd7625 Revert "hehe 3"
This reverts commit bebed1f788.
2026-05-09 01:18:08 +01:00
bebed1f788 hehe 3
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-09 01:11:05 +01:00
66848674e9 hehe 2
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-09 00:55:37 +01:00
138193ce13 hehe
All checks were successful
Build Org Website / build (push) Successful in 47s
2026-05-09 00:36:21 +01:00
a18debbf21 auth
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-09 00:26:04 +01:00
a504eb9179 makefile additions and cleanup
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-08 16:23:57 +01:00
aea803793a testing the pipeline 3
All checks were successful
Build Org Website / build (push) Successful in 55s
2026-05-08 16:11:07 +01:00
7645a34619 testing the pipeline 2
Some checks failed
Build Org Website / build (push) Failing after 52s
2026-05-08 16:04:36 +01:00
3bfc40b460 testing the pipeline
All checks were successful
Build Org Website / build (push) Successful in 55s
2026-05-08 15:52:56 +01:00
ee98d815d1 fixed api key
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-08 15:43:57 +01:00
a92d2eed57 env updates 3
Some checks failed
Build Org Website / build (push) Failing after 46s
2026-05-08 15:40:26 +01:00
1667225694 env updates 2
Some checks failed
Build Org Website / build (push) Failing after 45s
2026-05-08 15:34:52 +01:00
40dbbe3f12 env updates 2026-05-08 15:33:53 +01:00
f68bd5281b env
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-08 15:27:40 +01:00
f511163bee preamble changes
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-08 12:54:27 +01:00
f25d203105 removal of ai
All checks were successful
Build Org Website / build (push) Successful in 51s
2026-05-08 12:44:45 +01:00
a68e262730 changes
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-08 12:44:07 +01:00
dbc897370c adding sitemap style
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-08 12:28:07 +01:00
b8b3871650 testing image
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-08 12:10:29 +01:00
a82053f976 adding uploads to org authoring server
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-08 11:30:07 +01:00
fe9d55d638 changes 4
All checks were successful
Build Org Website / build (push) Successful in 53s
2026-05-08 00:03:57 +01:00
90724c22ff changes 3
Some checks failed
Build Org Website / build (push) Failing after 56s
2026-05-08 00:00:20 +01:00
4533c5129f changes 2
Some checks failed
Build Org Website / build (push) Failing after 53s
2026-05-07 23:57:44 +01:00
7ffca5e39b changes
Some checks failed
Build Org Website / build (push) Failing after 52s
2026-05-07 23:54:01 +01:00
97f497855f cha3
Some checks failed
Build Org Website / build (push) Failing after 52s
2026-05-07 23:44:56 +01:00
6af4cb1d23 cha
Some checks failed
Build Org Website / build (push) Failing after 52s
2026-05-07 23:40:40 +01:00
8262a076de changes 5
Some checks failed
Build Org Website / build (push) Failing after 52s
2026-05-07 16:46:18 +01:00
2df30b9c87 changes 4
Some checks failed
Build Org Website / build (push) Failing after 53s
2026-05-07 16:36:12 +01:00
6322e54ba1 changes 3
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-07 16:32:09 +01:00
3cb44b880c changes 2
All checks were successful
Build Org Website / build (push) Successful in 54s
2026-05-07 16:28:56 +01:00
c65df7d95a changes
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-07 16:21:41 +01:00
3d87e3d1e8 fix 3
All checks were successful
Build Org Website / build (push) Successful in 52s
2026-05-07 15:52:36 +01:00
10c1ffbb19 fix 2
All checks were successful
Build Org Website / build (push) Successful in 55s
2026-05-07 15:44:44 +01:00
c6d21e0530 fix
Some checks failed
Build Org Website / build (push) Failing after 49s
2026-05-07 15:31:52 +01:00
4f0b042f09 adding tests
Some checks failed
Build Org Website / build (push) Failing after 50s
2026-05-07 15:24:00 +01:00
3c1f76e65c makefile update 2
All checks were successful
Build Org Website / build (push) Successful in 48s
2026-05-07 14:34:39 +01:00
dc244c1e98 makefile update
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-07 14:21:32 +01:00
900ef114a3 authoring server 5
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-07 14:02:04 +01:00
2fd5a098b1 authoring server 4
All checks were successful
Build Org Website / build (push) Successful in 44s
2026-05-07 13:53:45 +01:00
d33e1771af authoring server 3
All checks were successful
Build Org Website / build (push) Successful in 1m1s
2026-05-07 13:46:25 +01:00
d3a668f353 authoring server 2
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-07 12:30:48 +01:00
72cd3e89ad authoring server
All checks were successful
Build Org Website / build (push) Successful in 49s
2026-05-07 12:19:23 +01:00
82813556f2 cleanup
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-07 10:25:48 +01:00
cc0b739e66 SHOULD WORK
All checks were successful
Build Org Website / build (push) Successful in 45s
2026-05-07 09:33:39 +01:00
11fbf2f331 retro
All checks were successful
Build Org Website / build (push) Successful in 42s
2026-05-07 00:12:39 +01:00
fa992de25a Update dashboard
All checks were successful
Build Org Website / build (push) Successful in 41s
2026-05-06 23:15:20 +01:00
687126b974 Update runner
All checks were successful
Build Org Website / build (push) Successful in 42s
2026-05-06 22:59:15 +01:00
ee15d02a91 Update path
All checks were successful
Build Org Website / build (push) Successful in 42s
2026-05-06 22:51:50 +01:00
fcf6e0b10e Updates to the build.yml
All checks were successful
Build Org Website / build (push) Successful in 41s
2026-05-06 22:47:52 +01:00
d2266ef1e0 update the script
All checks were successful
Build Org Website / build (push) Successful in 41s
2026-05-06 22:37:29 +01:00
b04154cd10 STUPID STUPID DUMB DUMB BOMBADILO CROCADILO
All checks were successful
Build Org Website / build (push) Successful in 43s
2026-05-06 22:24:27 +01:00
2ae4d73649 STUPID FIXING STUPID INDENTATION STUPID ISSUE STUPID
All checks were successful
Build Org Website / build (push) Successful in 43s
2026-05-06 22:14:17 +01:00
86b6e9baed Adding monitor ps1 2026-05-06 22:12:50 +01:00
0d96b3e81f changing tag
All checks were successful
Build Org Website / build (push) Successful in 43s
2026-05-06 15:24:12 +01:00
2e110ffcf5 2 Add nightly org website build
All checks were successful
Build Org Website / build (push) Successful in 46s
2026-05-06 15:09:10 +01:00
5ad20b29f5 Add nightly org website build
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-06 15:08:33 +01:00
2c2a393285 gitea runners 3
All checks were successful
Build Org Website / build (push) Successful in 50s
2026-05-06 15:05:46 +01:00
7072189dde gitea runners 2
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-06 15:01:14 +01:00
bb299b2f98 gitea runners
Some checks failed
Build Org Website / build (push) Has been cancelled
2026-05-06 14:55:10 +01:00
1f5bd9e089 adding content 2026-04-19 23:10:51 +01:00
172a1f85c3 updating modes and structures 2026-04-02 11:27:50 +01:00
529acd4fcc Wave changes 2026-03-27 15:40:37 +00:00
7fbe850756 updating wird 2026-03-21 17:38:26 +00:00
73ba1b3eb5 updating wird frontend 2026-03-21 17:38:04 +00:00
4d1ab5c244 updating script 2026-03-20 23:55:55 +00:00
8441402f49 all changes 2026-03-20 23:54:19 +00:00
0ea03a61cf more changes 2026-03-20 23:54:11 +00:00
623727e71c changes 2026-03-20 23:53:41 +00:00
20f770ad59 script changes 2026-03-20 23:53:36 +00:00
328 changed files with 107929 additions and 11076 deletions

60
.gitea/workflows/build.yml Executable file
View File

@@ -0,0 +1,60 @@
name: Build Org Website
on:
push:
branches:
- main
paths-ignore:
- assets/content/hidden-details.json
- assets/scripts/hidden-details.js
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
concurrency:
group: org-web-build
cancel-in-progress: false
jobs:
build:
runs-on: org-web
env:
SITE_OUTPUT_DIR: /home/zaine/master-folder/org-platform/org_web/output
steps:
- name: Commit hidden details files on scheduled/manual run
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
run: |
cd /home/zaine/master-folder/org-platform/org_web
git switch main
git config user.name "gitea-actions"
git config user.email "gitea-actions@localhost"
git add assets/content/hidden-details.json assets/scripts/hidden-details.js
if git diff --cached --quiet; then
echo "No hidden details changes to commit."
else
git commit -m "Update hidden details [skip actions]"
git push origin main
fi
- name: Build site
run: |
cd /home/zaine/master-folder/org-platform/org_web
mkdir -p /home/zaine/logs
SITE_CLEAN=1 make 2>&1 | tee /home/zaine/logs/org-web.log
- name: Monitor build and notify Discord
if: always()
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
cd /home/zaine/master-folder/org-platform/org_web
/snap/bin/pwsh -NoProfile -File ./build-logs/gitea-build-monitor.ps1

9
.gitignore vendored
View File

@@ -1,4 +1,9 @@
.venv/ .venv/
node_modules/
assets/games/two-rivers/images/characters/*-source.png
output/ output/
backup/ .org-timestamps/
*~ backups/
__pycache__/
*~
.syncthing*.tmp

5
.projectile Executable file
View File

@@ -0,0 +1,5 @@
-.venv/
-output/
-backup/
-*~
-.packages/

3
.stignore Executable file
View File

@@ -0,0 +1,3 @@
output
.packages
.org-timestamps

View File

@@ -1,19 +1,36 @@
.PHONY: all clean norm help .PHONY: all clean norm test test-elisp test-js author author-install author-start author-stop author-stop-legacy author-restart author-status author-logs author-health author-diagnostics help
VENV := .venv VENV := .venv
PY := $(VENV)/bin/python PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip PIP := $(VENV)/bin/pip
OUTPUT_DIR ?= $(if $(SITE_OUTPUT_DIR),$(SITE_OUTPUT_DIR),output)
PERM_DIR := /home/zaine/master-folder/projects/scripts/bash-scripts
all: fix-permissions clean-output build search clean-venv AUTHOR_SERVICE_DIR := /home/zaine/master-folder/org-platform/authoring-service
all: set-perms clean-output build search clean-venv
build: build:
@echo "Building project..." @echo "Building project..."
emacs -Q --script build-site.el emacs -Q --script build-site.el
set-perms:
@echo "Setting permissions for output directory..."
cd $(PERM_DIR) && sudo ./master-perms.sh
search: $(VENV) search: $(VENV)
@echo "Generating search index" @echo "Generating search index"
$(PY) search-index-json.py $(PY) search-index-json.py
test:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" test
test-elisp:
emacs -Q --batch -l lisp/tests/build-site-tests.el -f ert-run-tests-batch-and-exit
test-js:
node --test tests/*.test.js
$(VENV): $(VENV):
@echo "Generating virtual environment" @echo "Generating virtual environment"
python3 -m venv $(VENV) python3 -m venv $(VENV)
@@ -21,7 +38,7 @@ $(VENV):
clean-output: clean-output:
@echo "Cleaning output directory..." @echo "Cleaning output directory..."
rm -rf output/ rm -rf "$(OUTPUT_DIR)"
clean-venv: clean-venv:
@echo "Cleaning virtual environment..." @echo "Cleaning virtual environment..."
@@ -31,18 +48,52 @@ norm:
@echo "sorting out the backups..." @echo "sorting out the backups..."
find . -path ./backups -prune -o -type f -name '*~' -exec mv {} backups/ \; find . -path ./backups -prune -o -type f -name '*~' -exec mv {} backups/ \;
fix-permissions: author: author-start
echo "shakkal123" | sudo -S chown -R zaine:zaine .
author-install:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-install
author-start:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-start
author-stop:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-stop
author-stop-legacy:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-stop-legacy
author-restart:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-restart
author-health:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-health
author-status:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-status
author-diagnostics:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-diagnostics
author-logs:
$(MAKE) -C "$(AUTHOR_SERVICE_DIR)" author-logs
# Show help message
help: help:
@echo "Available targets:" @echo "Available targets:"
@echo " make - Full rebuild" @echo " make - Full rebuild"
@echo " make build - Rebuild the output directory" @echo " make build - Rebuild the output directory"
@echo " make clean-output - Remove output directory" @echo " make set-perms - Set permissions for output directory"
@echo " make clean-venv - Remove venv directory" @echo " make clean-output - Remove output directory"
@echo " make norm - Move all files that end with ~ to the backup folder" @echo " make clean-venv - Remove venv directory"
@echo " make search - creates the search index" @echo " make norm - Move backup files"
@echo " make help - Show this help message" @echo " make test - Run authoring server tests"
@echo " make test-elisp - Run Emacs Lisp build tests"
@echo " make test-js - Run browser-model JavaScript tests"
@echo " make author - Install and start the authoring UI service"
@echo " make author-install - Install and enable the authoring UI service"
@echo " make author-stop - Stop the authoring UI service"
@echo " make author-restart - Restart the authoring UI service"
@echo " make author-status - Show authoring UI service status and page count"
@echo " make author-diagnostics - Show authoring UI runtime diagnostics"
@echo " make author-logs - Show recent authoring UI service logs"
@echo " make search - Create the search index"
@echo " make help - Show this help message"

406
README.md
View File

@@ -1,5 +1,405 @@
# Introduction # zxh / org-web
This repository contains code for the website https://zainezq.com, which serves as a central familial hub for me. This repository is the source and publishing pipeline for [zainezq.com](https://zainezq.com), a personal and family website built primarily from Emacs Org-mode files. It contains the written content, the Emacs Lisp build system, browser assets, generated indexes, playful interactive pages, and the Gitea Actions workflow that publishes the site.
The site is written using Emacs and Org-mode, and exported using the `org-publish` functionality. See the `build-site.el` script for details on how the site is built and published. This README is the central technical guide for the repository: start here for architecture, authoring, builds, frontend work, testing, and deployment.
## System overview
The application has three boundaries:
1. **Site source (this repository):** Org/Markdown content, Emacs Lisp publishing code, CSS, JavaScript, images, and build tooling.
2. **Static output (**`output/`**):** generated HTML, copied assets, and `search-index.json`. This is the deployable website, not the source of truth.
3. **Runtime services (outside this repository):** the authoring service and the same-origin APIs used by interactive pages. The static site remains readable without them, but the relevant dynamic features will not work.
```mermaid
flowchart LR
Author["Author or authoring service"] --> Source["Org and Markdown source"]
Source --> Emacs["Emacs org-publish pipeline"]
Assets["CSS, JavaScript, images, fonts"] --> Emacs
Emacs --> HTML["output/: HTML and assets"]
HTML --> Search["Python search indexer"]
Search --> Deploy["Static site at zainezq.com"]
Deploy <-->|"same-origin /api requests"| APIs["Runtime APIs outside this repo"]
```
## Repository map
| Path | Purpose | Source or generated? |
|----------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| `index.org` | Home page source | Source |
| `blogs/` | Dated personal writing, grouped by year/month | Source, except generated list pages |
| `posts/` | Longer-lived posts; `posts/career/` has its own index | Source, except generated list pages |
| `home/` | Utility and dashboard-style pages | Source, except `categories.org` |
| `play/` | Interactive and experimental pages | Source |
| `rl/` | Server-synced Resource Loader SPA | Source |
| `lima/` | Org or Markdown pages with special attachment handling | Source, except `lima-list.org` |
| `assets/` | Browser code, styles, fonts, images, and content manifests | Mostly source; some manifests/scripts are generated externally |
| `lisp/` | Modular Emacs Lisp publishing system | Source |
| `lisp/tests/` | ERT tests for the publishing logic | Source |
| `tags/` | Per-tag Org pages created during a build | Generated |
| `recently-updated.org` | Recent-content list created during a build | Generated |
| `sitemap.org` and `*-list.org` | Sitemap/list sources created by `org-publish` | Generated |
| `output/` | Complete publish output | Generated |
| `.org-timestamps/` | Incremental publish cache | Generated |
| `.packages/` | Repository-local Emacs package directory | Generated/cache |
| `build-logs/` | Gitea build monitoring script, cache, and logs | Mixed; script is source, logs/cache are runtime data |
| `docs/` | Historical/design notes for individual features | Supplemental; this README is the central engineering guide |
## Build architecture
`build-site.el` is the entry point. It establishes absolute source/output roots, bootstraps `htmlize`, configures Org, loads the modules in `lisp/`, defines the publishing projects, and runs the build.
```mermaid
flowchart TD
Entry["build-site.el"] --> Core["build-core.el: packages, Org, logging"]
Core --> Modules["Load build modules"]
Modules --> Projects["build-projects.el: org-publish projects"]
Projects --> Prepare["Validate output and timestamp cache"]
Prepare --> Inputs["Generate tag pages and comment manifest"]
Inputs --> Publish["org-publish-all"]
Publish --> OrgHTML["Org/Markdown to HTML"]
Publish --> AssetCopy["Copy assets"]
OrgHTML --> Sync["Final asset sync"]
AssetCopy --> Sync
Sync --> Summary["Build summary"]
```
### Build modules
| Module | Responsibility |
|---------------------|-------------------------------------------------------------------------------------------------------|
| `build-core.el` | Logging, timing, local package bootstrap, and base Org configuration |
| `build-assets.el` | Shared CSS/JS `<head>` manifest, cache-busted asset URLs, and asset synchronization |
| `build-html.el` | Custom HTML backend, page shell, Mermaid export, macros, body classes, tags, and comments |
| `build-projects.el` | `org-publish-project-alist` for the main site, posts, blogs, career, tags, Lima, categories, and assets |
| `build-runner.el` | Cache integrity, generated inputs, publishing, and build metrics |
| `build-lima.el` | Lima Org/Markdown conversion and neighboring attachment copying |
| `build-comments.el` | Comment opt-in detection, comment HTML injection, and comment-page manifest generation |
| `sitemaps.el` | Main Mermaid sitemap and specialized blog/post/career/category lists |
| `tags.el` | Tag discovery, generated tag pages, and tag markup in exported HTML |
| `recently-updated.el` | Recent page list generation using `#+DATE` with mtime fallback |
| `sidenotes.el` | Sidenote suppression rules for special page layouts |
The main `org-main` project recursively publishes every Org file, while narrower projects provide specialized sitemap behavior and options. That overlap is intentional: Org's timestamp cache prevents unnecessary repeated work in normal incremental builds.
### Generated inputs and outputs
Before publishing, the build writes:
- `tags/*.org` from `#+FILETAGS`/`#+TAGS` in posts and blogs.
- `assets/content/comment-pages.json` from pages with `#+COMMENTS: t`.
- `recently-updated.org` from the 26 most recent eligible Org pages.
- sitemap/list Org files through `org-publish`.
After publishing, `search-index-json.py` walks generated HTML, strips script/style markup with Beautiful Soup, and writes `output/search-index.json`. The browser builds a Lunr index from that JSON at page load.
```mermaid
flowchart LR
Metadata["Org metadata"] --> Tags["tags/*.org"]
Metadata --> Comments["comment-pages.json"]
Metadata --> Recent["recently-updated.org"]
Tags --> Publish["HTML publish"]
Comments --> Publish
Recent --> Publish
Publish --> Indexer["search-index-json.py"]
Indexer --> SearchJSON["output/search-index.json"]
SearchJSON --> Lunr["Browser Lunr index"]
```
## Prerequisites
For a full local build, install:
- GNU Emacs with Org mode (Org is bundled with normal Emacs distributions).
- Python 3 with `venv` support.
- `pandoc`, required when publishing Markdown files under `lima/`.
- Network access on the first build if `htmlize` is not already cached in `.packages/`.
Python packages are listed in `requirements.txt` and installed into `.venv` by Make. The default `make` workflow also invokes a machine-specific permission script with `sudo`; use the direct build commands below on machines that do not have that script.
## Build and development commands
### Portable local build
```sh
emacs -Q --script build-site.el
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python search-index-json.py
```
The generated site is in `output/`. Serve it over HTTP so fetch-based features work correctly, for example:
```sh
python3 -m http.server 8000 --directory output
```
Then open `http://127.0.0.1:8000`. Dynamic `/api/...` features require the external runtime services or an equivalent local proxy.
### Make targets
| Command | Effect |
|-------------------|-----------------------------------------------------------------------------------------|
| `make build` | Run the Emacs publishing pipeline |
| `make search` | Create/update `output/search-index.json`, creating `.venv` when needed |
| `make` | Set machine-specific permissions, remove `output/`, build, index search, and remove `.venv` |
| `make clean-output` | Delete the configured output directory |
| `make test-elisp` | Run the Emacs Lisp ERT suite |
| `make test` | Run tests in the sibling authoring-service repository |
| `make norm` | Move Emacs backup files into `backups/` |
| `make help` | List supported targets |
### Build environment variables
| Variable | Meaning |
|-----------------------|--------------------------------------------------------------------------|
| `SITE_OUTPUT_DIR=/path` | Publish somewhere other than `output/`; the Python indexer respects it too |
| `SITE_FORCE=1` | Republish every file instead of using Org timestamps |
| `SITE_DRY=1` | Generate/prepare inputs but skip `org-publish-all` |
| `SITE_CLEAN=1` | Delete output and `.org-timestamps/`, then force a full publish |
The build checks that assets and an entry page exist in the output. If the output is missing or incomplete while timestamps remain, it clears the timestamp cache and forces a full publish to avoid a falsely successful incremental build.
## Authoring content
### Org pages
A typical page is:
```org
#+TITLE: Page title
#+DATE: <2026-07-12 Sun 12:00>
#+FILETAGS: :topic:notes:
#+OPTIONS: num:nil toc:t
#+COMMENTS: t
#+SLUG: stable-page-slug
* First heading
Content goes here.
```
The important metadata is:
- `#+TITLE`: displayed title and sitemap/search label.
- `#+DATE`: canonical publication time and ordering source. Recently updated falls back to filesystem modification time when absent or invalid.
- `#+FILETAGS`: colon-delimited tags used for category/tag pages and rendered under the page title.
- `#+OPTIONS`: normal Org export controls such as `toc:nil`, `num:nil`, or `title:nil`.
- `#+COMMENTS: t`: injects the comment UI and adds the page to the comment manifest.
- `#+SLUG`: stable comment API identifier; defaults to the filename when omitted.
- `#+NO_SIDENOTES`: any non-empty value adds the `no-sidenotes` body class. The `NO_SIDENOTES` or `KANBAN` file tag and a heading containing `KANBAN` do the same.
Use root-relative URLs (`/assets/...`) for shared site assets. Relative Org file links are rewritten to their corresponding HTML links by the exporter.
### Supported export helpers
Mermaid diagrams can be written directly in Org:
```org
#+begin_src mermaid
flowchart LR
A[Write] --> B[Publish]
#+end_src
```
The custom HTML backend emits a Mermaid container, and the shared browser initializer renders it. The build also provides these Org macros:
- `{{{sidenote(id,text)}}}`
- `{{{epigraph(quote,attribution)}}}` and related epigraph variants
- `{{{kbd(text)}}}`
- `{{{margimg(url,alt,caption)}}}`
- `{{{countdown(datetime,label)}}}`
### Where new pages belong
- Blog entry: `blogs/YYYY/MM-month/slug.org`.
- General post: `posts/slug.org`.
- Career post: `posts/career/slug.org`.
- Utility/dashboard page: `home/slug.org`.
- Interactive experiment: `play/slug.org`, usually paired with `assets/scripts/pages/slug.js` and `assets/styles/pages/slug.css`.
- Lima page: `.org` or `.md` under `lima/`.
Do not manually maintain generated list, category, recent, tag, or output files; rebuild them from source.
### Lima Markdown publishing
Markdown under `lima/` is converted to temporary Org with Pandoc, given generated title/date/comment metadata, and then exported through the normal HTML backend. The first Markdown H1 becomes the page title. Sibling directories matching `.attachments.*` are copied beside the published page. Org files in `lima/` use the same attachment-copy behavior without Pandoc.
## Frontend architecture
All static browser assets live under `assets/` and are copied to `output/assets/`. `lisp/build-assets.el` is the authoritative list of globally loaded styles and scripts; it appends deterministic mtime/size query strings for cache busting.
### Styles
The modular CSS hierarchy is:
```text
assets/styles/
├── themes/ design tokens and themes
├── base/ element defaults
├── layout/ site shell and layout
├── components/ reusable UI components
├── pages/ page-specific presentation
├── features/ optional feature presentation
├── utilities/ responsive/utility rules
└── vendor/ third-party CSS
```
Top-level CSS files are compatibility manifests that import the modular files. Prefer editing the modular source, while preserving the top-level entry points until old handwritten references have been removed.
### Scripts
The intended JavaScript boundaries are:
```text
assets/scripts/
├── application/ cross-page use-case orchestration
├── ui/ DOM presentation behavior
├── pages/ page-specific applications
├── features/ optional site-wide features
└── vendor/ vendored third-party libraries
```
The shared script list is loaded on every page, so each module must safely exit when its expected DOM is absent. Page modules should own page composition; reusable browser behavior belongs in `ui/` or `features/`. Top-level duplicate filenames are legacy loaders or compatibility copies—edit the modular file referenced by `build-assets.el` unless a page explicitly loads the legacy path.
Third-party browser dependencies are vendored: Lunr, Mermaid, BiggerPicture, and SVG Pan Zoom. Avoid editing minified vendor files directly.
## Dynamic features and service boundaries
The published HTML is static, but some browser modules rely on same-origin APIs:
| Feature | Browser module | Runtime endpoint |
|----------------------|----------------------------------|--------------------------------------------------|
| Comments and replies | `features/comments.js` | `GET /api/comments/:slug`, `POST /api/comments` |
| Notes board | `pages/notes.js` | `/api/notes` |
| Wird tracker | `pages/wird-tracker.js` | `/api/wird`, `/api/wird/motalah`, `/api/calibre/books` |
| Competency board | `pages/competency-status-board.js` | `/api/competencies/items...` |
| Resource Loader | `pages/resource-loader.js` | `/api/resource-loader`, `/api/resource-loader/thumbnails/:id` |
These APIs are not implemented here. Production routing must serve `output/` and proxy `/api/` to the appropriate backend. When changing an endpoint contract, coordinate the static client and its external service.
The hidden-details feature uses browser `localStorage`/`sessionStorage` for visit and discovery state and may call Open-Meteo after explicit geolocation interaction. Its friendly content source is `assets/content/hidden-details.json`; `assets/scripts/features/hidden-details.js` is generated by the external hidden-narrative authoring workflow. Treat both files as a pair and do not hand-edit only the generated JavaScript.
The Resource Loader at `/rl/` uses the same-origin `/api/resource-loader` backend as its sole metadata source, with ETags protecting concurrent edits. There is no browser metadata fallback. Generated cover thumbnails use `/api/resource-loader/thumbnails/:id` and are removed from local storage after upload. IndexedDB under `zxh-resource-loader` retains only imported PDF binaries on the device where they were added. JSON exports contain metadata and reading sessions, not PDF binaries. Its page code lives in `assets/scripts/pages/resource-loader.js`; the separately testable schema/progress/sync operations live in `resource-loader-model.js` and run with `make test-js`.
## Authoring service
The browser authoring UI is a separate sibling repository at:
```text
/home/zaine/master-folder/org-platform/authoring-service
```
It creates and edits the normal source files described above, stores Lima uploads under `assets/images/hzone/YYYY/MM/`, and enqueues site builds after saves. It is available locally at `http://127.0.0.1:8765` and, behind the production reverse proxy, at `https://author.zainezq.com`.
The Makefile delegates these commands to that repository:
- `make author` / `make author-start`
- `make author-install`
- `make author-stop`
- `make author-restart`
- `make author-health`
- `make author-status`
- `make author-diagnostics`
- `make author-logs`
They depend on the sibling repository existing at the hard-coded path and use its user systemd service.
## Testing and verification
Run the publishing tests:
```sh
make test-elisp
```
For a documentation/content-only change, a dry run validates package loading and generated inputs without rewriting all HTML:
```sh
SITE_DRY=1 emacs -Q --script build-site.el
```
For build or frontend changes, use a clean build, generate the search index, serve `output/`, and inspect the relevant pages in a browser:
```sh
SITE_CLEAN=1 emacs -Q --script build-site.el
.venv/bin/python search-index-json.py
python3 -m http.server 8000 --directory output
```
Verify at least:
- the home page and global navigation;
- a blog/post with a table of contents, tags, sidenotes, and comments as applicable;
- the relevant special page under `home/` or `play/`;
- search results and keyboard navigation;
- browser console/network failures;
- the generated sitemap and tag/category pages.
## Deployment and CI
`.gitea/workflows/build.yml` runs on pushes to `main`, nightly at midnight, and manual dispatch. Push events that only change the hidden-details JSON/generated JS pair are ignored to prevent a build loop.
```mermaid
sequenceDiagram
participant Git as Gitea main
participant Runner as org-web runner
participant Build as Site build
participant Monitor as PowerShell monitor
Git->>Runner: push, schedule, or manual dispatch
opt scheduled or manual
Runner->>Git: commit and push hidden-detail changes if present
end
Runner->>Build: SITE_CLEAN=1 make
Build-->>Runner: static output and logs
Runner->>Monitor: inspect result and authoring tests
Monitor-->>Runner: Discord notification
```
The runner is host-specific. It expects:
- the checkout at `/home/zaine/master-folder/org-platform/org_web`;
- output at that repository's `output/` directory;
- the permission helper referenced by the Makefile;
- PowerShell at `/snap/bin/pwsh`;
- the sibling authoring-service repository;
- Gitea/Discord credentials supplied as Actions secrets.
The workflow's monitor writes diagnostics and uses `DISCORD_WEBHOOK_URL` and `API_KEY`. Never commit those values or other service credentials.
## Safe change checklist
1. Edit source content, modular assets, or `lisp/`; do not edit `output/` as the source of a fix.
2. Check whether the file is generated or a compatibility wrapper before changing it.
3. Add new globally loaded assets to `lisp/build-assets.el`; keep page-only behavior scoped to its page when possible.
4. Preserve stable `#+SLUG` values on comment-enabled pages.
5. Run `make test-elisp` and the smallest representative build/browser checks for the change.
6. Review `git status` carefully: builds intentionally update generated Org files and manifests, while local logs and caches may also change.
7. Commit only the intended source and generated artifacts required by the workflow.
## Troubleshooting
### A build skips files that are missing from `output/`
Run with `SITE_CLEAN=1`. The normal integrity check catches a missing asset directory or entry page, but it does not validate every generated page.
### `htmlize` cannot be installed
The first build needs access to MELPA/ELPA unless `.packages/` already contains it. Restore network access or provide the cached package; the build cannot continue without syntax-highlighting support.
### Lima Markdown fails to publish
Confirm `pandoc` is installed and available on `PATH`. Plain Org publishing does not require Pandoc.
### Search is unavailable
Confirm `output/search-index.json` exists and the site is served over HTTP. Run `make search` after publishing HTML.
### Interactive data does not load locally
Static file serving does not provide `/api/...`. Run/proxy the relevant external services, or expect those features to show their empty/error state.
### The full `make` command fails outside the production host
`make` runs a hard-coded permission helper with `sudo`. Use `make build` followed by `make search`, or the portable commands above, on another machine.

Binary file not shown.

567
assets/content/comment-pages.json Executable file
View File

@@ -0,0 +1,567 @@
[
{
"slug": "benefits-of-reading",
"title": "Benefits of Reading",
"url": "/blogs/2025/08-august/benefits-of-reading.html"
},
{
"slug": "hilberts-hotel",
"title": "Hilbert's Hotel",
"url": "/blogs/2025/08-august/hilberts.hotel.html"
},
{
"slug": "spending-all-day-on-this-website",
"title": "09-08-2025: Website Changes",
"url": "/blogs/2025/08-august/spending-the-whole-day-on-this-website.html"
},
{
"slug": "third-time",
"title": "Third Time",
"url": "/blogs/2025/08-august/third-time.html"
},
{
"slug": "wacom-with-arch",
"title": "Wacom With Arch",
"url": "/blogs/2025/08-august/wacom-with-arch.html"
},
{
"slug": "what-i-want-to-do-with-emacs",
"title": "What Do I Want To Do With Emacs",
"url": "/blogs/2025/08-august/what-do-i-want-to-do-with-emacs.html"
},
{
"slug": "zettlekasten",
"title": "Zettelkasten Method",
"url": "/blogs/2025/08-august/zettelkasten.html"
},
{
"slug": "02-11-25-week-review",
"title": "[02-11-2025] - Weekly Review",
"url": "/blogs/2025/11-november/02-11-week-review.html"
},
{
"slug": "09-11-25-week-review",
"title": "[09-11-2025] - Weekly Review",
"url": "/blogs/2025/11-november/09-11-week-review.html"
},
{
"slug": "16-11-25-week-review",
"title": "[16-11-2025] - Weekly Review",
"url": "/blogs/2025/11-november/16-11-week-review.html"
},
{
"slug": "23-11-25-week-review",
"title": "[23-11-2025] - Weekly Review",
"url": "/blogs/2025/11-november/23-11-week-review.html"
},
{
"slug": "30-11-25-week-review",
"title": "[30-11-2025] - Weekly Review",
"url": "/blogs/2025/11-november/30-11-week-review.html"
},
{
"slug": "07-12-25-week-review",
"title": "[07-12-2025] - Weekly Review",
"url": "/blogs/2025/12-december/07-12-week-review.html"
},
{
"slug": "14-12-25-week-review",
"title": "[14-12-2025] - Weekly Review",
"url": "/blogs/2025/12-december/14-12-week-review.html"
},
{
"slug": "21-12-25-week-review",
"title": "[21-12-2025] - Weekly Review",
"url": "/blogs/2025/12-december/21-12-week-review.html"
},
{
"slug": "28-12-25-week-review",
"title": "[28-12-2025] - Weekly Review",
"url": "/blogs/2025/12-december/28-12-week-review.html"
},
{
"slug": "04-01-26-week-review",
"title": "[04-01-2026] - Weekly Review",
"url": "/blogs/2026/01-january/04-01-week-review.html"
},
{
"slug": "11-01-26-week-review",
"title": "[11-01-2026] - Weekly Review",
"url": "/blogs/2026/01-january/11-01-week-review.html"
},
{
"slug": "18-01-26-week-review",
"title": "[18-01-2026] - Weekly Review",
"url": "/blogs/2026/01-january/18-01-week-review.html"
},
{
"slug": "25-01-26-week-review",
"title": "[25-01-2026] - Weekly Review",
"url": "/blogs/2026/01-january/25-01-week-review.html"
},
{
"slug": "01-02-26-week-review",
"title": "[01-02-2026] - Weekly Review",
"url": "/blogs/2026/02-february/01-02-week-review.html"
},
{
"slug": "08-02-26-week-review",
"title": "[08-02-2026] - Weekly Review",
"url": "/blogs/2026/02-february/08-02-week-review.html"
},
{
"slug": "15-02-26-week-review",
"title": "[15-02-2026] - Weekly Review",
"url": "/blogs/2026/02-february/15-02-week-review.html"
},
{
"slug": "22-02-26-week-review",
"title": "[22-02-2026] - Weekly Review",
"url": "/blogs/2026/02-february/22-02-week-review.html"
},
{
"slug": "24-02-26",
"title": "Integration tests failing (sob)",
"url": "/blogs/2026/02-february/24-02-26.html"
},
{
"slug": "26-02-26",
"title": "Starting the Journeys Upgrade",
"url": "/blogs/2026/02-february/26-02-26.html"
},
{
"slug": "27-02-26",
"title": "Journeys rambles again...",
"url": "/blogs/2026/02-february/27-02-26.html"
},
{
"slug": "third-meeting-with-lima",
"title": "Third Meeting with lima :)",
"url": "/blogs/2026/02-february/third-meeting.html"
},
{
"slug": "01-03-26-week-review",
"title": "[01-03-2026] - Weekly Review",
"url": "/blogs/2026/03-march/01-03-week-review.html"
},
{
"slug": "08-03-26-week-review",
"title": "[08-03-2026] - Weekly Review",
"url": "/blogs/2026/03-march/08-03-week-review.html"
},
{
"slug": "15-03-26-week-review",
"title": "[15-03-2026] - Weekly Review",
"url": "/blogs/2026/03-march/15-03-week-review.html"
},
{
"slug": "22-03-26-week-review",
"title": "[22-03-2026] - Weekly Review",
"url": "/blogs/2026/03-march/22-03-week-review.html"
},
{
"slug": "29-03-26-week-review",
"title": "[29-03-2026] - Weekly Review",
"url": "/blogs/2026/03-march/29-03-week-review.html"
},
{
"slug": "cooking-dinner-29-03-26",
"title": "Cooking Dinner (num)",
"url": "/blogs/2026/03-march/cooking-dinner-29-03-26.html"
},
{
"slug": "feeling-sleepy",
"title": "Feeling extremely sleepy",
"url": "/blogs/2026/03-march/feeling-sleepy.html"
},
{
"slug": "fixing-the-dag-18-03",
"title": "DAG fixes",
"url": "/blogs/2026/03-march/fixing-the-dag-18-03.html"
},
{
"slug": "installing-emacs-23-03",
"title": "Installing Emacs on Windows",
"url": "/blogs/2026/03-march/installing-emacs-23-03.html"
},
{
"slug": "intellectually-challenging-myself-30-03-26",
"title": "Intellectually challenging oneself",
"url": "/blogs/2026/03-march/intellectually-challenging-myself-30-03-26.html"
},
{
"slug": "more-thoughts-regarding-changes-26-03-26",
"title": "More thoughts on schedule changes",
"url": "/blogs/2026/03-march/more-thoughts-regarding-changes-26-03-26.html"
},
{
"slug": "oversleeping-16-03",
"title": "Oversleeping and missing a meeting...",
"url": "/blogs/2026/03-march/oversleeping-16-03.html"
},
{
"slug": "quick-lunch-24-03",
"title": "Making Lunch QUiCk!!",
"url": "/blogs/2026/03-march/quick-lunch-24-03.html"
},
{
"slug": "setting-a-wird-tracker-19-03",
"title": "Setting a Wird Tracker",
"url": "/blogs/2026/03-march/setting-a-wird-tracker-19-03.html"
},
{
"slug": "thoughts-on-life-25-03-26",
"title": "Thoughts on life...",
"url": "/blogs/2026/03-march/thoughts-on-life-25-03-26.html"
},
{
"slug": "training-on-friday-afternoon-27-03-26",
"title": "Training time on Friday afternoon",
"url": "/blogs/2026/03-march/training-on-friday-afternoon-27-03-26.html"
},
{
"slug": "05-04-26-week-review",
"title": "[05-04-2026] - Weekly Review",
"url": "/blogs/2026/04-april/05-04-week-review.html"
},
{
"slug": "12-04-26-week-review",
"title": "[12-04-2026] - Weekly Review",
"url": "/blogs/2026/04-april/12-04-week-review.html"
},
{
"slug": "16-04-26",
"title": "Rambles",
"url": "/blogs/2026/04-april/16-04-26.html"
},
{
"slug": "19-04-26-week-review",
"title": "[19-04-2026] - Weekly Review",
"url": "/blogs/2026/04-april/19-04-week-review.html"
},
{
"slug": "26-04-26-week-review",
"title": "[26-04-2026] - Weekly Review",
"url": "/blogs/2026/04-april/26-04-week-review.html"
},
{
"slug": "action-plan-07-04-26",
"title": "Action plan to change teams",
"url": "/blogs/2026/04-april/action-plan-07-04-26.html"
},
{
"slug": "adding-more-wird-22-04-26",
"title": "Adding another Wird",
"url": "/blogs/2026/04-april/adding-more-wird-22-04-26.html"
},
{
"slug": "april-almost-over-28-04",
"title": "April is almost over...",
"url": "/blogs/2026/04-april/april-almost-over-28-04.html"
},
{
"slug": "comparison-14-04",
"title": "Comparison is the thief of joy",
"url": "/blogs/2026/04-april/comparison-14-04.html"
},
{
"slug": "ending-the-week-17-04-26",
"title": "End of week thoughts",
"url": "/blogs/2026/04-april/ending-the-week-17-04-26.html"
},
{
"slug": "joining-new-team-meeting-02-04-2026",
"title": "New team meeting",
"url": "/blogs/2026/04-april/joining-new-team-meeting-02-04-2026.html"
},
{
"slug": "making-notes-through-org-noter-06-05",
"title": "Making Notes through Org Noter",
"url": "/blogs/2026/04-april/making-notes-through-org-noter-06-05.html"
},
{
"slug": "sitting-outside-in-the-sun-21-04-26",
"title": "Sitting in the sun",
"url": "/blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.html"
},
{
"slug": "starting-new-rotation-08-04-26",
"title": "Starting new rotation",
"url": "/blogs/2026/04-april/starting-new-rotation-08-04-26.html"
},
{
"slug": "weekly-target-06-04-26",
"title": "[06-04-2026] - Weekly target",
"url": "/blogs/2026/04-april/weekly-target-06-04-26.html"
},
{
"slug": "wise-words-29-04",
"title": "Wise Words I need to engrain",
"url": "/blogs/2026/04-april/wise-words-29-04.html"
},
{
"slug": "03-05-26-week-review",
"title": "[03-05-2026] - Weekly Review",
"url": "/blogs/2026/05-may/03-05-week-review.html"
},
{
"slug": "10-05-26-week-review",
"title": "[10-05-2026] - Weekly Review",
"url": "/blogs/2026/05-may/10-05-week-review.html"
},
{
"slug": "17-05-26-week-review",
"title": "[17-05-2026] - Weekly Review",
"url": "/blogs/2026/05-may/17-05-week-review.html"
},
{
"slug": "24-05-26-week-review",
"title": "[24-05-2026] - Weekly Review",
"url": "/blogs/2026/05-may/24-05-week-review.html"
},
{
"slug": "31-05-26-week-review",
"title": "[31-05-2026] - Weekly Review",
"url": "/blogs/2026/05-may/31-05-week-review.html"
},
{
"slug": "ai-datacamp-08-05",
"title": "AI Datacamp",
"url": "/blogs/2026/05-may/ai-datacamp-08-05.html"
},
{
"slug": "authoring-service-09-05-26",
"title": "Authoring Service",
"url": "/blogs/2026/05-may/authoring-service-09-05-26.html"
},
{
"slug": "back-to-back-meetings-13-05",
"title": "Back to back meetings",
"url": "/blogs/2026/05-may/back-to-back-meetings-13-05.html"
},
{
"slug": "characters-12-05",
"title": "Meet the characters!",
"url": "/blogs/2026/05-may/characters-12-05.html"
},
{
"slug": "having-a-daily-checkpoint-s-12-05",
"title": "Having a daily checkpoint(s)",
"url": "/blogs/2026/05-may/having-a-daily-checkpoint-s-12-05.html"
},
{
"slug": "memory-observatory-12-05",
"title": "Memory Observatory",
"url": "/blogs/2026/05-may/memory-observatory-12-05.html"
},
{
"slug": "poppadoms-num-13-05",
"title": "Poppadoms num",
"url": "/blogs/2026/05-may/poppadoms-num-13-05.html"
},
{
"slug": "sun-soak-01-05",
"title": "Soaking in the sun",
"url": "/blogs/2026/05-may/sun-soak-01-05.html"
},
{
"slug": "using-codex-07-05-26",
"title": "Using Codex",
"url": "/blogs/2026/05-may/using-codex-07-05-26.html"
},
{
"slug": "using-gifs-13-05",
"title": "Sans spins for us!",
"url": "/blogs/2026/05-may/using-gifs-13-05.html"
},
{
"slug": "working-on-eid-day-27-05-26",
"title": "Working on Eid day",
"url": "/blogs/2026/05-may/working-on-eid-day-27-05-26.html"
},
{
"slug": "14-06-26-week-review",
"title": "[14-06-2026] - Weekly Review",
"url": "/blogs/2026/06-june/14-06-week-review.html"
},
{
"slug": "21-06-26-week-review",
"title": "[21-06-2026] - Weekly Review",
"url": "/blogs/2026/06-june/21-06-week-review.html"
},
{
"slug": "28-06-26-week-review",
"title": "[28-06-2026] - Weekly Review",
"url": "/blogs/2026/06-june/28-06-week-review.html"
},
{
"slug": "cleaning-up-the-repos",
"title": "Cleaning up the repos",
"url": "/blogs/2026/06-june/cleaning-up-the-repos.html"
},
{
"slug": "05-07-26-week-review",
"title": "[05-07-2026] - Weekly Review",
"url": "/blogs/2026/07-july/05-07-week-review.html"
},
{
"slug": "12-07-26-week-review",
"title": "[12-07-2026] - Weekly Review",
"url": "/blogs/2026/07-july/12-07-week-review.html"
},
{
"slug": "19-07-26-week-review",
"title": "[19-07-2026] - Weekly Review",
"url": "/blogs/2026/07-july/19-07-week-review.html"
},
{
"slug": "26-07-26-week-review",
"title": "[26-07-2026] - Weekly Review",
"url": "/blogs/2026/07-july/26-07-week-review.html"
},
{
"slug": "02-08-26-week-review",
"title": "[02-08-2026] - Weekly Review",
"url": "/blogs/2026/08-august/02-08-week-review.html"
},
{
"slug": "09-08-26-week-review",
"title": "[09-08-2026] - Weekly Review",
"url": "/blogs/2026/08-august/09-08-week-review.html"
},
{
"slug": "16-08-26-week-review",
"title": "[16-08-2026] - Weekly Review",
"url": "/blogs/2026/08-august/16-08-week-review.html"
},
{
"slug": "publish-pages",
"title": "How to publish pages using Org Publish",
"url": "/blogs/publish-pages.html"
},
{
"slug": "setup",
"title": "Setup",
"url": "/home/guide/setup.html"
},
{
"slug": "backlog",
"title": "Backlog",
"url": "/home/backlog.html"
},
{
"slug": "wedding-countdown",
"title": "Countdown",
"url": "/home/countdown.html"
},
{
"slug": "notes",
"title": "Notes",
"url": "/home/notes.html"
},
{
"slug": "services",
"title": "Service",
"url": "/home/services.html"
},
{
"slug": "competency-board",
"title": "Competency Status Board",
"url": "/home/status.html"
},
{
"slug": "airflow",
"title": "Datamarts, Airflow and DAG's",
"url": "/posts/career/airflow.html"
},
{
"slug": "career-intro",
"title": "Career Introduction",
"url": "/posts/career/career-intro.html"
},
{
"slug": "cross-site-scripting-xss",
"title": "Cross-Site Scripting (XSS)",
"url": "/posts/career/cross-site-scripting-xss.html"
},
{
"slug": "database-permissions",
"title": "Database Permissions, Roles, and Accounts",
"url": "/posts/career/database-permissions.html"
},
{
"slug": "high-availability-disaster-recovery",
"title": "High Availability, Disaster Recovery and Business Continuity",
"url": "/posts/career/ha-dr.html"
},
{
"slug": "invest-principles",
"title": "Invest Principles",
"url": "/posts/career/invest-principles.html"
},
{
"slug": "understands-the-javascript-language",
"title": "Understands the Javascript language",
"url": "/posts/career/javascript.html"
},
{
"slug": "lean",
"title": "Lean",
"url": "/posts/career/lean.html"
},
{
"slug": "management-of-self",
"title": "Management of self training",
"url": "/posts/career/management-of-self.html"
},
{
"slug": "monitoring-and-logging",
"title": "Monitoring and Logging",
"url": "/posts/career/monitoring-and-logging.html"
},
{
"slug": "normalisation",
"title": "Benefits of Normalisation",
"url": "/posts/career/normalisation.html"
},
{
"slug": "owasp-top-10",
"title": "OWASP Top Ten",
"url": "/posts/career/owasp.html"
},
{
"slug": "pipelines-learning",
"title": "Pipelines and how they work (as well as CI/CD)",
"url": "/posts/career/pipelines.html"
},
{
"slug": "probation-objectives",
"title": "Probation Objectives:",
"url": "/posts/career/probation-objectives.html"
},
{
"slug": "requirements-features",
"title": "Requirements, features, user stories, tasks, walking skeletons",
"url": "/posts/career/requirements-features.html"
},
{
"slug": "restful-api",
"title": "Restful API",
"url": "/posts/career/restful-api.html"
},
{
"slug": "retrospectives",
"title": "Retrospectives",
"url": "/posts/career/retrospectives.html"
},
{
"slug": "solid-principles",
"title": "SOLID Principles",
"url": "/posts/career/solid-principles.html"
},
{
"slug": "wireframe-designs",
"title": "Wireframe Designs",
"url": "/posts/career/wireframe-designs.html"
}
]

1785
assets/content/hidden-details.json Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"schemaVersion": 1,
"vaults": [
{
"id": "vault-work",
"name": "Work",
"colour": "#526d82",
"folders": [],
"resources": [],
"createdAt": "2026-08-19T00:00:00.000Z",
"updatedAt": "2026-08-19T00:00:00.000Z"
},
{
"id": "vault-personal-study",
"name": "Personal Study",
"colour": "#9b6b43",
"folders": [],
"resources": [],
"createdAt": "2026-08-19T00:00:00.000Z",
"updatedAt": "2026-08-19T00:00:00.000Z"
}
]
}

Binary file not shown.

0
assets/fonts/NotoSans-Regular.ttf Normal file → Executable file
View File

0
assets/fonts/Roboto-Regular.ttf Normal file → Executable file
View File

0
assets/fonts/times.ttf Normal file → Executable file
View File

BIN
assets/icons/pen-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

0
assets/images/240226-pr.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

0
assets/images/26-02-26-di-errors-2.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 265 KiB

0
assets/images/26-02-26-di-errors.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 324 KiB

0
assets/images/27-02-26-xserver.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

0
assets/images/blogs/2026-03-12-journeys-branch.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

0
assets/images/blogs/2026-03-18-slack-dag-manager.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 929 KiB

After

Width:  |  Height:  |  Size: 929 KiB

View File

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 291 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

0
assets/images/lima-third-meeting.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

0
assets/images/reviews/competency-powerbi-report.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -0,0 +1,32 @@
/* Function for setting the theme */
(function(){
const root = document.documentElement;
const storageKey = "theme";
const themes = ["light", "dark", "dark-academia"];
const labels = {
"light": "Light",
"dark": "Dark",
"dark-academia": "Academia"
};
const saved = localStorage.getItem(storageKey);
if (themes.includes(saved)) {
root.setAttribute("data-theme", saved);
}
const btn = document.getElementById("theme-toggle");
if (!btn) return;
const updateButton = () => {
const current = root.getAttribute("data-theme");
const label = labels[current] || "Auto";
btn.textContent = `Theme: ${label}`;
btn.setAttribute("aria-label", `Current theme: ${label}. Switch theme.`);
};
updateButton();
btn.addEventListener("click", () => {
const current = root.getAttribute("data-theme");
const index = themes.indexOf(current);
const target = themes[index === -1 ? 1 : (index + 1) % themes.length];
root.setAttribute("data-theme", target);
localStorage.setItem(storageKey, target);
updateButton();
});
})();

File diff suppressed because one or more lines are too long

View File

@@ -1,197 +1,7 @@
let pageSlug = null; (function () {
"use strict";
/* ----------------------------- var script = document.createElement("script");
* DOM builders script.src = "/assets/scripts/features/comments.js";
* ----------------------------- */ script.defer = true;
document.head.appendChild(script);
}());
function showReplyForm(container, parentId) {
if (container.querySelector(".reply-form")) return;
const form = document.createElement("form");
form.className = "reply-form";
form.innerHTML = `
<input type="text" name="author" placeholder="Your name (optional)">
<textarea name="content" required placeholder="Write a reply..."></textarea>
<button type="submit">Post reply</button>
`;
form.addEventListener("submit", async e => {
e.preventDefault();
const author = form.author.value.trim();
const content = form.content.value.trim();
if (!content) return;
const ok = await postComment(author, content, parentId);
if (ok) loadComments();
else alert("Failed to post reply");
});
container.appendChild(form);
}
function createComment(comment) {
const wrapper = document.createElement("div");
wrapper.className = "comment";
wrapper.dataset.id = comment.id;
const author = document.createElement("strong");
author.textContent = comment.author || "Anonymous";
const body = document.createElement("p");
body.textContent = comment.content;
const time = document.createElement("time");
time.className = "comment-date";
time.dateTime = comment.created_at;
time.textContent = new Date(comment.created_at).toLocaleString();
const replyBtn = document.createElement("button");
replyBtn.className = "comment-reply";
replyBtn.textContent = "Reply";
replyBtn.onclick = () =>
showReplyForm(wrapper, Number(comment.id));
wrapper.append(author, body, time, replyBtn);
if (comment.children?.length) {
const replies = document.createElement("div");
replies.className = "comment-children";
comment.children.forEach(child => {
replies.appendChild(createComment(child));
});
wrapper.appendChild(replies);
}
console.log("PARENT_ID RECEIVED:", comment.parent_id)
return wrapper;
}
function buildCommentTree(comments) {
const byId = {};
const roots = [];
comments.forEach(comment => {
comment.children = [];
byId[comment.id] = comment;
});
comments.forEach(comment => {
if (comment.parent_id) {
const parent = byId[comment.parent_id];
if (parent) parent.children.push(comment);
} else {
roots.push(comment);
}
});
return roots;
}
/* -----------------------------
* Rendering
* ----------------------------- */
function renderComments(comments) {
const list = document.getElementById("comments-list");
list.innerHTML = "";
if (!comments.length) {
const empty = document.createElement("p");
empty.className = "comments-empty";
empty.textContent = "No comments yet.";
list.appendChild(empty);
return;
}
const tree = buildCommentTree(comments);
tree.forEach(comment => list.appendChild(createComment(comment)));
}
/* -----------------------------
* API
* ----------------------------- */
async function fetchComments() {
const res = await fetch(`/api/comments/${pageSlug}`);
if (!res.ok) {
console.error("Failed to fetch comments");
return [];
}
return await res.json();
}
async function postComment(author, content, parentId = null) {
const res = await fetch("/api/comments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
page_slug: pageSlug,
author: author || null,
content,
parent_id: parentId
})
});
return res.ok;
}
/* -----------------------------
* Form handling
* ----------------------------- */
function wireCommentForm() {
const form = document.getElementById("comment-form");
form.addEventListener("submit", async event => {
event.preventDefault();
const authorInput = form.querySelector("input[name='author']");
const textarea = form.querySelector("textarea[name='content']");
const author = authorInput.value.trim();
const content = textarea.value.trim();
if (!content) return;
const ok = await postComment(author, content);
if (ok) {
textarea.value = "";
loadComments();
} else {
alert("Failed to post comment");
}
});
}
/* -----------------------------
* Entry point
* ----------------------------- */
async function loadComments() {
const comments = await fetchComments();
renderComments(comments);
}
function initComments() {
const section = document.getElementById("comments");
if (!section) return;
pageSlug = section.dataset.slug;
wireCommentForm();
loadComments();
}
initComments();

View File

@@ -1,240 +1,7 @@
const STATES = [ (function () {
{ key: "completed", label: "Completed" }, "use strict";
{ key: "manager_review", label: "Manager Review" }, var script = document.createElement("script");
{ key: "in_progress", label: "In Progress" }, script.src = "/assets/scripts/pages/competency-status-board.js";
{ key: "not_started", label: "Not Started" }, script.defer = true;
{ key: "comments", label: "Comments" }, document.head.appendChild(script);
]; }());
const LEVELS = [
{ key: "graduate", label: "Graduate" },
{ key: "engineer", label: "Engineer" },
];
let currentLevel = LEVELS[0].key;
function isMobile() {
return window.matchMedia("(max-width: 600px)").matches;
}
function renderLevelTabs() {
const board = document.getElementById("kanban-board");
if (!board || document.getElementById("level-tabs")) return;
const nav = document.createElement("div");
nav.id = "level-tabs";
LEVELS.forEach(({ key, label }) => {
const btn = document.createElement("button");
btn.textContent = label;
btn.dataset.level = key;
btn.className = "level-tab" + (key === currentLevel ? " active" : "");
btn.addEventListener("click", () => {
currentLevel = key;
document.querySelectorAll(".level-tab").forEach(b =>
b.classList.toggle("active", b.dataset.level === key)
);
loadBoard();
});
nav.appendChild(btn);
});
board.insertAdjacentElement("beforebegin", nav);
if (!document.getElementById("level-tab-styles")) {
const style = document.createElement("style");
style.id = "level-tab-styles";
style.textContent = `
#level-tabs {
display: flex;
gap: 0.5em;
margin-bottom: 1em;
}
.level-tab {
padding: 0.4em 1.2em;
border: 1px solid #ccc;
background: none;
cursor: pointer;
border-radius: 4px;
font-size: 0.95em;
}
.level-tab.active {
background: #333;
color: #fff;
border-color: #333;
}
`;
document.head.appendChild(style);
}
}
function populateMobileControls(items) {
if (!isMobile()) return;
const panel = document.getElementById("mobile-move-panel");
const itemSelect = document.getElementById("move-item");
const fromSelect = document.getElementById("move-from");
const toSelect = document.getElementById("move-to");
itemSelect.innerHTML = "<option value=''>Select competency</option>";
fromSelect.innerHTML = "<option value=''>From</option>";
toSelect.innerHTML = "<option value=''>To</option>";
items.forEach((item) => {
const opt = document.createElement("option");
opt.value = item.id;
opt.textContent = item.title;
opt.dataset.state = item.state;
itemSelect.appendChild(opt);
});
STATES.forEach((s) => {
fromSelect.appendChild(new Option(s.label, s.key));
toSelect.appendChild(new Option(s.label, s.key));
});
itemSelect.onchange = () => {
const selected = itemSelect.selectedOptions[0];
if (selected?.dataset.state) {
fromSelect.value = selected.dataset.state;
}
};
}
const moveConfirmBtn = document.getElementById("move-confirm");
if (moveConfirmBtn) {
moveConfirmBtn.addEventListener("click", async () => {
const itemId = document.getElementById("move-item").value;
const from = document.getElementById("move-from").value;
const to = document.getElementById("move-to").value;
if (!itemId || !to) {
alert("Select an item and target column");
return;
}
if (from === to) {
alert("Item is already in that column");
return;
}
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: to }),
});
if (res.ok) {
loadBoard();
} else {
alert("Failed to move competency");
}
});
}
let draggedItemId = null;
function countByState(items) {
return items.reduce((acc, item) => {
acc[item.state] = (acc[item.state] || 0) + 1;
return acc;
}, {});
}
async function updateProgress(items) {
const total = items.length;
const completed = items.filter(i => i.state === "completed").length;
const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
const fill = document.getElementById("progress-fill");
const label = document.getElementById("progress-label");
if (fill) fill.style.width = `${percent}%`;
if (label) label.textContent = `${percent}% completed`;
}
function createCard(item) {
const card = document.createElement("div");
card.className = "kanban-card";
card.draggable = true;
card.dataset.id = item.id;
card.textContent = item.title;
card.addEventListener("dragstart", () => {
draggedItemId = item.id;
card.classList.add("dragging");
});
card.addEventListener("dragend", () => {
draggedItemId = null;
card.classList.remove("dragging");
});
return card;
}
function createColumn(state, items, counts) {
const col = document.createElement("div");
col.className = "kanban-column";
col.dataset.state = state.key;
const header = document.createElement("h3");
header.innerHTML = `
<span class="kanban-title">${state.label}</span>
<span class="kanban-count">${counts[state.key] || 0}</span>
`;
const list = document.createElement("div");
list.className = "kanban-list";
list.addEventListener("dragover", (e) => e.preventDefault());
list.addEventListener("drop", async () => {
if (!draggedItemId) return;
const res = await fetch(`/api/competencies/items/${draggedItemId}/state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: state.key }),
});
if (res.ok) {
loadBoard();
} else {
alert("Failed to update state");
}
});
items
.filter((i) => i.state === state.key)
.forEach((i) => list.appendChild(createCard(i)));
col.appendChild(header);
col.appendChild(list);
return col;
}
function renderBoard(items) {
const board = document.getElementById("kanban-board");
if (!board) return;
board.innerHTML = "";
const counts = countByState(items);
STATES.forEach((state) => {
board.appendChild(createColumn(state, items, counts));
});
}
async function loadBoard() {
const res = await fetch(`/api/competencies/items?group=${currentLevel}`);
const items = await res.json();
updateProgress(items);
renderBoard(items);
populateMobileControls(items);
}
renderLevelTabs();
loadBoard();

View File

@@ -0,0 +1,196 @@
let pageSlug = null;
/* -----------------------------
* DOM builders
* ----------------------------- */
function showReplyForm(container, parentId) {
if (container.querySelector(".reply-form")) return;
const form = document.createElement("form");
form.className = "reply-form";
form.innerHTML = `
<input type="text" name="author" placeholder="Your name (optional)">
<textarea name="content" required placeholder="Write a reply..."></textarea>
<button type="submit">Post reply</button>
`;
form.addEventListener("submit", async e => {
e.preventDefault();
const author = form.author.value.trim();
const content = form.content.value.trim();
if (!content) return;
const ok = await postComment(author, content, parentId);
if (ok) loadComments();
else alert("Failed to post reply");
});
container.appendChild(form);
}
function createComment(comment) {
const wrapper = document.createElement("div");
wrapper.className = "comment";
wrapper.dataset.id = comment.id;
wrapper.id = `comment-${comment.id}`;
const author = document.createElement("strong");
author.textContent = comment.author || "Anonymous";
const body = document.createElement("p");
body.textContent = comment.content;
const time = document.createElement("time");
time.className = "comment-date";
time.dateTime = comment.created_at;
time.textContent = new Date(comment.created_at).toLocaleString();
const replyBtn = document.createElement("button");
replyBtn.className = "comment-reply";
replyBtn.textContent = "Reply";
replyBtn.onclick = () =>
showReplyForm(wrapper, Number(comment.id));
wrapper.append(author, body, time, replyBtn);
if (comment.children?.length) {
const replies = document.createElement("div");
replies.className = "comment-children";
comment.children.forEach(child => {
replies.appendChild(createComment(child));
});
wrapper.appendChild(replies);
}
return wrapper;
}
function buildCommentTree(comments) {
const byId = {};
const roots = [];
comments.forEach(comment => {
comment.children = [];
byId[comment.id] = comment;
});
comments.forEach(comment => {
if (comment.parent_id) {
const parent = byId[comment.parent_id];
if (parent) parent.children.push(comment);
} else {
roots.push(comment);
}
});
return roots;
}
/* -----------------------------
* Rendering
* ----------------------------- */
function renderComments(comments) {
const list = document.getElementById("comments-list");
list.innerHTML = "";
if (!comments.length) {
const empty = document.createElement("p");
empty.className = "comments-empty";
empty.textContent = "No comments yet.";
list.appendChild(empty);
return;
}
const tree = buildCommentTree(comments);
tree.forEach(comment => list.appendChild(createComment(comment)));
}
/* -----------------------------
* API
* ----------------------------- */
async function fetchComments() {
const res = await fetch(`/api/comments/${pageSlug}`);
if (!res.ok) {
console.error("Failed to fetch comments");
return [];
}
return await res.json();
}
async function postComment(author, content, parentId = null) {
const res = await fetch("/api/comments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
page_slug: pageSlug,
author: author || null,
content,
parent_id: parentId
})
});
return res.ok;
}
/* -----------------------------
* Form handling
* ----------------------------- */
function wireCommentForm() {
const form = document.getElementById("comment-form");
form.addEventListener("submit", async event => {
event.preventDefault();
const authorInput = form.querySelector("input[name='author']");
const textarea = form.querySelector("textarea[name='content']");
const author = authorInput.value.trim();
const content = textarea.value.trim();
if (!content) return;
const ok = await postComment(author, content);
if (ok) {
textarea.value = "";
loadComments();
} else {
alert("Failed to post comment");
}
});
}
/* -----------------------------
* Entry point
* ----------------------------- */
async function loadComments() {
const comments = await fetchComments();
renderComments(comments);
}
function initComments() {
const section = document.getElementById("comments");
if (!section) return;
pageSlug = section.dataset.slug;
wireCommentForm();
loadComments();
}
initComments();

View File

@@ -0,0 +1,655 @@
(function () {
"use strict";
/*
* Hidden site details
* -------------------
* This file adds small hidden interactions across the site: footer notes,
* click targets, search/keyboard secrets, rare messages, and page-specific
* behavior for the hidden /play pages.
*
* How to add your own things:
* - Add new text to EDITABLE CONTENT arrays such as `details`, `poems`,
* `greetings`, or `lore`.
* - Add search-triggered toast messages to `SEARCH_TOASTS`.
* - Add search-triggered page redirects to `SEARCH_ROUTES`.
* - Add typed keyboard phrases to `KEYBOARD_SECRETS` or `LONG_KEYBOARD_SECRETS`.
* - Add a new feature by writing an `addYourFeature(memory)` function and
* adding it to the `FEATURES` list at the bottom of the file.
*/
// Storage keys. v1 is read once so old visitors keep their hidden progress.
const STORAGE_KEY = "zxh_hidden_details_v2";
const OLD_STORAGE_KEY = "zxh_hidden_details_v1";
// Shared page context. These are captured once so daily random picks stay stable.
const now = new Date();
const hour = now.getHours();
const path = window.location.pathname;
const state = readState();
// -----------------------------
// EDITABLE CONTENT
// -----------------------------
// Generated by the hidden narrative authoring page.
// Friendly source of truth: assets/content/hidden-details.json
const familyLayers = [];
const details = [
"Keep one small appointment with yourself today.",
"Leave one kindness prepared for tomorrow-you.",
"Discipline can speak in a calm voice.",
"When the task feels heavy, remember the life it is trying to make possible.",
"Pause here: you are allowed to be proud of the person who kept returning."
];
const poems = [];
const greetings = [];
const nightMessages = [];
const lore = {
"quotes": [
"14/07/26 - big demo meeting with CEO tomorrow !!",
"Readiness often arrives ten minutes after you begin.",
"Rest is not a reward for becoming exhausted. It is maintenance for the person doing the becoming.",
"Before the world starts making requests, give your own life a few undivided minutes.",
"Every meaningful yes needs a few well-placed noes around it.",
"Motivation is weather. Build a shelter sturdy enough to keep going when the sky changes.",
"Do not design only for your best days. Create a minimum version of the habit that even a tired day can hold.",
"No repetition is wasted just because nobody applauded it.",
"Look backwards long enough to notice the ground you have already crossed.",
"Use the voice that helps you stand up again.",
"Sometimes courage is simply opening the document again.",
"When resistance feels enormous, promise only five minutes. The aim is not to finish; it is to make starting feel safe again.",
"If the habit keeps failing, redesign the doorway: put the book on the pillow, the shoes by the door, the difficult task at the hour your mind is clearest.",
"The exciting beginning and the visible result are connected by ordinary days. Learn to respect the bridge.",
"Completion creates its own kind of energy. Close one tab, send one message, clear one surface, finish one small loop."
],
"conversations": [],
"journals": [
"10/07/26 - surviving a heatwave whilst working :-)",
"You do not need the whole staircase today. Name the next honest step, make it small enough to begin, and let motion teach you what planning cannot.",
"Replace I am lazy with evidence: what drained me, what helped me, and what is the smallest change I can test tomorrow? Curiosity builds what shame only bruises.",
"A missed day is data, not destiny. Return before the story in your head turns one pause into an identity."
],
"warnings": [],
"dreams": [
"The path was hidden, but the compass still answered: values before mood, direction before speed, one careful footfall before certainty.",
"Long after inspiration went home, a single workshop light stayed on. Inside, patience was sanding rough effort into skill.",
"For a long time the seed had nothing visible to show. It was still becoming roots."
],
"cassettes": [],
"fakeUsers": [],
"seasonal": {},
"homepageTakeovers": [],
"roomLinks": []
};
// Exact search text -> toast message.
const SEARCH_TOASTS = {};
// Exact search text -> hidden page route.
const SEARCH_ROUTES = {};
// Short typed phrases. Stored in sessionStorage as a rolling key chain.
const KEYBOARD_SECRETS = [];
// Longer typed phrases and character names.
const LONG_KEYBOARD_SECRETS = [];
// -----------------------------
// STATE HELPERS
// -----------------------------
function readState() {
let current = {};
try {
current = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
} catch (_) {}
if (!current.visits) {
try {
const oldState = JSON.parse(localStorage.getItem(OLD_STORAGE_KEY) || "{}");
current = { ...oldState, migratedFromV1: true };
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
} catch (_) {}
}
return current;
}
function writeState(next) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
} catch (_) {}
}
function pick(list, salt) {
const seed = hash(`${path}|${now.toDateString()}|${salt || ""}`);
return list[seed % list.length];
}
function hash(value) {
let h = 2166136261;
for (let i = 0; i < value.length; i += 1) {
h ^= value.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return Math.abs(h >>> 0);
}
// -----------------------------
// UI HELPERS
// -----------------------------
function hiddenMessageMarkup(message) {
return `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
}
function setHiddenMessage(el, message) {
delete el.dataset.hiddenCharacter;
el.innerHTML = hiddenMessageMarkup(message);
}
function toast(message, duration) {
let el = document.querySelector(".hidden-toast");
if (!el) {
el = document.createElement("div");
el.className = "hidden-toast";
el.setAttribute("role", "status");
document.body.appendChild(el);
}
setHiddenMessage(el, message);
el.classList.add("is-visible");
window.clearTimeout(el._timer);
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600);
}
function redirectTo(route) {
window.location.href = route;
}
function runSecretAction(secret) {
if (secret.route) redirectTo(secret.route);
if (secret.message) toast(secret.message);
if (secret.song) playTinySong();
}
function layerForSearch(value) {
if (value.includes("future")) return 4;
if (value.includes("sensei") || value.includes("aphy")) return 3;
return 2;
}
function rememberVisit() {
const visits = (state.visits || 0) + 1;
const seen = Array.isArray(state.seen) ? state.seen : [];
const pathCounts = { ...(state.pathCounts || {}) };
pathCounts[path] = (pathCounts[path] || 0) + 1;
const layerVisits = { ...(state.layerVisits || {}) };
const next = {
...state,
visits,
lastPath: path,
pathCounts,
layerVisits,
seen: Array.from(new Set([...seen, path])).slice(-100),
firstSeen: state.firstSeen || new Date().toISOString(),
lastSeen: new Date().toISOString()
};
writeState(next);
return next;
}
function awardLayer(layer, reason) {
const next = readState();
next.layerVisits = { ...(next.layerVisits || {}) };
next.layerVisits[layer] = (next.layerVisits[layer] || 0) + 1;
next.lastLayerReason = reason;
writeState(next);
}
// -----------------------------
// GLOBAL FEATURES
// -----------------------------
function addFooterNote(memory) {
const footer = document.querySelector("footer");
if (!footer || !details.length) return;
const note = document.createElement("p");
note.className = "hidden-footer-note";
const base = pick(details, "footer");
setHiddenMessage(note, memory.visits > 4 ? `${base} You have passed through ${memory.visits} times.` : base);
footer.appendChild(note);
}
function addHomepageGreeting(memory) {
const target = document.querySelector("#db-greeting");
if (!target || !greetings.length) return;
setHiddenMessage(target, pick(greetings, `greeting-${memory.visits}`));
const extra = document.createElement("p");
extra.className = "hidden-greeting";
setHiddenMessage(extra, "");
target.closest("div")?.appendChild(extra);
}
function addWhispers() {
// Event: click one of the tiny "?" marks appended to long paragraphs.
const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80);
paragraphs.slice(0, 5).forEach((p, index) => {
if ((hash(path + index) + index) % 3 !== 0) return;
const mark = document.createElement("span");
mark.className = "hidden-whisper";
mark.tabIndex = 0;
mark.textContent = " ?";
const source = index % 2 ? poems : details;
if (!source.length) return;
mark.title = pick(source, `whisper-${index}`);
mark.addEventListener("click", () => {
awardLayer(index % 2 ? 2 : 1, "paragraph whisper");
toast(mark.title);
});
p.appendChild(mark);
});
}
function addCornerObject() {
// Event: click the small fixed button in the bottom-left corner.
const object = document.createElement("button");
object.className = "hidden-corner-object";
object.type = "button";
object.title = "hidden interaction";
object.textContent = state.visits % 2 ? "*" : "~";
document.body.appendChild(object);
let clicks = 0;
object.addEventListener("click", () => {
clicks += 1;
awardLayer(clicks > 3 ? 3 : 1, "corner object");
toast("Hidden interaction recorded.");
if (clicks === 7) window.location.href = "/play/left-behind.html";
});
}
function addLogoSearchAndKeyboardSecrets(memory) {
// Events:
// - Click `.site-brand` repeatedly on the homepage.
// - Type exact words into `#search-input`.
// - Type phrases anywhere on the page.
const nav = document.querySelector(".site-brand");
if (nav) {
let taps = Number(sessionStorage.getItem("zxh_logo_taps") || 0);
nav.addEventListener("click", (event) => {
if (path === "/" || path === "/index.html") {
event.preventDefault();
taps += 1;
sessionStorage.setItem("zxh_logo_taps", String(taps));
awardLayer(taps >= 5 ? 3 : 1, "logo taps");
toast(taps >= 5 ? "Hidden route available: /play/dream-corridor.html" : "Hidden interaction recorded.");
if (taps >= 8) window.location.href = "/play/dream-corridor.html";
}
});
}
const search = document.querySelector("#search-input");
if (search) {
search.addEventListener("input", () => {
const value = search.value.trim().toLowerCase();
if (SEARCH_TOASTS[value]) toast(SEARCH_TOASTS[value]);
if (SEARCH_ROUTES[value]) {
awardLayer(layerForSearch(value), `search ${value}`);
redirectTo(SEARCH_ROUTES[value]);
}
});
}
document.addEventListener("keydown", (event) => {
const key = event.key.toLowerCase();
const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-18);
sessionStorage.setItem("zxh_key_chain", chain);
KEYBOARD_SECRETS
.filter((secret) => chain.endsWith(secret.phrase))
.forEach(runSecretAction);
});
if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
window.setTimeout(() => {
toast("Hidden path recorded.");
writeState({ ...readState(), travellerNoteShown: true });
}, 1600);
}
}
function addRareEvents() {
if (hour >= 22 || hour < 5) {
document.body.classList.add("hidden-late-night");
if (nightMessages.length) window.setTimeout(() => toast(pick(nightMessages, "night"), 2200), 900);
}
const roll = Math.random();
if (roll < 0.003) {
window.setTimeout(() => showDriftNote("", ""), 1800);
awardLayer(3, "rare sensei appearance");
} else if (roll < 0.008) {
window.setTimeout(() => toast("Hidden warning."), 2400);
awardLayer(4, "future warning");
}
}
function showDriftNote(text, art) {
const panel = document.createElement("aside");
panel.className = "hidden-drift-note";
panel.innerHTML = `<div class="hidden-drift-message">${hiddenMessageMarkup(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
panel.querySelector("button").addEventListener("click", () => panel.remove());
document.body.appendChild(panel);
}
function addSecretLinks() {
[
["/play/left-behind.html", "left behind"],
["/play/dream-corridor.html", "dream corridor"],
["/play/kitchen-light.html", "kitchen light"],
...lore.roomLinks
].forEach(([href, label], index) => {
const link = document.createElement("a");
link.className = "hidden-secret-link";
link.href = href;
link.textContent = label;
link.style.left = `${index + 1}px`;
document.body.appendChild(link);
});
}
function enhanceErrors() {
if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) {
toast("Page not found.");
}
}
function addContinuity(memory) {
const milestones = {
64: "Hidden layer unlocked."
};
if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) {
const layer = memory.visits >= 31 ? 4 : memory.visits >= 9 ? 3 : 2;
awardLayer(layer, `visit milestone ${memory.visits}`);
window.setTimeout(() => toast(milestones[memory.visits], 7000), 1200);
writeState({ ...readState(), [`milestone_${memory.visits}`]: true });
}
const count = memory.pathCounts?.[path] || 0;
if ([3, 7, 12].includes(count)) {
window.setTimeout(() => toast("Page revisit recorded.", 6400), 1700);
}
}
function addSeasonAndDates(memory) {
const month = now.getMonth();
const day = now.getDate();
const season = month < 2 || month === 11 ? "winter" : month < 5 ? "spring" : month < 8 ? "summer" : "autumn";
if (Math.random() < 0.18 && lore.seasonal[season]) window.setTimeout(() => toast(lore.seasonal[season], 5600), 2600);
const first = memory.firstSeen ? new Date(memory.firstSeen) : null;
if (first && memory.visits > 1 && first.getMonth() === month && first.getDate() === day) {
window.setTimeout(() => toast("Visitor anniversary recorded.", 7000), 1400);
}
}
function addObjectConstellation() {
// Event: click the small keepsake buttons along the bottom rail.
const rail = document.createElement("div");
rail.className = "hidden-object-rail";
rail.setAttribute("aria-label", "Hidden objects");
const objects = [
["ring", "Hidden object recorded."],
["bot", "Hidden object recorded."],
["tea", "Hidden object recorded."],
["crayon", "Hidden object recorded."],
["clock", "Hidden object recorded."]
];
objects.forEach(([name, message]) => {
const button = document.createElement("button");
button.type = "button";
button.className = `hidden-keepsake hidden-keepsake--${name}`;
button.title = name;
button.textContent = { ring: "o", bot: "#", tea: "u", crayon: "/", clock: ":" }[name];
button.addEventListener("click", () => {
const next = readState();
next.keepsakes = Array.from(new Set([...(next.keepsakes || []), name]));
writeState(next);
awardLayer(next.keepsakes.length >= 3 ? 3 : 2, `keepsake ${name}`);
toast(message);
if (next.keepsakes.length >= objects.length) {
window.setTimeout(() => toast("Hidden object set completed."), 1100);
}
});
rail.appendChild(button);
});
document.body.appendChild(rail);
}
function addWeatherWindow(memory) {
// Event: click the small "window" button. This asks for browser geolocation.
if (memory.visits < 3 || !("geolocation" in navigator)) return;
const button = document.createElement("button");
button.className = "hidden-weather-window";
button.type = "button";
button.title = "Check outside weather";
button.textContent = "window";
button.addEventListener("click", () => {
toast("Checking outside weather.");
navigator.geolocation.getCurrentPosition((pos) => {
const { latitude, longitude } = pos.coords;
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude.toFixed(3)}&longitude=${longitude.toFixed(3)}&current=temperature_2m,precipitation&timezone=auto`)
.then((res) => res.json())
.then((data) => {
const current = data.current || {};
const rain = Number(current.precipitation || 0) > 0;
const temp = Math.round(Number(current.temperature_2m));
toast(rain ? `Outside: rain, ${temp}C.` : `Outside: ${temp}C.`);
})
.catch(() => toast("Weather check failed."));
}, () => toast("No outside weather today."));
});
document.body.appendChild(button);
}
function addOldWebLayer(memory) {
if (Math.random() < 0.08 || memory.visits > 10) {
const stamp = document.createElement("div");
stamp.className = "hidden-oldweb-stamp";
if (!lore.fakeUsers.length) return;
stamp.title = pick(lore.fakeUsers, "fake-user");
stamp.textContent = "best viewed with patience";
stamp.addEventListener("click", () => {
awardLayer(1, "old web stamp");
toast(stamp.title);
});
document.body.appendChild(stamp);
}
const comments = document.querySelector("#comments");
if (comments && lore.fakeUsers.length && !comments.querySelector(".hidden-guestbook-line")) {
const line = document.createElement("p");
line.className = "hidden-guestbook-line";
setHiddenMessage(line, pick(lore.fakeUsers, "comment-lore"));
comments.appendChild(line);
}
}
function addSourceRelics() {
const marker = document.createComment("hidden-details");
document.documentElement.appendChild(marker);
document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => {
if (index > 2) return;
img.alt = "Decorative image.";
});
}
function addLongKeyboardSecrets() {
// Event: type longer hidden phrases anywhere on the page.
document.addEventListener("keydown", (event) => {
const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-24);
sessionStorage.setItem("zxh_second_chain", chain);
LONG_KEYBOARD_SECRETS
.filter((secret) => chain.endsWith(secret.phrase))
.forEach(runSecretAction);
});
}
function addPatienceRewards(memory) {
// Event: no movement, key presses, scrolling, or clicking for 90 seconds.
if (memory.visits < 2) return;
let idleTimer = null;
const startIdle = () => {
window.clearTimeout(idleTimer);
idleTimer = window.setTimeout(() => {
awardLayer(5, "patience");
toast("Idle moment recorded.", 7600);
const next = readState();
next.patientMoments = (next.patientMoments || 0) + 1;
writeState(next);
}, 90000);
};
["mousemove", "keydown", "scroll", "click"].forEach((name) => document.addEventListener(name, startIdle, { passive: true }));
startIdle();
}
function addRareSecondLayer() {
const roll = Math.random();
if ((path === "/" || path === "/index.html") && roll < 0.006) {
document.body.classList.add("hidden-home-takeover");
if (lore.homepageTakeovers.length) window.setTimeout(() => showDriftNote(pick(lore.homepageTakeovers, "takeover"), ""), 600);
} else if (roll < 0.002) {
window.setTimeout(() => showDriftNote("", ""), 1500);
} else if (roll < 0.006) {
if (lore.dreams.length) window.setTimeout(() => toast(pick(lore.dreams, "rare-dream"), 7600), 2000);
} else if (roll < 0.014) {
if (lore.warnings.length) window.setTimeout(() => toast(pick(lore.warnings, "rare-warning"), 6800), 2300);
}
}
function addFamilyLayerIndex(memory) {
if (!path.includes("/play/family-layer-index")) return;
const target = document.querySelector("[data-family-layer-index]");
if (!target) return;
const visits = readState().layerVisits || {};
target.innerHTML = familyLayers.map((line, layer) => {
const count = visits[layer] || 0;
return `<li class="hidden-layer-message"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
}).join("");
}
function addPageSpecificSecrets() {
// Events only used by specific `/play/...` pages.
if (path.includes("/play/cassette-log")) {
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
button.addEventListener("click", () => {
awardLayer(index >= 2 ? 4 : 2, "voice note");
if (lore.cassettes.length) toast(lore.cassettes[index % lore.cassettes.length], 7600);
playTinySong();
});
});
}
if (path.includes("/play/patience-game")) {
const target = document.querySelector("[data-patience-target]");
const count = document.querySelector("[data-patience-count]");
if (target && count) {
let seconds = 0;
setInterval(() => {
seconds += 1;
count.textContent = String(seconds);
if ([30, 90, 180].includes(seconds)) {
setHiddenMessage(target, "Patience milestone recorded.");
}
}, 1000);
}
}
if (path.includes("/play/terminal-cupboard")) {
const input = document.querySelector("[data-cupboard-input]");
const log = document.querySelector("[data-cupboard-log]");
const commands = {
help: "commands: layer, exit",
layer: familyLayers.join("\n"),
exit: "Closed."
};
input?.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
const value = input.value.trim().toLowerCase();
const line = document.createElement("p");
setHiddenMessage(line, `> ${value}\n${commands[value] || "Unknown command."}`);
log.appendChild(line);
input.value = "";
});
}
}
function addInvisibleHoverSecrets() {
document.querySelectorAll("h1, h2").forEach((heading, index) => {
if (index > 5) return;
heading.classList.add("hidden-hover-memory");
const source = [...lore.quotes, ...lore.journals, ...poems];
if (!source.length) return;
heading.dataset.hiddenMemory = pick(source, `heading-${index}`);
});
}
function playTinySong() {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const notes = [392, 494, 440, 330, 392];
notes.forEach((freq, index) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.frequency.value = freq;
osc.type = "sine";
gain.gain.setValueAtTime(0.0001, ctx.currentTime + index * 0.18);
gain.gain.exponentialRampToValueAtTime(0.05, ctx.currentTime + index * 0.18 + 0.03);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + index * 0.18 + 0.16);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime + index * 0.18);
osc.stop(ctx.currentTime + index * 0.18 + 0.18);
});
} catch (_) {}
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;"
}[char]));
}
// Features run in this order on every page after the DOM is ready.
// Each function receives the current `memory` object.
const FEATURES = [
addFooterNote,
addHomepageGreeting,
addWhispers,
addCornerObject,
addLogoSearchAndKeyboardSecrets,
addRareEvents,
addSecretLinks,
enhanceErrors,
addContinuity,
addSeasonAndDates,
addObjectConstellation,
addWeatherWindow,
addOldWebLayer,
addSourceRelics,
addLongKeyboardSecrets,
addPatienceRewards,
addRareSecondLayer,
addFamilyLayerIndex,
addPageSpecificSecrets,
addInvisibleHoverSecrets
];
document.addEventListener("DOMContentLoaded", () => {
const memory = rememberVisit();
FEATURES.forEach((addFeature) => addFeature(memory));
});
}());

View File

@@ -1,317 +1,7 @@
document.addEventListener('DOMContentLoaded', () => { (function () {
if (typeof window.BiggerPicture !== 'function') { "use strict";
console.error('[gallery-init] BiggerPicture not found. Check script path.'); var script = document.createElement("script");
return; script.src = "/assets/scripts/ui/gallery-init.js";
} script.defer = true;
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'm4v', 'ogv']; document.head.appendChild(script);
}());
const isVideo = (href) => {
if (!href) return false;
href = href.toLowerCase();
return VIDEO_EXTENSIONS.some(ext => href.endsWith('.' + ext));
};
// 1b) Convert MP4 links into inline <video> players
// Convert video links into inline <video> players BUT keep the <a>
const videoLinks = document.querySelectorAll('a[href]');
videoLinks.forEach(a => {
const href = a.getAttribute("href");
if (!isVideo(href)) return;
const video = document.createElement("video");
video.controls = true;
video.preload = "metadata";
video.style.maxWidth = "100%";
video.style.borderRadius = "6px";
const source = document.createElement("source");
source.src = href;
source.type = "video/" + href.split('.').pop(); // guesses correct mime
video.appendChild(source);
// Replace the link with the inline video
a.parentNode.replaceChild(video, a);
});
// 1) Wrap Org-exported images so theyre clickable
const imgs = document.querySelectorAll('.figure img, img.org-svg');
imgs.forEach((img) => {
if (img.closest('a')) return; // already wrapped
const a = document.createElement('a');
const href = img.currentSrc || img.src;
a.href = href;
a.dataset.img = href; // lets BP pre-size/raster slides
a.dataset.alt = img.alt || '';
const setDims = () => {
a.dataset.width = img.naturalWidth || img.width || 1920;
a.dataset.height = img.naturalHeight || img.height || 1080;
};
if (img.complete) setDims(); else img.addEventListener('load', setDims);
img.style.cursor = 'zoom-in';
img.parentElement.insertBefore(a, img);
a.appendChild(img);
});
// 2) One global BP instance
const bp = BiggerPicture({ target: document.body });
// SVG pan/zoom handle
let activePanZoom = null;
const destroyPanZoom = () => { try { activePanZoom?.destroy(); } catch(_){} activePanZoom = null; };
// Simple rotate state (for non-SVG images)
let activeContainer = null;
let currentRotation = 0;
let rotateControls = null;
// 3) Build galleries per content container
const containers = document.querySelectorAll('main, article, .content, body');
containers.forEach((container) => {
//const links = Array.from(container.querySelectorAll('.figure a, a:has(img.org-svg)'));
const links = Array.from(container.querySelectorAll(
'.figure a, a:has(img.org-svg), a[href$=".mp4"], a[href$=".webm"], a[href$=".mov"], a[href$=".m4v"], a[href$=".ogv"]'
));
if (!links.length) return;
// Start the lightbox on click
links.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
document.querySelectorAll(".theme-toggle").forEach(el => {
el.classList.add("hidden");
});
bp.open({
// IMPORTANT: pass the anchor ELEMENTS, not custom objects
items: links,
el: link,
caption: (el) => el.querySelector('img')?.alt || el.title || '',
maxZoom: 40, // for raster images (PNG/JPG); SVG handled separately
// Fade-out polish + cleanup
onClose(containerEl) {
destroyPanZoom();
teardownRotation();
if (containerEl) containerEl.classList.add('bp-fadeout');
const themeToggle = document.querySelector(".theme-toggle");
if (themeToggle) {
themeToggle.classList.remove("hidden");
}
},
// Called once after open and on every slide change
//onOpen(containerEl) { setupRotation(containerEl); enhanceSVG(containerEl); }
onOpen(containerEl) {
const linkEl = bp.currItem;
const href = linkEl?.href || "";
if (href.endsWith(".mp4")) {
// Turn off image rotation (not applicable for video)
teardownRotation();
const htmlLayer = containerEl.querySelector(".bp-html");
const imgLayer = containerEl.querySelector(".bp-img");
if (!htmlLayer) return;
// Hide the default image layer
if (imgLayer) imgLayer.style.display = "none";
// Insert video player
htmlLayer.innerHTML = `
<video controls autoplay style="max-width:95vw; max-height:95vh">
<source src="${href}" type="video/mp4">
</video>
`;
return; // Do not run image/SVG enhancements
}
// Image behaviour
setupRotation(containerEl);
enhanceSVG(containerEl);
} ,
onUpdate(containerEl){ setupRotation(containerEl); enhanceSVG(containerEl); }
});
});
});
});
// 4) Simple rotate buttons for raster images
function ensureRotateControls() {
if (rotateControls) return rotateControls;
const wrapper = document.createElement('div');
wrapper.className = 'bp-rotate-controls';
Object.assign(wrapper.style, {
position: 'fixed',
bottom: '1.5rem',
right: '1.5rem',
display: 'flex',
gap: '0.5rem',
zIndex: '9999',
pointerEvents: 'auto'
});
const mkBtn = (label, title) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = label;
btn.title = title;
btn.setAttribute('aria-label', title);
Object.assign(btn.style, {
padding: '0.4rem 0.6rem',
borderRadius: '999px',
border: 'none',
fontSize: '1.2rem',
cursor: 'pointer',
background: 'rgba(30,30,30,0.8)',
color: '#fff'
});
return btn;
};
const left = mkBtn('⟲', 'Rotate image 90° left');
const right = mkBtn('⟳', 'Rotate image 90° right');
left.addEventListener('click', (e) => {
e.stopPropagation(); // dont close the lightbox
if (!activeContainer) return;
currentRotation = (currentRotation - 90 + 360) % 360;
applyRotation();
});
right.addEventListener('click', (e) => {
e.stopPropagation();
if (!activeContainer) return;
currentRotation = (currentRotation + 90) % 360;
applyRotation();
});
wrapper.append(left, right);
document.body.appendChild(wrapper);
rotateControls = wrapper;
return rotateControls;
}
function ensureRotateWrapper() {
if (!activeContainer) return null;
const imgRoot = activeContainer.querySelector('.bp-img');
if (!imgRoot) return null;
let imgEl = imgRoot.querySelector('img');
if (!imgEl) return null;
const src = (imgEl.currentSrc || imgEl.src || '').toLowerCase();
// We only rotate raster images; SVGs are handled via svg-pan-zoom
if (src.endsWith('.svg')) return null;
let wrapper = imgRoot.querySelector('.bp-rotate-wrapper');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.className = 'bp-rotate-wrapper';
wrapper.style.display = 'inline-block';
wrapper.style.transformOrigin = 'center center';
imgRoot.appendChild(wrapper);
wrapper.appendChild(imgEl);
} else if (!wrapper.contains(imgEl)) {
// Slide changed and BiggerPicture replaced the <img>
wrapper.innerHTML = '';
wrapper.appendChild(imgEl);
}
return wrapper;
}
function applyRotation() {
const wrapper = ensureRotateWrapper();
if (!wrapper) return;
wrapper.style.transform = `rotate(${currentRotation}deg)`;
}
function setupRotation(containerEl) {
activeContainer = containerEl;
currentRotation = 0;
const controls = ensureRotateControls();
controls.style.display = 'flex';
applyRotation();
}
function teardownRotation() {
activeContainer = null;
currentRotation = 0;
if (rotateControls) {
rotateControls.style.display = 'none';
}
}
// 4) If current slide is an SVG, swap to inline + enable svg-pan-zoom
async function enhanceSVG(containerEl) {
try {
destroyPanZoom();
const imgEl = containerEl.querySelector('.bp-img img');
if (!imgEl) return;
const src = imgEl.currentSrc || imgEl.src || '';
const isSVG = src.toLowerCase().endsWith('.svg');
const htmlLayer = containerEl.querySelector('.bp-html');
if (!isSVG || !htmlLayer) {
// ensure any previous holder is removed and bitmap is visible
const old = htmlLayer?.querySelector('.bp-svg-holder');
if (old) old.remove();
imgEl.style.visibility = '';
return;
}
// Create/clear holder
let holder = htmlLayer.querySelector('.bp-svg-holder');
if (!holder) {
holder = document.createElement('div');
holder.className = 'bp-svg-holder';
holder.style.maxWidth = '95vw';
holder.style.maxHeight = '95vh';
htmlLayer.appendChild(holder);
}
holder.innerHTML = '';
// Hide the bitmap so only the inline SVG shows
imgEl.style.visibility = 'hidden';
// Inline the SVG
const res = await fetch(src, { cache: 'force-cache' });
const text = await res.text();
holder.innerHTML = text;
const svg = holder.querySelector('svg');
if (!svg) { imgEl.style.visibility = ''; return; }
svg.style.maxWidth = '95vw';
svg.style.maxHeight = '95vh';
svg.style.display = 'block';
if (typeof window.svgPanZoom === 'function') {
activePanZoom = svgPanZoom(svg, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: true,
center: true,
minZoom: 0.05,
maxZoom: 400, // effectively "unlimited"
zoomScaleSensitivity: 0.25,
dblClickZoomEnabled: true
});
// Keep wheel inside lightbox
holder.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true });
} else {
console.warn('[gallery-init] svg-pan-zoom not loaded');
}
} catch (err) {
console.error('[gallery-init] SVG enhance failed:', err);
}
}
});

Some files were not shown because too many files have changed in this diff Show More