From 9443a04ad6e9e16bfb996b38eac3ae1c77c5f9fa Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 22 May 2026 15:33:22 +0200 Subject: [PATCH 1/3] feat(system): nginx access log --- module.nix | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/module.nix b/module.nix index 68b2833..53f77c6 100644 --- a/module.nix +++ b/module.nix @@ -123,6 +123,7 @@ in proxy_read_timeout 3600s; ''; withSSL = cfg.protocol == "https"; + listenPort = if withSSL then 443 else 80; in { "${cfg.hostname}" = { @@ -130,18 +131,19 @@ in forceSSL = withSSL; # Explicit listen so this vhost isn't shadowed by a default_server # created by other virtual hosts with forceSSL = true. - listen = - if withSSL then [ - { addr = "0.0.0.0"; port = 443; ssl = true; } - { addr = "[::]"; port = 443; ssl = true; } - ] else [ - { addr = "0.0.0.0"; port = 80; ssl = false; } - { addr = "[::]"; port = 80; ssl = false; } + listen = [ + { addr = "0.0.0.0"; port = listenPort; ssl = withSSL; } + { addr = "[::]"; port = listenPort; ssl = withSSL; } ]; locations."/" = { extraConfig = proxyConfig; proxyPass = "http://trictrac-api/"; }; + + extraConfig = '' + error_log /var/log/nginx/trictrac_error.log; + access_log /var/log/nginx/trictrac_access.log; + ''; }; }; }; From 25554126a89e613f279eb145f71776bccbb6b565 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 22 May 2026 16:54:37 +0200 Subject: [PATCH 2/3] feat(web client): date time format options --- clients/web/locales/fr.json | 2 +- clients/web/src/api.rs | 47 +++++++++++++++++++++++++-- clients/web/src/portal/game_detail.rs | 8 +++-- clients/web/src/portal/profile.rs | 14 ++++++-- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json index b3a05f0..569d66b 100644 --- a/clients/web/locales/fr.json +++ b/clients/web/locales/fr.json @@ -99,7 +99,7 @@ "resend_verification": "Renvoyer l'email de vérification", "verification_email_resent": "Email de vérification envoyé.", "loading": "Chargement…", - "member_since": "Membre depuis", + "member_since": "Membre depuis le", "stat_games": "Parties", "stat_wins": "Victoires", "stat_losses": "Défaites", diff --git a/clients/web/src/api.rs b/clients/web/src/api.rs index 9e0f57c..d826165 100644 --- a/clients/web/src/api.rs +++ b/clients/web/src/api.rs @@ -244,10 +244,53 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(), // ── Utilities ───────────────────────────────────────────────────────────────── -pub fn format_ts(ts: i64) -> String { +/// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`. +/// `Default` passes no options (browser default: full date + time). +pub struct DateFormatOptions { + /// "full" | "long" | "medium" | "short" — omit to suppress date part + pub date_style: Option<&'static str>, + /// "full" | "long" | "medium" | "short" — omit to suppress time part + pub time_style: Option<&'static str>, +} + +impl Default for DateFormatOptions { + fn default() -> Self { + Self { date_style: None, time_style: None } + } +} + +impl DateFormatOptions { + pub fn date_only() -> Self { + Self { date_style: Some("short"), time_style: None } + } + + pub fn time_only() -> Self { + Self { date_style: None, time_style: Some("short") } + } + + pub fn date_time() -> Self { + Self { date_style: Some("short"), time_style: Some("short") } + } + + fn to_js_value(&self) -> wasm_bindgen::JsValue { + if self.date_style.is_none() && self.time_style.is_none() { + return wasm_bindgen::JsValue::UNDEFINED; + } + let obj = js_sys::Object::new(); + if let Some(v) = self.date_style { + let _ = js_sys::Reflect::set(&obj, &"dateStyle".into(), &v.into()); + } + if let Some(v) = self.time_style { + let _ = js_sys::Reflect::set(&obj, &"timeStyle".into(), &v.into()); + } + obj.into() + } +} + +pub fn format_ts(ts: i64, locale: &str, opts: &DateFormatOptions) -> String { let ms = (ts * 1000) as f64; let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(ms)); - date.to_locale_string("en-GB", &wasm_bindgen::JsValue::UNDEFINED) + date.to_locale_string(locale, &opts.to_js_value()) .as_string() .unwrap_or_default() } diff --git a/clients/web/src/portal/game_detail.rs b/clients/web/src/portal/game_detail.rs index adc3643..d0d17d4 100644 --- a/clients/web/src/portal/game_detail.rs +++ b/clients/web/src/portal/game_detail.rs @@ -32,8 +32,12 @@ pub fn GameDetailPage() -> impl IntoView { #[component] fn GameDetailView(game: GameDetail) -> impl IntoView { let i18n = use_i18n(); - let started = api::format_ts(game.started_at); - let ended = game.ended_at.map(api::format_ts) + let locale_tag = match i18n.get_locale() { + Locale::en => "en-GB", + Locale::fr => "fr-FR", + }; + let started = api::format_ts(game.started_at, locale_tag, &api::DateFormatOptions::date_only()); + let ended = game.ended_at.map(|ts| api::format_ts(ts, locale_tag, &api::DateFormatOptions::date_only())) .unwrap_or_else(|| t_string!(i18n, game_ongoing).to_string()); view! { diff --git a/clients/web/src/portal/profile.rs b/clients/web/src/portal/profile.rs index 9a94b3f..bd3c9c2 100644 --- a/clients/web/src/portal/profile.rs +++ b/clients/web/src/portal/profile.rs @@ -37,7 +37,11 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView { async move { api::get_user_games(&u, p).await } }); - let joined = api::format_ts(profile.created_at); + let locale_tag = match i18n.get_locale() { + Locale::en => "en-GB", + Locale::fr => "fr-FR", + }; + let joined = api::format_ts(profile.created_at, locale_tag, &api::DateFormatOptions::date_only()); view! {
@@ -84,6 +88,10 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView { #[component] fn GamesTable(games: Vec, page: RwSignal) -> impl IntoView { let i18n = use_i18n(); + let locale_tag = match i18n.get_locale() { + Locale::en => "en-GB", + Locale::fr => "fr-FR", + }; let rows = games.clone(); let has_next = games.len() == 20; @@ -100,8 +108,8 @@ fn GamesTable(games: Vec, page: RwSignal) -> impl IntoView { {rows.into_iter().map(|g| { - let started = api::format_ts(g.started_at); - let ended = g.ended_at.map(api::format_ts).unwrap_or_else(|| "—".into()); + let started = api::format_ts(g.started_at, locale_tag, &api::DateFormatOptions::date_only()); + let ended = g.ended_at.map(|ts| api::format_ts(ts, locale_tag, &api::DateFormatOptions::date_only())).unwrap_or_else(|| "—".into()); let outcome_class = match g.outcome.as_deref() { Some("win") => "outcome-win", Some("loss") => "outcome-loss", From 4003fc0ef21b6d86aedd5a68e4c45728deeaaafa Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 22 May 2026 17:30:23 +0200 Subject: [PATCH 3/3] fix(web client): profile page without draws --- clients/web/assets/style.css | 2 +- clients/web/src/portal/profile.rs | 11 ++++++----- server/relay-server/src/main.rs | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index 428d693..e81e0de 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -161,7 +161,7 @@ body { /* ── Stats grid ──────────────────────────────────────────────────── */ .stats-grid { display: grid; - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1.5rem; } diff --git a/clients/web/src/portal/profile.rs b/clients/web/src/portal/profile.rs index bd3c9c2..c727bbd 100644 --- a/clients/web/src/portal/profile.rs +++ b/clients/web/src/portal/profile.rs @@ -41,7 +41,12 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView { Locale::en => "en-GB", Locale::fr => "fr-FR", }; - let joined = api::format_ts(profile.created_at, locale_tag, &api::DateFormatOptions::date_only()); + let date_format = api::DateFormatOptions { + date_style: Some("long"), + time_style: None, + }; + let joined = api::format_ts(profile.created_at, locale_tag, &date_format); + // let joined = api::format_ts(profile.created_at, locale_tag, &api::DateFormatOptions::date_only()); view! {
@@ -61,10 +66,6 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
{ profile.losses }
{t!(i18n, stat_losses)}
-
-
{ profile.draws }
-
{t!(i18n, stat_draws)}
-
diff --git a/server/relay-server/src/main.rs b/server/relay-server/src/main.rs index 0dfea0c..32baf70 100644 --- a/server/relay-server/src/main.rs +++ b/server/relay-server/src/main.rs @@ -99,9 +99,9 @@ async fn main() { .route("/ws", get(websocket_handler)) .merge(http::router()) .with_state(app_state) + .fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html"))) .layer(auth_layer) - .layer(cors) - .fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html"))); + .layer(cors); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await