Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a1b8ac4
add tick, a mechanical watch timegrapher at /tick, with a rate graph,…
jaemk Oct 10, 2026
ae4d75b
detect quieter ticks in tick, ignore knocks, add sensitivity and an i…
jaemk Oct 10, 2026
51468a7
check tick sensitivity and input meter in the acceptance suite
jaemk Oct 10, 2026
4f4511c
show a measuring state on the tick rate graph until the averaging win…
jaemk Oct 10, 2026
49c9834
gate tick triggers around the next beat, choose the beat rate by phas…
jaemk Oct 10, 2026
f75b2e1
revert tick trigger gating and phase-folded beat rate, which lost the…
jaemk Oct 10, 2026
e9eb8f1
add a tick record button that saves 30 s of raw mic input as a float wav
jaemk Oct 10, 2026
257ae34
ignore tick audio blocks that arrive after stop
jaemk Oct 10, 2026
fdef9c2
show tick status listening until a beat is heard, rename the cfd leve…
jaemk Oct 10, 2026
0263437
show a tick save link for a finished recording instead of an unprompt…
jaemk Oct 10, 2026
cb67f64
index tick beats by the fit, keep the better onset per beat, refit un…
jaemk Oct 10, 2026
05cf8f8
test the tick detector against quiet and noisy recordings of a real 1…
jaemk Oct 10, 2026
0bbfab6
fix tick ticks ignored as knocks once quieter clicks fill the beat pe…
jaemk Oct 10, 2026
724d7bd
add tick offline analysis: record 30 s from idle then analyze, analyz…
jaemk Oct 10, 2026
a5a0898
add tick info buttons explaining each graph and value in a dialog
jaemk Oct 10, 2026
2603ebc
add tick positions table, big readout, target band, graph cursor, wak…
jaemk Oct 10, 2026
0da2123
merge tick start and stop into one run button, rename big readout to …
jaemk Oct 10, 2026
a051315
add tick beat rate picker with hold and doubt, show status and beat r…
jaemk Oct 10, 2026
ce6c5b6
fix tick target band test comparing the rounded rate, offer tick beat…
jaemk Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,19 @@ jobs:

web:
runs-on: ubuntu-latest
strategy:
matrix:
crate: [komino, tick]
defaults:
run:
working-directory: crates/komino/web
working-directory: crates/${{ matrix.crate }}/web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: crates/komino/web/package-lock.json
cache-dependency-path: crates/${{ matrix.crate }}/web/package-lock.json
- run: npm ci
- run: npm run coverage

Expand Down
10 changes: 5 additions & 5 deletions AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
- `cd` is aliased in this shell — always use `builtin cd` instead

## Project
- Rust workspace with six crates: `crates/common`, `crates/spot`, `crates/mono`, `crates/paste`, `crates/mapour`, `crates/komino`
- Rust workspace with seven crates: `crates/common`, `crates/spot`, `crates/mono`, `crates/paste`, `crates/mapour`, `crates/komino`, `crates/tick`
- Feature specs live in `spec/`; see `spec/README.md` for status
- Main binary is `mono` in `crates/mono`

Expand All @@ -30,14 +30,14 @@ make test
```
`make test` needs no setup beyond docker and cargo: `bin/test-db.sh` uses a postgres
reachable without a password (as in CI) or starts a throwaway `postgres:16` container, and
`bin/test-js.sh` runs the komino client tests (`crates/komino/web`) with node from PATH or
nvm, or in a `node:24` container. `make test-rust` and `make test-js` run either half.
`bin/test-js.sh` runs the komino and tick client tests (`crates/{komino,tick}/web`) with node
from PATH or nvm, or in a `node:24` container. `make test-rust` and `make test-js` run either half.

## CI
`.github/workflows/ci.yml` runs on every PR and push to main: fmt and clippy, `cargo audit
--deny warnings` (ignores in `.cargo/audit.toml`), the full `bin/test-db.sh` suite against
postgres plus a RustFS S3 store (so S3-backed tests run), the komino client tests with
coverage thresholds, and a Docker image build. Deploys are still manual.
postgres plus a RustFS S3 store (so S3-backed tests run), the komino and tick client tests
with coverage thresholds, and a Docker image build. Deploys are still manual.

After a deploy, `make acceptance` (or the manual `acceptance` workflow) runs
`acceptance/live.test.mjs` against the live site: every mounted app, a paste round trip,
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"crates/paste",
"crates/mapour",
"crates/komino",
"crates/tick",
]
resolver = "2"

Expand Down Expand Up @@ -59,3 +60,4 @@ spot = { path = "crates/spot" }
paste = { path = "crates/paste" }
mapour = { path = "crates/mapour" }
komino = { path = "crates/komino" }
tick = { path = "crates/tick" }
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.95.0-bookworm as builder

Check warning on line 1 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/

RUN apt-get update && apt-get install --yes ca-certificates curl pkg-config libssl-dev libpq-dev
RUN cargo install migrant --features postgres
Expand All @@ -17,6 +17,7 @@
COPY crates/paste/Cargo.toml crates/paste/Cargo.toml
COPY crates/mapour/Cargo.toml crates/mapour/Cargo.toml
COPY crates/komino/Cargo.toml crates/komino/Cargo.toml
COPY crates/tick/Cargo.toml crates/tick/Cargo.toml

# Generate stub source files for every workspace member so cargo can
# compile and cache all third-party dependencies without the real source.
Expand All @@ -28,7 +29,7 @@
RUN rm -f target/release/deps/mono* target/release/mono \
target/release/deps/libcommon* target/release/deps/libspot* \
target/release/deps/libpaste* target/release/deps/libmapour* \
target/release/deps/libkomino*
target/release/deps/libkomino* target/release/deps/libtick*

# Now copy the real source code and build
COPY . .
Expand All @@ -51,6 +52,7 @@
COPY --from=builder /app/crates/paste/templates ./crates/paste/templates
COPY --from=builder /app/crates/mapour/assets ./crates/mapour/assets
COPY --from=builder /app/crates/komino/assets ./crates/komino/assets
COPY --from=builder /app/crates/tick/assets ./crates/tick/assets
COPY --from=builder /app/commit_hash.txt ./commit_hash.txt
COPY --from=builder /app/migrations ./migrations
COPY --from=builder /app/bin ./bin
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test: test-rust test-js
test-rust:
./bin/test-db.sh

# komino client tests (jsdom) with CI's coverage thresholds
# komino and tick client tests (jsdom) with CI's coverage thresholds
test-js:
./bin/test-js.sh

Expand Down
30 changes: 30 additions & 0 deletions acceptance/live.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,36 @@ test("flip serves the page, flips, and keeps odds and coin per cookie", async ()
);
});

// ---------------------------------------------------------------------------
// tick (static: the page and its client scripts)
// ---------------------------------------------------------------------------

test("tick serves the page and its scripts", async () => {
const c = new Client();
const page = await expectStatus(await c.get("/tick"), 200, "tick page");
assert.match(page, /<title>tick<\/title>/);
// the sensitivity setting and the input meter
for (const id of ["set-sensitivity", "meter-level", "meter-threshold", "meter-note", "record", "save", "open", "file", "rate-label", "info", "info-close", "run", "sim", "focus", "focus-view", "focus-run", "focus-settings", "set-target", "session-add", "session-body", "drop", "settings-box", "bph", "focus-bph", "focus-detail", "focus-note", "bph-dialog", "bph-choices", "bph-close"]) {
assert.ok(page.includes(`id="${id}"`), `tick page has #${id}`);
}
expectRedirect(await c.get("/tick/"), "/tick", "tick trailing slash");
for (const [path, marker] of [
["/tick/static/detector.js", "SENSITIVITY"],
// the measuring state while the averaging window fills
["/tick/static/app.js", "ticks heard, finding the beat rate"],
["/tick/static/worklet.js", "registerProcessor"],
["/tick/static/app.css", "--phosphor"],
]) {
const resp = await c.get(path);
const body = await expectStatus(resp, 200, path);
assert.ok(body.includes(marker), `${path} should contain ${marker}`);
}
// the worklet must load as javascript
const worklet = await c.get("/tick/static/worklet.js");
await worklet.text();
assert.match(worklet.headers.get("content-type"), /javascript/);
});

// ---------------------------------------------------------------------------
// mapour (mounted only when MAPOUR_ENABLED=true)
// ---------------------------------------------------------------------------
Expand Down
29 changes: 17 additions & 12 deletions bin/test-js.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
#!/usr/bin/env bash
# bin/test-js.sh
#
# Run the komino client tests (jsdom) with coverage thresholds, as CI does.
# Run the client tests (jsdom) with coverage thresholds, as CI does, for each
# crate with a web/ suite: komino and tick.
#
# Finds node on PATH, then under nvm (~/.nvm), and otherwise runs the suite
# Finds node on PATH, then under nvm (~/.nvm), and otherwise runs the suites
# in a throwaway node docker container, so no manual setup is needed.
# Override the container image with TEST_NODE_IMAGE (default node:24).

set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WEB="${ROOT}/crates/komino/web"
CRATES=(komino tick)

if ! command -v npm >/dev/null 2>&1 && [ -s "${NVM_DIR:-${HOME}/.nvm}/nvm.sh" ]; then
# shellcheck disable=SC1091
Expand All @@ -19,17 +20,21 @@ if ! command -v npm >/dev/null 2>&1 && [ -s "${NVM_DIR:-${HOME}/.nvm}/nvm.sh" ];
fi

if command -v npm >/dev/null 2>&1; then
echo "==> Running komino client tests with $(command -v node) ($(node --version))"
builtin cd "${WEB}"
npm ci --no-audit --no-fund --loglevel=error
npm run coverage
for crate in "${CRATES[@]}"; do
echo "==> Running ${crate} client tests with $(command -v node) ($(node --version))"
builtin cd "${ROOT}/crates/${crate}/web"
npm ci --no-audit --no-fund --loglevel=error
npm run coverage
done
elif command -v docker >/dev/null 2>&1; then
image="${TEST_NODE_IMAGE:-node:24}"
echo "==> No node found; running komino client tests in '${image}'"
# the assets the tests load sit beside web/, so mount the whole crate;
# node_modules stays inside the container
docker run --rm -v "${ROOT}/crates/komino:/komino:ro" -w /komino/web "${image}" \
sh -c 'cp -r /komino /tmp/komino && cd /tmp/komino/web && npm ci --no-audit --no-fund --loglevel=error && npm run coverage'
for crate in "${CRATES[@]}"; do
echo "==> No node found; running ${crate} client tests in '${image}'"
# the assets the tests load sit beside web/, so mount the whole crate;
# node_modules stays inside the container
docker run --rm -v "${ROOT}/crates/${crate}:/crate:ro" -w /crate/web "${image}" \
sh -c 'cp -r /crate /tmp/crate && cd /tmp/crate/web && npm ci --no-audit --no-fund --loglevel=error && npm run coverage'
done
else
echo "ERROR: node is not installed (on PATH or via nvm) and docker is not" >&2
echo " available to run it in a container." >&2
Expand Down
1 change: 1 addition & 0 deletions crates/mono/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ spot.workspace = true
paste.workspace = true
mapour.workspace = true
komino.workspace = true
tick.workspace = true
tokio.workspace = true
axum.workspace = true
axum-extra.workspace = true
Expand Down
5 changes: 5 additions & 0 deletions crates/mono/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ pub fn app(
let mut router = Router::new()
.nest("/spot", spot::service::router(state.clone()))
.nest("/paste", paste::service::router(state.clone()))
.nest("/tick", tick::router())
.route(
"/tick/",
get(|| async { axum::response::Redirect::permanent("/tick") }),
)
.merge(flip::router());
if state.mapour_state.is_some() {
router = router.nest("/mapour", mapour::service::router(state.clone()));
Expand Down
16 changes: 16 additions & 0 deletions crates/mono/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,22 @@ async fn test_komino_app_serves() {
assert_eq!(response.header("location"), "/komino");
}

#[tokio::test]
async fn test_tick_app_serves() {
let server = get_server().await;
let response = server.get("/tick").await;
response.assert_status_ok();
assert!(response.text().contains("<title>tick</title>"));

let response = server.get("/tick/").await;
response.assert_status(StatusCode::PERMANENT_REDIRECT);
assert_eq!(response.header("location"), "/tick");

let response = server.get("/tick/static/detector.js").await;
response.assert_status_ok();
assert!(response.text().contains("createDetector"));
}

/// With mapour disabled (state None) the router is not mounted and the rest
/// of the app still works.
#[tokio::test]
Expand Down
13 changes: 13 additions & 0 deletions crates/tick/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "tick"
version.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
axum.workspace = true
tower-http.workspace = true

[dev-dependencies]
axum-test.workspace = true
tokio.workspace = true
Loading
Loading