feat(parametres): lien de création d'issue GitLab dans la carte CompteUnverified
Description préremplie dans la langue de l'utilisateur avec la version compilée et le commit déployé (COMMIT_ID de Clever Cloud, « dev » en local) ; la ligne v0.1.0 · commit s'affiche sous le lien.
16eb9ff parent: e722076 modified
src/config.rs +10 -0 | @@ -46,6 +46,16 @@ pub fn signup_key() -> Option<String> { | ||
| 46 | 46 | .filter(|k| !k.is_empty()) |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | +/// Commit déployé — `COMMIT_ID`, posée par Clever Cloud dans l'environnement | |
| 50 | +/// de l'app. Tronqué à 8 caractères ; « dev » en local (variable absente). | |
| 51 | +pub fn commit_id() -> String { | |
| 52 | + std::env::var("COMMIT_ID") | |
| 53 | + .ok() | |
| 54 | + .map(|c| c.trim().chars().take(8).collect::<String>()) | |
| 55 | + .filter(|c| !c.is_empty()) | |
| 56 | + .unwrap_or_else(|| "dev".to_string()) | |
| 57 | +} | |
| 58 | + | |
| 49 | 59 | pub fn biscuit_private_key_hex() -> Result<String> { |
| 50 | 60 | std::env::var("BISCUIT_PRIVATE_KEY") |
| 51 | 61 | .context("BISCUIT_PRIVATE_KEY doit être définie (générer avec `exego keygen`)") |
| @@ -46,6 +46,16 @@ pub fn signup_key() -> Option<String> { | |||
| 46 | .filter(|k| !k.is_empty()) | 46 | .filter(|k| !k.is_empty()) |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | +/// Commit déployé — `COMMIT_ID`, posée par Clever Cloud dans l'environnement | ||
| 50 | +/// de l'app. Tronqué à 8 caractères ; « dev » en local (variable absente). | ||
| 51 | +pub fn commit_id() -> String { | ||
| 52 | + std::env::var("COMMIT_ID") | ||
| 53 | + .ok() | ||
| 54 | + .map(|c| c.trim().chars().take(8).collect::<String>()) | ||
| 55 | + .filter(|c| !c.is_empty()) | ||
| 56 | + .unwrap_or_else(|| "dev".to_string()) | ||
| 57 | +} | ||
| 58 | + | ||
| 49 | pub fn biscuit_private_key_hex() -> Result<String> { | 59 | pub fn biscuit_private_key_hex() -> Result<String> { |
| 50 | std::env::var("BISCUIT_PRIVATE_KEY") | 60 | std::env::var("BISCUIT_PRIVATE_KEY") |
| 51 | .context("BISCUIT_PRIVATE_KEY doit être définie (générer avec `exego keygen`)") | 61 | .context("BISCUIT_PRIVATE_KEY doit être définie (générer avec `exego keygen`)") |
modified
src/routes/settings.rs +32 -0 | @@ -55,6 +55,25 @@ pub struct SettingsPage { | ||
| 55 | 55 | pub can_leave: bool, |
| 56 | 56 | /// Lien d'invitation fraîchement forgé, à copier (absent le reste du temps). |
| 57 | 57 | pub invite_link: Option<String>, |
| 58 | + /// Création d'issue GitLab préremplie (version + commit, langue de l'utilisateur). | |
| 59 | + pub feedback_href: String, | |
| 60 | + /// « v0.0.2 · bb2bb6b6 » — affiché sous le lien (aucun texte à traduire). | |
| 61 | + pub version_line: String, | |
| 62 | +} | |
| 63 | + | |
| 64 | +/// Percent-encoding d'un composant de query string (RFC 3986, unreserved | |
| 65 | +/// seuls) — évite une dépendance pour un unique lien. | |
| 66 | +fn urlencode(s: &str) -> String { | |
| 67 | + let mut out = String::with_capacity(s.len() * 3); | |
| 68 | + for b in s.bytes() { | |
| 69 | + match b { | |
| 70 | + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { | |
| 71 | + out.push(b as char) | |
| 72 | + } | |
| 73 | + _ => out.push_str(&format!("%{b:02X}")), | |
| 74 | + } | |
| 75 | + } | |
| 76 | + out | |
| 58 | 77 | } |
| 59 | 78 | |
| 60 | 79 | #[derive(Deserialize)] |
| @@ -172,6 +191,17 @@ pub async fn settings_page( | ||
| 172 | 191 | current: ctx.stats_rows, |
| 173 | 192 | }, |
| 174 | 193 | ]; |
| 194 | + | |
| 195 | + // Lien « créer une issue » : description préremplie (langue de | |
| 196 | + // l'utilisateur) portant version compilée + commit déployé. | |
| 197 | + let version = env!("CARGO_PKG_VERSION"); | |
| 198 | + let commit = crate::config::commit_id(); | |
| 199 | + let feedback_href = format!( | |
| 200 | + "https://gitlab.com/juuduu/ex-ego/-/issues/new?issue%5Bdescription%5D={}", | |
| 201 | + urlencode(&p2(tr.issue_body_pat, version, &commit)) | |
| 202 | + ); | |
| 203 | + let version_line = format!("v{version} · {commit}"); | |
| 204 | + | |
| 175 | 205 | Ok(HtmlTemplate(SettingsPage { |
| 176 | 206 | csrf: ctx.user.csrf_token(&state), |
| 177 | 207 | shell: shell(&state, &ctx, tr.title_settings, "settings", toast).await?, |
| @@ -188,6 +218,8 @@ pub async fn settings_page( | ||
| 188 | 218 | sent, |
| 189 | 219 | can_leave, |
| 190 | 220 | invite_link: q.lien.filter(|l| l.starts_with("http")), |
| 221 | + feedback_href, | |
| 222 | + version_line, | |
| 191 | 223 | })) |
| 192 | 224 | } |
| 193 | 225 | |
| @@ -55,6 +55,25 @@ pub struct SettingsPage { | |||
| 55 | pub can_leave: bool, | 55 | pub can_leave: bool, |
| 56 | /// Lien d'invitation fraîchement forgé, à copier (absent le reste du temps). | 56 | /// Lien d'invitation fraîchement forgé, à copier (absent le reste du temps). |
| 57 | pub invite_link: Option<String>, | 57 | pub invite_link: Option<String>, |
| 58 | + /// Création d'issue GitLab préremplie (version + commit, langue de l'utilisateur). | ||
| 59 | + pub feedback_href: String, | ||
| 60 | + /// « v0.0.2 · bb2bb6b6 » — affiché sous le lien (aucun texte à traduire). | ||
| 61 | + pub version_line: String, | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +/// Percent-encoding d'un composant de query string (RFC 3986, unreserved | ||
| 65 | +/// seuls) — évite une dépendance pour un unique lien. | ||
| 66 | +fn urlencode(s: &str) -> String { | ||
| 67 | + let mut out = String::with_capacity(s.len() * 3); | ||
| 68 | + for b in s.bytes() { | ||
| 69 | + match b { | ||
| 70 | + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { | ||
| 71 | + out.push(b as char) | ||
| 72 | + } | ||
| 73 | + _ => out.push_str(&format!("%{b:02X}")), | ||
| 74 | + } | ||
| 75 | + } | ||
| 76 | + out | ||
| 58 | } | 77 | } |
| 59 | 78 | ||
| 60 | #[derive(Deserialize)] | 79 | #[derive(Deserialize)] |
| @@ -172,6 +191,17 @@ pub async fn settings_page( | |||
| 172 | current: ctx.stats_rows, | 191 | current: ctx.stats_rows, |
| 173 | }, | 192 | }, |
| 174 | ]; | 193 | ]; |
| 194 | + | ||
| 195 | + // Lien « créer une issue » : description préremplie (langue de | ||
| 196 | + // l'utilisateur) portant version compilée + commit déployé. | ||
| 197 | + let version = env!("CARGO_PKG_VERSION"); | ||
| 198 | + let commit = crate::config::commit_id(); | ||
| 199 | + let feedback_href = format!( | ||
| 200 | + "https://gitlab.com/juuduu/ex-ego/-/issues/new?issue%5Bdescription%5D={}", | ||
| 201 | + urlencode(&p2(tr.issue_body_pat, version, &commit)) | ||
| 202 | + ); | ||
| 203 | + let version_line = format!("v{version} · {commit}"); | ||
| 204 | + | ||
| 175 | Ok(HtmlTemplate(SettingsPage { | 205 | Ok(HtmlTemplate(SettingsPage { |
| 176 | csrf: ctx.user.csrf_token(&state), | 206 | csrf: ctx.user.csrf_token(&state), |
| 177 | shell: shell(&state, &ctx, tr.title_settings, "settings", toast).await?, | 207 | shell: shell(&state, &ctx, tr.title_settings, "settings", toast).await?, |
| @@ -188,6 +218,8 @@ pub async fn settings_page( | |||
| 188 | sent, | 218 | sent, |
| 189 | can_leave, | 219 | can_leave, |
| 190 | invite_link: q.lien.filter(|l| l.starts_with("http")), | 220 | invite_link: q.lien.filter(|l| l.starts_with("http")), |
| 221 | + feedback_href, | ||
| 222 | + version_line, | ||
| 191 | })) | 223 | })) |
| 192 | } | 224 | } |
| 193 | 225 | ||
modified
templates/settings.html +4 -0 | @@ -26,6 +26,10 @@ | ||
| 26 | 26 | <div class="card-kicker">{{ shell.tr.account_kicker }}</div> |
| 27 | 27 | <div style="font-size:19px;font-family:var(--font-heading)">{{ shell.username }}</div> |
| 28 | 28 | <p class="card-body">{{ account_line }}</p> |
| 29 | + {# Pas .card-body : son opacité 0.8 délaverait le lien sous 4.5:1 (axe). #} | |
| 30 | + <p style="margin:var(--space-2) 0 0;font-size:13px">{{ shell.tr.feedback_title }} | |
| 31 | + <a href="{{ feedback_href }}" target="_blank" rel="noopener">{{ shell.tr.feedback_link }} ↗</a></p> | |
| 32 | + <p style="font-size:11px;opacity:.7;margin:var(--space-1) 0 0">{{ version_line }}</p> | |
| 29 | 33 | </div> |
| 30 | 34 | |
| 31 | 35 | <a class="card times-link" href="/temps" style="margin-bottom:var(--space-3)"> |
| @@ -26,6 +26,10 @@ | |||
| 26 | <div class="card-kicker">{{ shell.tr.account_kicker }}</div> | 26 | <div class="card-kicker">{{ shell.tr.account_kicker }}</div> |
| 27 | <div style="font-size:19px;font-family:var(--font-heading)">{{ shell.username }}</div> | 27 | <div style="font-size:19px;font-family:var(--font-heading)">{{ shell.username }}</div> |
| 28 | <p class="card-body">{{ account_line }}</p> | 28 | <p class="card-body">{{ account_line }}</p> |
| 29 | + {# Pas .card-body : son opacité 0.8 délaverait le lien sous 4.5:1 (axe). #} | ||
| 30 | + <p style="margin:var(--space-2) 0 0;font-size:13px">{{ shell.tr.feedback_title }} | ||
| 31 | + <a href="{{ feedback_href }}" target="_blank" rel="noopener">{{ shell.tr.feedback_link }} ↗</a></p> | ||
| 32 | + <p style="font-size:11px;opacity:.7;margin:var(--space-1) 0 0">{{ version_line }}</p> | ||
| 29 | </div> | 33 | </div> |
| 30 | 34 | ||
| 31 | <a class="card times-link" href="/temps" style="margin-bottom:var(--space-3)"> | 35 | <a class="card times-link" href="/temps" style="margin-bottom:var(--space-3)"> |