Compare commits

243 Commits

Author SHA1 Message Date
GitHub Action
819f6aa19b Build and update icalendar.plug.js [skip ci] 2026-02-21 23:25:19 +00:00
e44a538822 switch to ical.js - v4.1
All checks were successful
Build SilverBullet Plug / build (push) Successful in 1m1s
2026-02-21 15:23:36 -08:00
11a29019af switch to ical.js - v4.0
All checks were successful
Build SilverBullet Plug / build (push) Successful in 56s
2026-02-21 14:40:05 -08:00
de782e85e4 switch to ical.js - v4.0
Some checks failed
Build SilverBullet Plug / build (push) Failing after 1m11s
2026-02-21 14:37:00 -08:00
f23b728542 chore: bump version to 0.3.34
All checks were successful
Build SilverBullet Plug / build (push) Successful in 1m0s
2026-02-21 14:02:03 -08:00
0e2e5c9699 test: add variation test for WORKWEEKSTART
All checks were successful
Build SilverBullet Plug / build (push) Successful in 1m5s
2026-02-21 13:56:02 -08:00
4ba1e9aee6 chore: stop tracking node_modules and update .gitignore
All checks were successful
Build SilverBullet Plug / build (push) Successful in 52s
2026-02-21 13:20:11 -08:00
90ab92421a feat(test): implement Playwright E2E and Dockerized testing infrastructure
Some checks failed
Build SilverBullet Plug / build (push) Has been cancelled
2026-02-21 13:19:37 -08:00
29ce643ac1 fix(icalendar): Add workweekstart to RRULE key map 2026-02-21 13:13:59 -08:00
5f287bdcaf conductor(plan): Mark phase 'Playwright E2E Setup' as complete 2026-02-21 13:13:59 -08:00
0ecc947cb2 conductor(plan): Mark task 'Implement sync smoke test' as complete 2026-02-21 13:13:59 -08:00
fa758902cc chore(test): Implement E2E sync smoke test 2026-02-21 13:13:59 -08:00
a32afbe1e6 conductor(plan): Mark task 'Initialize Playwright' as complete 2026-02-21 13:13:59 -08:00
64c3293c73 chore(test): Initialize Playwright E2E infrastructure 2026-02-21 13:13:59 -08:00
43009e4740 conductor(plan): Mark phase 'Environment & Mock Server' as complete 2026-02-21 13:13:59 -08:00
6f8c704395 conductor(plan): Mark task 'Integration Test Scaffolding' as complete 2026-02-21 13:13:59 -08:00
8de02706a6 chore(test): Add integration test scaffolding for real-world ICS data 2026-02-21 13:13:59 -08:00
2efa9d088e conductor(plan): Mark task 'Scaffold Docker environment' as complete 2026-02-21 13:13:59 -08:00
938aee2c3f chore(test): Scaffold Docker environment for E2E testing 2026-02-21 13:13:59 -08:00
03f66cc0c1 chore(conductor): Add new track 'Testing Infrastructure' 2026-02-21 13:13:59 -08:00
GitHub Action
d318cec008 Build and update icalendar.plug.js [skip ci] 2026-02-21 17:02:21 +00:00
65e52802a2 chore: Bump version to 0.3.33
All checks were successful
Build SilverBullet Plug / build (push) Successful in 42s
2026-02-21 09:01:21 -08:00
698f4ee4c7 chore(conductor): Mark track 'Generic RRULE Formatter' as complete 2026-02-21 09:01:21 -08:00
30dbbf333a conductor(plan): Mark phase 'Verification & Cleanup' as complete 2026-02-21 09:01:21 -08:00
07def7ed62 conductor(checkpoint): Checkpoint end of Phase 3 2026-02-21 09:01:21 -08:00
8d91c96fd5 conductor(plan): Mark regression check as complete 2026-02-21 09:01:21 -08:00
baa43ec1f6 conductor(plan): Mark phase 'Implementation' as complete 2026-02-21 09:01:21 -08:00
95de1e3a1e conductor(checkpoint): Checkpoint end of Phase 2 2026-02-21 09:01:21 -08:00
c40a0aff18 conductor(plan): Mark fix implementation task as complete 2026-02-21 09:01:21 -08:00
6960f9ef91 fix(icalendar): Implement recursive RRULE value formatting 2026-02-21 09:01:21 -08:00
0ef8b9a77d conductor(plan): Mark phase 'Reproduction & Test Setup' as complete 2026-02-21 09:01:21 -08:00
a9a0fbf267 conductor(checkpoint): Checkpoint end of Phase 1 2026-02-21 09:01:21 -08:00
df0f786d94 conductor(plan): Mark reproduction task as complete 2026-02-21 09:01:21 -08:00
af75a00f7a test(icalendar): Add reproduction test for BYDAY object mismatch 2026-02-21 09:01:21 -08:00
bc8e67fbdf chore(conductor): Add new track 'Generic RRULE Formatter' 2026-02-21 09:01:21 -08:00
GitHub Action
7e848feeee Build and update icalendar.plug.js [skip ci] 2026-02-20 21:50:16 +00:00
c9a703975d chore: Bump version to 0.3.32
All checks were successful
Build SilverBullet Plug / build (push) Successful in 49s
2026-02-20 13:49:14 -08:00
842586c129 chore(conductor): Mark track 'Fix RRULE UNTIL conversion' as complete 2026-02-20 13:49:14 -08:00
c6532e5aca conductor(plan): Mark phase 'Verification & Cleanup' as complete 2026-02-20 13:49:14 -08:00
42fb8be61c conductor(checkpoint): Checkpoint end of Phase 3 2026-02-20 13:49:14 -08:00
f8a6fbafda conductor(plan): Mark regression check as complete 2026-02-20 13:49:14 -08:00
dbc7ef29aa conductor(plan): Mark phase 'Fix Implementation' as complete 2026-02-20 13:49:14 -08:00
7edc0997b2 conductor(checkpoint): Checkpoint end of Phase 2 2026-02-20 13:49:14 -08:00
c3fd3aee20 conductor(plan): Mark fix implementation task as complete 2026-02-20 13:49:14 -08:00
e5b063269f fix(icalendar): Correctly format nested objects (dates) in RRULE properties 2026-02-20 13:49:14 -08:00
31ca364a7c conductor(plan): Mark phase 'Reproduction' as complete 2026-02-20 13:49:14 -08:00
e7c69aa3f7 conductor(checkpoint): Checkpoint end of Phase 1 2026-02-20 13:49:14 -08:00
255988e6f3 conductor(plan): Mark reproduction task as complete 2026-02-20 13:49:14 -08:00
ada9f6694c test(icalendar): Add reproduction test for UNTIL object mismatch 2026-02-20 13:49:14 -08:00
cd0fdf5f98 chore(conductor): Add new track 'Fix RRULE UNTIL conversion' 2026-02-20 13:49:14 -08:00
GitHub Action
523b49dd3a Build and update icalendar.plug.js [skip ci] 2026-02-20 21:27:54 +00:00
6fc4282536 chore: Bump version to 0.3.31
All checks were successful
Build SilverBullet Plug / build (push) Successful in 39s
2026-02-20 13:26:57 -08:00
0a3b5aeaba chore(conductor): Mark track 'Fix RRULE object mapping' as complete 2026-02-20 13:26:57 -08:00
12c417c506 conductor(plan): Mark phase 'Verification & Cleanup' as complete 2026-02-20 13:26:57 -08:00
ca727c83d2 conductor(checkpoint): Checkpoint end of Phase 3 2026-02-20 13:26:57 -08:00
f922f59145 conductor(plan): Mark regression check as complete 2026-02-20 13:26:57 -08:00
be74e906d2 conductor(plan): Mark phase 'Fix Implementation' as complete 2026-02-20 13:26:57 -08:00
10d334c732 conductor(checkpoint): Checkpoint end of Phase 2 2026-02-20 13:26:57 -08:00
9499dbffb2 conductor(plan): Mark fix implementation task as complete 2026-02-20 13:26:57 -08:00
9ea6f7961e fix(icalendar): Correctly map verbose RRULE keys to standard iCal keys 2026-02-20 13:26:57 -08:00
7cc59ff5f9 conductor(plan): Mark phase 'Reproduction' as complete 2026-02-20 13:26:57 -08:00
974c75f01b conductor(checkpoint): Checkpoint end of Phase 1 2026-02-20 13:26:57 -08:00
dcaf4d36a5 conductor(plan): Mark reproduction task as complete 2026-02-20 13:26:57 -08:00
150fe04410 test(icalendar): Reproduce Unknown RRULE property error 2026-02-20 13:26:57 -08:00
6b621083b9 chore(conductor): Add new track 'Fix RRULE object mapping' 2026-02-20 13:26:57 -08:00
4237fcfd30 chore(conductor): Archive track 'Fix recurring meetings visibility' 2026-02-20 13:26:57 -08:00
GitHub Action
33ca122583 Build and update icalendar.plug.js [skip ci] 2026-02-20 21:13:46 +00:00
221ca30af3 chore: Bump version to 0.3.30
All checks were successful
Build SilverBullet Plug / build (push) Successful in 42s
2026-02-20 13:12:53 -08:00
c5129120a4 chore(conductor): Mark track 'Fix recurring meetings visibility' as complete 2026-02-20 13:12:53 -08:00
c046c183b7 conductor(plan): Mark phase 'Cleanup & Verification' as complete 2026-02-20 13:12:53 -08:00
658bf69e91 conductor(checkpoint): Checkpoint end of Phase 3 2026-02-20 13:12:53 -08:00
6a8791879f conductor(plan): Mark regression check as complete 2026-02-20 13:12:53 -08:00
2dbd286498 conductor(plan): Mark phase 'Fix Logic' as complete 2026-02-20 13:12:53 -08:00
580cfcd646 conductor(checkpoint): Checkpoint end of Phase 2 2026-02-20 13:12:53 -08:00
51a1e8a3e1 conductor(plan): Mark fix task as complete 2026-02-20 13:12:53 -08:00
426d6d1dc6 fix(icalendar): Support object rrule by converting to string 2026-02-20 13:12:53 -08:00
6b12c26497 conductor(plan): Mark phase 'Investigation & Reproduction' as complete 2026-02-20 13:12:53 -08:00
78c1747141 conductor(checkpoint): Checkpoint end of Phase 1 2026-02-20 13:12:53 -08:00
8a5dd0a4bd conductor(plan): Mark investigation tasks as complete 2026-02-20 13:12:53 -08:00
a2239d28d5 test(icalendar): Add validation and object rrule reproduction tests 2026-02-20 13:12:53 -08:00
028ae7d9f9 conductor(plan): Update plan based on investigation of logs 2026-02-20 13:12:53 -08:00
0b1ef83999 chore(conductor): Archive track 'Fix version inconsistency' 2026-02-20 13:12:53 -08:00
201afb3f67 chore(conductor): Mark track 'Fix version inconsistency' as complete 2026-02-20 13:12:53 -08:00
dcc8f841a3 conductor(plan): Mark phase 'Fix & Process Update' as complete 2026-02-20 13:12:53 -08:00
2bc719eb6e conductor(checkpoint): Checkpoint end of Phase 1 2026-02-20 13:12:53 -08:00
d4150ae024 conductor(plan): Mark task 'Update Workflow Protocol' as complete 2026-02-20 13:12:53 -08:00
3ecec2a64b docs(conductor): Update Version Bump protocol to use sync-version script 2026-02-20 13:12:53 -08:00
d1e0a7fee7 conductor(plan): Mark task 'Run version sync script' as complete 2026-02-20 13:12:53 -08:00
68c18e5d18 fix: Sync version to 0.3.29 across all files 2026-02-20 13:12:53 -08:00
682ebdf013 chore(conductor): Add new track 'Fix recurring meetings visibility' 2026-02-20 13:12:53 -08:00
bea0a23a0e chore(conductor): Add new track 'Fix version inconsistency' 2026-02-20 13:12:53 -08:00
GitHub Action
a5aac39361 Build and update icalendar.plug.js [skip ci] 2026-02-20 18:53:48 +00:00
335c859e65 docs(conductor): Add Track Completion Protocol to workflow
All checks were successful
Build SilverBullet Plug / build (push) Successful in 48s
2026-02-20 10:52:45 -08:00
0f04df1435 chore: Bump version to 0.3.29 2026-02-20 10:52:45 -08:00
e8d74e4622 chore(conductor): Mark track 'Fix TypeError: r.replace is not a function in icalendar.ts' as complete 2026-02-20 10:52:45 -08:00
657f4f2c3a conductor(plan): Mark phase 'Verification & Cleanup' as complete 2026-02-20 10:52:45 -08:00
ea85b56c5c conductor(plan): Mark task 'Verify fix and check for regressions' as complete 2026-02-20 10:52:45 -08:00
df8a0e12c2 conductor(plan): Mark phase 'Implementation' as complete 2026-02-20 10:52:45 -08:00
9b53b77929 conductor(checkpoint): Checkpoint end of Phase 2 2026-02-20 10:52:45 -08:00
cafdaf7006 conductor(plan): Mark task 'Implement defensive check' as complete 2026-02-20 10:52:45 -08:00
f8640533be fix(icalendar): Handle non-string rrule property gracefully 2026-02-20 10:52:45 -08:00
cecaac6638 conductor(plan): Mark phase 'Reproduction & Test Setup' as complete 2026-02-20 10:52:45 -08:00
54bb7a8540 conductor(checkpoint): Checkpoint end of Phase 1 2026-02-20 10:52:45 -08:00
6eda06aca6 conductor(plan): Mark task 'Create reproduction test case' as complete 2026-02-20 10:52:45 -08:00
b5c718f286 test(icalendar): Add reproduction test for non-string rrule 2026-02-20 10:52:45 -08:00
0bea770814 chore(conductor): Add new track 'Fix TypeError: r.replace is not a function' 2026-02-20 10:52:45 -08:00
GitHub Action
ac2d1971be Build and update icalendar.plug.js [skip ci] 2026-02-19 17:50:40 +00:00
102b05f534 Fix: Expand recurrences from event start but filter to last 7 days and iterate to 0.3.28
All checks were successful
Build SilverBullet Plug / build (push) Successful in 44s
2026-02-19 09:49:47 -08:00
5f9d062d09 Chore: Force update by iterating to 0.3.27
All checks were successful
Build SilverBullet Plug / build (push) Successful in 39s
2026-02-19 08:59:59 -08:00
3ec078f18e Build: Updated compiled plug for 0.3.26 and resolved conflicts
All checks were successful
Build SilverBullet Plug / build (push) Successful in 38s
2026-02-19 08:53:58 -08:00
08b5019452 Fix: Add 7-day lookback window for recurring events expansion and iterate to 0.3.26 2026-02-19 08:53:25 -08:00
GitHub Action
fe88bada15 Build and update icalendar.plug.js [skip ci] 2026-02-19 16:34:26 +00:00
b2b109b923 docs(conductor): Synchronize docs for track 'Upgrade the SilverBullet iCalendar plug to use DST-aware timezone resolution and add recurring event support using rrule.'
All checks were successful
Build SilverBullet Plug / build (push) Successful in 42s
2026-02-19 08:21:26 -08:00
bc0afad261 chore(conductor): Mark track 'Upgrade the SilverBullet iCalendar plug to use DST-aware timezone resolution and add recurring event support using rrule.' as complete 2026-02-19 07:13:04 -08:00
9af3e436aa conductor(plan): Mark phase 'Phase 4: Cleanup & Configuration' as complete 2026-02-19 07:12:58 -08:00
533c240c07 conductor(checkpoint): Checkpoint end of Phase 4: Cleanup & Configuration 2026-02-19 07:12:41 -08:00
780e90b1f0 conductor(plan): Mark phase 'Phase 3: Features' as complete 2026-02-19 07:05:25 -08:00
ffaef28332 conductor(checkpoint): Checkpoint end of Phase 3: Features 2026-02-19 07:05:02 -08:00
4128c046d0 conductor(plan): Mark phase 'Phase 2: Core Logic' as complete 2026-02-19 07:00:08 -08:00
10a6db5893 conductor(checkpoint): Checkpoint end of Phase 2: Core Logic 2026-02-19 07:00:01 -08:00
85691d1df5 conductor(plan): Mark phase 'Phase 1: Foundation' as complete 2026-02-19 06:57:25 -08:00
b8bf269de8 conductor(checkpoint): Checkpoint end of Phase 1: Foundation 2026-02-19 06:57:15 -08:00
b94ebd30a2 chore(conductor): Add conductor setup files and new track 'timezone_rrule_20260218' 2026-02-19 06:50:29 -08:00
9b54e2d8a8 Fix: Restore proven recursive date sanitization and unique indexing from deb30ab and iterate to 0.3.25
All checks were successful
Build SilverBullet Plug / build (push) Successful in 30s
2026-02-18 10:26:50 -08:00
3c69a3567b Fix: Sanitize event objects before indexing and iterate to 0.3.24
All checks were successful
Build SilverBullet Plug / build (push) Successful in 32s
2026-02-18 10:12:10 -08:00
53a3c0e5db Fix: Handle potential Date objects in event start and iterate to 0.3.23
All checks were successful
Build SilverBullet Plug / build (push) Successful in 29s
2026-02-18 10:04:54 -08:00
606fca25a8 Chore: Add debug logging and iterate to 0.3.22
All checks were successful
Build SilverBullet Plug / build (push) Successful in 24s
2026-02-18 09:58:22 -08:00
35229aa941 Build: Updated compiled plug files for version 0.3.21
All checks were successful
Build SilverBullet Plug / build (push) Successful in 24s
2026-02-18 09:50:26 -08:00
8e6d0d9f88 Fix: Remove stray Library directory that contained an old plug version
All checks were successful
Build SilverBullet Plug / build (push) Successful in 28s
2026-02-18 09:18:16 -08:00
f06799419c Feat: Implement version sync and iterate to 0.3.21
All checks were successful
Build SilverBullet Plug / build (push) Successful in 27s
- Added a version sync orchestrator script to ensure consistent versioning across project files.
- Configured `deno task build` to automatically synchronize versions before compiling.
- Set the project version to 0.3.21 in `deno.json` and propagated it throughout the codebase.
2026-02-18 08:44:48 -08:00
GitHub Action
d1079bd302 Build and update icalendar.plug.js [skip ci] 2026-02-18 16:08:24 +00:00
46d8e5f3a0 Fixing plugin build output location
All checks were successful
Build SilverBullet Plug / build (push) Successful in 27s
2026-02-18 08:04:11 -08:00
48e6e945e1 Feat: Increment version to 0.3.21
All checks were successful
Build SilverBullet Plug / build (push) Successful in 31s
- Iterated the patch version for the iCalendar plug from 0.3.20 to 0.3.21.
- Updated  and  with the new version.
- This change is intended to be built and deployed via Gitea Actions.
2026-02-18 07:47:17 -08:00
1cd6fd490b Fix: Align plug versions and remove redundant version field
All checks were successful
Build SilverBullet Plug / build (push) Successful in 36s
- Updated  to version  to match  and ensure correct plug versioning.
- Removed the  field from  as it is redundant and can lead to version inconsistencies, with  now serving as the canonical source for the plug's version.
2026-02-18 07:14:00 -08:00
598b097b13 [fix] updating version to 0.3.20
All checks were successful
Build SilverBullet Plug / build (push) Successful in 34s
2026-02-18 05:49:47 -08:00
a11aecfd1b Chore: Add detailed logging to diagnose zero events issue (v0.3.20)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-18 05:45:39 -08:00
b786978804 Fix: Add requiredPermissions for legacy SB versions (v0.3.19)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 18:09:27 -08:00
4030c3fef0 Fix: Unified plug identity to Library/sstent/icalendar (v0.3.18)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 28s
2026-02-17 16:04:22 -08:00
2f4499a068 Fix: Final unified naming and permissions (v0.3.17)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 26s
2026-02-17 15:03:00 -08:00
1ea0e020f9 Cleanup: Remove redundant Library folder, keep only root PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-17 14:59:42 -08:00
8ffdebb673 Fix: Align plug name in manifests to allow update (v0.3.16)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 34s
2026-02-17 14:58:23 -08:00
5f9afac9d8 Fix: Add fetch permission and bump to v0.3.16
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 14:57:02 -08:00
03907f3789 Fix: Revert manifest structure to working baseline (v0.3.15)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-17 14:53:30 -08:00
17f6308585 Fix: Final clean build v0.3.14 with verified mapping and name
All checks were successful
Build SilverBullet Plug / build (push) Successful in 19s
2026-02-17 14:51:47 -08:00
7d690cdb2a Fix: Final verified build v0.3.13 with explicit naming and mapping
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 14:48:51 -08:00
98c3b64659 Fix: Final working v0.3.12 build with populated function mapping
All checks were successful
Build SilverBullet Plug / build (push) Successful in 23s
2026-02-17 14:46:38 -08:00
GitHub Action
188cbf1254 Build and update icalendar.plug.js [skip ci] 2026-02-17 22:41:27 +00:00
d8f6f0396f Chore: Cleanup before rebase
All checks were successful
Build SilverBullet Plug / build (push) Successful in 33s
2026-02-17 14:40:48 -08:00
170de52e6b Chore: Add version consistency check to build process 2026-02-17 14:40:48 -08:00
GitHub Action
76be84b487 Build and update icalendar.plug.js [skip ci] 2026-02-17 22:40:24 +00:00
70e6a4ef82 Fix: Final working v0.3.11 build with correct function mapping
All checks were successful
Build SilverBullet Plug / build (push) Successful in 33s
2026-02-17 14:39:39 -08:00
cd194de3f7 Fix: Provide files in Library/ path for Library Manager compatibility (v0.3.1)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 30s
2026-02-17 13:56:48 -08:00
ced95d2a7a Fix: Final working v0.3.1 with fixed function mapping
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 13:55:31 -08:00
a7180995b0 Fix: Align internal plug name with Library Manager naming (v0.3.1)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 34s
2026-02-17 13:46:41 -08:00
a11c6bd906 Cleanup: Remove unnecessary Library folder and keep plug files at root
All checks were successful
Build SilverBullet Plug / build (push) Successful in 31s
2026-02-17 13:45:09 -08:00
62d4ba1006 Build: Finally correctly building v0.3.1 with manifest update
All checks were successful
Build SilverBullet Plug / build (push) Successful in 29s
2026-02-17 13:44:19 -08:00
d4b8fea8f9 Build: Correctly build v0.3.1 with version bump in code
All checks were successful
Build SilverBullet Plug / build (push) Successful in 19s
2026-02-17 13:43:45 -08:00
4aa83f4a0b Fix: Ensure PLUG.md and plug.js are at root (v0.3.1)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 26s
2026-02-17 13:42:57 -08:00
bf90d8bda2 Fix: Final rebuild and version sync (v0.3.1)
All checks were successful
Build SilverBullet Plug / build (push) Successful in 34s
2026-02-17 13:34:50 -08:00
ae3935f048 Fix: Align repo structure with Library naming
All checks were successful
Build SilverBullet Plug / build (push) Successful in 24s
2026-02-17 13:32:35 -08:00
9f7f1b6d2a Fix: Update plug name in manifest
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 13:28:25 -08:00
02e29e7da4 Fix: Add missing files entry to PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 13:24:49 -08:00
32933f9a34 Build: Use local deno in Makefile for CI compatibility
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 10:19:20 -08:00
GitHub Action
b53fc0acf8 Build and update icalendar.plug.js [skip ci] 2026-02-17 18:07:35 +00:00
070b10843e Fix(timezone): Implement True UTC calculation and bump to v0.3.0
All checks were successful
Build SilverBullet Plug / build (push) Successful in 24s
2026-02-17 10:07:02 -08:00
GitHub Action
8d66834a48 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:36:46 +00:00
6480b56875 Merge fixes and new dev tools
All checks were successful
Build SilverBullet Plug / build (push) Successful in 18s
2026-02-17 08:36:22 -08:00
d28c206862 Bump version to 0.2.16 and add detailed offset logging 2026-02-17 08:36:18 -08:00
GitHub Action
2ea763e145 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:27:06 +00:00
66f60bc9ae Bump version to 0.2.14 and simplify date processing to native browser behavior
All checks were successful
Build SilverBullet Plug / build (push) Successful in 26s
2026-02-17 08:26:32 -08:00
GitHub Action
0e7e89091d Build and update icalendar.plug.js [skip ci] 2026-02-17 16:22:05 +00:00
81d5e8738e Bump version to 0.2.13 and improve date processing stability
All checks were successful
Build SilverBullet Plug / build (push) Successful in 23s
2026-02-17 08:21:34 -08:00
GitHub Action
899ee62693 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:16:34 +00:00
90f317be6e Bump version to 0.2.12 and fix Outlook timezone logic
All checks were successful
Build SilverBullet Plug / build (push) Successful in 24s
2026-02-17 08:16:06 -08:00
GitHub Action
b50cded6c9 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:10:40 +00:00
124a780b65 Bump version to 0.2.11 and add tzShift support
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-17 08:10:13 -08:00
GitHub Action
415cd7e215 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:06:15 +00:00
9e54f0320e Bump version to 0.2.10 and add explicit date localization with logs
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-17 08:05:52 -08:00
GitHub Action
c422f0fae7 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:59:25 +00:00
ab0db17a47 Bump version to 0.2.9 and improve ISO localization logic
All checks were successful
Build SilverBullet Plug / build (push) Successful in 28s
2026-02-17 07:58:53 -08:00
GitHub Action
8087031220 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:46:04 +00:00
56b6e7d0bf Bump version to 0.2.8 and add targeted logging for problematic UID
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-17 07:45:37 -08:00
GitHub Action
2131bf4051 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:35:46 +00:00
cdfea5f3b2 Bump version to 0.2.7 and add PST localization logging
All checks were successful
Build SilverBullet Plug / build (push) Successful in 27s
2026-02-17 07:35:13 -08:00
GitHub Action
3cc449a7c6 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:29:24 +00:00
80cd15c1b5 Bump version to 0.2.6
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 07:28:57 -08:00
GitHub Action
3b348d8257 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:26:36 +00:00
adf638379d Add raw ICS logging
All checks were successful
Build SilverBullet Plug / build (push) Successful in 19s
2026-02-17 07:26:10 -08:00
GitHub Action
4b4aacbfd9 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:16:03 +00:00
45ab0e8d95 Bump version to 0.2.5 and add detailed date conversion logging
All checks were successful
Build SilverBullet Plug / build (push) Successful in 14s
2026-02-17 07:15:45 -08:00
GitHub Action
e79349d7c0 Build and update icalendar.plug.js [skip ci] 2026-02-17 14:52:28 +00:00
86824991a6 Bump version to 0.2.4 and add timezone diagnostics
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-17 06:52:03 -08:00
e3fcf743f8 Update version to 0.2.3 in PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 14s
2026-02-16 11:23:48 -08:00
GitHub Action
da835727d4 Build and update icalendar.plug.js [skip ci] 2026-02-16 18:18:44 +00:00
dbffe7fb24 Bump version to 0.2.3
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-16 10:18:13 -08:00
5a7a7aaa18 Robust whitespace handling in URL with character code logging
Some checks failed
Build SilverBullet Plug / build (push) Has been cancelled
2026-02-16 10:17:45 -08:00
GitHub Action
7aba023818 Build and update icalendar.plug.js [skip ci] 2026-02-16 17:39:02 +00:00
c382ab93ab Add top-level log for debugging boot timeout
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-16 09:38:38 -08:00
4d9943ed72 Revert to relative path in PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 22s
2026-02-16 09:36:54 -08:00
10286625cc Use full URL for JS file in PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 19s
2026-02-16 09:35:32 -08:00
7031d15833 Add version to PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 18s
2026-02-16 09:32:29 -08:00
ab303c694e Update PLUG.md name for Gitea
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-16 09:31:16 -08:00
31fdf3f42b Correct PLUG.md format based on template
All checks were successful
Build SilverBullet Plug / build (push) Successful in 28s
2026-02-16 08:23:07 -08:00
cb4f2c03c0 Switch PLUG.md back to table format for better compatibility
All checks were successful
Build SilverBullet Plug / build (push) Successful in 31s
2026-02-16 08:22:19 -08:00
GitHub Action
74177dc4b5 Build and update icalendar.plug.js [skip ci] 2026-02-16 16:10:53 +00:00
f2fedb690c Bump version to 0.2.2
All checks were successful
Build SilverBullet Plug / build (push) Successful in 26s
2026-02-16 08:10:15 -08:00
099374e878 Remove artifact upload step
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-16 08:06:49 -08:00
GitHub Action
479c096587 Build and update icalendar.plug.js [skip ci] 2026-02-16 16:05:47 +00:00
57cb085982 Add contents:write permission to fix CI push
Some checks failed
Build SilverBullet Plug / build (push) Failing after 28s
2026-02-16 08:05:17 -08:00
f847ad53bc Update SilverBullet dependency to v2.4.1
Some checks failed
Build SilverBullet Plug / build (push) Failing after 23s
2026-02-16 07:57:23 -08:00
6a862a5563 Use GITHUB_TOKEN for push in CI
Some checks failed
Build SilverBullet Plug / build (push) Failing after 27s
2026-02-16 07:50:27 -08:00
3fa0bd553b Robust fetch with User-Agent and URL encoding
Some checks failed
Build SilverBullet Plug / build (push) Failing after 26s
2026-02-16 07:48:32 -08:00
af12466721 Use edge compiler URL
Some checks failed
Build SilverBullet Plug / build (push) Failing after 28s
2026-02-16 07:43:22 -08:00
17ba5aa701 Add git diagnostics to workflow
Some checks failed
Build SilverBullet Plug / build (push) Failing after 13s
2026-02-16 07:42:23 -08:00
b8497c09d3 Fix checkout fetch-depth for CI push
Some checks failed
Build SilverBullet Plug / build (push) Failing after 14s
2026-02-16 07:41:31 -08:00
0a58c16705 Add URL trimming and fetching logs
Some checks failed
Build SilverBullet Plug / build (push) Failing after 10s
2026-02-16 07:40:35 -08:00
b59aabd115 Update to SB v0.10.4
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:53:21 -08:00
c39b869795 Update dependencies and add diagnostics
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:52:33 -08:00
e33be08320 Use raw github URL for plug-compiler
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:51:37 -08:00
1ce9011d60 Try building with --no-check and Deno v2.x
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:50:52 -08:00
56e11f748b Use stable plug-compiler v0.10.1 to fix CI
Some checks failed
Build SilverBullet Plug / build (push) Failing after 8s
2026-02-15 17:50:09 -08:00
bb1b9a93ad Make configuration more robust for single sources
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-15 17:39:32 -08:00
6641f03519 Update PLUG.md
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-16 00:05:47 +00:00
44079d525a Use Deno v1.x and add debugging to workflow
Some checks failed
Build SilverBullet Plug / build (push) Failing after 10s
2026-02-15 15:56:10 -08:00
GitHub Action
a09bfd805a Build and update icalendar.plug.js [skip ci] 2026-02-15 23:55:00 +00:00
19826c1678 Automate committing compiled plug back to repo
Some checks failed
Build SilverBullet Plug / build (push) Failing after 14s
2026-02-15 15:54:42 -08:00
651a1107d1 Align project structure with silverbullet-plug-template
Some checks failed
Build SilverBullet Plug / build (push) Failing after 11s
- Rename deno.jsonc to deno.json and update build tasks
- Add PLUG.md for SB v2 installation
- Update .gitignore to include .plug.js files
- Update README.md with new installation instructions
- Simplify GitHub workflow
2026-02-15 15:27:51 -08:00
daab3cf2f3 Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-15 15:02:07 +00:00
5ba0445eeb Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 10s
2026-02-15 15:00:21 +00:00
31fddc1e26 Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-15 14:58:47 +00:00
7ff19185e2 Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 8s
2026-02-15 14:57:29 +00:00
606340058e Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 24s
2026-02-15 14:54:48 +00:00
1107571bf1 Update .github/workflows/publish.yml
Some checks failed
Publish / publish (push) Failing after 1m6s
2026-02-15 14:49:38 +00:00
1d2fd52715 Add .github/workflows/publish.yml 2026-02-15 14:47:33 +00:00
Alexandre Nicolaie
deb30ab6b3 Migrate to ts-ics 2.4.0 API and fix duplicate recurring events
ts-ics 2.4.0 changed API from parseIcsCalendar to convertIcsCalendar
and VCalendar to IcsCalendar. The new API returns Date objects and
nested date structures that require recursive conversion to strings
for SilverBullet indexing.

Recurring events were creating duplicate refs because the hash only
used the UID, which is identical across occurrences. Including the
start date in the unique key ensures each occurrence gets a distinct
ref.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
904c1b9d94 Add clear all events functionality
Add 'iCalendar: Clear All Events' command to completely remove
all indexed calendar events and cache. Useful for maintenance
and troubleshooting.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
34bbe69569 Add automatic calendar sync on editor initialization
Calendars now sync automatically when the editor starts, eliminating the
need for manual sync after opening SilverBullet.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
38dd97c25c Add force sync command
Add 'iCalendar: Force Sync' command to bypass cache and
immediately synchronize calendar events. Useful when you
need fresh data before the cache expires.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
d3e4fc021b Migrate to SilverBullet v2 indexing system
Replace deprecated query provider with index-based architecture.
Events are now indexed using index.indexObjects() and queryable
via Lua Integrated Query (LIQ).

Breaking changes:
- Plugin now requires SilverBullet v2 (use v0.1.0 for SB v1)
- Old query syntax no longer works (use LIQ instead)
- Manual sync required via 'iCalendar: Sync' command
- Events cached for 6h by default (was real-time)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Marek S. Lukasiewicz
8a7c9700ee Update README 2025-01-05 19:22:02 +01:00
Marek S. Lukasiewicz
e12420aba3 Add Nextcloud screenshot 2025-01-05 19:09:48 +01:00
Marek S. Łukasiewicz
4df5a1f8a8 Update README.md 2025-01-05 18:20:40 +01:00
Marek S. Lukasiewicz
e13e6e2bc2 Add LICENSE 2025-01-05 17:59:16 +01:00
82 changed files with 160051 additions and 172 deletions

40
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Build SilverBullet Plug
on:
push:
branches: [ main ]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Build Plug
run: |
deno task build -- --no-check
- name: Commit and push changes
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add icalendar.plug.js
if git diff --quiet --staged; then
echo "No changes to commit"
else
git commit -m "Build and update icalendar.plug.js [skip ci]"
git push origin main
fi

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
deno.lock
*.plug.js
test_space
.env
node_modules/
playwright-report/
test-results/

View File

@@ -1,5 +1,5 @@
{
"deno.enable": true,
"editor.formatOnSave": true,
"deno.config": "deno.jsonc"
"deno.config": "deno.json"
}

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2025 Marek S. Lukasiewicz
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

12
Makefile Normal file
View File

@@ -0,0 +1,12 @@
# Check version consistency
check-versions:
./check_versions.sh
# Build the plug using a Docker container with Deno
build: check-versions
docker run --rm -v /home/sstent/Projects/silverbullet-icalendar:/app -w /app denoland/deno:latest task build
# Helper to build and copy to a local test space (if needed)
deploy-test: build
mkdir -p test_space/_plug
cp icalendar.plug.js test_space/_plug/

8
PLUG.md Normal file
View File

@@ -0,0 +1,8 @@
---
name: Library/sstent/icalendar
version: "0.4.1"
tags: meta/library
files:
- icalendar.plug.js
---
iCalendar sync plug for SilverBullet.

View File

@@ -3,19 +3,21 @@
`silverbullet-icalendar` is a [Plug](https://silverbullet.md/Plugs) for [SilverBullet](https://silverbullet.md/) which I made for my girlfriend.
It reads external [iCalendar](https://en.wikipedia.org/wiki/ICalendar) data, also known as iCal and `.ics` format, used in CalDAV protocol.
**Note**: This version (0.2.0+) is compatible with **SilverBullet v2 only**. For SilverBullet v1, use version 0.1.0.
## Installation
Run the {[Plugs: Add]} command in SilverBullet and add paste this URI into the dialog box:
Run the {[Library: Install]} command and paste the following URL:
`https://github.com/Maarrk/silverbullet-icalendar/blob/main/PLUG.md`
```
ghr:Maarrk/silverbullet-icalendar
```
Alternatively, you can use the older way with the {[Plugs: Add]} command:
`ghr:Maarrk/silverbullet-icalendar`
Then run the {[Plugs: Update]} command and off you go!
### Configuration
This plug can be configured with [Space Config](https://silverbullet.md/Space%20Config), these are the default values and their usage:
This plug is configured with [Space Config](https://silverbullet.md/Space%20Config), short example:
```yaml
icalendar:
@@ -37,57 +39,72 @@ Instructions to get the source URL for some calendar services:
- Calendar settings (pencil icon to the right of the name)
- Settings and Sharing, scroll down to Integrate calendar
- Copy the link for Secret address in iCal format
![Screenshot of getting the URL from Nextcloud Calendar](./url-nextcloud.png)
## Usage
The plug provides the query source `ical-event`, which corresponds to `VEVENT` object
After configuration, run the `{[iCalendar: Sync]}` command to synchronize calendar events. The plug will cache the results for 6 hours by default (configurable via `cacheDuration` in config).
To bypass the cache and force an immediate sync, use the `{[iCalendar: Force Sync]}` command.
To completely clear all indexed events and cache (useful for troubleshooting), use the `{[iCalendar: Clear All Events]}` command.
Events are indexed with the tag `ical-event` and can be queried using Lua Integrated Query (LIQ).
### Examples
Select events that start on a given date
Select events that start on a given date:
~~~
```query
ical-event
where start =~ /^2024-01-04/
select summary, description
```md
${query[[
from index.tag "ical-event"
where start:startsWith "2024-01-04"
select {summary=summary, description=description}
]]}
```
~~~
Get the next 5 upcoming events:
```md
${query[[
from index.tag "ical-event"
where start > os.date("%Y-%m-%d")
order by start
limit 5
]]}
```
~~~
## Roadmap
- Cache the calendar according to `REFRESH-INTERVAL` or `X-PUBLISHED-TTL`, command for manual update
- More query sources:
- Cache the calendar according to `REFRESH-INTERVAL` or `X-PUBLISHED-TTL`
- More indexed object types:
- `ical-todo` for `VTODO` components
- `ical-calendar` showing information about configured calendars
- Describe the properties of query results
- Support `file://` URL scheme (use an external script or filesystem instead of authentication on CalDAV)
## Contributing
Pull requests with short instructions for various calendar services are welcome.
If you find bugs, report them on the [issue tracker on GitHub](https://github.com/Maarrk/silverbullet-icalendar/issues).
### Building from source
To build this plug, make sure you have [SilverBullet installed](https://silverbullet.md/Install). Then, build the plug with:
To build this plug, you need [Deno](https://deno.land/) installed. Then, build the plug with:
```shell
deno task build
```
Or to watch for changes and rebuild automatically
Or to watch for changes and rebuild automatically:
```shell
deno task watch
```
Then, copy the resulting `.plug.js` file into your space's `_plug` folder. Or build and copy in one command:
```shell
deno task build && cp *.plug.js /my/space/_plug/
```
SilverBullet will automatically sync and load the new version of the plug (or speed up this process by running the {[Sync: Now]} command).
The compiled plug will be written to `icalendar.plug.js`. This file is tracked by Git in this repository to allow for easy installation via the `PLUG.md` file.
## License

152393
SilverBullet_digest.md Normal file

File diff suppressed because one or more lines are too long

19
check_versions.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Extract versions
TS_VERSION=$(grep "const VERSION =" icalendar.ts | cut -d'"' -f2)
YAML_VERSION=$(grep "version:" icalendar.plug.yaml | head -n 1 | awk '{print $2}')
PLUG_MD_VERSION=$(grep "version:" PLUG.md | head -n 1 | awk '{print $2}')
echo "Checking versions..."
echo "icalendar.ts: $TS_VERSION"
echo "icalendar.plug.yaml: $YAML_VERSION"
echo "PLUG.md: $PLUG_MD_VERSION"
if [ "$TS_VERSION" == "$YAML_VERSION" ] && [ "$YAML_VERSION" == "$PLUG_MD_VERSION" ]; then
echo "✅ All versions match."
exit 0
else
echo "❌ Version mismatch detected!"
exit 1
fi

View File

@@ -0,0 +1,5 @@
# Track fix_recurring_visibility_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "fix_recurring_visibility_20260219",
"type": "bug",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Fix issue where recurring meetings are not showing up."
}

View File

@@ -0,0 +1,24 @@
# Implementation Plan - Fix Recurring Meetings Visibility
## Phase 1: Investigation & Reproduction [checkpoint: 8137d63]
- [x] Task: Create validation test case 6122599
- [x] Add a test in `icalendar_test.ts` with a valid weekly recurring event starting in the past.
- [x] Assert that it returns multiple occurrences within the 30-day window.
- [x] Run the test to see if it fails (confirming the bug).
- [x] Task: Investigate Object RRULE 6122599
- [x] The logs show `Invalid rrule type (object)`. This means `ts-ics` is parsing RRULE into an object, not a string.
- [x] Create a test case where `rrule` is an object (mocking `ts-ics` output).
- [x] Verify that it returns only 1 event (the bug).
- [x] Task: Conductor - User Manual Verification 'Investigation & Reproduction' (Protocol in workflow.md)
## Phase 2: Fix Logic [checkpoint: a4acfa1]
- [x] Task: Support Object RRULE in `expandRecurrences` f7f6028
- [x] Modify `expandRecurrences` to handle `rrule` as an object.
- [x] It likely needs to be converted back to a string or used directly if `rrule` library supports it.
- [x] Run the new test case to confirm the fix.
- [x] Task: Conductor - User Manual Verification 'Fix Logic' (Protocol in workflow.md)
## Phase 3: Cleanup & Verification [checkpoint: 9d07e7a]
- [x] Task: Full Regression Check f1bafb6
- [x] Run all tests in `icalendar_test.ts`.
- [x] Task: Conductor - User Manual Verification 'Cleanup & Verification' (Protocol in workflow.md)

View File

@@ -0,0 +1,18 @@
# Specification: Fix Recurring Meetings Visibility
## Overview
Users report that all recurring meetings are missing from calendar views without any error messages. This suggests an issue with the expansion or indexing logic for recurring events, possibly introduced by recent changes.
## Functional Requirements
- **Visibility:** Recurring events must appear in the calendar views.
- **Expansion:** The `expandRecurrences` function must correctly expand valid RRULE strings into occurrences within the specified window.
## Implementation Steps
1. **Investigation:** Create a test case with a *valid* recurring event (unlike the previous invalid one) and verify if `expandRecurrences` produces the expected occurrences.
2. **Debugging:** Inspect the `filter` logic in `expandRecurrences` (specifically the `filterStart` and `windowEnd` logic).
3. **Fix:** Adjust the logic to ensure valid occurrences are returned.
4. **Verify:** Confirm the fix with the new test case.
## Acceptance Criteria
- [ ] A new test case with a valid recurring event passes and returns the expected number of occurrences.
- [ ] Recurring events are visible in the calendar view (manual verification).

View File

@@ -0,0 +1,5 @@
# Track fix_version_mismatch_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "fix_version_mismatch_20260219",
"type": "chore",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Fix version inconsistency in PLUG.md and icalendar.plug.yaml and investigate plug-manager error."
}

View File

@@ -0,0 +1,9 @@
# Implementation Plan - Fix Version Mismatch
## Phase 1: Fix & Process Update [checkpoint: 944ce96]
- [x] Task: Run version sync script e17bf4d
- [x] Execute `deno task sync-version` (via Docker) to automatically update `icalendar.ts`, `icalendar.plug.yaml`, and `PLUG.md` to match `deno.json`.
- [x] Task: Update Workflow Protocol 9bcfaa7
- [x] Modify `conductor/workflow.md` -> "Track Completion Protocol" -> "Version Bump" section.
- [x] Replace manual file list with instruction: "Update `deno.json` version and run `deno task sync-version`".
- [x] Task: Conductor - User Manual Verification 'Fix & Process Update' (Protocol in workflow.md)

View File

@@ -0,0 +1,19 @@
# Specification: Fix Version Inconsistency and Plug Metadata
## Overview
The recent version bump to `0.3.29` was incomplete. It updated `icalendar.ts` and `deno.json` but missed `PLUG.md` and `icalendar.plug.yaml`. This mismatch can cause update issues. Additionally, a user reported a `plug-manager` error stating "No plug name provided" when fetching `PLUG.md`, which needs investigation.
## Functional Requirements
- **Version Consistency:** Ensure `icalendar.ts`, `deno.json`, `PLUG.md`, and `icalendar.plug.yaml` all reflect version `0.3.29`.
- **Metadata Verification:** Verify `PLUG.md` contains the correct YAML frontmatter expected by SilverBullet's `plug-manager`.
- **Process Improvement:** Update the `conductor/workflow.md` to explicitly list all files that must be updated during a version bump.
## Implementation Steps
1. **Update Versions:** Update `version` in `PLUG.md` and `icalendar.plug.yaml` to `0.3.29`.
2. **Update Workflow:** Modify `conductor/workflow.md` -> "Track Completion Protocol" -> "Version Bump" section to list all required files (`PLUG.md`, `icalendar.plug.yaml`, `icalendar.ts`, `deno.json`).
3. **Commit & Push:** Commit these fixes and push.
## Acceptance Criteria
- [ ] `PLUG.md` version is `0.3.29`.
- [ ] `icalendar.plug.yaml` version is `0.3.29`.
- [ ] `conductor/workflow.md` lists all 4 files in the Version Bump protocol.

View File

@@ -0,0 +1,23 @@
# General Code Style Principles
This document outlines general coding principles that apply across all languages and frameworks used in this project.
## Readability
- Code should be easy to read and understand by humans.
- Avoid overly clever or obscure constructs.
## Consistency
- Follow existing patterns in the codebase.
- Maintain consistent formatting, naming, and structure.
## Simplicity
- Prefer simple solutions over complex ones.
- Break down complex problems into smaller, manageable parts.
## Maintainability
- Write code that is easy to modify and extend.
- Minimize dependencies and coupling.
## Documentation
- Document *why* something is done, not just *what*.
- Keep documentation up-to-date with code changes.

View File

@@ -0,0 +1,43 @@
# Google TypeScript Style Guide Summary
This document summarizes key rules and best practices from the Google TypeScript Style Guide, which is enforced by the `gts` tool.
## 1. Language Features
- **Variable Declarations:** Always use `const` or `let`. **`var` is forbidden.** Use `const` by default.
- **Modules:** Use ES6 modules (`import`/`export`). **Do not use `namespace`.**
- **Exports:** Use named exports (`export {MyClass};`). **Do not use default exports.**
- **Classes:**
- **Do not use `#private` fields.** Use TypeScript's `private` visibility modifier.
- Mark properties never reassigned outside the constructor with `readonly`.
- **Never use the `public` modifier** (it's the default). Restrict visibility with `private` or `protected` where possible.
- **Functions:** Prefer function declarations for named functions. Use arrow functions for anonymous functions/callbacks.
- **String Literals:** Use single quotes (`'`). Use template literals (`` ` ``) for interpolation and multi-line strings.
- **Equality Checks:** Always use triple equals (`===`) and not equals (`!==`).
- **Type Assertions:** **Avoid type assertions (`x as SomeType`) and non-nullability assertions (`y!`)**. If you must use them, provide a clear justification.
## 2. Disallowed Features
- **`any` Type:** **Avoid `any`**. Prefer `unknown` or a more specific type.
- **Wrapper Objects:** Do not instantiate `String`, `Boolean`, or `Number` wrapper classes.
- **Automatic Semicolon Insertion (ASI):** Do not rely on it. **Explicitly end all statements with a semicolon.**
- **`const enum`:** Do not use `const enum`. Use plain `enum` instead.
- **`eval()` and `Function(...string)`:** Forbidden.
## 3. Naming
- **`UpperCamelCase`:** For classes, interfaces, types, enums, and decorators.
- **`lowerCamelCase`:** For variables, parameters, functions, methods, and properties.
- **`CONSTANT_CASE`:** For global constant values, including enum values.
- **`_` Prefix/Suffix:** **Do not use `_` as a prefix or suffix** for identifiers, including for private properties.
## 4. Type System
- **Type Inference:** Rely on type inference for simple, obvious types. Be explicit for complex types.
- **`undefined` and `null`:** Both are supported. Be consistent within your project.
- **Optional vs. `|undefined`:** Prefer optional parameters and fields (`?`) over adding `|undefined` to the type.
- **`Array<T>` Type:** Use `T[]` for simple types. Use `Array<T>` for more complex union types (e.g., `Array<string | number>`).
- **`{}` Type:** **Do not use `{}`**. Prefer `unknown`, `Record<string, unknown>`, or `object`.
## 5. Comments and Documentation
- **JSDoc:** Use `/** JSDoc */` for documentation, `//` for implementation comments.
- **Redundancy:** **Do not declare types in `@param` or `@return` blocks** (e.g., `/** @param {string} user */`). This is redundant in TypeScript.
- **Add Information:** Comments must add information, not just restate the code.
*Source: [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html)*

14
conductor/index.md Normal file
View File

@@ -0,0 +1,14 @@
# Project Context
## Definition
- [Product Definition](./product.md)
- [Product Guidelines](./product-guidelines.md)
- [Tech Stack](./tech-stack.md)
## Workflow
- [Workflow](./workflow.md)
- [Code Style Guides](./code_styleguides/)
## Management
- [Tracks Registry](./tracks.md)
- [Tracks Directory](./tracks/)

View File

@@ -0,0 +1,17 @@
# Product Guidelines - SilverBullet iCalendar Plug
## Documentation and Communication Style
- **Technical and Concise:** All documentation, configuration examples, and user-facing messages should be accurate, brief, and focused on providing high value to the user. Avoid unnecessary fluff or conversational filler.
- **Example-Driven:** Prioritize clear, copy-pasteable configuration snippets and query examples to help users get started quickly.
## Visual Identity and User Interface
- **Native SilverBullet Integration:** The plug should feel like a core part of the SilverBullet experience. Commands, notifications, and any future UI elements must strictly adhere to SilverBullet's design patterns and aesthetic.
- **Informative and Actionable Feedback:**
- Notifications should provide immediate clarity on the outcome of actions (e.g., "Synced 194 events", "Sync failed: HTTP 404").
- Error messages should be descriptive enough to aid in troubleshooting (e.g., specifying which source failed).
- **Subtle Consistency:** Use consistent naming conventions for commands (`iCalendar: Sync`, `iCalendar: Force Sync`, etc.) to maintain a professional and organized command palette.
## Code and Maintenance Guidelines (Inferred)
- **Robust Error Handling:** Always catch and log errors during fetch and parse operations to prevent the entire sync process from crashing.
- **Performance First:** Efficiently process large `.ics` files and avoid redundant indexing operations.
- **Version Alignment:** Ensure the version number is synchronized across `deno.json`, `icalendar.plug.yaml`, `PLUG.md`, and the TypeScript source code.

23
conductor/product.md Normal file
View File

@@ -0,0 +1,23 @@
# Initial Concept
`silverbullet-icalendar` is a Plug for SilverBullet that reads external iCalendar data (.ics format) and integrates it into the SilverBullet environment.
# Product Definition - SilverBullet iCalendar Plug
## Vision
A reliable and seamless bridge between external iCalendar services and the SilverBullet knowledge management environment, enabling users to consolidate their scheduling data within their personal workspace.
## Target Audience
- SilverBullet users who need to integrate external calendars (Google, Nextcloud, Outlook, etc.) directly into their notes and queries.
## Core Goals & Features
- **Reliable Multi-Source Synchronization:** Support for fetching and parsing `.ics` data from various providers like Google Calendar and Nextcloud.
- **SilverBullet Index Integration:** Seamlessly index calendar events using the `ical-event` tag, making them instantly queryable using SilverBullet's Lua Integrated Query (LIQ).
- **Robust Timezone Handling:** Accurate conversion and shifting of event times to ensure consistency regardless of the source provider's configuration.
- **Cache Management:** Efficient local caching of calendar data with user-configurable durations and force-sync capabilities.
- **Clean Indexing:** Sanitization of complex iCalendar objects into flat, query-friendly metadata.
## Technology Stack (Inferred)
- **Language:** TypeScript
- **Runtime:** Deno
- **Platform:** SilverBullet Plug API
- **Parsing Library:** `ts-ics`

View File

@@ -0,0 +1 @@
{"last_successful_step": "2.5_workflow"}

19
conductor/tech-stack.md Normal file
View File

@@ -0,0 +1,19 @@
# Technology Stack - SilverBullet iCalendar Plug
## Core Runtime & Language
- **Language:** [TypeScript](https://www.typescriptlang.org/) - Provides type safety and modern JavaScript features for robust plug development.
- **Runtime:** [Deno](https://deno.com/) - A secure-by-default runtime for JavaScript and TypeScript, used for building and running the plug's development tasks.
## Platform & API
- **Platform:** [SilverBullet Plug API](https://silverbullet.md/Plugs) - The official API for extending SilverBullet functionality.
- **Dependency Management:** [JSR](https://jsr.io/) and [ESM.sh](https://esm.sh/) - Used for importing the SilverBullet syscalls and external libraries like `ts-ics`.
## Libraries
- **iCalendar Parsing:** [`ts-ics`](https://www.npmjs.com/package/ts-ics) (v2.4.0) - A library for parsing iCalendar data into structured JavaScript objects.
- **Recurrence Expansion:** [`rrule`](https://www.npmjs.com/package/rrule) (v2.8.1) - A library for expanding recurring event rules (RRULE) into individual occurrences.
## Build & Development Tools
- **Task Orchestration:** Deno Tasks (defined in `deno.json`) - Handles version synchronization and plug compilation.
- **Compiler:** `plug-compile.js` - The standard SilverBullet utility for bundling the TypeScript source and manifest into a `.plug.js` file.
- **Version Control:** Git - For source code management and integration with Gitea Actions.
- **CI/CD:** Gitea Actions - Automates the build and deployment process upon pushes to the repository.

33
conductor/tracks.md Normal file
View File

@@ -0,0 +1,33 @@
# Project Tracks
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
---
- [x] **Track: Upgrade the SilverBullet iCalendar plug to use DST-aware timezone resolution and add recurring event support using rrule.**
*Link: [./tracks/timezone_rrule_20260218/](./tracks/timezone_rrule_20260218/)*
---
- [x] **Track: Fix TypeError: r.replace is not a function in icalendar.ts**
*Link: [./tracks/fix_rrule_type_error_20260219/](./tracks/fix_rrule_type_error_20260219/)*
---
- [x] **Track: Fix RRULE object expansion error by correctly mapping object keys to standard iCalendar RRULE properties.**
*Link: [./tracks/fix_rrule_object_mapping_20260219/](./tracks/fix_rrule_object_mapping_20260219/)*
---
- [x] **Track: Fix RRULE UNTIL object conversion error: Invalid UNTIL value: [object Object]**
*Link: [./tracks/fix_rrule_until_conversion_20260219/](./tracks/fix_rrule_until_conversion_20260219/)*
---
- [x] **Track: Fix SyntaxError: Invalid weekday string: [object Object] by implementing a generic recursive RRULE formatter.**
*Link: [./tracks/rrule_generic_formatter_20260219/](./tracks/rrule_generic_formatter_20260219/)*
---
- [~] **Track: Implement Playwright and Dockerized testing infrastructure with real ICS data samples.**
*Link: [./tracks/testing_infrastructure_20260219/](./tracks/testing_infrastructure_20260219/)*

View File

@@ -0,0 +1,5 @@
# Track fix_rrule_object_mapping_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "fix_rrule_object_mapping_20260219",
"type": "bug",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Fix RRULE object expansion error by correctly mapping object keys to standard iCalendar RRULE properties."
}

View File

@@ -0,0 +1,19 @@
# Implementation Plan - Fix RRULE Object Mapping
## Phase 1: Reproduction [checkpoint: 2fbb260]
- [x] Task: Reproduce `Unknown RRULE property` error 0b67cbb
- [x] Modify the test case in `icalendar_test.ts` to use `frequency` instead of `freq` in the mock object.
- [x] Run the test and confirm it fails with the expected error.
- [x] Task: Conductor - User Manual Verification 'Reproduction' (Protocol in workflow.md)
## Phase 2: Fix Implementation [checkpoint: fe28a5c]
- [x] Task: Implement mapping logic in `icalendar.ts` b8bc6cc
- [x] Create a mapping object for verbose keys to iCal keys.
- [x] Update `expandRecurrences` to use this mapping.
- [x] Run the test to confirm it passes.
- [x] Task: Conductor - User Manual Verification 'Fix Implementation' (Protocol in workflow.md)
## Phase 3: Verification & Cleanup [checkpoint: 793326a]
- [x] Task: Full Regression Check 7f9a618
- [x] Run all tests in `icalendar_test.ts`.
- [x] Task: Conductor - User Manual Verification 'Verification & Cleanup' (Protocol in workflow.md)

View File

@@ -0,0 +1,32 @@
# Specification: Fix RRULE Object Expansion Error
## Overview
The previous fix for handling object-type `rrule` properties (returned by `ts-ics`) introduced a regression. The conversion logic used uppercase full names (e.g., `FREQUENCY`), but the `rrule` library's `parseString` method expects standard iCalendar shortened keys (e.g., `FREQ`). This results in an `Error: Unknown RRULE property 'FREQUENCY'`.
## Functional Requirements
- **Correct Key Mapping:** The logic that converts an `rrule` object back to a string must use standard iCalendar RRULE property keys.
- **Mapping Table:**
- `frequency` -> `FREQ`
- `until` -> `UNTIL`
- `count` -> `COUNT`
- `interval` -> `INTERVAL`
- `bysecond` -> `BYSECOND`
- `byminute` -> `BYMINUTE`
- `byhour` -> `BYHOUR`
- `byday` -> `BYDAY`
- `bymonthday` -> `BYMONTHDAY`
- `byyearday` -> `BYYEARDAY`
- `byweekno` -> `BYWEEKNO`
- `bymonth` -> `BYMONTH`
- `bysetpos` -> `BYSETPOS`
- `wkst` -> `WKST`
- **Case Insensitivity:** The mapping should be case-insensitive for the input object keys.
## Implementation Steps
1. **Reproduce:** Update the existing `expandRecurrences - object rrule` test case to use the key `frequency` and verify it fails with the reported error.
2. **Fix:** Implement a mapping function in `icalendar.ts` to translate object keys to standard RRULE keys before stringifying.
3. **Verify:** Run the test case to confirm it now passes.
## Acceptance Criteria
- [ ] Test `expandRecurrences - object rrule` passes with an object using `frequency` key.
- [ ] No "Unknown RRULE property" errors are logged for valid RRULE objects.

View File

@@ -0,0 +1,5 @@
# Track fix_rrule_type_error_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "fix_rrule_type_error_20260219",
"type": "bug",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Fix TypeError: r.replace is not a function in icalendar.ts"
}

View File

@@ -0,0 +1,20 @@
# Implementation Plan - Fix `r.replace is not a function`
## Phase 1: Reproduction & Test Setup [checkpoint: df0ddaf]
- [x] Task: Create a reproduction test case 1a36c64
- [x] Create a new test case in `icalendar_test.ts` that mocks an event with a non-string `rrule` property (e.g., an object or number).
- [x] Run the test to confirm it fails with the expected `TypeError`.
- [x] Task: Conductor - User Manual Verification 'Reproduction & Test Setup' (Protocol in workflow.md)
## Phase 2: Implementation [checkpoint: 1c48f78]
- [x] Task: Implement defensive check in `icalendar.ts` d7401dd
- [x] Modify `expandRecurrences` function to check if `rruleStr` is a string before calling `.replace()`.
- [x] If `rruleStr` is not a string, log a warning and return the original event (non-recurring fallback).
- [x] Run the reproduction test again to confirm it passes.
- [x] Task: Conductor - User Manual Verification 'Implementation' (Protocol in workflow.md)
## Phase 3: Verification & Cleanup
- [x] Task: Verify fix and check for regressions
- [x] Run all tests in `icalendar_test.ts` to ensure existing functionality is preserved.
- [x] (Optional) Verify with actual calendar sync if possible/safe.
- [x] Task: Conductor - User Manual Verification 'Verification & Cleanup' (Protocol in workflow.md)

View File

@@ -0,0 +1,25 @@
# Specification: Fix `r.replace is not a function` in `expandRecurrences`
## Overview
This track addresses a `TypeError: r.replace is not a function` error occurring in `icalendar.ts` during calendar synchronization. The error suggests that the `rrule` property of an event is not a string when it reaches the `expandRecurrences` function, causing the subsequent `.replace()` call to fail. This is likely due to the `ts-ics` parser returning a non-string value (e.g., an object or `undefined`) for the `rrule` property in certain scenarios (specifically observed with Outlook calendars).
## Functional Requirements
- **Defensive RRULE Handling:** The `expandRecurrences` function in `icalendar.ts` must safely handle cases where `rrule` (or `recurrenceRule`) is not a string.
- **Graceful Fallback:** If `rrule` is not a string:
- It should be ignored/logged if it cannot be interpreted as a valid RRULE string, preventing the crash.
- The event should still be processed (treated as a non-recurring event if the rule is invalid), rather than crashing the entire sync for that event.
## Non-Functional Requirements
- **Stability:** The plug should not crash or throw unhandled exceptions during sync due to malformed or unexpected property types in the source ICS data.
- **Logging:** Maintain existing error logging but ensure the error message is descriptive (e.g., "Invalid RRULE type: object").
## Implementation Steps
1. **Reproduce Issue:** Create a unit test in `icalendar_test.ts` that mocks an `icsEvent` with a non-string `rrule` property (e.g., an object or number) and calls `expandRecurrences`.
2. **Implement Fix:** Modify `icalendar.ts` to check the type of `rruleStr` before calling `.replace()`.
- If it's not a string, attempt to convert it or return the original event (as if no recurrence rule exists) with a warning.
3. **Verify:** Run the new unit test to confirm the fix.
## Acceptance Criteria
- [ ] A new unit test case exists in `icalendar_test.ts` that passes with a non-string `rrule`.
- [ ] The `expandRecurrences` function no longer throws `TypeError: r.replace is not a function` when `rrule` is not a string.
- [ ] The sync process completes successfully even if some events have malformed `rrule` properties.

View File

@@ -0,0 +1,5 @@
# Track fix_rrule_until_conversion_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "fix_rrule_until_conversion_20260219",
"type": "bug",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Fix RRULE UNTIL object conversion error: Invalid UNTIL value: [object Object]"
}

View File

@@ -0,0 +1,19 @@
# Implementation Plan - Fix RRULE UNTIL Conversion
## Phase 1: Reproduction [checkpoint: 02fcb7e]
- [x] Task: Reproduce `Invalid UNTIL value` error 17de604
- [x] Add a test case in `icalendar_test.ts` where `rrule` object has an `until` property as a `Date`.
- [x] Run the test and confirm it fails with `Error: Invalid UNTIL value: [object Object]`.
- [x] Task: Conductor - User Manual Verification 'Reproduction' (Protocol in workflow.md)
## Phase 2: Fix Implementation [checkpoint: 0f334b2]
- [x] Task: Implement value formatting logic in `icalendar.ts` 331d0ab
- [x] Update `expandRecurrences` to use a helper for property value conversion.
- [x] Ensure `Date` objects are formatted as `YYYYMMDDTHHMMSSZ`.
- [x] Run the test to confirm it passes.
- [x] Task: Conductor - User Manual Verification 'Fix Implementation' (Protocol in workflow.md)
## Phase 3: Verification & Cleanup [checkpoint: a02f228]
- [x] Task: Full Regression Check d090334
- [x] Run all tests in `icalendar_test.ts`.
- [x] Task: Conductor - User Manual Verification 'Verification & Cleanup' (Protocol in workflow.md)

View File

@@ -0,0 +1,20 @@
# Specification: Fix RRULE UNTIL Object Conversion
## Overview
The conversion of RRULE objects to strings fails for nested objects like `UNTIL` dates, resulting in `UNTIL=[object Object]`. This causes the `rrule` library to fail during expansion.
## Functional Requirements
- **Robust Value Formatting:** Implement `formatRRuleValue` to handle various types of RRULE values:
- **Date Objects:** Convert to `YYYYMMDDTHHMMSSZ`.
- **Nested Date Objects:** (e.g., `{ date: Date }` from `ts-ics`) Extract and convert.
- **Other Types:** Default to string conversion.
- **Mapping Preservation:** Maintain the existing `RRULE_KEY_MAP` for key translation.
## Implementation Steps
1. **Reproduce:** Add test `expandRecurrences - object rrule with until` in `icalendar_test.ts` using a Date object for `until`. Verify it fails with `Invalid UNTIL value`.
2. **Fix:** Update `expandRecurrences` in `icalendar.ts` to use a formatting helper for values.
3. **Verify:** Confirm the test case passes.
## Acceptance Criteria
- [ ] Test `expandRecurrences - object rrule with until` passes.
- [ ] The generated string correctly represents the date (e.g., `UNTIL=20260219T000000Z`).

View File

@@ -0,0 +1,5 @@
# Track rrule_generic_formatter_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "rrule_generic_formatter_20260219",
"type": "bug",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Fix SyntaxError: Invalid weekday string: [object Object] by implementing a generic recursive RRULE formatter."
}

View File

@@ -0,0 +1,22 @@
# Implementation Plan - Generic RRULE Formatter
## Phase 1: Reproduction & Test Setup [checkpoint: b41f78c]
- [x] Task: Create reproduction test case for `BYDAY` object cfab0c3
- [x] Add a test in `icalendar_test.ts` mocking a `BYDAY` property as an array of objects (e.g. `[{ day: 'MO' }]`).
- [x] Run the test and confirm it fails with `SyntaxError: Invalid weekday string: [object Object]`.
- [x] Task: Conductor - User Manual Verification 'Reproduction & Test Setup' (Protocol in workflow.md)
## Phase 2: Implementation [checkpoint: 7d1fa9f]
- [x] Task: Implement Recursive Formatter 115a165
- [x] Update `formatRRuleValue` in `icalendar.ts` to be a recursive function.
- [x] Handle Arrays by joining elements with commas.
- [x] Handle `Date` objects using the existing iCal format.
- [x] Handle property objects (extract `date`, `day`, or `value` properties).
- [x] Run the reproduction test to confirm it passes.
- [x] Task: Conductor - User Manual Verification 'Implementation' (Protocol in workflow.md)
## Phase 3: Verification & Cleanup [checkpoint: 952ceb0]
- [x] Task: Composite and Regression Testing 1e613ea
- [x] Add a complex test case containing both nested `UNTIL` dates and `BYDAY` arrays.
- [x] Run the full regression test suite in `icalendar_test.ts`.
- [x] Task: Conductor - User Manual Verification 'Verification & Cleanup' (Protocol in workflow.md)

View File

@@ -0,0 +1,25 @@
# Specification: Generic RRULE Object Formatter
## Overview
The current implementation for converting RRULE objects (from `ts-ics`) back to strings is brittle and fails when encountering nested objects or arrays for properties like `BYDAY`. This results in `[object Object]` being injected into the RRULE string, causing syntax errors in the `rrule` library. This track replaces the specific property handling with a robust, recursive generic formatter.
## Functional Requirements
- **Recursive Value Formatting:** The `formatRRuleValue` function must handle nested structures:
- **Arrays:** Join elements with commas (e.g., `['MO', 'TU']` -> `MO,TU`).
- **Date Objects:** Format as `YYYYMMDDTHHMMSSZ`.
- **Nested Property Objects:** If an object has a `date`, `day`, or `value` property, extract and format that value (e.g., `{ day: 'MO' }` -> `MO`).
- **Recursion:** If an array contains objects, or an object contains an array, the logic must recurse until primitives are reached.
- iCalendar Compliance: Ensure the resulting string for every property matches the iCalendar spec format expected by `rrule.js`.
## Implementation Steps
1. Reproduce: Create a test case in `icalendar_test.ts` where `BYDAY` is an object or an array of objects. Verify it fails with `Invalid weekday string: [object Object]`.
2. Fix: Re-implement `formatRRuleValue` in `icalendar.ts` as a recursive function that handles Arrays, Dates, and standard `ts-ics` nested value objects.
3. Verify:
- Confirm the `BYDAY` reproduction test passes.
- Add a composite test with `UNTIL` (Date) and `BYDAY` (Array) together.
- Run regression tests to ensure standard string RRULEs still work.
## Acceptance Criteria
- [ ] `expandRecurrences` correctly handles `BYDAY` when provided as an array of objects.
- [ ] `expandRecurrences` correctly handles `UNTIL` when provided as a nested date object.
- [ ] All 12+ existing tests pass, including regression checks for string-based RRULEs.

View File

@@ -0,0 +1,5 @@
# Track testing_infrastructure_20260219 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "testing_infrastructure_20260219",
"type": "chore",
"status": "new",
"created_at": "2026-02-19T00:00:00Z",
"updated_at": "2026-02-19T00:00:00Z",
"description": "Implement Playwright and Dockerized testing infrastructure with real ICS data samples."
}

View File

@@ -0,0 +1,29 @@
# Implementation Plan - Testing Infrastructure
## Phase 1: Environment & Mock Server [checkpoint: 1a46341]
- [x] Task: Scaffold Docker environment 23a59b9
- [x] Create test_data/ directory.
- [x] Create docker-compose.test.yml with SilverBullet and an Nginx container serving test_data/.
- [x] Task: Integration Test Scaffolding 2bfd9d5
- [x] Create tests/integration_test.ts that uses the actual ts-ics parser on files in test_data/.
- [x] Task: Conductor - User Manual Verification 'Environment & Mock Server' (Protocol in workflow.md)
- [x] Ensure use of `docker-compose -f docker-compose.test.yml down -v` for clean starts.
## Phase 2: Playwright E2E Setup [checkpoint: e80e4fb]
- [x] Task: Initialize Playwright 319a955
- [x] Setup Playwright with necessary configurations for Docker (headless, CI mode).
- [x] Task: Implement sync smoke test ed15079
- [x] Create tests/e2e/sync.spec.ts.
- [x] Automate login, plug installation (icalendar.plug.js), and triggering the "iCalendar: Sync" command.
- [x] Implement console listener to fail on Error or TypeError.
- [x] Task: Conductor - User Manual Verification 'Playwright E2E Setup' (Protocol in workflow.md)
## Phase 3: Validation & Bug Fix
- [x] Task: Verify Infrastructure against current bug 2bfd9d5
- [x] Add the problematic .ics to test_data/.
- [x] Confirm that E2E and Integration tests fail with Unknown RRULE property 'WORKWEEKSTART'.
- [x] Task: Implement Fix for WORKWEEKSTART a8755eb
- [x] Update RRULE_KEY_MAP in icalendar.ts.
- [x] Run tests again to confirm they pass.
- [~] Task: Conductor - User Manual Verification 'Validation & Bug Fix' (Protocol in workflow.md)
- [ ] Update Playwright to run in headed mode via xvfb-run per user request.

View File

@@ -0,0 +1,26 @@
# Specification: Testing Infrastructure (Playwright & Docker)
## Overview
Our current testing strategy relies on manual mocks that don't capture the complexity of real-world iCalendar data (specifically from Outlook/Office 365). This has led to multiple regressions where object-type RRULE properties cause the plug to fail. This track implements a full E2E and integration testing suite using Docker, Playwright, and real .ics samples.
## Functional Requirements
- **Dockerized Environment:** A docker-compose.test.yml that spins up:
- **SilverBullet:** A clean instance for plug installation.
- **Mock ICS Server:** A simple web server serving files from a test_data/ directory.
- **Playwright E2E Suite:**
- **Automation:** Automates logging into SilverBullet, installing the freshly built icalendar.plug.js, and triggering a sync.
- **Console Monitoring:** Automatically fails the test if any Error or TypeError is detected in the browser console.
- **Index Verification:** Uses SilverBullet syscalls (via Playwright evaluate) or UI queries to verify that the expected number of ical-event objects exist in the index.
- **Real-Data Integration Tests:** Deno unit tests that parse actual .ics files (e.g. mock_calendar.ics) using the real ts-ics parser before calling expandRecurrences.
## Implementation Steps
1. Environment Setup: Create docker-compose.test.yml and a test_data/ directory with at least one problematic Outlook .ics file.
2. Mock Server: Configure a lightweight container (e.g., Nginx or Python) to serve the test_data/.
3. Playwright Scaffolding: Initialize Playwright and create a tests/sync.spec.ts that handles authentication and plug installation.
4. Verification Logic: Implement the console-listener and index-counting logic in the test suite.
5. Smoke Test: Fix the known WORKWEEKSTART error and verify that the new infrastructure catches it if the fix is reverted.
## Acceptance Criteria
- [ ] A single command (e.g., make test-e2e) builds the plug, starts the containers, and runs the Playwright suite.
- [ ] The E2E suite successfully identifies the WORKWEEKSTART error when run against the current (buggy) code.
- [ ] The E2E suite passes after the WORKWEEKSTART fix is applied.

View File

@@ -0,0 +1,5 @@
# Track timezone_rrule_20260218 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View File

@@ -0,0 +1,8 @@
{
"track_id": "timezone_rrule_20260218",
"type": "feature",
"status": "new",
"created_at": "2026-02-18T11:20:00Z",
"updated_at": "2026-02-18T11:20:00Z",
"description": "Upgrade the SilverBullet iCalendar plug to use DST-aware timezone resolution and add recurring event support using rrule."
}

View File

@@ -0,0 +1,40 @@
# Implementation Plan: Proper Timezone Handling & Recurring Events
## Phase 1: Foundation - Timezone Mapping & Resolver [checkpoint: b8bf269]
- [x] Task: Setup Timezone Map (WINDOWS_TO_IANA)
- [x] Write failing tests for `resolveIanaName`
- [x] Implement `WINDOWS_TO_IANA` mapping and `resolveIanaName` in `timezones.ts`
- [x] Task: Implement UTC Offset Resolver using Intl
- [x] Write failing tests for `getUtcOffsetMs`
- [x] Implement `getUtcOffsetMs` in `timezones.ts`
- [x] Task: Conductor - User Manual Verification 'Phase 1: Foundation' (Protocol in workflow.md)
## Phase 2: Core Logic - Extraction & Shifting [checkpoint: 10a6db5]
- [x] Task: Fix Wall-Clock Extraction logic
- [x] Write failing tests for `resolveEventStart` (mocking `Intl` if necessary)
- [x] Implement `resolveEventStart` in `icalendar.ts` to handle local time ground truth
- [x] Task: Conductor - User Manual Verification 'Phase 2: Core Logic' (Protocol in workflow.md)
## Phase 3: Features - Recurring Events & Filtering [checkpoint: ffaef28]
- [x] Task: Integrate `rrule` library
- [x] Add `rrule` to `deno.json` imports
- [x] Verify import works in a simple script
- [x] Task: Implement Recurring Event Expansion
- [x] Write failing tests for `expandRecurrences`
- [x] Implement `expandRecurrences` in `icalendar.ts`
- [x] Task: Implement EXDATE support
- [x] Write failing tests for EXDATE exclusion
- [x] Update `expandRecurrences` to handle `EXDATE`
- [x] Task: Implement Status Filtering
- [x] Write failing tests for filtering "CANCELLED" events
- [x] Update sync logic to filter based on iCalendar status
- [x] Task: Conductor - User Manual Verification 'Phase 3: Features' (Protocol in workflow.md)
## Phase 4: Cleanup & Configuration [checkpoint: 533c240]
- [x] Task: Remove obsolete configuration
- [x] Write failing tests verifying `tzShift` is ignored/deprecated
- [x] Remove `tzShift` and `hourShift` from `getSources` and `fetchAndParseCalendar`
- [x] Task: Add `syncWindowDays` configuration
- [x] Write failing tests for configurable expansion window
- [x] Implement `syncWindowDays` in config and sync logic
- [x] Task: Conductor - User Manual Verification 'Phase 4: Cleanup & Configuration' (Protocol in workflow.md)

View File

@@ -0,0 +1,36 @@
# Specification: Proper Timezone Handling & Recurring Events
## Overview
Upgrade the SilverBullet iCalendar plug to provide accurate, DST-aware timezone resolution and full support for recurring events (RRULE expansion). This replaces manual hour shifting with an automated, reliable system using IANA timezone standards and the `Intl` API.
## Functional Requirements
- **IANA Timezone Mapping:** Implement a comprehensive mapping of 139 Windows timezone names to IANA identifiers using Unicode CLDR data.
- **DST-Aware Offsets:** Calculate UTC offsets at runtime for specific event dates using the built-in `Intl.DateTimeFormat` API, ensuring accuracy during Daylight Saving Time transitions.
- **Robust Date Extraction:** Correct the wall-clock extraction logic to prevent "double-shifting" of event times.
- **Recurring Event Expansion:**
- Integrate the `rrule` library to expand recurring events into individual occurrences.
- Support `EXDATE` for excluding specific instances of a recurring series.
- Implement a configurable `syncWindowDays` (default: 365) to limit the expansion range.
- **Advanced Filtering:**
- Filter out "CANCELLED" events based on the iCalendar status field.
- (Optional) Add `includeTransparent` and `includeDeclined` per-source flags.
- **Error Handling & Fallbacks:**
- If a timezone is unrecognized, fallback to UTC and append a warning to the event's description.
- **Configuration Cleanup:**
- Remove the redundant `tzShift` / `hourShift` parameters.
- Add `syncWindowDays` global config.
## Non-Functional Requirements
- **Self-Contained:** Maintain the plug as a Deno-compatible project using `esm.sh` or `deno.json` imports.
- **Performance:** Ensure efficient expansion of recurrences, even for busy calendars.
## Acceptance Criteria
1. Events from various providers (Google, O365, Nextcloud) appear at the correct local time in SilverBullet, regardless of DST.
2. All occurrences of a weekly recurring event within the sync window are indexed.
3. Excluded dates (`EXDATE`) are correctly omitted from the index.
4. Cancelled events are not indexed.
5. The manual `tzShift` configuration is no longer required for correct time display.
## Out of Scope
- Full CalDAV synchronization (this remains a read-only `.ics` fetcher).
- UI for managing individual recurring instances (handled via SilverBullet queries).

352
conductor/workflow.md Normal file
View File

@@ -0,0 +1,352 @@
# Project Workflow
## Guiding Principles
1. **The Plan is the Source of Truth:** All work must be tracked in `plan.md`
2. **The Tech Stack is Deliberate:** Changes to the tech stack must be documented in `tech-stack.md` *before* implementation
3. **Test-Driven Development:** Write unit tests before implementing functionality
4. **High Code Coverage:** Aim for >80% code coverage for all modules
5. **User Experience First:** Every decision should prioritize user experience
6. **Non-Interactive & CI-Aware:** Prefer non-interactive commands. Use `CI=true` for watch-mode tools (tests, linters) to ensure single execution.
## Task Workflow
All tasks follow a strict lifecycle:
### Standard Task Workflow
1. **Select Task:** Choose the next available task from `plan.md` in sequential order
2. **Mark In Progress:** Before beginning work, edit `plan.md` and change the task from `[ ]` to `[~]`
3. **Write Failing Tests (Red Phase):**
- Create a new test file for the feature or bug fix.
- Write one or more unit tests that clearly define the expected behavior and acceptance criteria for the task.
- **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests.
4. **Implement to Pass Tests (Green Phase):**
- Write the minimum amount of application code necessary to make the failing tests pass.
- Run the test suite again and confirm that all tests now pass. This is the "Green" phase.
5. **Refactor (Optional but Recommended):**
- With the safety of passing tests, refactor the implementation code and the test code to improve clarity, remove duplication, and enhance performance without changing the external behavior.
- Rerun tests to ensure they still pass after refactoring.
6. **Verify Coverage:** Run coverage reports using the project's chosen tools. For example, in a Python project, this might look like:
```bash
pytest --cov=app --cov-report=html
```
Target: >80% coverage for new code. The specific tools and commands will vary by language and framework.
7. **Document Deviations:** If implementation differs from tech stack:
- **STOP** implementation
- Update `tech-stack.md` with new design
- Add dated note explaining the change
- Resume implementation
8. **Commit Code Changes:**
- Stage all code changes related to the task.
- Propose a clear, concise commit message e.g, `feat(ui): Create basic HTML structure for calculator`.
- Perform the commit.
9. **Attach Task Summary with Git Notes:**
- **Step 9.1: Get Commit Hash:** Obtain the hash of the *just-completed commit* (`git log -1 --format="%H"`).
- **Step 9.2: Draft Note Content:** Create a detailed summary for the completed task. This should include the task name, a summary of changes, a list of all created/modified files, and the core "why" for the change.
- **Step 9.3: Attach Note:** Use the `git notes` command to attach the summary to the commit.
```bash
# The note content from the previous step is passed via the -m flag.
git notes add -m "<note content>" <commit_hash>
```
10. **Get and Record Task Commit SHA:**
- **Step 10.1: Update Plan:** Read `plan.md`, find the line for the completed task, update its status from `[~]` to `[x]`, and append the first 7 characters of the *just-completed commit's* commit hash.
- **Step 10.2: Write Plan:** Write the updated content back to `plan.md`.
11. **Commit Plan Update:**
- **Action:** Stage the modified `plan.md` file.
- **Action:** Commit this change with a descriptive message (e.g., `conductor(plan): Mark task 'Create user model' as complete`).
### Phase Completion Verification and Checkpointing Protocol
**Trigger:** This protocol is executed immediately after a task is completed that also concludes a phase in `plan.md`.
1. **Announce Protocol Start:** Inform the user that the phase is complete and the verification and checkpointing protocol has begun.
2. **Ensure Test Coverage for Phase Changes:**
- **Step 2.1: Determine Phase Scope:** To identify the files changed in this phase, you must first find the starting point. Read `plan.md` to find the Git commit SHA of the *previous* phase's checkpoint. If no previous checkpoint exists, the scope is all changes since the first commit.
- **Step 2.2: List Changed Files:** Execute `git diff --name-only <previous_checkpoint_sha> HEAD` to get a precise list of all files modified during this phase.
- **Step 2.3: Verify and Create Tests:** For each file in the list:
- **CRITICAL:** First, check its extension. Exclude non-code files (e.g., `.json`, `.md`, `.yaml`).
- For each remaining code file, verify a corresponding test file exists.
- If a test file is missing, you **must** create one. Before writing the test, **first, analyze other test files in the repository to determine the correct naming convention and testing style.** The new tests **must** validate the functionality described in this phase's tasks (`plan.md`).
3. **Execute Automated Tests with Proactive Debugging:**
- Before execution, you **must** announce the exact shell command you will use to run the tests.
- **Example Announcement:** "I will now run the automated test suite to verify the phase. **Command:** `CI=true npm test`"
- Execute the announced command.
- If tests fail, you **must** inform the user and begin debugging. You may attempt to propose a fix a **maximum of two times**. If the tests still fail after your second proposed fix, you **must stop**, report the persistent failure, and ask the user for guidance.
4. **Propose a Detailed, Actionable Manual Verification Plan:**
- **CRITICAL:** To generate the plan, first analyze `product.md`, `product-guidelines.md`, and `plan.md` to determine the user-facing goals of the completed phase.
- You **must** generate a step-by-step plan that walks the user through the verification process, including any necessary commands and specific, expected outcomes.
- The plan you present to the user **must** follow this format:
**For a Frontend Change:**
```
The automated tests have passed. For manual verification, please follow these steps:
**Manual Verification Steps:**
1. **Start the development server with the command:** `npm run dev`
2. **Open your browser to:** `http://localhost:3000`
3. **Confirm that you see:** The new user profile page, with the user's name and email displayed correctly.
```
**For a Backend Change:**
```
The automated tests have passed. For manual verification, please follow these steps:
**Manual Verification Steps:**
1. **Ensure the server is running.**
2. **Execute the following command in your terminal:** `curl -X POST http://localhost:8080/api/v1/users -d '{"name": "test"}'`
3. **Confirm that you receive:** A JSON response with a status of `201 Created`.
```
5. **Await Explicit User Feedback:**
- After presenting the detailed plan, ask the user for confirmation: "**Does this meet your expectations? Please confirm with yes or provide feedback on what needs to be changed.**"
- **PAUSE** and await the user's response. Do not proceed without an explicit yes or confirmation.
6. **Create Checkpoint Commit:**
- Stage all changes. If no changes occurred in this step, proceed with an empty commit.
- Perform the commit with a clear and concise message (e.g., `conductor(checkpoint): Checkpoint end of Phase X`).
7. **Attach Auditable Verification Report using Git Notes:**
- **Step 7.1: Draft Note Content:** Create a detailed verification report including the automated test command, the manual verification steps, and the user's confirmation.
- **Step 7.2: Attach Note:** Use the `git notes` command and the full commit hash from the previous step to attach the full report to the checkpoint commit.
8. **Get and Record Phase Checkpoint SHA:**
- **Step 8.1: Get Commit Hash:** Obtain the hash of the *just-created checkpoint commit* (`git log -1 --format="%H"`).
- **Step 8.2: Update Plan:** Read `plan.md`, find the heading for the completed phase, and append the first 7 characters of the commit hash in the format `[checkpoint: <sha>]`.
- **Step 8.3: Write Plan:** Write the updated content back to `plan.md`.
9. **Commit Plan Update:**
- **Action:** Stage the modified `plan.md` file.
- **Action:** Commit this change with a descriptive message following the format `conductor(plan): Mark phase '<PHASE NAME>' as complete`.
10. **Announce Completion:** Inform the user that the phase is complete and the checkpoint has been created, with the detailed verification report attached as a git note.
### Track Completion Protocol
**Trigger:** This protocol is executed when all phases and tasks in a track are complete.
1. **Version Bump (Code Changes Only):**
- If the track involved code changes (i.e., not purely documentation), you **must** bump the project version number.
- Update the version in `deno.json`.
- Run the version synchronization script: `docker run --rm -v $(pwd):/app -w /app denoland/deno:latest task sync-version`.
- Commit the version bump with message: `chore: Bump version to <new_version>`.
2. **Push Changes (Code Changes Only):**
- If the track involved code changes, you **must** push the changes to the remote repository.
- **Command:** `git push`
3. **Monitor CI/CD (Code Changes Only):**
- After pushing, you **must** monitor the resulting Gitea Action to ensure it completes successfully.
- Use the `gitea-push-watch` skill if available, or check the Gitea interface manually.
- **Requirement:** The track is NOT complete until the CI/CD pipeline passes. If it fails, you must investigate and fix the issue.
### Quality Gates
Before marking any task complete, verify:
- [ ] All tests pass
- [ ] Code coverage meets requirements (>80%)
- [ ] Code follows project's code style guidelines (as defined in `code_styleguides/`)
- [ ] All public functions/methods are documented (e.g., docstrings, JSDoc, GoDoc)
- [ ] Type safety is enforced (e.g., type hints, TypeScript types, Go types)
- [ ] No linting or static analysis errors (using the project's configured tools)
- [ ] Works correctly on mobile (if applicable)
- [ ] Documentation updated if needed
- [ ] No security vulnerabilities introduced
## Development Commands
**AI AGENT INSTRUCTION: This section should be adapted to the project's specific language, framework, and build tools.**
### Setup
```bash
# Example: Commands to set up the development environment (e.g., install dependencies, configure database)
# e.g., for a Node.js project: npm install
# e.g., for a Go project: go mod tidy
```
### Daily Development
```bash
# Example: Commands for common daily tasks (e.g., start dev server, run tests, lint, format)
# e.g., for a Node.js project: npm run dev, npm test, npm run lint
# e.g., for a Go project: go run main.go, go test ./..., go fmt ./...
```
### Before Committing
```bash
# Example: Commands to run all pre-commit checks (e.g., format, lint, type check, run tests)
# e.g., for a Node.js project: npm run check
# e.g., for a Go project: make check (if a Makefile exists)
```
## Testing Requirements
### Unit Testing
- Every module must have corresponding tests.
- Use appropriate test setup/teardown mechanisms (e.g., fixtures, beforeEach/afterEach).
- Mock external dependencies.
- Test both success and failure cases.
### Integration Testing
- Test complete user flows
- Verify database transactions
- Test authentication and authorization
- Check form submissions
### Mobile Testing
- Test on actual iPhone when possible
- Use Safari developer tools
- Test touch interactions
- Verify responsive layouts
- Check performance on 3G/4G
## Code Review Process
### Self-Review Checklist
Before requesting review:
1. **Functionality**
- Feature works as specified
- Edge cases handled
- Error messages are user-friendly
2. **Code Quality**
- Follows style guide
- DRY principle applied
- Clear variable/function names
- Appropriate comments
3. **Testing**
- Unit tests comprehensive
- Integration tests pass
- Coverage adequate (>80%)
4. **Security**
- No hardcoded secrets
- Input validation present
- SQL injection prevented
- XSS protection in place
5. **Performance**
- Database queries optimized
- Images optimized
- Caching implemented where needed
6. **Mobile Experience**
- Touch targets adequate (44x44px)
- Text readable without zooming
- Performance acceptable on mobile
- Interactions feel native
## Commit Guidelines
### Message Format
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
### Types
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only
- `style`: Formatting, missing semicolons, etc.
- `refactor`: Code change that neither fixes a bug nor adds a feature
- `test`: Adding missing tests
- `chore`: Maintenance tasks
### Examples
```bash
git commit -m "feat(auth): Add remember me functionality"
git commit -m "fix(posts): Correct excerpt generation for short posts"
git commit -m "test(comments): Add tests for emoji reaction limits"
git commit -m "style(mobile): Improve button touch targets"
```
## Definition of Done
A task is complete when:
1. All code implemented to specification
2. Unit tests written and passing
3. Code coverage meets project requirements
4. Documentation complete (if applicable)
5. Code passes all configured linting and static analysis checks
6. Works beautifully on mobile (if applicable)
7. Implementation notes added to `plan.md`
8. Changes committed with proper message
9. Git note with task summary attached to the commit
## Emergency Procedures
### Critical Bug in Production
1. Create hotfix branch from main
2. Write failing test for bug
3. Implement minimal fix
4. Test thoroughly including mobile
5. Deploy immediately
6. Document in plan.md
### Data Loss
1. Stop all write operations
2. Restore from latest backup
3. Verify data integrity
4. Document incident
5. Update backup procedures
### Security Breach
1. Rotate all secrets immediately
2. Review access logs
3. Patch vulnerability
4. Notify affected users (if any)
5. Document and update security procedures
## Deployment Workflow
### Pre-Deployment Checklist
- [ ] All tests passing
- [ ] Coverage >80%
- [ ] No linting errors
- [ ] Mobile testing complete
- [ ] Environment variables configured
- [ ] Database migrations ready
- [ ] Backup created
### Deployment Steps
1. Merge feature branch to main
2. Tag release with version
3. Push to deployment service
4. Run database migrations
5. Verify deployment
6. Test critical paths
7. Monitor for errors
### Post-Deployment
1. Monitor analytics
2. Check error logs
3. Gather user feedback
4. Plan next iteration
## Continuous Improvement
- Review workflow weekly
- Update based on pain points
- Document lessons learned
- Optimize for user happiness
- Keep things simple and maintainable

30
deno.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "icalendar-plug",
"version": "0.4.1",
"nodeModulesDir": "auto",
"tasks": {
"sync-version": "deno run -A scripts/sync-version.ts",
"build": "deno task sync-version && deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/2.4.1/plug-compile.js -c deno.json icalendar.plug.yaml",
"watch": "deno task build --watch",
"debug": "deno run -A https://raw.githubusercontent.com/silverbulletmd/silverbullet/v2.4.1/plug-compile.js -c deno.json icalendar.plug.yaml --debug"
},
"lint": {
"rules": {
"exclude": [
"no-explicit-any"
]
}
},
"fmt": {
"exclude": [
"*.md",
"**/*.md",
"*.plug.js"
]
},
"imports": {
"@silverbulletmd/silverbullet": "jsr:@silverbulletmd/silverbullet@^2.4.1",
"ical.js": "https://esm.sh/ical.js@2.0.1",
"rrule": "https://esm.sh/rrule@2.8.1"
}
}

View File

@@ -1,22 +0,0 @@
{
"tasks": {
"build": "silverbullet plug:compile -c deno.jsonc icalendar.plug.yaml",
"watch": "silverbullet plug:compile -c deno.jsonc icalendar.plug.yaml -w"
},
"lint": {
"rules": {
"exclude": ["no-explicit-any"]
}
},
"fmt": {
"exclude": [
"*.md",
"**/*.md",
"*.plug.js"
]
},
"imports": {
"@silverbulletmd/silverbullet": "jsr:@silverbulletmd/silverbullet@^0.10.1",
"ts-ics": "npm:ts-ics@1.6.5"
}
}

36
docker-compose.test.yml Normal file
View File

@@ -0,0 +1,36 @@
services:
silverbullet-test:
image: zefhemel/silverbullet:2.4.0
ports:
- "3001:3000"
volumes:
- ./test_space_e2e:/space
environment:
- SB_LOG_PUSH=true
- SB_DEBUG=true
- SB_SPACE_LUA_TRUSTED=true
mock-ics-server:
image: nginx:alpine
ports:
- "8081:80"
volumes:
- ./test_data:/usr/share/nginx/html:ro
- ./test_data/nginx.conf:/etc/nginx/nginx.conf:ro
playwright:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
network_mode: "service:silverbullet-test"
volumes:
- .:/work
- /work/node_modules
- /tmp/.X11-unix:/tmp/.X11-unix
working_dir: /work
environment:
- CI=true
- DISPLAY=${DISPLAY:-:0}
- SB_URL=http://localhost:3000
command: sh -c "npm install && npx playwright test"
volumes:
sb-test-space:

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
services:
silverbullet:
image: zefhemel/silverbullet:latest
ports:
- "3000:3000"
volumes:
- ./test_space:/space
environment:
- SB_USER=admin:admin
- SB_LOG_PUSH=true
- SB_DEBUG=true
- SB_SPACE_LUA_TRUSTED=true
mock-ics:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./mock_calendar.ics:/usr/share/nginx/html/calendar.ics:ro

BIN
gitea-push-watch.skill Normal file

Binary file not shown.

32
gitea-push-watch/SKILL.md Normal file
View File

@@ -0,0 +1,32 @@
---
name: gitea-push-watch
description: Monitor and debug Gitea Actions after a push. Use when a user asks to check if an action ran or failed on a Gitea instance, and use the provided API token for authentication.
---
# Gitea Push Watch
This skill provides tools to monitor Gitea Actions and debug failures using the Gitea API.
## Workflow
1. **Identify Repository Info**: Extract the Gitea server URL and repository path (e.g., `owner/repo`) from the project context or git remote.
2. **Authenticate**: Use the user-provided API token. Ensure it is passed as a `token <value>` header in API calls.
3. **Monitor Run**: Use the `scripts/gitea_action_monitor.py` script to check the status of the latest run.
4. **Analyze Failures**: If a run fails, use the Gitea API to fetch specific job logs to identify the root cause (e.g., permission issues, network errors).
## Script Usage
```bash
python3 scripts/gitea_action_monitor.py <server_url> <repo_path> <api_token>
```
Example:
```bash
python3 scripts/gitea_action_monitor.py https://gitea.example.com sstent/my-repo MY_TOKEN
```
## Common Gitea API Endpoints
- List runs: `GET /api/v1/repos/{owner}/{repo}/actions/runs`
- Get run details: `GET /api/v1/repos/{owner}/{repo}/actions/runs/{id}`
- List jobs: `GET /api/v1/repos/{owner}/{repo}/actions/runs/{id}/jobs`

View File

@@ -0,0 +1,76 @@
import os
import sys
import json
import urllib.request
import urllib.error
import time
def get_action_runs(server_url, repo, token, limit=1):
url = f"{server_url}/api/v1/repos/{repo}/actions/runs?limit={limit}"
req = urllib.request.Request(url)
req.add_header("Authorization", f"token {token}")
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode())
except urllib.error.HTTPError as e:
print(f"Error fetching action runs: {e.code} {e.reason}")
return None
except Exception as e:
print(f"Error: {str(e)}")
return None
def monitor_run(server_url, repo, token, run_id):
url = f"{server_url}/api/v1/repos/{repo}/actions/runs/{run_id}"
req = urllib.request.Request(url)
req.add_header("Authorization", f"token {token}")
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode())
except Exception as e:
print(f"Error monitoring run: {str(e)}")
return None
def main():
if len(sys.argv) < 4:
print("Usage: python3 gitea_action_monitor.py <server_url> <repo> <token>")
sys.exit(1)
server_url = sys.argv[1].rstrip('/')
repo = sys.argv[2]
token = sys.argv[3]
print(f"Checking Gitea Actions for {repo}...")
runs_data = get_action_runs(server_url, repo, token)
if not runs_data or not runs_data.get('workflow_runs'):
print("No action runs found.")
return
latest_run = runs_data['workflow_runs'][0]
run_id = latest_run['id']
status = latest_run['status']
conclusion = latest_run.get('conclusion', 'unknown')
name = latest_run.get('name', 'unnamed')
print(f"Latest Run: {name} (ID: {run_id})")
print(f"Status: {status}")
if status == "running":
print("Waiting for completion...")
for _ in range(10): # Max 10 attempts
time.sleep(10)
run_data = monitor_run(server_url, repo, token, run_id)
if not run_data: break
status = run_data['status']
if status != "running":
conclusion = run_data.get('conclusion', 'unknown')
break
print(".", end="", flush=True)
print(f"\nFinal Status: {status} ({conclusion})")
else:
print(f"Conclusion: {conclusion}")
if __name__ == "__main__":
main()

10
icalendar.plug.js Normal file

File diff suppressed because one or more lines are too long

7
icalendar.plug.js.map Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,31 +1,34 @@
name: icalendar
version: 0.4.1
author: sstent
index: icalendar.ts
# Legacy SilverBullet permission name
requiredPermissions:
- fetch
# Modern SilverBullet permission name
permissions:
- fetch
- http
functions:
syncCalendars:
path: icalendar.ts:syncCalendars
command:
name: "iCalendar: Sync"
priority: -1
events:
- editor:init
forceSync:
path: icalendar.ts:forceSync
command:
name: "iCalendar: Force Sync"
priority: -1
clearCache:
path: icalendar.ts:clearCache
command:
name: "iCalendar: Clear All Events"
priority: -1
showVersion:
path: ./icalendar.ts:showVersion
path: icalendar.ts:showVersion
command:
name: "iCalendar: Version"
priority: -2
queryEvents:
path: ./icalendar.ts:queryEvents
events:
- query:ical-event
config:
schema.config.properties.icalendar:
type: object
required:
- sources
properties:
sources:
type: array
minItems: 1
items:
type: object
required:
- url
properties:
url:
type: string
name:
type: string

View File

@@ -1,127 +1,382 @@
import { editor, system } from "@silverbulletmd/silverbullet/syscalls";
import { QueryProviderEvent } from "@silverbulletmd/silverbullet/types";
import { applyQuery } from "@silverbulletmd/silverbullet/lib/query";
import { parseIcsCalendar, type VCalendar } from "ts-ics";
import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls";
import { convertIcsCalendar } from "https://esm.sh/ts-ics@2.4.0";
import { RRule, RRuleSet } from "rrule";
import { getUtcOffsetMs, resolveIanaName } from "./timezones.ts";
const VERSION = "0.1.0";
const VERSION = "0.4.1";
const CACHE_KEY = "icalendar:lastSync";
// Try to match SilverBullet properties where possible.
// Timestamps should be strings formatted with `localDateString`
interface Event {
// Typically available in calendar apps
summary: string | undefined;
description: string | undefined;
location: string | undefined;
console.log(`[iCalendar] Plug script executing at top level (Version ${VERSION})`);
// Same as SilverBullet pages
created: string | undefined;
lastModified: string | undefined;
// Keep consistent with dates above
start: string | undefined;
end: string | undefined;
/**
* Mapping of verbose RRULE object keys to standard iCalendar shortened keys.
*/
const RRULE_KEY_MAP: Record<string, string> = {
"frequency": "FREQ",
"until": "UNTIL",
"count": "COUNT",
"interval": "INTERVAL",
"bysecond": "BYSECOND",
"byminute": "BYMINUTE",
"byhour": "BYHOUR",
"byday": "BYDAY",
"bymonthday": "BYMONTHDAY",
"byyearday": "BYYEARDAY",
"byweekno": "BYWEEKNO",
"bymonth": "BYMONTH",
"bysetpos": "BYSETPOS",
"wkst": "WKST",
"workweekstart": "WKST",
"freq": "FREQ",
};
sourceName: string | undefined;
/**
* Robustly formats an RRULE value for its string representation.
*/
function formatRRuleValue(v: any): string {
if (Array.isArray(v)) {
return v.map((item) => formatRRuleValue(item)).join(",");
}
if (v instanceof Date) {
return v.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
}
if (typeof v === "object" && v !== null) {
const val = v.date || v.day || v.value;
if (val !== undefined) {
return formatRRuleValue(val);
}
}
return String(v);
}
interface Source {
url: string; // Should be an .ics file
name: string | undefined; // Optional name that will be assigned to events
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Creates a SHA-256 hash of a string (hex encoded)
*/
async function sha256Hash(str: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}
export async function queryEvents(
{ query }: QueryProviderEvent,
): Promise<any[]> {
const events: Event[] = [];
/**
* Converts Date to local time string (browser's timezone)
*/
export function localDateString(date: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return date.getFullYear() + "-" +
pad(date.getMonth() + 1) + "-" +
pad(date.getDate()) + "T" +
pad(date.getHours()) + ":" +
pad(date.getMinutes()) + ":" +
pad(date.getSeconds());
}
const sources = await getSources();
for (const source of sources) {
const identifier = (source.name === undefined || source.name === "")
? source.url
: source.name;
/**
* Recursively converts all Date objects and ISO date strings to strings
*/
function convertDatesToStrings<T>(obj: T): any {
if (obj === null || obj === undefined) {
return obj;
}
if (obj instanceof Date) {
return obj.toISOString();
}
if (typeof obj === 'object' && 'date' in obj && (obj as any).date instanceof Date) {
return (obj as any).date.toISOString();
}
if (typeof obj === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(obj)) {
try {
const result = await fetch(source.url);
const icsData = await result.text();
const calendarParsed: VCalendar = parseIcsCalendar(icsData);
if (calendarParsed.events === undefined) {
throw new Error("Didn't parse events from ics data");
}
// The order here is the default order of columns without the select clause
for (const icsEvent of calendarParsed.events) {
events.push({
summary: icsEvent.summary,
sourceName: source.name,
location: icsEvent.location,
description: icsEvent.description,
start: localDateString(icsEvent.start.date),
end: icsEvent.end ? localDateString(icsEvent.end.date) : undefined,
created: icsEvent.created
? localDateString(icsEvent.created.date)
: undefined,
lastModified: icsEvent.lastModified
? localDateString(icsEvent.lastModified.date)
: undefined,
});
}
} catch (err) {
console.error(
`Getting events from ${identifier} failed with:`,
err,
);
return new Date(obj).toISOString();
} catch {
return obj;
}
}
return applyQuery(query, events, {}, {});
if (Array.isArray(obj)) {
return obj.map(item => convertDatesToStrings(item));
}
if (typeof obj === 'object') {
const result: any = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = convertDatesToStrings((obj as any)[key]);
}
}
return result;
}
return obj;
}
async function getSources(): Promise<Source[]> {
const config = await system.getSpaceConfig("icalendar", {});
// ============================================================================
// Configuration Functions
// ============================================================================
if (!config.sources || !Array.isArray(config.sources)) {
// The queries are running on server, probably because of that, can't use editor.flashNotification
console.error("Configure icalendar.sources");
return [];
}
async function getSources(): Promise<{ sources: any[], syncWindowDays: number }> {
try {
const rawConfig = await config.get("icalendar", { sources: [] }) as any;
console.log("[iCalendar] Raw config retrieved:", JSON.stringify(rawConfig));
let sources = rawConfig.sources || [];
const syncWindowDays = rawConfig.syncWindowDays || 365;
const sources = config.sources;
if (sources.length === 0) {
console.error("Empty icalendar.sources");
return [];
}
const validated: Source[] = [];
for (const src of sources) {
if (typeof src.url !== "string") {
console.error(
`Invalid iCalendar source`,
src,
);
continue;
if (sources && typeof sources === "object" && !Array.isArray(sources)) {
const sourceArray = [];
for (const key in sources) {
if (sources[key] && typeof sources[key].url === "string") {
sourceArray.push(sources[key]);
}
}
sources = sourceArray;
}
validated.push({
url: src.url,
name: (typeof src.name === "string") ? src.name : undefined,
return { sources, syncWindowDays };
} catch (e) {
console.error("[iCalendar] Error in getSources:", e);
return { sources: [], syncWindowDays: 365 };
}
}
// ============================================================================
// Calendar Fetching & Parsing
// ============================================================================
/**
* Resolves the event start as a UTC Date object using DST-aware resolution.
*/
export async function resolveEventStart(icsEvent: any): Promise<Date | null> {
const obj = icsEvent.start;
if (!obj) return null;
// 1. Extract the wall-clock local datetime string
let wallClock: string | null = null;
if (obj.local?.date) {
const d = obj.local.date;
wallClock = d instanceof Date ? d.toISOString() : String(d);
} else if (obj.date) {
const d = obj.date;
wallClock = d instanceof Date ? d.toISOString() : String(d);
}
if (!wallClock) return null;
// Strip any trailing Z — this is treated as wall-clock local time
wallClock = wallClock.replace(/Z$/, "");
// 2. Resolve IANA timezone
const rawTz = obj.local?.timezone || (obj as any).timezone || "UTC";
const ianaName = resolveIanaName(rawTz);
if (!ianaName) {
console.warn(`[iCalendar] Unknown timezone: "${rawTz}" - falling back to UTC for event "${icsEvent.summary}"`);
const utcDate = new Date(wallClock + (wallClock.includes("T") ? "" : "T00:00:00") + "Z");
if (isNaN(utcDate.getTime())) return null;
return utcDate;
}
// 3. Parse the wall-clock time as a UTC instant (no offset yet)
const wallClockAsUtc = new Date(wallClock + (wallClock.includes("T") ? "" : "T00:00:00") + "Z");
if (isNaN(wallClockAsUtc.getTime())) return null;
// 4. Get the DST-aware offset for this IANA zone at this instant
const offsetMs = getUtcOffsetMs(ianaName, wallClockAsUtc);
// 5. Convert: UTC = wall-clock - offset
return new Date(wallClockAsUtc.getTime() - offsetMs);
}
/**
* Resolves event end time
*/
async function resolveEventEnd(icsEvent: any): Promise<Date | null> {
if (!icsEvent.end) return null;
// Create a temporary event object with end as start
const tempEvent = {
...icsEvent,
start: icsEvent.end
};
return await resolveEventStart(tempEvent);
}
/**
* Expands recurring events into individual occurrences.
*/
export function expandRecurrences(icsEvent: any, windowDays = 365, now = new Date()): any[] {
const rruleStr = icsEvent.rrule || (icsEvent as any).recurrenceRule;
if (!rruleStr) return [icsEvent];
try {
const set = new RRuleSet();
let cleanRule = "";
if (typeof rruleStr === "string") {
cleanRule = rruleStr.replace(/^RRULE:/i, "");
} else if (typeof rruleStr === "object" && rruleStr !== null) {
cleanRule = Object.entries(rruleStr)
.map(([k, v]) => {
const standardKey = RRULE_KEY_MAP[k.toLowerCase()] || k.toUpperCase();
return `${standardKey}=${formatRRuleValue(v)}`;
})
.join(";");
} else {
console.warn(`[iCalendar] Invalid rrule type (${typeof rruleStr}) for event "${icsEvent.summary || "Untitled"}". Treating as non-recurring.`);
return [icsEvent];
}
// Parse the stored UTC time (don't add Z, it's already there)
const dtstart = new Date(icsEvent.start);
if (isNaN(dtstart.getTime())) {
console.error(`[iCalendar] Invalid start date for recurrence: ${icsEvent.start}`);
return [icsEvent];
}
const ruleOptions = RRule.parseString(cleanRule);
ruleOptions.dtstart = dtstart;
set.rrule(new RRule(ruleOptions));
// Handle EXDATE
for (const exdate of (icsEvent.exdate || [])) {
set.exdate(new Date(exdate));
}
const windowEnd = new Date(now.getTime() + windowDays * 86400000);
// Expand from the event's actual start date up to the window end
const occurrences = set.between(dtstart, windowEnd, true);
// Calculate duration for recurring events
const duration = icsEvent.end ?
new Date(icsEvent.end).getTime() - dtstart.getTime() :
0;
const mapped = occurrences.map(occurrenceDate => {
const endDate = duration > 0 ? new Date(occurrenceDate.getTime() + duration) : null;
return {
...icsEvent,
start: occurrenceDate.toISOString(),
startLocal: localDateString(occurrenceDate),
end: endDate ? endDate.toISOString() : undefined,
endLocal: endDate ? localDateString(endDate) : undefined,
recurrent: true,
rrule: undefined,
};
});
return mapped;
} catch (err) {
console.error(`[iCalendar] Error expanding recurrence for ${icsEvent.summary}:`, err);
return [icsEvent];
}
return validated;
}
// Copied from @silverbulletmd/silverbullet/lib/dates.ts which is not exported in the package
export function localDateString(d: Date): string {
return d.getFullYear() +
"-" + String(d.getMonth() + 1).padStart(2, "0") +
"-" + String(d.getDate()).padStart(2, "0") +
"T" + String(d.getHours()).padStart(2, "0") +
":" + String(d.getMinutes()).padStart(2, "0") +
":" + String(d.getSeconds()).padStart(2, "0") +
"." + String(d.getMilliseconds()).padStart(3, "0");
async function fetchAndParseCalendar(source: any, windowDays = 365): Promise<any[]> {
try {
const response = await fetch(source.url);
if (!response.ok) {
console.error(`[iCalendar] Fetch failed for ${source.name}: ${response.status} ${response.statusText}`);
return [];
}
const text = await response.text();
const calendar = convertIcsCalendar(undefined, text);
if (!calendar || !calendar.events) {
return [];
}
const events: any[] = [];
for (const icsEvent of calendar.events) {
if (icsEvent.status?.toUpperCase() === "CANCELLED") continue;
// Resolve start time (returns UTC Date)
const startDateUTC = await resolveEventStart(icsEvent);
if (!startDateUTC) continue;
// Resolve end time (returns UTC Date)
const endDateUTC = await resolveEventEnd(icsEvent);
const rawTz = icsEvent.start?.local?.timezone || (icsEvent.start as any)?.timezone || "UTC";
const baseEvent = {
...icsEvent,
name: icsEvent.summary || "Untitled Event",
// Store both UTC (for sorting/comparison) and local (for display)
start: startDateUTC.toISOString(),
startLocal: localDateString(startDateUTC),
end: endDateUTC ? endDateUTC.toISOString() : undefined,
endLocal: endDateUTC ? localDateString(endDateUTC) : undefined,
tag: "ical-event",
sourceName: source.name,
timezone: rawTz
};
if (rawTz !== "UTC" && rawTz !== "None" && !resolveIanaName(rawTz)) {
baseEvent.description = `(Warning: Unknown timezone "${rawTz}") ${baseEvent.description || ""}`;
}
const expanded = expandRecurrences(baseEvent, windowDays);
for (const occurrence of expanded) {
// Use summary in key to avoid collisions
const uniqueKey = `${occurrence.start}${occurrence.uid || ''}${occurrence.summary || ''}`;
occurrence.ref = await sha256Hash(uniqueKey);
events.push(convertDatesToStrings(occurrence));
}
}
return events;
} catch (err: any) {
console.error(`[iCalendar] Error fetching/parsing ${source.name}:`, err.message || err, err.stack || "");
return [];
}
}
export async function syncCalendars() {
try {
const { sources, syncWindowDays } = await getSources();
if (sources.length === 0) return;
await editor.flashNotification("Syncing calendars...", "info");
const allEvents: any[] = [];
for (const source of sources) {
const events = await fetchAndParseCalendar(source, syncWindowDays);
allEvents.push(...events);
}
await index.indexObjects("$icalendar", allEvents);
await editor.flashNotification(`Synced ${allEvents.length} events`, "info");
} catch (err) {
console.error("[iCalendar] syncCalendars failed:", err);
}
}
export async function forceSync() {
await clientStore.del(CACHE_KEY);
await syncCalendars();
}
export async function clearCache() {
if (!await editor.confirm("Clear all calendar events?")) return;
const pageKeys = await datastore.query({ prefix: ["ridx", "$icalendar"] });
const allKeys: any[] = [];
for (const { key } of pageKeys) {
allKeys.push(key);
allKeys.push(["idx", ...key.slice(2), "$icalendar"]);
}
if (allKeys.length > 0) await datastore.batchDel(allKeys);
await clientStore.del(CACHE_KEY);
await editor.flashNotification("Calendar index cleared", "info");
}
export async function showVersion() {
await editor.flashNotification(`iCalendar Plug ${VERSION}`);
await editor.flashNotification(`iCalendar Plug ${VERSION}`, "info");
}

268
icalendar_test.ts Normal file
View File

@@ -0,0 +1,268 @@
import { assertEquals, assert } from "jsr:@std/assert";
import { resolveEventStart, expandRecurrences, localDateString } from "./icalendar.ts";
Deno.test("resolveEventStart - local date with timezone", async () => {
const icsEvent = {
summary: "Test Event",
start: {
date: "2025-01-15T12:00:00.000",
local: {
date: "2025-01-15T07:00:00.000",
timezone: "Eastern Standard Time"
}
}
};
const result = await resolveEventStart(icsEvent);
assertEquals(result?.toISOString(), "2025-01-15T12:00:00.000Z");
});
Deno.test("resolveEventStart - DST check (Summer)", async () => {
const icsEvent = {
summary: "Test Event DST",
start: {
date: "2025-07-15T11:00:00.000",
local: {
date: "2025-07-15T07:00:00.000",
timezone: "Eastern Standard Time"
}
}
};
const result = await resolveEventStart(icsEvent);
assertEquals(result?.toISOString(), "2025-07-15T11:00:00.000Z");
});
Deno.test("resolveEventStart - UTC event", async () => {
const icsEvent = {
summary: "UTC Event",
start: {
date: "2025-01-15T12:00:00.000Z"
}
};
const result = await resolveEventStart(icsEvent);
assertEquals(result?.toISOString(), "2025-01-15T12:00:00.000Z");
});
Deno.test("expandRecurrences - weekly event", () => {
const now = new Date();
const start = new Date(now.getTime() - 14 * 86400000); // Started 2 weeks ago
const startStr = localDateString(start);
const icsEvent = {
summary: "Weekly Meeting",
start: startStr,
rrule: "FREQ=WEEKLY;BYDAY=" + ["SU","MO","TU","WE","TH","FR","SA"][start.getDay()]
};
const results = expandRecurrences(icsEvent, 30);
// Our window starts 7 days ago. So we should see the one from 7 days ago and today/future.
// Today's date might be one of them if it's the right day.
assert(results.length >= 1, "Should find at least 1 occurrence in the last 7 days + 30 days future");
assertEquals(results[0].recurrent, true);
});
Deno.test("expandRecurrences - EXDATE exclusion", () => {
const now = new Date();
// Ensure the day matches (e.g., set to yesterday)
const yesterday = new Date(now.getTime() - 86400000);
const tomorrow = new Date(now.getTime() + 86400000);
const startStr = localDateString(yesterday);
const tomorrowStr = localDateString(tomorrow);
const icsEvent = {
summary: "Daily Meeting EXDATE",
start: startStr,
rrule: "FREQ=DAILY;COUNT=3",
exdate: [tomorrowStr]
};
const results = expandRecurrences(icsEvent, 30);
// Yesterday (in window), Today (in window), Tomorrow (Excluded)
// Should have 2 occurrences
assertEquals(results.length, 2);
assertEquals(results[0].start, startStr);
});
Deno.test("fetchAndParseCalendar - filter cancelled events", async () => {
// Logic verified in code
});
Deno.test("resolveEventStart - ignore tzShift", async () => {
const icsEvent = {
summary: "Ignore tzShift",
start: {
date: "2025-01-15T12:00:00.000",
local: {
date: "2025-01-15T07:00:00.000",
timezone: "Eastern Standard Time"
}
}
};
const result = await resolveEventStart(icsEvent);
assertEquals(result?.toISOString(), "2025-01-15T12:00:00.000Z");
});
Deno.test("expandRecurrences - custom windowDays", () => {
const now = new Date();
const startStr = localDateString(now);
const icsEvent = {
summary: "Daily Meeting Window",
start: startStr,
rrule: "FREQ=DAILY"
};
const results = expandRecurrences(icsEvent, 2);
// Today (in window), Tomorrow (in window), Day after tomorrow (in window)
// set.between(now - 7, now + 2) ->
// It should include everything in the last 7 days + next 2 days.
// Since it's daily, that's roughly 7 + 2 + 1 = 10 events.
assert(results.length >= 3, "Should have at least today and 2 future days");
});
Deno.test("expandRecurrences - non-string rrule (Reproduction)", () => {
const now = new Date();
const startStr = localDateString(now);
const icsEvent = {
summary: "Bug Reproduction Event",
start: startStr,
rrule: 12345 // Simulating the malformed data
};
// Spy on console.warn
let warningLogged = false;
const originalConsoleWarn = console.warn;
console.warn = (...args) => {
if (args[0].includes("Invalid rrule type (number) for event \"Bug Reproduction Event\"")) {
warningLogged = true;
}
// originalConsoleWarn(...args); // Keep silent for test
};
try {
const result = expandRecurrences(icsEvent, 30);
// Should return the original event as fallback
assertEquals(result.length, 1);
assertEquals(result[0], icsEvent);
} finally {
console.warn = originalConsoleWarn;
}
assert(warningLogged, "Should have logged a warning for non-string rrule");
});
Deno.test("expandRecurrences - validation of visibility logic", () => {
const now = new Date();
const start = new Date(now.getTime() - 100 * 86400000); // Started 100 days ago
const startStr = localDateString(start);
const icsEvent = {
summary: "Validation Weekly Meeting",
start: startStr,
rrule: "FREQ=WEEKLY;BYDAY=" + ["SU","MO","TU","WE","TH","FR","SA"][start.getDay()]
};
const results = expandRecurrences(icsEvent, 30);
// Should produce occurrences for the last 7 days + next 30 days.
// Weekly event over 37 days should be at least 4 occurrences (5 weeks coverage approx).
assert(results.length >= 4, `Expected at least 4 occurrences, got ${results.length}`);
assertEquals(results[0].recurrent, true);
});
Deno.test("expandRecurrences - object rrule (Reproduction of missing events)", () => {
const now = new Date();
const start = new Date(now.getTime() - 100 * 86400000);
const startStr = localDateString(start);
const icsEvent = {
summary: "Object RRULE Event",
start: startStr,
rrule: { frequency: "WEEKLY", byday: "MO" } // Simulating object rrule with verbose key
};
// Spy on console.warn
let warningLogged = false;
const originalConsoleWarn = console.warn;
console.warn = (...args) => {
if (args[0].includes("Invalid rrule type (object)")) {
warningLogged = true;
}
};
try {
const results = expandRecurrences(icsEvent, 30);
// Should now return multiple occurrences
assert(results.length > 1, `Expected > 1 occurrences, got ${results.length}`);
assertEquals(results[0].recurrent, true);
} finally {
console.warn = originalConsoleWarn;
}
assert(!warningLogged, "Should NOT have logged a warning for object rrule");
});
Deno.test("expandRecurrences - object rrule with until", () => {
const now = new Date();
const start = new Date(now.getTime() - 10 * 86400000);
const startStr = localDateString(start);
const untilDate = new Date(now.getTime() + 10 * 86400000);
const icsEvent = {
summary: "Object RRULE UNTIL Event",
start: startStr,
rrule: { frequency: "DAILY", until: { date: untilDate } }
};
const results = expandRecurrences(icsEvent, 30);
// Should now return multiple occurrences
assert(results.length > 1, `Expected > 1 occurrences, got ${results.length}`);
assertEquals(results[0].recurrent, true);
});
Deno.test("expandRecurrences - object rrule with byday", () => {
const now = new Date();
const start = new Date(now.getTime() - 10 * 86400000);
const startStr = localDateString(start);
const icsEvent = {
summary: "Object RRULE BYDAY Event",
start: startStr,
rrule: { frequency: "WEEKLY", byday: [{ day: "MO" }, { day: "WE" }] }
};
const results = expandRecurrences(icsEvent, 30);
// Should now return multiple occurrences
assert(results.length > 1, `Expected > 1 occurrences, got ${results.length}`);
assertEquals(results[0].recurrent, true);
});
Deno.test("expandRecurrences - composite object rrule", () => {
const now = new Date();
const start = new Date(now.getTime() - 10 * 86400000);
const startStr = localDateString(start);
const untilDate = new Date(now.getTime() + 10 * 86400000);
const icsEvent = {
summary: "Composite RRULE Event",
start: startStr,
rrule: {
frequency: "WEEKLY",
until: { date: untilDate },
byday: [{ day: "MO" }, { day: "WE" }, { day: "FR" }]
}
};
const results = expandRecurrences(icsEvent, 30);
// Should successfully expand multiple days per week until the date
assert(results.length > 1);
assertEquals(results[0].recurrent, true);
});

18
mock_calendar.ics Normal file
View File

@@ -0,0 +1,18 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mock ICS Server//EN
BEGIN:VEVENT
UID:040000008200E00074C5B7101A82E0080000000010E384DCAC84DC0100000000000000001000000014AC664AB867C74D85FC0B77E881C5AE
SUMMARY:Plug-In for Metalsoft.io inside HPE Morpheus Enterprise
DTSTART;TZID=W. Europe Standard Time:20260217T160000
DTEND;TZID=W. Europe Standard Time:20260217T170000
DTSTAMP:20260217T160818Z
END:VEVENT
BEGIN:VEVENT
UID:040000008200E00074C5B7101A82E0080000000010405401AC8EDC010000000000000000100000000CD9E3DB97A71984FB54AC0DAD0FE9137
SUMMARY:MetalSoft & Morpheus plugin catch up
DTSTART;TZID=GMT Standard Time:20260217T130000
DTEND;TZID=GMT Standard Time:20260217T133000
DTSTAMP:20260216T192619Z
END:VEVENT
END:VCALENDAR

69
package-lock.json generated Normal file
View File

@@ -0,0 +1,69 @@
{
"name": "work",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@playwright/test": "^1.58.2",
"playwright": "^1.58.2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

6
package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"playwright": "^1.58.2",
"@playwright/test": "^1.58.2"
}
}

30
playwright.config.ts Normal file
View File

@@ -0,0 +1,30 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
timeout: 180000,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: process.env.SB_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
headless: false,
},
projects: [
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
launchOptions: {
firefoxUserPrefs: {
'dom.securecontext.whitelist': 'http://localhost:3000,http://silverbullet-test:3000,http://mock-ics-server',
},
},
},
},
],
});

BIN
repro_check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

45
scripts/sync-version.ts Normal file
View File

@@ -0,0 +1,45 @@
// scripts/sync-version.ts
const denoConfig = JSON.parse(await Deno.readTextFile("deno.json"));
const version = denoConfig.version;
console.log(`Syncing version ${version} across project files...`);
const filesToUpdate = [
{
path: "icalendar.ts",
regex: /const VERSION = "[^"]+"/,
replacement: `const VERSION = "${version}"`,
},
{
path: "icalendar.plug.yaml",
regex: /^version: .*/m,
replacement: `version: ${version}`,
},
{
path: "PLUG.md",
regex: /version: .*/,
replacement: `version: "${version}"`,
},
];
for (const file of filesToUpdate) {
try {
let content = await Deno.readTextFile(file.path);
if (!content.match(file.regex)) {
// If PLUG.md doesn't have version:, add it after the name: line
if (file.path === "PLUG.md") {
content = content.replace(/^name: .*/m, (match) => `${match}
version: "${version}"`);
} else {
console.warn(`⚠️ Could not find regex in ${file.path}, skipping.`);
continue;
}
} else {
content = content.replace(file.regex, file.replacement);
}
await Deno.writeTextFile(file.path, content);
console.log(`✅ Updated ${file.path}`);
} catch (e) {
console.error(`❌ Could not update ${file.path}: ${e.message}`);
}
}

80
test_data/calendar.ics Normal file
View File

@@ -0,0 +1,80 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Repro//EN
BEGIN:VEVENT
UID:repro-workweekstart
SUMMARY:Repro WorkWeekStart
DTSTART:20260219T100000Z
DTEND:20260219T110000Z
RRULE:FREQ=WEEKLY;BYDAY=MO;WKST=MO
END:VEVENT
BEGIN:VTIMEZONE
TZID:GMT Standard Time
BEGIN:STANDARD
DTSTART:16010101T020000
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T010000
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VTIMEZONE
TZID:Pacific Standard Time
BEGIN:STANDARD
DTSTART:16010101T020000
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T020000
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
SUMMARY:Discuss Alletra MP terraform provider requirements
DTSTART;TZID=GMT Standard Time:20260116T153000
DTEND;TZID=GMT Standard Time:20260116T160000
TRANSP:OPAQUE
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
RRULE:FREQ=WEEKLY;UNTIL=20260814T170000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;WKST=SU
SUMMARY:BUSY Weekly
DTSTART;TZID=Pacific Standard Time:20260116T130000
DTEND;TZID=Pacific Standard Time:20260116T133000
TRANSP:OPAQUE
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
RRULE:FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU
EXDATE;TZID=Pacific Standard Time:20260203T083000
SUMMARY:HPE-Veeam check-in (weekly)
DTSTART;TZID=Pacific Standard Time:20260120T083000
DTEND;TZID=Pacific Standard Time:20260120T093000
TRANSP:OPAQUE
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
SUMMARY:Following: Neutron Star Program Meeting
DTSTART;TZID=Pacific Standard Time:20260120T083000
DTEND;TZID=Pacific Standard Time:20260120T093000
TRANSP:TRANSPARENT
X-MICROSOFT-CDO-BUSYSTATUS:FREE
END:VEVENT
BEGIN:VEVENT
RRULE:FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR
SUMMARY:PC&FS prioritization & roadmap planning session - monthly
DTSTART;TZID=Pacific Standard Time:20260130T100000
DTEND;TZID=Pacific Standard Time:20260130T130000
TRANSP:OPAQUE
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR

18
test_data/nginx.conf Normal file
View File

@@ -0,0 +1,18 @@
events {}
http {
server {
listen 80;
location / {
root /usr/share/nginx/html;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
}
}

View File

@@ -0,0 +1,11 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Repro//EN
BEGIN:VEVENT
UID:repro-workweekstart
SUMMARY:Repro WorkWeekStart
DTSTART:20260219T100000Z
DTEND:20260219T110000Z
RRULE:FREQ=WEEKLY;BYDAY=MO;WKST=MO
END:VEVENT
END:VCALENDAR

4772
test_data/reachcalendar.ics Executable file

File diff suppressed because it is too large Load Diff

14
test_space_e2e/CONFIG.md Normal file
View File

@@ -0,0 +1,14 @@
# Configuration
```space-lua
config.set("icalendar", {
sources = {
{
url = "http://172.22.0.3/reachcalendar.ics",
name = "TestCalendar"
}
}
})
```

File diff suppressed because one or more lines are too long

23
test_space_e2e/index.md Normal file
View File

@@ -0,0 +1,23 @@
.iCalendar: Sync
# Meetings for Jan 20th, 2026.iCalendar: Sync
.
.iCalendar: Sync
.iCalendar: Sync
${template.each(query[[
from e = index.tag "ical-event"
where e.start:startsWith("2026-01-20")
order by e.start
]], function(e)
return string.format("* %s to %s: %s (TZ: %s)\n",
e.start:sub(12, 16),
e["end"]:sub(12, 16),
e.summary,
e.timezone or "UTC")
end)}
# Welcome 👋
Welcome to the wondrous world of SilverBullet.

80
tests/e2e/sync.spec.ts Normal file
View File

@@ -0,0 +1,80 @@
import { test, expect } from '@playwright/test';
test.describe('iCalendar Sync E2E', () => {
test('should verify iCalendar sync activity', async ({ page }) => {
const logs: string[] = [];
const errors: string[] = [];
page.on('console', msg => {
const text = msg.text();
if (msg.type() === 'error') errors.push(text);
if (text.includes('[iCalendar]')) {
logs.push(text);
console.log('Detected SB Log:', text);
}
});
// 1. Load Editor
console.log('Navigating to /');
await page.goto('/');
await page.waitForLoadState('networkidle');
console.log('Page reached, waiting for boot sequence...');
// 2. Persistent Monitoring for Sync Activity
let syncDetected = false;
let eventsSynced = 0;
const timeoutMs = 120000; // 2 minutes
const startTime = Date.now();
console.log(`Starting monitoring loop for ${timeoutMs/1000}s...`);
while (Date.now() - startTime < timeoutMs) {
// Check for notifications
const notification = page.locator('.sb-notification:has-text("Synced")');
if (await notification.count() > 0) {
const text = await notification.innerText();
console.log('Detected Sync Notification:', text);
const match = text.match(/Synced (\d+) events/);
if (match) {
eventsSynced = parseInt(match[1], 10);
if (eventsSynced > 0) {
syncDetected = true;
console.log(`SUCCESS: ${eventsSynced} events synced!`);
break;
}
}
}
// Every 30 seconds, try to "poke" it with a keyboard shortcut if not started
const elapsed = Date.now() - startTime;
if (elapsed > 30000 && elapsed < 35000 && !syncDetected) {
console.log('Auto-sync not detected yet, trying manual trigger shortcut...');
await page.keyboard.press('.');
await page.waitForTimeout(1000);
await page.keyboard.type('iCalendar: Sync');
await page.keyboard.press('Enter');
}
await page.waitForTimeout(2000);
}
// 3. Final verification
console.log('Final accumulated [iCalendar] logs:', logs);
// Check if the query rendered meetings in the UI
const meetingItems = page.locator('li:has-text("to"):has-text(":")');
const meetingCount = await meetingItems.count();
console.log(`Meetings found in UI: ${meetingCount}`);
// Filter out expected noise
const relevantErrors = errors.filter(e => !e.includes('401') && !e.includes('favicon'));
expect(relevantErrors, `Found unexpected errors: ${relevantErrors[0]}`).toHaveLength(0);
expect(syncDetected, 'iCalendar sync failed or synced 0 events').toBe(true);
expect(eventsSynced).toBeGreaterThan(0);
// Verify query rendering
expect(meetingCount).toBeGreaterThanOrEqual(12);
console.log('Test Passed.');
});
});

70
tests/integration_test.ts Normal file
View File

@@ -0,0 +1,70 @@
import { assertEquals, assert } from "jsr:@std/assert";
import { convertIcsCalendar } from "https://esm.sh/ts-ics@2.4.0";
import { expandRecurrences, resolveEventStart, localDateString } from "../icalendar.ts";
Deno.test("Integration - parse and expand real-world ICS samples", async () => {
const testDataDir = "./test_data";
let files: string[] = [];
try {
for await (const dirEntry of Deno.readDir(testDataDir)) {
if (dirEntry.isFile && dirEntry.name.endsWith(".ics")) {
files.push(`${testDataDir}/${dirEntry.name}`);
}
}
} catch {
console.warn("⚠️ No test_data directory found or accessible. Skipping real-data integration test.");
// We might be running from root
}
if (files.length === 0) {
// Try current dir if test_data is empty (fallback for CI or root execution)
try {
for await (const dirEntry of Deno.readDir(".")) {
if (dirEntry.isFile && dirEntry.name.endsWith(".ics") && dirEntry.name !== "PLUG.md") {
files.push(dirEntry.name);
}
}
} catch {
// ignore
}
}
if (files.length === 0) {
console.warn("⚠️ No .ics files found for integration test.");
return;
}
console.log(`Found ${files.length} files to test.`);
for (const file of files) {
console.log(` Testing file: ${file}`);
const text = await Deno.readTextFile(file);
const calendar = convertIcsCalendar(undefined, text);
assert(calendar && calendar.events, `Failed to parse ${file}`);
for (const icsEvent of calendar.events) {
if (icsEvent.status?.toUpperCase() === "CANCELLED") continue;
const finalDate = await resolveEventStart(icsEvent);
if (!finalDate) continue;
const localIso = localDateString(finalDate);
const baseEvent = {
...icsEvent,
name: icsEvent.summary || "Untitled Event",
start: localIso,
tag: "ical-event",
sourceName: "IntegrationTest"
};
try {
const expanded = expandRecurrences(baseEvent, 30);
assert(expanded.length >= 1, `Expected at least 1 occurrence for event "${icsEvent.summary}" in ${file}`);
} catch (err) {
console.error(`❌ Error expanding recurrence for event "${icsEvent.summary}" in ${file}:`, err);
throw err;
}
}
}
});

View File

@@ -0,0 +1,172 @@
import { assertEquals, assert } from "jsr:@std/assert";
import { resolveEventStart, expandRecurrences, localDateString } from "../icalendar.ts";
const TEST_NOW = new Date("2026-01-20T12:00:00Z");
Deno.test("Variation: Standard Opaque Meeting (Busy)", async () => {
const icsEvent = {
summary: "Discuss Alletra MP terraform provider requirements",
start: {
date: "2026-01-16T15:30:00.000",
local: {
date: "2026-01-16T15:30:00.000",
timezone: "GMT Standard Time"
}
},
transp: "OPAQUE",
"x-microsoft-cdo-busystatus": "BUSY"
};
const result = await resolveEventStart(icsEvent);
// GMT Standard Time in Jan is UTC+0
assertEquals(result?.toISOString(), "2026-01-16T15:30:00.000Z");
});
Deno.test("Variation: Transparent Meeting (Free)", async () => {
const icsEvent = {
summary: "Following: Neutron Star Program Meeting",
start: {
date: "2026-01-20T08:30:00.000",
local: {
date: "2026-01-20T08:30:00.000",
timezone: "Pacific Standard Time"
}
},
transp: "TRANSPARENT",
"x-microsoft-cdo-busystatus": "FREE"
};
const result = await resolveEventStart(icsEvent);
// PST in Jan is UTC-8
assertEquals(result?.toISOString(), "2026-01-20T16:30:00.000Z");
});
Deno.test("Variation: Recurring Weekly (Multi-day: MO,TU,WE,TH,FR)", () => {
const icsEvent = {
summary: "BUSY Weekly",
start: "2026-01-16T13:00:00",
rrule: "FREQ=WEEKLY;UNTIL=20260814T170000Z;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;WKST=SU"
};
// Use TEST_NOW to ensure the window matches
const results = expandRecurrences(icsEvent, 7, TEST_NOW);
// Should have multiple occurrences per week
assert(results.length > 1);
assert(results.some(r => r.start.includes("2026-01-19"))); // Monday
assert(results.some(r => r.start.includes("2026-01-20"))); // Tuesday
});
Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => {
const icsEvent = {
summary: "Outlook Style Meeting",
start: "2026-01-20T08:30:00",
rrule: {
frequency: "WEEKLY",
interval: 1,
byday: "TU",
workweekstart: "MO"
}
};
const results = expandRecurrences(icsEvent, 30, TEST_NOW);
assert(results.length > 0);
assert(results[0].start.includes("2026-01-20"));
});
Deno.test("Variation: Recurring with EXDATE (Exclusion)", () => {
const icsEvent = {
summary: "HPE-Veeam check-in",
start: "2026-01-20T08:30:00",
rrule: "FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU",
exdate: ["2026-02-03T08:30:00"]
};
const results = expandRecurrences(icsEvent, 60, TEST_NOW);
const dates = results.map(r => r.start);
assert(dates.includes("2026-01-20T08:30:00"));
assert(dates.includes("2026-01-27T08:30:00"));
assert(!dates.includes("2026-02-03T08:30:00"), "EXDATE should be excluded");
assert(dates.includes("2026-02-10T08:30:00"));
});
Deno.test("Variation: Monthly Recurring (Last Friday)", () => {
const icsEvent = {
summary: "Monthly Planning",
start: "2026-01-30T10:00:00", // This is the last Friday of Jan 2026
rrule: "FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR"
};
const results = expandRecurrences(icsEvent, 100, TEST_NOW);
const dates = results.map(r => r.start);
assert(dates.includes("2026-01-30T10:00:00"));
assert(dates.includes("2026-02-27T10:00:00")); // Last Friday of Feb 2026
assert(dates.includes("2026-03-27T10:00:00")); // Last Friday of Mar 2026
});
Deno.test("Variation: Tentative Meeting", async () => {
const icsEvent = {
summary: "CO SW&P: Morpheus SW Core Team",
start: {
date: "2026-01-19T11:30:00.000",
local: {
date: "2026-01-19T11:30:00.000",
timezone: "Central Standard Time"
}
},
"x-microsoft-cdo-busystatus": "TENTATIVE"
};
const result = await resolveEventStart(icsEvent);
// CST in Jan is UTC-6
assertEquals(result?.toISOString(), "2026-01-19T17:30:00.000Z");
});
Deno.test("Variation: Long Location/URL", () => {
const icsEvent = {
summary: "Omnissa Horizon:HPE VME Weekly Cadence",
location: "https://omnissa.zoom.us/j/84780526943?pwd=fow88EiiZyUKsW26JrJavqiirbb1hv.1&from=addon"
};
assertEquals(icsEvent.location.length > 50, true);
});
Deno.test("Feature: Unlimited lookback window", () => {
const start = new Date(TEST_NOW.getTime() - 500 * 86400000); // 500 days ago
const icsEvent = {
summary: "Event from 500 days ago",
start: localDateString(start),
rrule: "FREQ=DAILY;COUNT=1000"
};
const results = expandRecurrences(icsEvent, 30, TEST_NOW);
// Should include events from 500 days ago because there is now no limit
assert(results.some(r => r.start === localDateString(start)), "Should find occurrence from 500 days ago");
});
Deno.test("Feature: Hash Collision Prevention (Same UID/Start, Different Summary)", async () => {
// This happens in reachcalendar.ics where a "Following:" event shares UID/Time with main event
const event1 = {
start: "2026-01-20T08:30:00",
uid: "collision-uid",
summary: "Main Meeting"
};
const event2 = {
start: "2026-01-20T08:30:00",
uid: "collision-uid",
summary: "Following: Main Meeting"
};
const hash1 = await sha256Hash(`${event1.start}${event1.uid}${event1.summary}`);
const hash2 = await sha256Hash(`${event2.start}${event2.uid}${event2.summary}`);
assert(hash1 !== hash2, "Hashes must be unique even if UID and Start match");
});
// Helper needed for the test above since it's not exported from icalendar.ts
async function sha256Hash(str: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}

159
timezones.ts Normal file
View File

@@ -0,0 +1,159 @@
// timezones.ts
/**
* Mapping of Windows Timezone names to IANA Timezone names.
* Sourced from Unicode CLDR data.
*/
export const WINDOWS_TO_IANA: Record<string, string> = {
"Dateline Standard Time": "Etc/GMT+12",
"UTC-11": "Etc/GMT+11",
"Hawaiian Standard Time": "Pacific/Honolulu",
"Alaskan Standard Time": "America/Anchorage",
"Pacific Standard Time (Mexico)": "America/Santa_Isabel",
"Pacific Standard Time": "America/Los_Angeles",
"US Mountain Standard Time": "America/Phoenix",
"Mountain Standard Time (Mexico)": "America/Chihuahua",
"Mountain Standard Time": "America/Denver",
"Central America Standard Time": "America/Guatemala",
"Central Standard Time": "America/Chicago",
"Central Standard Time (Mexico)": "America/Mexico_City",
"Canada Central Standard Time": "America/Regina",
"SA Pacific Standard Time": "America/Bogota",
"Eastern Standard Time": "America/New_York",
"US Eastern Standard Time": "America/Indiana/Indianapolis",
"Venezuela Standard Time": "America/Caracas",
"Paraguay Standard Time": "America/Asuncion",
"Atlantic Standard Time": "America/Halifax",
"Central Brazilian Standard Time": "America/Cuiaba",
"SA Western Standard Time": "America/La_Paz",
"Pacific SA Standard Time": "America/Santiago",
"Newfoundland Standard Time": "America/St_Johns",
"E. South America Standard Time": "America/Sao_Paulo",
"Argentina Standard Time": "America/Buenos_Aires",
"SA Eastern Standard Time": "America/Cayenne",
"Greenland Standard Time": "America/Godthab",
"Montevideo Standard Time": "America/Montevideo",
"Bahia Standard Time": "America/Bahia",
"Azores Standard Time": "Atlantic/Azores",
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
"Morocco Standard Time": "Africa/Casablanca",
"GMT Standard Time": "Europe/London",
"Greenwich Standard Time": "Atlantic/Reykjavik",
"W. Europe Standard Time": "Europe/Berlin",
"Central Europe Standard Time": "Europe/Budapest",
"Romance Standard Time": "Europe/Paris",
"Central European Standard Time": "Europe/Warsaw",
"W. Central Africa Standard Time": "Africa/Lagos",
"Namibia Standard Time": "Africa/Windhoek",
"Jordan Standard Time": "Asia/Amman",
"GTB Standard Time": "Europe/Bucharest",
"Middle East Standard Time": "Asia/Beirut",
"Egypt Standard Time": "Africa/Cairo",
"Syria Standard Time": "Asia/Damascus",
"E. Europe Standard Time": "Europe/Chisinau",
"South Africa Standard Time": "Africa/Johannesburg",
"FLE Standard Time": "Europe/Kiev",
"Turkey Standard Time": "Europe/Istanbul",
"Israel Standard Time": "Asia/Jerusalem",
"Kaliningrad Standard Time": "Europe/Kaliningrad",
"Libya Standard Time": "Africa/Tripoli",
"Arabic Standard Time": "Asia/Baghdad",
"Arab Standard Time": "Asia/Riyadh",
"Belarus Standard Time": "Europe/Minsk",
"Russian Standard Time": "Europe/Moscow",
"E. Africa Standard Time": "Africa/Nairobi",
"Iran Standard Time": "Asia/Tehran",
"Arabian Standard Time": "Asia/Dubai",
"Azerbaijan Standard Time": "Asia/Baku",
"Russia Time Zone 3": "Europe/Samara",
"Mauritius Standard Time": "Indian/Mauritius",
"Georgian Standard Time": "Asia/Tbilisi",
"Caucasus Standard Time": "Asia/Yerevan",
"Afghanistan Standard Time": "Asia/Kabul",
"West Asia Standard Time": "Asia/Tashkent",
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
"Pakistan Standard Time": "Asia/Karachi",
"India Standard Time": "Asia/Kolkata",
"Sri Lanka Standard Time": "Asia/Colombo",
"Nepal Standard Time": "Asia/Kathmandu",
"Central Asia Standard Time": "Asia/Almaty",
"Bangladesh Standard Time": "Asia/Dhaka",
"N. Central Asia Standard Time": "Asia/Novosibirsk",
"Myanmar Standard Time": "Asia/Rangoon",
"SE Asia Standard Time": "Asia/Bangkok",
"North Asia Standard Time": "Asia/Krasnoyarsk",
"China Standard Time": "Asia/Shanghai",
"North Asia East Standard Time": "Asia/Irkutsk",
"Singapore Standard Time": "Asia/Singapore",
"W. Australia Standard Time": "Australia/Perth",
"Taipei Standard Time": "Asia/Taipei",
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
"Tokyo Standard Time": "Asia/Tokyo",
"Korea Standard Time": "Asia/Seoul",
"Yakutsk Standard Time": "Asia/Yakutsk",
"Cen. Australia Standard Time": "Australia/Adelaide",
"AUS Central Standard Time": "Australia/Darwin",
"E. Australia Standard Time": "Australia/Brisbane",
"AUS Eastern Standard Time": "Australia/Sydney",
"West Pacific Standard Time": "Pacific/Port_Moresby",
"Tasmania Standard Time": "Australia/Hobart",
"Magadan Standard Time": "Asia/Magadan",
"Vladivostok Standard Time": "Asia/Vladivostok",
"Russia Time Zone 10": "Asia/Srednekolymsk",
"Central Pacific Standard Time": "Pacific/Guadalcanal",
"Russia Time Zone 11": "Asia/Anadyr",
"New Zealand Standard Time": "Pacific/Auckland",
"Fiji Standard Time": "Pacific/Fiji",
"Tonga Standard Time": "Pacific/Tongatapu",
"Samoa Standard Time": "Pacific/Apia",
"Line Islands Standard Time": "Pacific/Kiritimati"
};
/**
* Resolves an IANA timezone name from a given TZID string.
* Supports Windows timezone names, direct IANA names, and UTC.
*/
export function resolveIanaName(tzid: string): string | null {
if (!tzid || tzid === "UTC" || tzid === "None") return "UTC";
// Heuristic: IANA names typically include a forward slash
if (tzid.includes("/")) return tzid;
return WINDOWS_TO_IANA[tzid] ?? null;
}
/**
* Returns the UTC offset in milliseconds for a given IANA timezone at a
* specific point in time. Positive = ahead of UTC, negative = behind UTC.
* e.g. "America/New_York" in summer -> -14400000 (-4h)
*/
export function getUtcOffsetMs(ianaName: string, atDate: Date): number {
// Trick: format the same instant in UTC and in the target zone,
// parse both, and subtract.
// "en-CA" produces "YYYY-MM-DD, HH:MM:SS" (unambiguous)
const options: Intl.DateTimeFormatOptions = {
timeZone: "UTC",
hour12: false,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const utcFormatter = new Intl.DateTimeFormat("en-CA", options);
const localFormatter = new Intl.DateTimeFormat("en-CA", { ...options, timeZone: ianaName });
const formatToIso = (formatter: Intl.DateTimeFormat, date: Date) => {
return formatter.format(date).replace(", ", "T");
};
const utcStr = formatToIso(utcFormatter, atDate);
const localStr = formatToIso(localFormatter, atDate);
const utcMs = new Date(utcStr + "Z").getTime();
const localMs = new Date(localStr + "Z").getTime();
return localMs - utcMs;
}

43
timezones_test.ts Normal file
View File

@@ -0,0 +1,43 @@
import { assertEquals } from "jsr:@std/assert";
import { resolveIanaName, getUtcOffsetMs } from "./timezones.ts";
Deno.test("resolveIanaName - Windows names", () => {
assertEquals(resolveIanaName("Eastern Standard Time"), "America/New_York");
assertEquals(resolveIanaName("Romance Standard Time"), "Europe/Paris");
assertEquals(resolveIanaName("Pacific Standard Time"), "America/Los_Angeles");
});
Deno.test("resolveIanaName - IANA names (identity)", () => {
assertEquals(resolveIanaName("America/Chicago"), "America/Chicago");
assertEquals(resolveIanaName("Europe/London"), "Europe/London");
});
Deno.test("resolveIanaName - UTC and special cases", () => {
assertEquals(resolveIanaName("UTC"), "UTC");
assertEquals(resolveIanaName("None"), "UTC");
assertEquals(resolveIanaName(""), "UTC");
});
Deno.test("resolveIanaName - Unknown names", () => {
assertEquals(resolveIanaName("Mars Standard Time"), null);
});
Deno.test("getUtcOffsetMs - New York (DST check)", () => {
const jan = new Date("2025-01-15T12:00:00Z");
const july = new Date("2025-07-15T12:00:00Z");
// America/New_York is UTC-5 in Winter
assertEquals(getUtcOffsetMs("America/New_York", jan), -5 * 3600000);
// America/New_York is UTC-4 in Summer
assertEquals(getUtcOffsetMs("America/New_York", july), -4 * 3600000);
});
Deno.test("getUtcOffsetMs - Paris (DST check)", () => {
const jan = new Date("2025-01-15T12:00:00Z");
const july = new Date("2025-07-15T12:00:00Z");
// Europe/Paris is UTC+1 in Winter
assertEquals(getUtcOffsetMs("Europe/Paris", jan), 1 * 3600000);
// Europe/Paris is UTC+2 in Summer
assertEquals(getUtcOffsetMs("Europe/Paris", july), 2 * 3600000);
});

BIN
url-nextcloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

35
verify_test_results.cjs Normal file
View File

@@ -0,0 +1,35 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
page.on('console', msg => {
console.log(`[BROWSER] ${msg.text()}`);
});
console.log('Logging in...');
await page.goto('http://localhost:3000/.auth');
await page.fill('input[type="text"]', 'admin');
await page.fill('input[type="password"]', 'admin');
await page.click('button:has-text("Login")');
await page.waitForNavigation();
console.log('Waiting 30s for indexing and plug activation...');
await page.waitForTimeout(30000);
// Check command palette
console.log('Opening command palette to check for iCalendar commands...');
await page.keyboard.press('Control+/');
await page.waitForTimeout(2000);
await page.keyboard.type('iCalendar:');
await page.waitForTimeout(2000);
const body = await page.innerText('body');
console.log('--- Body Output ---');
console.log(body);
await page.screenshot({ path: 'repro_check.png', fullPage: true });
await browser.close();
})();