Opening hours, cuisine pages, area rename, I-went date

6dd7389 · Gabriel Morell · 2026-10-28 20:32

43 files +850 -84
Message
{commit_body(@commit)}

Files changed

modified Cargo.toml
+1 −0
@@ -50,6 +50,7 @@ migration = { path = "./migration" }
50 50 # OSM APIs (Photon geocoder + POI lookup)
51 51 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
52 52 rand = "0.9"
53 +opening-hours = { version = "2.1.4", default-features = false }
53 54
54 55 # Auth
55 56 argon2 = { version = "0.5", features = ["std"] }
modified README.md
+14 −0
@@ -98,6 +98,10 @@ All in `src/suggest.rs`, pure functions with unit tests. For the people going:
98 98 Neutral 1. Obsessed records when it started; picking it again keeps that
99 99 date, and it can be backdated from the rank widget.
100 100 Unranked counts as Neutral, so nobody has to rank everything.
101 +- **Open.** If OSM says a place is closed at the meal time (noon by
102 + default; the planner and votes take a time), it's out. Unknown or
103 + unreadable hours count as open. Parsed with the `opening-hours` crate at
104 + naive local time, so trip areas in other time zones need no tz handling.
101 105 - **Freshness.** `1 / (1 + 3 × load)`, where load sums every visit in the last
102 106 70 days, each decayed by a 10-day half-life. Yesterday's spot keeps about a
103 107 quarter of its weight; one from a month ago is mostly forgiven.
@@ -132,6 +136,16 @@ when `/poi/nearby` is timing out), then the public OSM API (`OSM_API_URL`) for
132 136 anything the tile server's import predates. Picking a candidate copies them
133 137 into the form. Places OSM has no `cuisine` tag for stay blank to type in.
134 138
139 +Restaurants added before hours were tracked can pick them up with
140 +**Refresh from OSM** on the restaurant page, or for every OSM-linked place at
141 +once under **Manage** on the area page. Refresh replaces hours (they change)
142 +but only fills cuisine, website and address where blank.
143 +
144 +**Cuisines** (`/cuisines`, optionally `?area=`) lists every cuisine tag with
145 +counts; each tag's page shows every matching restaurant grouped by area, with
146 +your rank and whether it's open at noon today. Cuisine tags link there
147 +everywhere they appear.
148 +
135 149 Anything OSM doesn't know about can be added by dropping a pin and typing the
136 150 name. Adding an OSM place that's already here just records your rank on the
137 151 existing one.
modified migration/src/lib.rs
+2 −0
@@ -4,6 +4,7 @@ mod m20240101_000001_create_auth_tables;
4 4 mod m20261028_000001_create_lunch_tables;
5 5 mod m20261028_000002_create_outings;
6 6 mod m20261028_000003_obsessed;
7 +mod m20261028_000004_opening_hours;
7 8
8 9 pub struct Migrator;
9 10
@@ -15,6 +16,7 @@ impl MigratorTrait for Migrator {
15 16 Box::new(m20261028_000001_create_lunch_tables::Migration),
16 17 Box::new(m20261028_000002_create_outings::Migration),
17 18 Box::new(m20261028_000003_obsessed::Migration),
19 + Box::new(m20261028_000004_opening_hours::Migration),
18 20 ]
19 21 }
20 22 }
added migration/src/m20261028_000004_opening_hours.rs
+50 −0
@@ -0,0 +1,50 @@
1 +//! Opening hours (the raw OSM `opening_hours` expression) on restaurants, and
2 +//! the time an outing is for, so suggestions can skip places that are closed
3 +//! when you'd arrive.
4 +use sea_orm_migration::prelude::*;
5 +
6 +#[derive(DeriveMigrationName)]
7 +pub struct Migration;
8 +
9 +#[async_trait::async_trait]
10 +impl MigrationTrait for Migration {
11 + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
12 + manager
13 + .alter_table(
14 + Table::alter()
15 + .table(Restaurants::Table)
16 + .add_column(ColumnDef::new(Restaurants::OpeningHours).string_len(512).null())
17 + .to_owned(),
18 + )
19 + .await?;
20 + manager
21 + .alter_table(
22 + Table::alter()
23 + .table(Outings::Table)
24 + .add_column(ColumnDef::new(Outings::MealTime).time().not_null().default("12:00:00"))
25 + .to_owned(),
26 + )
27 + .await
28 + }
29 +
30 + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
31 + manager
32 + .alter_table(Table::alter().table(Outings::Table).drop_column(Outings::MealTime).to_owned())
33 + .await?;
34 + manager
35 + .alter_table(Table::alter().table(Restaurants::Table).drop_column(Restaurants::OpeningHours).to_owned())
36 + .await
37 + }
38 +}
39 +
40 +#[derive(DeriveIden)]
41 +enum Restaurants {
42 + Table,
43 + OpeningHours,
44 +}
45 +
46 +#[derive(DeriveIden)]
47 +enum Outings {
48 + Table,
49 + MealTime,
50 +}
modified src/bin/webservice.rs
+5 −0
@@ -83,6 +83,10 @@ async fn main() -> anyhow::Result<()> {
83 83 get(handlers::lunch::show_planner).post(handlers::lunch::make_plan),
84 84 )
85 85 .route("/areas/{id}/history", get(handlers::lunch::show_history))
86 + .route("/areas/{id}/rename", post(handlers::areas::rename_area))
87 + .route("/areas/{id}/refresh", post(handlers::areas::refresh_area))
88 + .route("/cuisines", get(handlers::cuisines::list_cuisines))
89 + .route("/cuisines/{name}", get(handlers::cuisines::show_cuisine))
86 90 .route("/areas/{id}/outings", post(handlers::outings::create_outing))
87 91 .route("/outings/{id}", get(handlers::outings::show_outing))
88 92 .route("/outings/{id}/panel", get(handlers::outings::panel))
@@ -102,6 +106,7 @@ async fn main() -> anyhow::Result<()> {
102 106 )
103 107 .route("/restaurants/{id}/rank", post(handlers::restaurants::set_rank))
104 108 .route("/restaurants/{id}/archive", post(handlers::restaurants::toggle_archive))
109 + .route("/restaurants/{id}/refresh", post(handlers::restaurants::refresh_restaurant))
105 110 .route("/lunches", post(handlers::lunch::log_lunch))
106 111 .route("/lunches/{id}/delete", post(handlers::lunch::delete_lunch))
107 112 .layer(axum::middleware::from_fn_with_state(db.clone(), web_auth_middleware));
modified src/entities/outings.rs
+2 −0

Click to load diff…

modified src/entities/restaurants.rs
+20 −0

Click to load diff…

modified src/handlers/areas.rs
+69 −1

Click to load diff…

added src/handlers/cuisines.rs
+155 −0

Click to load diff…

modified src/handlers/lunch.rs
+12 −2

Click to load diff…

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

Click to load diff…

modified src/handlers/outings.rs
+3 −2

Click to load diff…

modified src/handlers/restaurants.rs
+67 −0

Click to load diff…

added src/hours.rs
+83 −0

Click to load diff…

modified src/lib.rs
+1 −0

Click to load diff…

modified src/osm.rs
+75 −43

Click to load diff…

modified src/suggest.rs
+61 −21

Click to load diff…

modified templates/_macros.html
+16 −0

Click to load diff…

modified templates/areas/show.html
+32 −2

Click to load diff…

added templates/cuisines/list.html
+27 −0

Click to load diff…

added templates/cuisines/show.html
+44 −0

Click to load diff…

modified templates/lunch/_plan.html
+9 −5

Click to load diff…

modified templates/lunch/plan.html
+6 −1

Click to load diff…

modified templates/outings/_panel.html
+1 −1

Click to load diff…

modified templates/outings/show.html
+1 −1

Click to load diff…

modified templates/places/_candidates.html
+1 −0

Click to load diff…

modified templates/restaurants/new.html
+4 −0

Click to load diff…

modified templates/restaurants/show.html
+19 −3

Click to load diff…

modified tests/templates.rs
+60 −2

Click to load diff…

Parents: 2475dea