Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
20 changed files with 66 additions and 534 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -1,42 +0,0 @@
|
||||||
## [0.2.0] - 2026-04-08
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Scroll to current contact in sidebar by default
|
|
||||||
- Clicking off sidebar on small windows closes it
|
|
||||||
- Sort contacts sidebar ignoring case
|
|
||||||
- Report version information
|
|
||||||
|
|
||||||
### Refactor
|
|
||||||
|
|
||||||
- Cloak input elements while alpine.js loads
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- *(test)* Test performance improvements
|
|
||||||
- Large cache time on immutable hashed statics
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
- Deflake by waiting for Save response
|
|
||||||
- Open sidebar on mobile
|
|
||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Prepare for tagged releases
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-04-05
|
|
||||||
|
|
||||||
# Features
|
|
||||||
* In-app contacts
|
|
||||||
* For each contact:
|
|
||||||
* Names
|
|
||||||
* Birthday
|
|
||||||
* Last-contact-time mapping
|
|
||||||
* Address as single field (plus code? lat/long? go crazy!)
|
|
||||||
* Free-text-entry field
|
|
||||||
* Desired contact periodicity
|
|
||||||
* Journal with Obsidian-like `[[link]]` syntax
|
|
||||||
* Contact groups (e.g. "Met with `[[Brunch Bunch]]` at the diner")
|
|
||||||
* ical server for birthday reminders
|
|
||||||
* Mark contacts as inactive or prevent stale checks
|
|
||||||
192
Cargo.lock
generated
192
Cargo.lock
generated
|
|
@ -400,39 +400,6 @@ dependencies = [
|
||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "camino"
|
|
||||||
version = "1.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo-platform"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo_metadata"
|
|
||||||
version = "0.23.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
|
|
||||||
dependencies = [
|
|
||||||
"camino",
|
|
||||||
"cargo-platform",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.29"
|
version = "1.2.29"
|
||||||
|
|
@ -615,41 +582,6 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.20.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"darling_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.20.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"strsim",
|
|
||||||
"syn 2.0.106",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.20.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.106",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "debug-helper"
|
name = "debug-helper"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
|
@ -677,37 +609,6 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derive_builder"
|
|
||||||
version = "0.20.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
|
||||||
dependencies = [
|
|
||||||
"derive_builder_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derive_builder_core"
|
|
||||||
version = "0.20.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.106",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derive_builder_macro"
|
|
||||||
version = "0.20.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
|
||||||
dependencies = [
|
|
||||||
"derive_builder_core",
|
|
||||||
"syn 2.0.106",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deunicode"
|
name = "deunicode"
|
||||||
version = "1.6.2"
|
version = "1.6.2"
|
||||||
|
|
@ -1288,12 +1189,6 @@ dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -1560,7 +1455,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mascarpone"
|
name = "mascarpone"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -1589,14 +1484,12 @@ dependencies = [
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
"tower-sessions-sqlx-store",
|
"tower-sessions-sqlx-store",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"vcard",
|
"vcard",
|
||||||
"vergen-gitcl",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1756,9 +1649,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.1"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
|
|
@ -1790,15 +1683,6 @@ dependencies = [
|
||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_threads"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.37.3"
|
version = "0.37.3"
|
||||||
|
|
@ -2329,16 +2213,6 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|
@ -2874,32 +2748,30 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.47"
|
version = "0.3.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
"libc",
|
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"num_threads",
|
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.8"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.27"
|
version = "0.2.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
|
@ -2986,9 +2858,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -3322,46 +3194,6 @@ version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vergen"
|
|
||||||
version = "9.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"cargo_metadata",
|
|
||||||
"derive_builder",
|
|
||||||
"regex",
|
|
||||||
"rustversion",
|
|
||||||
"time",
|
|
||||||
"vergen-lib",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vergen-gitcl"
|
|
||||||
version = "9.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"derive_builder",
|
|
||||||
"rustversion",
|
|
||||||
"time",
|
|
||||||
"vergen",
|
|
||||||
"vergen-lib",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vergen-lib"
|
|
||||||
version = "9.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"derive_builder",
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mascarpone"
|
name = "mascarpone"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
@ -35,8 +35,7 @@ sqlx = { version = "0.8", features = ["macros", "runtim
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
time = "0.3.44"
|
time = "0.3.44"
|
||||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "signal"] }
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||||
tower = "0.5.3"
|
tower-http = { version = "0.6.6", features = ["fs", "trace"] }
|
||||||
tower-http = { version = "0.6.6", features = ["fs", "set-header", "trace"] }
|
|
||||||
tower-sessions = { version = "0.14.0", features = ["signed"] }
|
tower-sessions = { version = "0.14.0", features = ["signed"] }
|
||||||
tower-sessions-sqlx-store = { version = "0.15.0", features = ["sqlite"] }
|
tower-sessions-sqlx-store = { version = "0.15.0", features = ["sqlite"] }
|
||||||
tracing = { version = "0.1.41", features = ["attributes"] }
|
tracing = { version = "0.1.41", features = ["attributes"] }
|
||||||
|
|
@ -45,4 +44,3 @@ vcard = "0.4.13"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cache_bust = "0.2.0"
|
cache_bust = "0.2.0"
|
||||||
vergen-gitcl = { version = "9.1.0", features = ["build", "cargo"] }
|
|
||||||
|
|
|
||||||
12
Taskfile
12
Taskfile
|
|
@ -44,16 +44,4 @@ dev() {
|
||||||
find src static migrations | entr -ccdr ./Taskfile _cargo run -- serve
|
find src static migrations | entr -ccdr ./Taskfile _cargo run -- serve
|
||||||
}
|
}
|
||||||
|
|
||||||
release() {
|
|
||||||
set -euo pipefail
|
|
||||||
bash e2e/Taskfile playwright:ci
|
|
||||||
_cargo test
|
|
||||||
new_tag=$(git-cliff --unreleased --bumped-version)
|
|
||||||
git tag -m "$new_tag" "$new_tag"
|
|
||||||
cargo set-version "${new_tag#v}"
|
|
||||||
mv CHANGELOG.md CHANGELOG.old
|
|
||||||
cat <(git cliff) <(printf "\n") CHANGELOG.old > CHANGELOG.md
|
|
||||||
rm CHANGELOG.old
|
|
||||||
}
|
|
||||||
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
|
||||||
11
build.rs
11
build.rs
|
|
@ -1,5 +1,4 @@
|
||||||
use cache_bust::CacheBust;
|
use cache_bust::CacheBust;
|
||||||
use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=migrations");
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
|
@ -10,14 +9,4 @@ fn main() {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
cache_bust.hash_dir().expect("Cache busting failed");
|
cache_bust.hash_dir().expect("Cache busting failed");
|
||||||
|
|
||||||
let build = BuildBuilder::all_build().expect("build information failed");
|
|
||||||
let gitcl = GitclBuilder::all_git().expect("gitcl information failed");
|
|
||||||
Emitter::default()
|
|
||||||
.add_instructions(&build)
|
|
||||||
.unwrap()
|
|
||||||
.add_instructions(&gitcl)
|
|
||||||
.unwrap()
|
|
||||||
.emit()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
cliff.toml
94
cliff.toml
|
|
@ -1,94 +0,0 @@
|
||||||
# git-cliff ~ configuration file
|
|
||||||
# https://git-cliff.org/docs/configuration
|
|
||||||
|
|
||||||
|
|
||||||
[changelog]
|
|
||||||
# A Tera template to be rendered for each release in the changelog.
|
|
||||||
# See https://keats.github.io/tera/docs/#introduction
|
|
||||||
body = """
|
|
||||||
{% if version %}\
|
|
||||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
||||||
{% else %}\
|
|
||||||
## [unreleased]
|
|
||||||
{% endif %}\
|
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | striptags | trim | upper_first }}
|
|
||||||
{% for commit in commits %}
|
|
||||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
|
||||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
|
||||||
{{ commit.message | upper_first }}\
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
"""
|
|
||||||
# Remove leading and trailing whitespaces from the changelog's body.
|
|
||||||
trim = true
|
|
||||||
# Render body even when there are no releases to process.
|
|
||||||
render_always = true
|
|
||||||
# An array of regex based postprocessors to modify the changelog.
|
|
||||||
postprocessors = [
|
|
||||||
# Replace the placeholder <REPO> with a URL.
|
|
||||||
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
|
|
||||||
]
|
|
||||||
# render body even when there are no releases to process
|
|
||||||
# render_always = true
|
|
||||||
# output file path
|
|
||||||
# output = "test.md"
|
|
||||||
|
|
||||||
[git]
|
|
||||||
# Parse commits according to the conventional commits specification.
|
|
||||||
# See https://www.conventionalcommits.org
|
|
||||||
conventional_commits = true
|
|
||||||
# Exclude commits that do not match the conventional commits specification.
|
|
||||||
filter_unconventional = true
|
|
||||||
# Require all commits to be conventional.
|
|
||||||
# Takes precedence over filter_unconventional.
|
|
||||||
require_conventional = false
|
|
||||||
# Split commits on newlines, treating each line as an individual commit.
|
|
||||||
split_commits = false
|
|
||||||
# An array of regex based parsers to modify commit messages prior to further processing.
|
|
||||||
commit_preprocessors = [
|
|
||||||
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
|
|
||||||
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
|
||||||
# Check spelling of the commit message using https://github.com/crate-ci/typos.
|
|
||||||
# If the spelling is incorrect, it will be fixed automatically.
|
|
||||||
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
|
||||||
]
|
|
||||||
# Prevent commits that are breaking from being excluded by commit parsers.
|
|
||||||
protect_breaking_commits = false
|
|
||||||
# An array of regex based parsers for extracting data from the commit message.
|
|
||||||
# Assigns commits to groups.
|
|
||||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = "^feat", group = "<!-- 0 -->Features" },
|
|
||||||
{ message = "^fix", group = "<!-- 1 -->Bug Fixes" },
|
|
||||||
{ message = "^doc", group = "<!-- 3 -->Documentation" },
|
|
||||||
{ message = "^perf", group = "<!-- 4 -->Performance" },
|
|
||||||
{ message = "^refactor", group = "<!-- 2 -->Refactor" },
|
|
||||||
{ message = "^style", group = "<!-- 5 -->Styling" },
|
|
||||||
{ message = "^test", group = "<!-- 6 -->Testing" },
|
|
||||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
|
||||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pr\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pull\\)", skip = true },
|
|
||||||
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
|
||||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
|
||||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
|
||||||
{ message = ".*", group = "<!-- 10 -->💼 Other" },
|
|
||||||
]
|
|
||||||
# Exclude commits that are not matched by any commit parser.
|
|
||||||
filter_commits = false
|
|
||||||
# Fail on a commit that is not matched by any commit parser.
|
|
||||||
fail_on_unmatched_commit = false
|
|
||||||
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
|
|
||||||
link_parsers = []
|
|
||||||
# Include only the tags that belong to the current branch.
|
|
||||||
use_branch_tags = false
|
|
||||||
# Order releases topologically instead of chronologically.
|
|
||||||
topo_order = false
|
|
||||||
# Order commits topologically instead of chronologically.
|
|
||||||
topo_order_commits = true
|
|
||||||
# Order of commits in each group/release within the changelog.
|
|
||||||
# Allowed values: newest, oldest
|
|
||||||
sort_commits = "oldest"
|
|
||||||
# Process submodules commits
|
|
||||||
recurse_submodules = false
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { login, verifyCreateUser, todate } from './util';
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
await verifyCreateUser(page, { names: ['Test Testerson'] });
|
await verifyCreateUser(page, { names: ['Test Testerson'] });
|
||||||
|
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('manual-freshen date is editable', async ({ page }) => {
|
test('manual-freshen date is editable', async ({ page }) => {
|
||||||
|
|
@ -18,21 +19,19 @@ test('last-contact date on display resolves journal mentions and manual-freshen'
|
||||||
const entryBox = page.getByPlaceholder(/new entry/i);
|
const entryBox = page.getByPlaceholder(/new entry/i);
|
||||||
await entryDate.fill("2025-05-05");
|
await entryDate.fill("2025-05-05");
|
||||||
await entryBox.fill("[[Test Testerson]]");
|
await entryBox.fill("[[Test Testerson]]");
|
||||||
|
|
||||||
let load = page.waitForResponse('/journal_entry');
|
|
||||||
await page.getByRole('button', { name: /add entry/i }).click();
|
await page.getByRole('button', { name: /add entry/i }).click();
|
||||||
await load;
|
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.locator('#fields')).toContainText("freshened2025-05-05");
|
await expect(page.locator('#fields')).toContainText("freshened2025-05-05");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("groups wrap nicely", async ({ page }) => {
|
test.skip("groups wrap nicely", async ({ page }) => {
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
|
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||||
|
|
||||||
const groupBox = page.getByPlaceholder(/group name/i);
|
const groupBox = page.getByPlaceholder(/group name/i);
|
||||||
await groupBox.fill('this is a long group name');
|
await groupBox.fill('this is a long group name');
|
||||||
await page.getByRole('button', { name: /save/i }).click();
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
|
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||||
|
|
||||||
// TODO: this drives to the right location but i can't figure out how to assert
|
// TODO: this drives to the right location but i can't figure out how to assert
|
||||||
// that the text is all on one line. Manual inspection looks good at time of writing.
|
// that the text is all on one line. Manual inspection looks good at time of writing.
|
||||||
|
|
@ -40,6 +39,7 @@ test.skip("groups wrap nicely", async ({ page }) => {
|
||||||
|
|
||||||
test('allow marking as inactive', async ({ page }) => {
|
test('allow marking as inactive', async ({ page }) => {
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
|
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||||
|
|
||||||
await page.getByLabel('status').selectOption('Inactive');
|
await page.getByLabel('status').selectOption('Inactive');
|
||||||
await page.getByRole('button', { name: /save/i }).click();
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
|
|
@ -55,7 +55,6 @@ test('allow exempting from stale', async ({ page }) => {
|
||||||
|
|
||||||
await page.getByLabel('status').selectOption('Cannot go stale');
|
await page.getByLabel('status').selectOption('Cannot go stale');
|
||||||
await page.getByRole('button', { name: /save/i }).click();
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
await page.waitForURL(/contact\/\d+$/);
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.locator('#freshness')).not.toContainText('Test Testersonnever');
|
await expect(page.locator('#freshness')).not.toContainText('Test Testersonnever');
|
||||||
});
|
});
|
||||||
|
|
@ -97,5 +96,9 @@ test('bullet points in free text display well', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
home: contact list scrolls in screen, not off screen
|
||||||
|
home: clicking off contact list closes it
|
||||||
|
home: contact list is sorted ignoring case
|
||||||
|
home: contact list should scroll to current contact in center of view
|
||||||
journal: bullet points don't display
|
journal: bullet points don't display
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@ test('has no contacts', async ({ page }) => {
|
||||||
test('can add contacts', async ({ page }) => {
|
test('can add contacts', async ({ page }) => {
|
||||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||||
await verifyCreateUser(page, { names: ['Jack Contact'] });
|
await verifyCreateUser(page, { names: ['Jack Contact'] });
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(2);
|
await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,10 +41,6 @@ test('sidebar is sorted alphabetically', async ({ page }) => {
|
||||||
await verifyCreateUser(page, { names: ['Alfa'] });
|
await verifyCreateUser(page, { names: ['Alfa'] });
|
||||||
await verifyCreateUser(page, { names: ['Golf'] });
|
await verifyCreateUser(page, { names: ['Golf'] });
|
||||||
|
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(page.getByRole('navigation')).toHaveText(/Alfa\s*Golf\s*Zulu/);
|
await expect(page.getByRole('navigation')).toHaveText(/Alfa\s*Golf\s*Zulu/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -88,48 +81,3 @@ test('upcoming and recent show at least one birthday a week away', async ({ page
|
||||||
await expect(page.locator('#upcoming').getByRole('link')).toHaveCount(4);
|
await expect(page.locator('#upcoming').getByRole('link')).toHaveCount(4);
|
||||||
await expect(page.locator('#recent').getByRole('link')).toHaveCount(4);
|
await expect(page.locator('#recent').getByRole('link')).toHaveCount(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('contact list scrolls (independently) to current contact in center of view', async ({ page }) => {
|
|
||||||
for (let count = 0; count < 30; count++) {
|
|
||||||
await verifyCreateUser(page, { names: [`Contact${count < 10 ? '0' + count : count}`] });
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.goto('/contact/28');
|
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await expect(page.getByRole('navigation').getByRole('link', { name: /Contact28/ })).toBeVisible();
|
|
||||||
expect(await page.locator('main').evaluate(e => e.scrollTop)).toEqual(0);
|
|
||||||
|
|
||||||
await page.goto('/contact/16');
|
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await expect(page.locator('#nav-link-16')).toBeVisible();
|
|
||||||
const linkPos: number = await page.locator('#nav-link-16').evaluate(e => e.getBoundingClientRect().y);
|
|
||||||
|
|
||||||
// roughly centered is fine, not that fussy about headers and whatnot
|
|
||||||
expect(await page.getByRole('navigation').evaluate(e => e.scrollTop)).not.toEqual(0);
|
|
||||||
expect(Math.abs(linkPos - (await page.evaluate('window.innerHeight/2') as number))).toBeLessThan(300);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clicking off contact list when expanded closes it', async ({ page }) => {
|
|
||||||
await page.setViewportSize({
|
|
||||||
width: 640,
|
|
||||||
height: 1000,
|
|
||||||
});
|
|
||||||
// TODO aria-label
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
await expect(page.getByRole('button', { name: /add contact/i })).toBeVisible();
|
|
||||||
await page.mouse.click(600, 500);
|
|
||||||
await expect(page.getByRole('button', { name: /add contact/i })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('contact list is sorted ignoring case', async ({ page }) => {
|
|
||||||
await verifyCreateUser(page, { names: ['Alfa'] });
|
|
||||||
await verifyCreateUser(page, { names: ['bob'] });
|
|
||||||
await verifyCreateUser(page, { names: ['Charlie'] });
|
|
||||||
|
|
||||||
await expect(page.locator('#contacts-sidebar')).toContainText(/alfa\s*bob\s*charlie/i);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ test('can add journal entries', async ({ page }) => {
|
||||||
|
|
||||||
const entryBox = page.getByPlaceholder(/new entry/i);
|
const entryBox = page.getByPlaceholder(/new entry/i);
|
||||||
await entryBox.fill('banana banana banana');
|
await entryBox.fill('banana banana banana');
|
||||||
let load = page.waitForResponse('/journal_entry');
|
|
||||||
await page.getByRole('button', { name: /add entry/i }).click();
|
await page.getByRole('button', { name: /add entry/i }).click();
|
||||||
await load;
|
|
||||||
|
|
||||||
await expect(entryBox).toBeEmpty();
|
await expect(entryBox).toBeEmpty();
|
||||||
await expect(page.getByText('banana banana banana')).toBeVisible();
|
await expect(page.getByText('banana banana banana')).toBeVisible();
|
||||||
|
|
@ -20,9 +18,7 @@ test('journal entries autolink', async ({ page }) => {
|
||||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||||
|
|
||||||
await page.getByPlaceholder(/new entry/i).fill('met with [[John Contact]]');
|
await page.getByPlaceholder(/new entry/i).fill('met with [[John Contact]]');
|
||||||
let load = page.waitForResponse('/journal_entry');
|
|
||||||
await page.getByRole('button', { name: /add entry/i }).click();
|
await page.getByRole('button', { name: /add entry/i }).click();
|
||||||
await load;
|
|
||||||
|
|
||||||
await expect(page.locator('#journal').getByRole('link', { name: 'John Contact' })).toBeVisible();
|
await expect(page.locator('#journal').getByRole('link', { name: 'John Contact' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
@ -33,9 +29,7 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await verifyCreateUser(page, { names: ['Jack Contact'] });
|
await verifyCreateUser(page, { names: ['Jack Contact'] });
|
||||||
|
|
||||||
await page.getByPlaceholder(/new entry/i).fill('met with [[JC]]');
|
await page.getByPlaceholder(/new entry/i).fill('met with [[JC]]');
|
||||||
let load = page.waitForResponse('/journal_entry');
|
|
||||||
await page.getByRole('button', { name: /add entry/i }).click();
|
await page.getByRole('button', { name: /add entry/i }).click();
|
||||||
await load;
|
|
||||||
|
|
||||||
const nav = page.getByRole('navigation');
|
const nav = page.getByRole('navigation');
|
||||||
const journal = page.locator('#journal');
|
const journal = page.locator('#journal');
|
||||||
|
|
@ -43,9 +37,6 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
||||||
|
|
||||||
// add a new name
|
// add a new name
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
||||||
|
|
@ -55,9 +46,6 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
||||||
|
|
||||||
// delete an existing name
|
// delete an existing name
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
await page.getByRole('button', { name: '×', disabled: false }).click();
|
await page.getByRole('button', { name: '×', disabled: false }).click();
|
||||||
|
|
@ -66,9 +54,6 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
||||||
|
|
||||||
// put it back, then...
|
// put it back, then...
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
||||||
|
|
@ -78,9 +63,6 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
||||||
|
|
||||||
// ...add a name that makes it no longer n=1
|
// ...add a name that makes it no longer n=1
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await nav.getByRole("link", { name: 'Jack Contact' }).click();
|
await nav.getByRole("link", { name: 'Jack Contact' }).click();
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
||||||
|
|
@ -89,9 +71,6 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
||||||
|
|
||||||
// delete a name that makes it now n=1
|
// delete a name that makes it now n=1
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
await page.getByRole('button', { name: '×', disabled: false }).click();
|
await page.getByRole('button', { name: '×', disabled: false }).click();
|
||||||
|
|
@ -106,9 +85,7 @@ test('can edit existing journal entries on home page', async ({ page }) => {
|
||||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||||
|
|
||||||
await page.getByPlaceholder(/new entry/i).fill("[[John Contact]]'s banana banana banana");
|
await page.getByPlaceholder(/new entry/i).fill("[[John Contact]]'s banana banana banana");
|
||||||
let load = page.waitForResponse('/journal_entry');
|
|
||||||
await page.getByRole('button', { name: /add entry/i }).click();
|
await page.getByRole('button', { name: /add entry/i }).click();
|
||||||
await load;
|
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
|
|
@ -131,9 +108,7 @@ test('can have multiple links', async ({ page }) => {
|
||||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||||
|
|
||||||
await page.getByPlaceholder(/new entry/i).fill('met with [[alice]] and [[bob]] and their kids');
|
await page.getByPlaceholder(/new entry/i).fill('met with [[alice]] and [[bob]] and their kids');
|
||||||
let load = page.waitForResponse('/journal_entry');
|
|
||||||
await page.getByRole('button', { name: /add entry/i }).click();
|
await page.getByRole('button', { name: /add entry/i }).click();
|
||||||
await load;
|
|
||||||
|
|
||||||
const journal = page.locator('#journal');
|
const journal = page.locator('#journal');
|
||||||
await expect.soft(journal.getByRole('link', { name: 'alice' })).toHaveCount(1);
|
await expect.soft(journal.getByRole('link', { name: 'alice' })).toHaveCount(1);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
export const login = async (page: Page) => {
|
export const login = async (page: Page) => {
|
||||||
await page.goto('/login');
|
await page.goto('/');
|
||||||
await page.getByLabel("Username").fill("test");
|
await page.getByLabel("Username").fill("test");
|
||||||
await page.getByLabel("Password").fill("test");
|
await page.getByLabel("Password").fill("test");
|
||||||
await page.getByRole("button", { name: /login/i }).click();
|
await page.getByRole("button", { name: /login/i }).click();
|
||||||
await page.waitForURL('/');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const todate = () => new Date().toISOString().split('T')[0];
|
export const todate = () => new Date().toISOString().split('T')[0];
|
||||||
|
|
@ -16,11 +14,10 @@ type UserFields = {
|
||||||
birthday?: string,
|
birthday?: string,
|
||||||
};
|
};
|
||||||
export const verifyCreateUser = async (page: Page, fields: UserFields) => {
|
export const verifyCreateUser = async (page: Page, fields: UserFields) => {
|
||||||
if (await page.locator('#sidebar-show-hide').isVisible()) {
|
|
||||||
await page.locator('#sidebar-show-hide').click();
|
|
||||||
}
|
|
||||||
await page.getByRole('button', { name: /add contact/i }).click();
|
await page.getByRole('button', { name: /add contact/i }).click();
|
||||||
await page.waitForURL(/contact\/\d+\/edit$/);
|
|
||||||
|
// TODO this is stupid but playwright kept filling while alpine was initializing
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
const { names, ...simple } = fields;
|
const { names, ...simple } = fields;
|
||||||
for (const name of (names ?? [])) {
|
for (const name of (names ?? [])) {
|
||||||
|
|
@ -33,6 +30,5 @@ export const verifyCreateUser = async (page: Page, fields: UserFields) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: /save/i }).click();
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
await page.waitForURL(/contact\/\d+$/);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,3 @@
|
||||||
"rust-analyzer" = "latest"
|
"rust-analyzer" = "latest"
|
||||||
"jj" = "latest"
|
"jj" = "latest"
|
||||||
node = "24"
|
node = "24"
|
||||||
git-cliff = "latest"
|
|
||||||
|
|
|
||||||
67
src/main.rs
67
src/main.rs
|
|
@ -12,9 +12,7 @@ use std::sync::{Arc, RwLock};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tokio::task::AbortHandle;
|
use tokio::task::AbortHandle;
|
||||||
use tower::ServiceBuilder;
|
use tower_http::services::{ServeDir,ServeFile};
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
|
||||||
use tower_http::set_header::SetResponseHeaderLayer;
|
|
||||||
use tower_sessions::{ExpiredDeletion, Expiry, SessionManagerLayer, cookie::Key};
|
use tower_sessions::{ExpiredDeletion, Expiry, SessionManagerLayer, cookie::Key};
|
||||||
use tower_sessions_sqlx_store::SqliteStore;
|
use tower_sessions_sqlx_store::SqliteStore;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
@ -126,8 +124,6 @@ enum Commands {
|
||||||
ephemeral: bool,
|
ephemeral: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// print version information
|
|
||||||
Version,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve(port: &u32) -> Result<(), anyhow::Error> {
|
async fn serve(port: &u32) -> Result<(), anyhow::Error> {
|
||||||
|
|
@ -184,19 +180,8 @@ async fn serve(port: &u32) -> Result<(), anyhow::Error> {
|
||||||
.route_layer(login_required!(Backend, login_url = "/login"))
|
.route_layer(login_required!(Backend, login_url = "/login"))
|
||||||
.merge(auth::router())
|
.merge(auth::router())
|
||||||
.merge(ics::router())
|
.merge(ics::router())
|
||||||
.nest_service(
|
.nest_service("/static", ServeDir::new("./hashed_static"))
|
||||||
"/static",
|
.nest_service("/favicon.ico", ServeFile::new(format!("./hashed_static/{}", asset!("favicon.ico"))))
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(SetResponseHeaderLayer::overriding(
|
|
||||||
http::header::CACHE_CONTROL,
|
|
||||||
http::header::HeaderValue::from_static("public, max-age=31536000, immutable"),
|
|
||||||
))
|
|
||||||
.service(ServeDir::new("./hashed_static")),
|
|
||||||
)
|
|
||||||
.nest_service(
|
|
||||||
"/favicon.ico",
|
|
||||||
ServeFile::new(format!("./hashed_static/{}", asset!("favicon.ico"))),
|
|
||||||
)
|
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(tower_http::trace::TraceLayer::new_for_http())
|
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
@ -246,10 +231,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
println!("No update was made; probably something went wrong.");
|
println!("No update was made; probably something went wrong.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::SetEphemeral {
|
Some(Commands::SetEphemeral { username, ephemeral }) => {
|
||||||
username,
|
|
||||||
ephemeral,
|
|
||||||
}) => {
|
|
||||||
let users_db = {
|
let users_db = {
|
||||||
let db_options = SqliteConnectOptions::from_str("users.db")?
|
let db_options = SqliteConnectOptions::from_str("users.db")?
|
||||||
.create_if_missing(true)
|
.create_if_missing(true)
|
||||||
|
|
@ -260,24 +242,23 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
db
|
db
|
||||||
};
|
};
|
||||||
|
|
||||||
let eph: Option<bool> =
|
let eph: Option<bool> = sqlx::query_scalar(
|
||||||
sqlx::query_scalar("select ephemeral from users where username = ?")
|
"select ephemeral from users where username = ?"
|
||||||
.bind(&username)
|
)
|
||||||
.fetch_optional(&users_db)
|
.bind(&username)
|
||||||
.await?;
|
.fetch_optional(&users_db)
|
||||||
|
.await?;
|
||||||
if let Some(eph) = eph {
|
if let Some(eph) = eph {
|
||||||
if eph == *ephemeral {
|
if eph == *ephemeral {
|
||||||
println!(
|
println!("User {} is already {}.", username, if eph { "ephemeral" } else { "not ephemeral" });
|
||||||
"User {} is already {}.",
|
|
||||||
username,
|
|
||||||
if eph { "ephemeral" } else { "not ephemeral" }
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let update = sqlx::query("update users set ephemeral=$1 where username = $2")
|
let update = sqlx::query(
|
||||||
.bind(ephemeral)
|
"update users set ephemeral=$1 where username = $2",
|
||||||
.bind(&username)
|
)
|
||||||
.execute(&users_db)
|
.bind(ephemeral)
|
||||||
.await?;
|
.bind(&username)
|
||||||
|
.execute(&users_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if update.rows_affected() > 0 {
|
if update.rows_affected() > 0 {
|
||||||
println!("Updated ephemerality for {}.", username);
|
println!("Updated ephemerality for {}.", username);
|
||||||
|
|
@ -286,19 +267,13 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!("User {} does not exist. Create them first with set-password.", username);
|
||||||
"User {} does not exist. Create them first with set-password.",
|
|
||||||
username
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Some(Commands::Serve { port }) => {
|
Some(Commands::Serve { port }) => {
|
||||||
serve(port).await?;
|
serve(port).await?;
|
||||||
}
|
}
|
||||||
Some(Commands::Version) => {
|
|
||||||
println!("mascarpone v{}", env!("CARGO_PKG_VERSION"));
|
|
||||||
println!("from git commit {}", env!("VERGEN_GIT_SHA"));
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
serve(&3000).await?;
|
serve(&3000).await?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,28 +81,22 @@ mod get {
|
||||||
link rel="manifest" href=(format!("/static/{}", asset!("site.webmanifest")));
|
link rel="manifest" href=(format!("/static/{}", asset!("site.webmanifest")));
|
||||||
title { "Mascarpone CRM" }
|
title { "Mascarpone CRM" }
|
||||||
meta name="viewport" content="width=device-width";
|
meta name="viewport" content="width=device-width";
|
||||||
@if cfg!(debug_assertions) {
|
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {}
|
||||||
script src=(format!("/static/{}", asset!("htmx.min.js"))) defer {}
|
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {}
|
||||||
script src=(format!("/static/{}", asset!("htmx-ext-response-targets.js"))) defer {}
|
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
||||||
script src=(format!("/static/{}", asset!("alpinejs.min.js"))) defer {}
|
|
||||||
} @else {
|
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" defer {}
|
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" defer {}
|
|
||||||
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
|
||||||
}
|
|
||||||
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css")));
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css")));
|
||||||
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("login.css")));
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("login.css")));
|
||||||
title { "Mascarpone" }
|
title { "Mascarpone" }
|
||||||
}
|
}
|
||||||
body hx-ext="response-targets" {
|
body hx-ext="response-targets" {
|
||||||
h1 { "Mascarpone" }
|
h1 { "Mascarpone" }
|
||||||
form hx-post=(post_url) hx-target-error="#error" x-data="{ user: '', pass: '', htmx: false }" "x-on:htmx:load.document"="htmx = true" {
|
form hx-post=(post_url) hx-target-error="#error" x-data="{ user: '', pass: '' }" {
|
||||||
label for="username" { "Username" }
|
label for="username" { "Username" }
|
||||||
input name="username" #username autofocus x-model="user" x-cloak;
|
input name="username" #username autofocus x-model="user";
|
||||||
label for="password" { "Password" }
|
label for="password" { "Password" }
|
||||||
input name="password" #password type="password" x-model="pass" x-cloak;
|
input name="password" #password type="password" x-model="pass";
|
||||||
|
|
||||||
input type="submit" value="login" x-bind:disabled="!(user.length && pass.length && htmx)" hx-disabled-elt;
|
input type="submit" value="login" x-bind:disabled="!(user.length && pass.length)";
|
||||||
#error {}
|
#error {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,20 +61,6 @@ fn human_delta(span: &jiff::Span) -> String {
|
||||||
mod get {
|
mod get {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn scroll_to(id: DbId) -> String {
|
|
||||||
format!(
|
|
||||||
"\
|
|
||||||
const top=document\
|
|
||||||
.getElementById('nav-link-{}')\
|
|
||||||
?.getBoundingClientRect()\
|
|
||||||
?.top;\
|
|
||||||
top&&document\
|
|
||||||
.getElementById('contacts-sidebar')\
|
|
||||||
.scrollTo({{top:top-window.innerHeight/2,left:0,behavior:'instant'}});",
|
|
||||||
id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn contact(
|
pub async fn contact(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -139,7 +125,7 @@ mod get {
|
||||||
html! {
|
html! {
|
||||||
a href=(format!("/contact/{}/edit", contact_id)) { "Edit" }
|
a href=(format!("/contact/{}/edit", contact_id)) { "Edit" }
|
||||||
|
|
||||||
div #fields x-init=(scroll_to(contact_id)) {
|
div id="fields" {
|
||||||
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
||||||
div {
|
div {
|
||||||
@for name in &contact.names {
|
@for name in &contact.names {
|
||||||
|
|
@ -263,7 +249,7 @@ mod get {
|
||||||
div #error;
|
div #error;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fields x-data=(json!({ "status": contact.status() })) x-init=(scroll_to(contact_id)) x-cloak {
|
#fields x-data=(json!({ "status": contact.status() })){
|
||||||
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
||||||
div #names x-data=(format!("{{ names: {:?}, new_name: '' }}", &contact.names)) {
|
div #names x-data=(format!("{{ names: {:?}, new_name: '' }}", &contact.names)) {
|
||||||
template x-for="(name, idx) in names" {
|
template x-for="(name, idx) in names" {
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ pub async fn journal_section(
|
||||||
are now, or leave everything blank to default to 'today'. Entries will be
|
are now, or leave everything blank to default to 'today'. Entries will be
|
||||||
added to the top of the list regardless of date; refresh the page to re-sort."
|
added to the top of the list regardless of date; refresh the page to re-sort."
|
||||||
}
|
}
|
||||||
form hx-post="/journal_entry" hx-target="next .entries" hx-target-error="#journal-error" hx-swap="afterbegin" hx-on::after-request="if(event.detail.successful) this.reset()" x-cloak {
|
form hx-post="/journal_entry" hx-target="next .entries" hx-target-error="#journal-error" hx-swap="afterbegin" hx-on::after-request="if(event.detail.successful) this.reset()" {
|
||||||
input name="date" placeholder=(Zoned::now().date().to_string());
|
input name="date" placeholder=(Zoned::now().date().to_string());
|
||||||
textarea name="value" placeholder="New entry..." autofocus {}
|
textarea name="value" placeholder="New entry..." autofocus {}
|
||||||
input type="submit" value="Add Entry";
|
input type="submit" value="Add Entry";
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ impl FromRequestParts<AppState> for Layout {
|
||||||
left join names n on c.id = n.contact_id
|
left join names n on c.id = n.contact_id
|
||||||
where n.sort is null or n.sort = 0
|
where n.sort is null or n.sort = 0
|
||||||
and c.active = true
|
and c.active = true
|
||||||
order by name collate nocase asc",
|
order by name asc",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.db(&user).pool)
|
.fetch_all(&state.db(&user).pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -63,7 +63,7 @@ impl FromRequestParts<AppState> for Layout {
|
||||||
left join names n on c.id = n.contact_id
|
left join names n on c.id = n.contact_id
|
||||||
where n.sort is null or n.sort = 0
|
where n.sort is null or n.sort = 0
|
||||||
and c.active = false
|
and c.active = false
|
||||||
order by name collate nocase asc",
|
order by name asc",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.db(&user).pool)
|
.fetch_all(&state.db(&user).pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -94,15 +94,9 @@ impl Layout {
|
||||||
link rel="manifest" href=(format!("/static/{}", asset!("site.webmanifest")));
|
link rel="manifest" href=(format!("/static/{}", asset!("site.webmanifest")));
|
||||||
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css")));
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css")));
|
||||||
meta name="viewport" content="width=device-width";
|
meta name="viewport" content="width=device-width";
|
||||||
@if cfg!(debug_assertions) {
|
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {}
|
||||||
script src=(format!("/static/{}", asset!("htmx.min.js"))) defer {}
|
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {}
|
||||||
script src=(format!("/static/{}", asset!("htmx-ext-response-targets.js"))) defer {}
|
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
||||||
script src=(format!("/static/{}", asset!("alpinejs.min.js"))) defer {}
|
|
||||||
} @else {
|
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" defer {}
|
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" defer {}
|
|
||||||
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
|
||||||
}
|
|
||||||
@if let Some(hrefs) = css {
|
@if let Some(hrefs) = css {
|
||||||
@for href in hrefs {
|
@for href in hrefs {
|
||||||
link rel="stylesheet" type="text/css" href=(format!("/static/{}", href));
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", href));
|
||||||
|
|
@ -118,12 +112,12 @@ impl Layout {
|
||||||
a href="/logout" { "Logout" }
|
a href="/logout" { "Logout" }
|
||||||
}
|
}
|
||||||
section #content {
|
section #content {
|
||||||
nav #contacts-sidebar x-bind:class="sidebar ? 'show' : 'hide'" "x-on:click.self"="sidebar = !sidebar" {
|
nav #contacts-sidebar x-bind:class="sidebar ? 'show' : 'hide'" {
|
||||||
ul {
|
ul {
|
||||||
li { button hx-post="/contact/new" { "+ Add Contact" } }
|
li { button hx-post="/contact/new" { "+ Add Contact" } }
|
||||||
@for link in &self.contact_links {
|
@for link in &self.contact_links {
|
||||||
li {
|
li {
|
||||||
a id=(format!("nav-link-{}", link.contact_id)) href=(format!("/contact/{}", link.contact_id)) {
|
a href=(format!("/contact/{}", link.contact_id)) {
|
||||||
(link.name)
|
(link.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +145,7 @@ impl Layout {
|
||||||
(content)
|
(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
template #alpine-loaded x-cloak {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
static/alpinejs.min.js
vendored
5
static/alpinejs.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
(function(){var f;var o="hx-target-";function g(e,r){return e.substring(0,r.length)===r}function s(e,r){if(!e||!r)return null;var t=r.toString();var s=[t,t.substring(0,2)+"*",t.substring(0,2)+"x",t.substring(0,1)+"*",t.substring(0,1)+"x",t.substring(0,1)+"**",t.substring(0,1)+"xx","*","x","***","xxx"];if(g(t,"4")||g(t,"5")){s.push("error")}for(var n=0;n<s.length;n++){var i=o+s[n];var a=f.getClosestAttributeValue(e,i);if(a){if(a==="this"){return f.findThisElement(e,i)}else{return f.querySelectorExt(e,a)}}}return null}function n(e){if(e.detail.isError){if(htmx.config.responseTargetUnsetsError){e.detail.isError=false}}else if(htmx.config.responseTargetSetsError){e.detail.isError=true}}htmx.defineExtension("response-targets",{init:function(e){f=e;if(htmx.config.responseTargetUnsetsError===undefined){htmx.config.responseTargetUnsetsError=true}if(htmx.config.responseTargetSetsError===undefined){htmx.config.responseTargetSetsError=false}if(htmx.config.responseTargetPrefersExisting===undefined){htmx.config.responseTargetPrefersExisting=false}if(htmx.config.responseTargetPrefersRetargetHeader===undefined){htmx.config.responseTargetPrefersRetargetHeader=true}},onEvent:function(e,r){if(e==="htmx:beforeSwap"&&r.detail.xhr&&r.detail.xhr.status!==200){if(r.detail.target){if(htmx.config.responseTargetPrefersExisting){r.detail.shouldSwap=true;n(r);return true}if(htmx.config.responseTargetPrefersRetargetHeader&&r.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)){r.detail.shouldSwap=true;n(r);return true}}if(!r.detail.requestConfig){return true}var t=s(r.detail.requestConfig.elt,r.detail.xhr.status);if(t){n(r);r.detail.shouldSwap=true;r.detail.target=t}return true}}})})();
|
|
||||||
1
static/htmx.min.js
vendored
1
static/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -57,8 +57,7 @@ section#content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.hide {
|
&.hide {
|
||||||
left: -200%;
|
display: none;
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,5 +106,3 @@ a, a:visited {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
text-decoration: underline dotted;
|
text-decoration: underline dotted;
|
||||||
}
|
}
|
||||||
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue