The alarms that matter were already being written to the server, which is the half that counts: a phone reads them at six in the morning with this tab long closed. Two things were wrong with the writing, and the telling was missing. `RELATED` was being dropped. The alarm row read the offset out of the trigger and wrote a new one back with `related: Start` hard-coded, so an alarm made elsewhere as "ten minutes before it ends" became ten minutes before the start the first time this app touched the event -- the exact v1 bug the model was given the parameter to prevent. It is a control now, beside the offset, and changing either keeps the other. The offsets also cover "when it starts" and "fifteen minutes after", which are things people set. `alarms::due` is the arithmetic, and it is a pure function of the events on screen and the time: only `DISPLAY` alarms, because acting on an `EMAIL` or an `AUDIO` one is not a browser tab's business; relative triggers off whichever end they name; absolute ones converted through the difference between the two spellings of the occurrence's own start, so no zone database is needed for it. A five-minute window decides what is late enough to skip -- a page opened at noon should not recite the whole morning, and one that was behind another window for four minutes should not lose what came due while it was there. The telling is one effect on the clock that was already ticking for the now-line, and one set in memory of what has been said. Either the browser says it, tagged so a second tab replaces the notice rather than stacking beside it, or the page does, in a note that can be dismissed -- never both, because the granted one arrives when the tab is behind something else, which is when a reminder is worth having. v1 spent a 2,008-line component polling every thirty seconds, a `localStorage` ledger that grew without bound, and a service worker that could not read that ledger and so did nothing but ping the tab that was already polling. Permission is asked from settings, on a click. A prompt fired at a page nobody has looked at yet is one they dismiss without reading, and a dismissed prompt is indistinguishable from here from a refusal. The panel re-reads where it stands each time it opens, since that is the browser's to change from a control this page does not own. Chips that carry any alarm say so with a small mark -- any action, not only the ones this browser would act on, because the mark means "somebody will be told" and the phone reading the same calendar is somebody. The browser checks cover both channels: a reminder due three minutes ago arriving on the page and being dismissed and not coming back on the next tick, a trigger hung off the end surviving a round trip through CalDAV, and, with the notification API standing in for the real one, the same reminder going to the browser instead and nothing being said twice.
86 lines
2.0 KiB
TOML
86 lines
2.0 KiB
TOML
[workspace]
|
|
resolver = "3"
|
|
members = [
|
|
"crates/runway-core",
|
|
"crates/runway-caldav",
|
|
"crates/runway-server",
|
|
"crates/runway-web",
|
|
"crates/runway-cli",
|
|
]
|
|
|
|
[workspace.package]
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
license = "MIT"
|
|
repository = "https://github.com/connorjohnstone/runway"
|
|
|
|
[workspace.lints.rust]
|
|
dead_code = "deny"
|
|
unused_must_use = "deny"
|
|
unsafe_code = "forbid"
|
|
|
|
[workspace.lints.clippy]
|
|
# v1 had 65 of these. Tests opt out locally.
|
|
unwrap_used = "deny"
|
|
expect_used = "deny"
|
|
|
|
[workspace.dependencies]
|
|
# Internal
|
|
runway-core = { path = "crates/runway-core" }
|
|
runway-caldav = { path = "crates/runway-caldav" }
|
|
|
|
# Core data
|
|
chrono = { version = "0.4", default-features = false, features = ["serde", "clock", "std"] }
|
|
chrono-tz = { version = "0.10", features = ["serde"] }
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
uuid = { version = "1", features = ["v4", "serde"] }
|
|
|
|
# Calendar domain
|
|
icalendar = "0.17"
|
|
rrule = "0.14"
|
|
|
|
# Crypto
|
|
sha2 = "0.10"
|
|
chacha20poly1305 = "0.10"
|
|
rand = "0.9"
|
|
base64 = "0.22"
|
|
|
|
# Errors + logging
|
|
thiserror = "2"
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
|
|
# Backend
|
|
axum = "0.8"
|
|
axum-extra = { version = "0.10", features = ["cookie", "query"] }
|
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "signal"] }
|
|
tower = "0.5"
|
|
tower-http = { version = "0.6", features = ["cors", "trace", "request-id"] }
|
|
sqlx = { version = "0.9", features = ["runtime-tokio", "sqlite", "chrono", "uuid", "migrate"] }
|
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
|
quick-xml = "0.42"
|
|
|
|
# Frontend
|
|
leptos = "0.8"
|
|
leptos_router = "0.8"
|
|
gloo-net = { version = "0.6", features = ["json"] }
|
|
wasm-bindgen = "0.2"
|
|
wasm-bindgen-futures = "0.4"
|
|
web-sys = "0.3"
|
|
js-sys = "0.3"
|
|
console_error_panic_hook = "0.1"
|
|
|
|
# CLI
|
|
clap = { version = "4", features = ["derive", "env"] }
|
|
iana-time-zone = "0.1"
|
|
|
|
# Dev
|
|
pretty_assertions = "1"
|
|
|
|
[profile.release]
|
|
opt-level = "z"
|
|
lto = true
|
|
codegen-units = 1
|
|
strip = true
|