Compare commits

172 Commits

Author SHA1 Message Date
8710d92e3d fix type error - v0.4.8 [skip-ci]
All checks were successful
Build SilverBullet Plug / build (push) Successful in 14s
2026-02-25 05:49:23 -08:00
fc03f0e0de fix duplicates and filter canceled/declined events - v0.4.7 [skip-ci]
Some checks failed
Build SilverBullet Plug / build (push) Has been cancelled
2026-02-25 05:49:05 -08:00
139ab71db7 switch to ical.js and cleanup obsolete files - v0.4.6 [skip-ci]
All checks were successful
Build SilverBullet Plug / build (push) Successful in 11s
2026-02-21 16:05:54 -08:00
30731c7752 switch to ical.js - v4.4 [skip-ci]
All checks were successful
Build SilverBullet Plug / build (push) Successful in 10s
2026-02-21 15:45:09 -08:00
4aa8019de1 switch to ical.js - v4.3
All checks were successful
Build SilverBullet Plug / build (push) Successful in 14s
2026-02-21 15:39:00 -08:00
017afa0f11 switch to ical.js - v4.2
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-21 15:31:36 -08:00
a096b8d18f switch to ical.js - v4.2 2026-02-21 15:31:22 -08: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
67 changed files with 159664 additions and 228 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
deno.lock
test_space
.env
node_modules/
playwright-report/
test-results/

View File

@@ -1,34 +1,32 @@
.PHONY: build up down logs clean
# Makefile for iCalendar Plug
# Build the plug using a Docker container with Deno
build:
docker run --rm -v $(PWD):/app -w /app denoland/deno:latest task build
.PHONY: build test bump release check-versions
# Start the SilverBullet test container
up:
mkdir -p test_space
docker compose up -d
# Run all tests
test:
deno task test
# Stop the SilverBullet test container
down:
docker compose down
# Increment patch version in deno.json
bump:
deno task bump-version
# View logs from the SilverBullet container
logs:
docker compose logs -f
# Sync version from deno.json to all other files
sync-version:
deno task sync-version
# Watch for changes and rebuild automatically using Deno's internal watch
watch:
@echo "Starting watch in background..."
docker run -d --name ical-watch -v $(PWD):/app -w /app denoland/deno:latest task watch
@echo "Watching... Use 'docker logs -f ical-watch' to see build progress."
# Check version consistency
check-versions:
./check_versions.sh
# Stop the watch container
stop-watch:
docker rm -f ical-watch
# Build the plug using local Deno
build: sync-version
deno task build
# Clean up build artifacts and test space
clean:
rm -f *.plug.js
# Be careful with test_space if you have notes there you want to keep
# rm -rf test_space
# Bump version and build
release: bump build
@echo "Release built successfully."
# 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/

View File

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

152393
SilverBullet_digest.md Normal file

File diff suppressed because one or more lines are too long

21
check_versions.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Extract versions
DENO_VERSION=$(grep '"version":' deno.json | cut -d'"' -f4)
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}' | tr -d '"')
echo "Checking versions..."
echo "deno.json: $DENO_VERSION"
echo "icalendar.ts: $TS_VERSION"
echo "icalendar.plug.yaml: $YAML_VERSION"
echo "PLUG.md: $PLUG_MD_VERSION"
if [ "$DENO_VERSION" == "$TS_VERSION" ] && [ "$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

View File

@@ -1,8 +1,14 @@
{
"name": "icalendar-plug",
"version": "0.4.8",
"nodeModulesDir": "auto",
"tasks": {
"build": "deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/edge/plug-compile.js -c deno.json icalendar.plug.yaml",
"watch": "deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/edge/plug-compile.js -c deno.json icalendar.plug.yaml -w",
"debug": "deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/edge/plug-compile.js -c deno.json icalendar.plug.yaml --debug"
"sync-version": "deno run -A scripts/sync-version.ts",
"bump-version": "deno run -A scripts/bump-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",
"test": "deno test -A icalendar_test.ts tests/integration_test.ts tests/reach_variations_test.ts timezones_test.ts"
},
"lint": {
"rules": {
@@ -20,6 +26,7 @@
},
"imports": {
"@silverbulletmd/silverbullet": "jsr:@silverbulletmd/silverbullet@^2.4.1",
"ts-ics": "npm:ts-ics@2.4.0"
"ical.js": "https://esm.sh/ical.js@2.0.1",
"rrule": "https://esm.sh/rrule@2.8.1"
}
}

View File

@@ -1,10 +0,0 @@
services:
silverbullet:
image: zefhemel/silverbullet:latest
ports:
- "3000:3000"
volumes:
- ./test_space:/space
- ./icalendar.plug.js:/space/_plug/icalendar.plug.js:ro
environment:
- SB_USER=admin:admin # Default for easy local testing

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()

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,47 +1,34 @@
name: icalendar
version: 0.4.8
author: sstent
index: icalendar.ts
# Legacy SilverBullet permission name
requiredPermissions:
- fetch
# Modern SilverBullet permission name
permissions:
- fetch
- http
functions:
syncCalendars:
path: ./icalendar.ts:syncCalendars
path: icalendar.ts:syncCalendars
command:
name: "iCalendar: Sync"
priority: -1
events:
- editor:init
forceSync:
path: ./icalendar.ts:forceSync
path: icalendar.ts:forceSync
command:
name: "iCalendar: Force Sync"
priority: -1
clearCache:
path: ./icalendar.ts: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
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
cacheDuration:
type: number
description: "Interval between two calendar synchronizations (default: 21600 = 6 hours)"

View File

@@ -1,52 +1,52 @@
import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls";
import { convertIcsCalendar, type IcsCalendar, type IcsEvent, type IcsDateObjects } from "ts-ics";
import ICAL from "ical.js";
import { RRule, RRuleSet } from "rrule";
import { getUtcOffsetMs, resolveIanaName } from "./timezones.ts";
const VERSION = "0.2.15";
const VERSION = "0.4.8";
const CACHE_KEY = "icalendar:lastSync";
const DEFAULT_CACHE_DURATION_SECONDS = 21600; // 6 hours
// Mapping of common Windows/Outlook timezones to their standard offsets (in hours)
const TIMEZONE_OFFSETS: Record<string, number> = {
"GMT Standard Time": 0,
"W. Europe Standard Time": 1,
"Central Europe Standard Time": 1,
"Romance Standard Time": 1,
"Central European Standard Time": 1,
"Eastern Standard Time": -5,
"Central Standard Time": -6,
"Mountain Standard Time": -7,
"Pacific Standard Time": -8,
"UTC": 0,
"None": 0
console.log(`[iCalendar] Plug script executing at top level (Version ${VERSION})`);
/**
* 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",
};
console.log(`[iCalendar] Plug loading (Version ${VERSION})...`);
// ============================================================================
// Types
// ============================================================================
type DateToString<T> = T extends Date ? string
: T extends IcsDateObjects ? string
: T extends object ? { [K in keyof T]: DateToString<T[K]> }
: T extends Array<infer U> ? Array<DateToString<U>>
: T;
interface Source {
url: string;
name: string | undefined;
}
interface PlugConfig {
sources: Source[];
cacheDuration: number | undefined;
tzShift: number | undefined;
}
interface CalendarEvent extends DateToString<IcsEvent> {
ref: string;
tag: "ical-event";
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);
}
// ============================================================================
@@ -54,62 +54,8 @@ interface CalendarEvent extends DateToString<IcsEvent> {
// ============================================================================
/**
* Standard SilverBullet local date string formatter
* Creates a SHA-256 hash of a string (hex encoded)
*/
function toLocalISO(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return d.getFullYear() +
"-" + pad(d.getMonth() + 1) +
"-" + pad(d.getDate()) +
"T" + pad(d.getHours()) +
":" + pad(d.getMinutes()) +
":" + pad(d.getSeconds()) +
"." + String(d.getMilliseconds()).padStart(3, "0");
}
/**
* Robustly converts an ICS date object to a localized PST string
*/
function processIcsDate(obj: any, manualShift = 0): string {
if (!obj) return "";
// 1. Get the "Wall Time" (the hour shown in the organizer's calendar)
// ts-ics often puts this in obj.local.date but marks it with 'Z'
let wallTimeStr = (obj.local && typeof obj.local.date === "string")
? obj.local.date
: (typeof obj.date === "string" ? obj.date : "");
if (!wallTimeStr) return "";
// Remove any 'Z' to treat it as a raw floating time initially
wallTimeStr = wallTimeStr.replace("Z", "");
// Parse as UTC so we have a stable starting point
const baseDate = new Date(wallTimeStr + "Z");
// 2. Identify the Source Timezone
const tzName = obj.local?.timezone || obj.timezone || "UTC";
const sourceOffset = TIMEZONE_OFFSETS[tzName] ?? 0;
// 3. Calculate True UTC
// UTC = WallTime - SourceOffset
// Example: 16:00 WallTime in GMT+1 (+1) -> 15:00 UTC
const utcMillis = baseDate.getTime() - (sourceOffset * 3600000);
const envOffset = new Date().getTimezoneOffset();
console.log(`[iCalendar] Date Calc: Wall=${wallTimeStr}, TZ=${tzName}, SourceOffset=${sourceOffset}, EnvOffset=${envOffset}, UTC=${new Date(utcMillis).toISOString()}`);
// 4. Apply User's Manual Shift (if any)
const finalMillis = utcMillis + (manualShift * 3600000);
// 5. Localize to environment
return toLocalISO(new Date(finalMillis));
}
function isIcsDateObjects(obj: any): obj is IcsDateObjects {
return obj && typeof obj === 'object' && ('date' in obj && 'type' in obj);
}
async function sha256Hash(str: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
@@ -118,97 +64,399 @@ async function sha256Hash(str: string): Promise<string> {
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}
function convertDatesToStrings<T>(obj: T, hourShift = 0): DateToString<T> {
if (obj === null || obj === undefined) return obj as DateToString<T>;
/**
* Converts UTC Date to a specific timezone string
* Uses toLocaleString for better compatibility
*/
export function dateToTimezoneString(date: Date, timezone: string = "America/Los_Angeles"): string {
try {
// Use toLocaleString which has better worker support
const localeString = date.toLocaleString('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
console.log(`[iCalendar] Converting ${date.toISOString()} to ${timezone}: ${localeString}`);
// Parse the result: "MM/DD/YYYY, HH:MM:SS"
const match = localeString.match(/(\d{2})\/(\d{2})\/(\d{4}),\s*(\d{2}):(\d{2}):(\d{2})/);
if (match) {
const [_, month, day, year, hour, minute, second] = match;
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
}
throw new Error("Failed to parse toLocaleString result");
} catch (err) {
console.error(`[iCalendar] Error converting to timezone ${timezone}:`, err);
// Fallback to UTC
const pad = (n: number) => String(n).padStart(2, "0");
return date.getUTCFullYear() + "-" +
pad(date.getUTCMonth() + 1) + "-" +
pad(date.getUTCDate()) + "T" +
pad(date.getUTCHours()) + ":" +
pad(date.getUTCMinutes()) + ":" +
pad(date.getUTCSeconds());
}
}
if (isIcsDateObjects(obj)) {
return processIcsDate(obj, hourShift) as DateToString<T>;
/**
* 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 toLocalISO(new Date(obj.getTime() + (hourShift * 3600000))) as DateToString<T>;
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 {
return new Date(obj).toISOString();
} catch {
return obj;
}
}
if (Array.isArray(obj)) {
return obj.map(item => convertDatesToStrings(item, hourShift)) as DateToString<T>;
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], hourShift);
result[key] = convertDatesToStrings((obj as any)[key]);
}
}
return result as DateToString<T>;
return result;
}
return obj as DateToString<T>;
return obj;
}
// ============================================================================
// Configuration & Commands
// Configuration Functions
// ============================================================================
async function getSources(): Promise<Source[]> {
const plugConfig = await config.get<PlugConfig>("icalendar", { sources: [] });
if (!plugConfig.sources) return [];
let sources = plugConfig.sources;
if (!Array.isArray(sources)) sources = [sources as unknown as Source];
return sources.filter(s => typeof s.url === "string");
async function getSources(): Promise<{ sources: any[], syncWindowDays: number, displayTimezone: string }> {
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;
// Get user's display timezone, default to America/Los_Angeles (PST)
const displayTimezone = rawConfig.displayTimezone || "America/Los_Angeles";
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;
}
return { sources, syncWindowDays, displayTimezone };
} catch (e) {
console.error("[iCalendar] Error in getSources:", e);
return { sources: [], syncWindowDays: 365, displayTimezone: "America/Los_Angeles" };
}
}
async function fetchAndParseCalendar(source: Source, hourShift = 0): Promise<CalendarEvent[]> {
let url = source.url.trim();
if (url.includes(" ")) url = encodeURI(url);
// ============================================================================
// 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);
const response = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
});
// 5. Convert: UTC = wall-clock - offset
return new Date(wallClockAsUtc.getTime() - offsetMs);
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
/**
* 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);
}
const icsData = await response.text();
const calendar: IcsCalendar = convertIcsCalendar(undefined, icsData);
/**
* Expands recurring events into individual occurrences.
*/
export function expandRecurrences(icsEvent: any, windowDays = 365, displayTimezone = "America/Los_Angeles", now = new Date()): any[] {
const rruleStr = icsEvent.rrule || (icsEvent as any).recurrenceRule;
if (!rruleStr) return [icsEvent];
if (!calendar.events) return [];
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];
}
return await Promise.all(calendar.events.map(async (icsEvent: IcsEvent): Promise<CalendarEvent> => {
const uniqueKey = `${icsEvent.start?.date || ''}${icsEvent.uid || icsEvent.summary || ''}`;
const ref = await sha256Hash(uniqueKey);
const ruleOptions = RRule.parseString(cleanRule);
ruleOptions.dtstart = dtstart;
set.rrule(new RRule(ruleOptions));
return convertDatesToStrings({
...icsEvent,
ref,
tag: "ical-event" as const,
sourceName: source.name,
}, hourShift);
}));
// 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: dateToTimezoneString(occurrenceDate, displayTimezone),
end: endDate ? endDate.toISOString() : undefined,
endLocal: endDate ? dateToTimezoneString(endDate, displayTimezone) : undefined,
recurrent: true,
rrule: undefined,
};
});
return mapped;
} catch (err) {
console.error(`[iCalendar] Error expanding recurrence for ${icsEvent.summary}:`, err);
return [icsEvent];
}
}
async function fetchAndParseCalendar(source: any, windowDays = 365, displayTimezone = "America/Los_Angeles"): 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 jcalData = ICAL.parse(text);
const vcalendar = new ICAL.Component(jcalData);
const vevents = vcalendar.getAllSubcomponents("vevent");
// First pass: map of UID -> Set of ISO strings for RECURRENCE-ID exceptions
const overrides = new Map<string, Set<string>>();
for (const vevent of vevents) {
const recId = vevent.getFirstPropertyValue("recurrence-id") as any | null;
const uid = vevent.getFirstPropertyValue("uid") as string | null;
if (recId && uid) {
if (!overrides.has(uid)) overrides.set(uid, new Set());
overrides.get(uid)!.add(recId.toJSDate().toISOString());
}
}
const events: any[] = [];
for (const vevent of vevents) {
const icsEvent = new ICAL.Event(vevent);
const status = vevent.getFirstPropertyValue("status") as string | null;
const summary = icsEvent.summary || "";
// 1. Skip explicitly cancelled events
if (status?.toUpperCase() === "CANCELLED") continue;
if (summary.toLowerCase().startsWith("canceled:") || summary.toLowerCase().startsWith("cancelled:")) continue;
// 2. Skip declined events (look for PARTSTAT=DECLINED in attendees)
const attendees = vevent.getAllProperties("attendee");
let declined = false;
for (const attendee of attendees) {
const partstat = attendee.getParameter("partstat");
if (partstat?.toUpperCase() === "DECLINED") {
// Note: In a multi-user environment, we'd need to check if this is *the* user's status.
// For a personal plug, we assume any DECLINED attendee means the user declined or the event is out.
declined = true;
break;
}
}
if (declined) continue;
// Extract raw properties for recurrence expansion
const uid = icsEvent.uid;
const description = icsEvent.description;
const location = icsEvent.location;
const rrule = vevent.getFirstPropertyValue("rrule");
const exdates = vevent.getAllProperties("exdate").map((p: any) => p.getFirstValue().toJSDate().toISOString());
// Add recurrence-id overrides to exdates so the Master doesn't expand them
if (uid && overrides.has(uid) && !vevent.getFirstPropertyValue("recurrence-id")) {
for (const overDate of overrides.get(uid)!) {
exdates.push(overDate);
}
}
// Resolve start/end times
const startDateUTC = icsEvent.startDate.toJSDate();
const endDateUTC = icsEvent.endDate ? icsEvent.endDate.toJSDate() : null;
const rawTz = icsEvent.startDate.timezone || "UTC";
const baseEvent = {
uid,
summary,
name: summary || "Untitled Event",
description,
location,
// Store both UTC (for sorting/comparison) and local (for display)
start: startDateUTC.toISOString(),
startLocal: dateToTimezoneString(startDateUTC, displayTimezone),
end: endDateUTC ? endDateUTC.toISOString() : undefined,
endLocal: endDateUTC ? dateToTimezoneString(endDateUTC, displayTimezone) : undefined,
tag: "ical-event",
sourceName: source.name,
timezone: rawTz,
rrule: rrule ? rrule.toString() : undefined,
exdate: exdates.length > 0 ? exdates : undefined
};
if (rawTz !== "UTC" && rawTz !== "None" && !resolveIanaName(rawTz)) {
baseEvent.description = `(Warning: Unknown timezone "${rawTz}") ${baseEvent.description || ""}`;
}
const expanded = expandRecurrences(baseEvent, windowDays, displayTimezone);
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);
// Save our correctly formatted time strings
const savedTimes = {
start: occurrence.start,
startLocal: occurrence.startLocal,
end: occurrence.end,
endLocal: occurrence.endLocal
};
// Convert any remaining Date objects in other fields
const converted = convertDatesToStrings(occurrence);
// Restore our time strings (don't let them get reconverted)
converted.start = savedTimes.start;
converted.startLocal = savedTimes.startLocal;
converted.end = savedTimes.end;
converted.endLocal = savedTimes.endLocal;
events.push(converted);
}
}
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 plugConfig = await config.get<PlugConfig>("icalendar", { sources: [] });
const hourShift = plugConfig.tzShift ?? 0;
const sources = await getSources();
const { sources, syncWindowDays, displayTimezone } = await getSources();
if (sources.length === 0) return;
console.log(`[iCalendar] Using display timezone: ${displayTimezone}`);
// Test timezone conversion
const testDate = new Date("2026-02-21T14:00:00.000Z"); // 14:00 UTC
const converted = dateToTimezoneString(testDate, displayTimezone);
console.log(`[iCalendar] Timezone test: ${testDate.toISOString()}${converted} (should be 06:00 PST)`);
await editor.flashNotification("Syncing calendars...", "info");
const allEvents: CalendarEvent[] = [];
const allEvents: any[] = [];
for (const source of sources) {
try {
const events = await fetchAndParseCalendar(source, hourShift);
allEvents.push(...events);
} catch (err) {
console.error(`[iCalendar] Failed to sync ${source.name}:`, err);
}
const events = await fetchAndParseCalendar(source, syncWindowDays, displayTimezone);
allEvents.push(...events);
}
await index.indexObjects("$icalendar", allEvents);
await editor.flashNotification(`Synced ${allEvents.length} events`, "info");
} catch (err) {
console.error("[iCalendar] Sync failed:", err);
console.error("[iCalendar] syncCalendars failed:", err);
}
}
@@ -232,4 +480,4 @@ export async function clearCache() {
export async function showVersion() {
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, dateToTimezoneString } 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 = dateToTimezoneString(start, "UTC");
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 = dateToTimezoneString(yesterday, "UTC");
const tomorrowStr = dateToTimezoneString(tomorrow, "UTC");
const icsEvent = {
summary: "Daily Meeting EXDATE",
start: yesterday.toISOString(),
rrule: "FREQ=DAILY;COUNT=3",
exdate: [tomorrow.toISOString()]
};
const results = expandRecurrences(icsEvent, 30, "UTC", now);
// Yesterday (in window), Today (in window), Tomorrow (Excluded)
// Should have 2 occurrences
assertEquals(results.length, 2);
assertEquals(results[0].startLocal, dateToTimezoneString(yesterday, "UTC"));
});
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 start = new Date(now.getTime() - 7 * 86400000); // 7 days ago
const icsEvent = {
summary: "Daily Meeting Window",
start: start.toISOString(),
rrule: "FREQ=DAILY"
};
const results = expandRecurrences(icsEvent, 2, "UTC", now);
// 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 = dateToTimezoneString(now, "UTC");
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 = dateToTimezoneString(start, "UTC");
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 = dateToTimezoneString(start, "UTC");
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 = dateToTimezoneString(start, "UTC");
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 = dateToTimezoneString(start, "UTC");
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 = dateToTimezoneString(start, "UTC");
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);
});

3
package.json Normal file
View File

@@ -0,0 +1,3 @@
{
"dependencies": {}
}

19
scripts/bump-version.ts Normal file
View File

@@ -0,0 +1,19 @@
// scripts/bump-version.ts
const denoConfigPath = "deno.json";
const denoConfig = JSON.parse(await Deno.readTextFile(denoConfigPath));
const currentVersion = denoConfig.version;
const parts = currentVersion.split(".").map(Number);
if (parts.length !== 3 || parts.some(isNaN)) {
console.error(`Invalid version format in deno.json: ${currentVersion}`);
Deno.exit(1);
}
// Increment patch version
parts[2]++;
const newVersion = parts.join(".");
denoConfig.version = newVersion;
await Deno.writeTextFile(denoConfigPath, JSON.stringify(denoConfig, null, 2) + "\n");
console.log(`Bumped version: ${currentVersion} -> ${newVersion}`);

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

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

79
tests/integration_test.ts Normal file
View File

@@ -0,0 +1,79 @@
import { assertEquals, assert } from "jsr:@std/assert";
import ICAL from "ical.js";
import { expandRecurrences, resolveEventStart, dateToTimezoneString } 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 jcalData = ICAL.parse(text);
const vcalendar = new ICAL.Component(jcalData);
const vevents = vcalendar.getAllSubcomponents("vevent");
assert(vevents.length > 0, `Failed to parse ${file} or no events found`);
for (const vevent of vevents) {
const icsEvent = new ICAL.Event(vevent);
const status = vevent.getFirstPropertyValue("status") as string | null;
if (status?.toUpperCase() === "CANCELLED") continue;
const startDateUTC = icsEvent.startDate.toJSDate();
const summary = icsEvent.summary;
const rrule = vevent.getFirstPropertyValue("rrule");
const exdates = vevent.getAllProperties("exdate").map((p: any) => p.getFirstValue().toJSDate().toISOString());
const localIso = dateToTimezoneString(startDateUTC, "UTC");
const baseEvent = {
summary,
name: summary || "Untitled Event",
start: startDateUTC.toISOString(),
startLocal: localIso,
tag: "ical-event",
sourceName: "IntegrationTest",
rrule: rrule ? rrule.toString() : undefined,
exdate: exdates.length > 0 ? exdates : undefined
};
try {
const expanded = expandRecurrences(baseEvent, 30);
assert(expanded.length >= 1, `Expected at least 1 occurrence for event "${summary}" in ${file}`);
} catch (err) {
console.error(`❌ Error expanding recurrence for event "${summary}" in ${file}:`, err);
throw err;
}
}
}
});

View File

@@ -0,0 +1,176 @@
import { assertEquals, assert } from "jsr:@std/assert";
import { resolveEventStart, expandRecurrences, dateToTimezoneString } 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 start = new Date("2026-01-16T13:00:00");
const icsEvent = {
summary: "BUSY Weekly",
start: start.toISOString(),
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, "UTC", TEST_NOW);
// Should have multiple occurrences per week
assert(results.length > 1);
assert(results.some(r => r.startLocal.includes("2026-01-19"))); // Monday
assert(results.some(r => r.startLocal.includes("2026-01-20"))); // Tuesday
});
Deno.test("Variation: Recurring with WORKWEEKSTART (Outlook style)", () => {
const start = new Date("2026-01-20T08:30:00");
const icsEvent = {
summary: "Outlook Style Meeting",
start: start.toISOString(),
rrule: {
frequency: "WEEKLY",
interval: 1,
byday: "TU",
workweekstart: "MO"
}
};
const results = expandRecurrences(icsEvent, 30, "UTC", TEST_NOW);
assert(results.length > 0);
assert(results[0].startLocal.includes("2026-01-20"));
});
Deno.test("Variation: Recurring with EXDATE (Exclusion)", () => {
const start = new Date("2026-01-20T08:30:00");
const icsEvent = {
summary: "HPE-Veeam check-in",
start: start.toISOString(),
rrule: "FREQ=WEEKLY;UNTIL=20260324T143000Z;INTERVAL=1;BYDAY=TU;WKST=SU",
exdate: ["2026-02-03T08:30:00"]
};
const results = expandRecurrences(icsEvent, 60, "UTC", TEST_NOW);
const dates = results.map(r => r.startLocal);
assert(dates.some(d => d.includes("2026-01-20T")), "Should find first occurrence");
assert(dates.some(d => d.includes("2026-01-27T")), "Should find second occurrence");
assert(!dates.some(d => d.includes("2026-02-03T")), "EXDATE 2026-02-03 should be excluded");
assert(dates.some(d => d.includes("2026-02-10T")), "Should find fourth occurrence");
});
Deno.test("Variation: Monthly Recurring (Last Friday)", () => {
const start = new Date("2026-01-30T10:00:00");
const icsEvent = {
summary: "Monthly Planning",
start: start.toISOString(), // This is the last Friday of Jan 2026
rrule: "FREQ=MONTHLY;UNTIL=20260731T170000Z;INTERVAL=1;BYDAY=-1FR"
};
const results = expandRecurrences(icsEvent, 100, "UTC", TEST_NOW);
const dates = results.map(r => r.startLocal);
assert(dates.some(d => d.includes("2026-01-30")), "Should find Jan occurrence");
assert(dates.some(d => d.includes("2026-02-27")), "Should find Feb occurrence");
assert(dates.some(d => d.includes("2026-03-27")), "Should find Mar occurrence");
});
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: start.toISOString(),
rrule: "FREQ=DAILY;COUNT=1000"
};
const results = expandRecurrences(icsEvent, 30, "UTC", TEST_NOW);
// Should include events from 500 days ago because there is now no limit
assert(results.some(r => r.startLocal === dateToTimezoneString(start, "UTC")), "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);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB