Outings: vote on a lunch together, live over SSE

7def6a5 · Gabriel Morell · 2026-10-28 18:07

41 files +1424 -83
Message
{commit_body(@commit)}

Files changed

modified Cargo.toml
+1 −0
@@ -22,6 +22,7 @@ axum = "0.8.5"
22 22 axum-htmx = { version = "0.7.0", features = ["tokio"] }
23 23 tower-http = { version = "0.6", features = ["fs", "trace"] }
24 24 tokio = { version = "1", features = ["full"] }
25 +tokio-stream = { version = "0.1", features = ["sync"] }
25 26
26 27 # Templating
27 28 minijinja = { version = "2.10", features = ["loader", "builtins", "json", "urlencode"] }
modified README.md
+25 −2
@@ -67,6 +67,28 @@ Registration is invite-only by default. Set `REGISTRATION_ENABLED=true` to let
67 67 anyone with a valid invite token self-serve, or keep it off and hand out links
68 68 from the in-app **Invites** page.
69 69
70 +## Deciding together: outings
71 +
72 +"Start a vote" on an area's planner opens an **outing**: a ballot of a few
73 +options (3 by default) for today. Share the link; people join, and vote for
74 +**every** option they'd happily go to (approval voting). The most-voted option
75 +is marked as leading, and anyone can lock one in with "Go here", which logs
76 +the lunch for everyone going.
77 +
78 +**Re-roll keeps every option that has a vote** and swaps the rest for places
79 +this outing hasn't shown yet, re-drawn for whoever is going now. Only when an
80 +area runs out of fresh places does it reach back for ones rolled away earlier.
81 +If someone who joined after a draw ranked one of the options "never", it's
82 +flagged rather than silently swapped out.
83 +
84 +Outing pages update live over SSE. Each change runs `pg_notify('outing_changed',
85 +<id>)`; every replica `LISTEN`s and passes the id to its open streams, and the
86 +page re-fetches its panel when it gets the event. Because it goes through
87 +Postgres, this works across any number of pods. A 30 s poll covers a dropped
88 +stream. If the app sits behind ingress-nginx, the `X-Accel-Buffering: no`
89 +header the stream sends stops events being buffered; long-lived connections
90 +may also need `proxy-read-timeout` raised past the 15 s keep-alive.
91 +
70 92 ## How suggestions work
71 93
72 94 All in `src/suggest.rs`, pure functions with unit tests. For the people going:
@@ -139,11 +161,12 @@ src/
139 161 bin/ webservice, registration-token
140 162 config.rs env-driven configuration
141 163 lib.rs AppState + minijinja environment
142 − entities/ sea-orm models (auth, areas, restaurants, rankings, lunches)
164 + entities/ sea-orm models (auth, areas, restaurants, rankings, lunches, outings)
143 165 auth/ password hashing, sessions, invite tokens, user ops
144 166 middleware/ cookie-session auth layers
145 − handlers/ axum handlers (auth, areas, restaurants, places, lunch)
167 + handlers/ axum handlers (auth, areas, restaurants, places, lunch, outings)
146 168 osm.rs Photon + POI client
169 + live.rs LISTEN/NOTIFY → SSE relay for outings
147 170 suggest.rs scoring and planning
148 171 migration/ sea-orm migrations
149 172 templates/ minijinja templates (excluded from generation templating)
added assets/f/js/htmx-global.js
+7 −0
@@ -0,0 +1,7 @@
1 +// htmx extensions (htmx-ext-sse) look for a global `htmx` when they load, and
2 +// ES imports run before any statement in the importing module. Setting the
3 +// global in its own module, imported first, is what orders it ahead of them.
4 +import htmx from "htmx.org";
5 +
6 +window.htmx = htmx;
7 +export default htmx;
modified assets/f/js/src.js
+3 −3
@@ -1,11 +1,11 @@
1 1 // Bundled to f/js/app.js by esbuild (see the Justfile). Importing htmx here
2 2 // registers it globally and processes hx-* attributes on load, which is what
3 3 // powers hx-boost navigation and the partial updates (ranks, plans, lookups).
4 −import htmx from "htmx.org";
4 +// The SSE extension drives live outing updates (see templates/outings/).
5 +import "./htmx-global.js";
6 +import "htmx-ext-sse";
5 7 import { installMaps } from "./map.js";
6 8
7 −// map.js and inline handlers use the global; esbuild's bundle doesn't set it.
8 −window.htmx = htmx;
9 9 installMaps();
10 10
11 11 // Small toast helper available as window.toast(msg, {kind}) for ad-hoc use.
modified assets/package-lock.json
+6 −0
@@ -10,6 +10,7 @@
10 10 "@fortawesome/fontawesome-free": "^6.6.0",
11 11 "@tailwindcss/forms": "^0.5.10",
12 12 "daisyui": "^4.12.24",
13 + "htmx-ext-sse": "^2.2.2",
13 14 "htmx.org": "^2.0.4",
14 15 "leaflet": "^1.9.4",
15 16 "tailwindcss": "^3.4.17"
@@ -881,6 +882,11 @@
881 882 "node": ">= 0.4"
882 883 }
883 884 },
885 + "node_modules/htmx-ext-sse": {
886 + "version": "2.2.2",
887 + "resolved": "https://registry.npmjs.org/htmx-ext-sse/-/htmx-ext-sse-2.2.2.tgz",
888 + "integrity": "sha512-MTnKkBzA2t4sI8gOXrRiPaceTlkUbrw3+3qOy1BfuBNIPBalsJiT4qxUGd6W48ggOkfe2akOnB8uxICJKw+Dsg=="
889 + },
884 890 "node_modules/htmx.org": {
885 891 "version": "2.0.10",
886 892 "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
modified assets/package.json
+1 −0

Click to load diff…

modified migration/src/lib.rs
+2 −0

Click to load diff…

added migration/src/m20261028_000002_create_outings.rs
+240 −0

Click to load diff…

modified src/bin/webservice.rs
+16 −0

Click to load diff…

modified src/entities/mod.rs
+6 −1

Click to load diff…

added src/entities/outing_attendees.rs
+16 −0

Click to load diff…

added src/entities/outing_options.rs
+23 −0

Click to load diff…

added src/entities/outing_votes.rs
+19 −0

Click to load diff…

added src/entities/outings.rs
+52 −0

Click to load diff…

modified src/entities/prelude.rs
+4 −0

Click to load diff…

modified src/handlers/areas.rs
+3 −0

Click to load diff…

modified src/handlers/lunch.rs
+84 −73

Click to load diff…

modified src/handlers/mod.rs
+19 −0

Click to load diff…

added src/handlers/outings.rs
+551 −0

Click to load diff…

modified src/lib.rs
+2 −0

Click to load diff…

added src/live.rs
+86 −0

Click to load diff…

modified src/suggest.rs
+40 −0

Click to load diff…

modified templates/areas/show.html
+11 −0

Click to load diff…

modified templates/lunch/plan.html
+16 −3

Click to load diff…

added templates/outings/_panel.html
+105 −0

Click to load diff…

added templates/outings/show.html
+25 −0

Click to load diff…

modified tests/templates.rs
+61 −1

Click to load diff…

Parents: 25c4237