v3 : publication — un envoi refusé repart, un dépôt neuf reçoit l'historique
Douzième relecture (850f9c4 : ok), défauts anciens de publish.rs mis en
jeu par le nouveau dépôt :
- un commit que le dépôt n'a jamais reçu (premier envoi refusé, clé sans
droit d'écriture) repart au passage suivant, au lieu d'une file marquée
faite avec un dépôt vide ;
- un changement de remote vers un dépôt neuf et vide lui envoie
l'historique ;
- au démarrage, ce qui attendait repart aussitôt (le délai croissant
pouvait atteindre 6 h).
trustinWallet montre l'état de la publication (en attente, dernier échec,
dernier commit) et offre « Publier maintenant ».
Boucle : 69 tests Rust, svelte-check, 9 vitest, e2e 13/13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+5
-4
@@ -11,7 +11,7 @@ pas encore en ligne : les points des septième, huitième et neuvième relecture
|
||||
(demandés par Yvv le 2026-10-21) et la publication vers git.open.us.org.
|
||||
|
||||
- v3 Rust + Svelte à la racine du dépôt ; la maquette v2 intacte dans `maquette/`.
|
||||
- Boucle : Rust (fmt, clippy `-D warnings`, 68 tests dont 15 scénarios d'API et la
|
||||
- Boucle : Rust (fmt, clippy `-D warnings`, 69 tests dont 16 scénarios d'API et la
|
||||
publication vers un dépôt nu) · interface (svelte-check 0 avertissement, 9 vitest) ·
|
||||
e2e 13 / 13 (C1–C12, M1). L'image Docker rejoue tout et a été essayée localement :
|
||||
santé, interface, volume non-root, clé de publication, lien admin initial (0600, absent
|
||||
@@ -22,7 +22,7 @@ pas encore en ligne : les points des septième, huitième et neuvième relecture
|
||||
- Relectures indépendantes avant mise en ligne, chacune traitée avant de livrer : `1f9c241 :
|
||||
ok`, `94907af : ok`, puis quatre « à reprendre » (`ac9c904`, `9203fd2`, `1eac6f8`, `6c0754a`),
|
||||
toutes sur la même question : une « coquille » (sans vote, publiée) qui change un sens, puis
|
||||
`fa8e8dc : ok` (mis en ligne), `c3d6bf8 : ok`, `fd19dbf : ok`, `753b522 : ok`. La
|
||||
`fa8e8dc : ok` (mis en ligne), `c3d6bf8 : ok`, `fd19dbf : ok`, `753b522 : ok`, `8a32160 : ok`, `850f9c4 : ok`. La
|
||||
règle est désormais simple et explicable : **une coquille touche les lettres et les espaces,
|
||||
rien d'autre** (CHANTIER.md, ADR 0003). Autres points traités : ordre des workflows, anciens
|
||||
conteneurs en `restart: always`, vote réservé aux membres de la toile dans Monnaie libre (une
|
||||
@@ -40,8 +40,9 @@ pas encore en ligne : les points des septième, huitième et neuvième relecture
|
||||
`yvv/decision` et que le dépôt se transfère d'un bloc (Yvv : « pour l'instant sur open.us ; on
|
||||
fera un dépôt pour l'ouverture, un transfert, et le dépôt officiel pour les textes en
|
||||
vigueur »). Reste à y ajouter la **clé de déploiement** du serveur, **en écriture** (sa partie publique s'affiche dans trustinWallet,
|
||||
`/acces`, section Publication). Le serveur réessaie toutes les 60 s et fait lui-même le
|
||||
premier envoi. Changer de dépôt plus tard : `LD_PUBLISH_REMOTE` dans `docker/app-compose.yml`.
|
||||
`/acces`, section Publication). Le serveur fait lui-même le premier envoi ; en cas d'échec il
|
||||
réessaie avec un délai croissant (1 min, 2, 4… jusqu'à 6 h), repart d'une minute à chaque
|
||||
redémarrage, et `/acces` → Publication montre l'état et offre « Publier maintenant ». Changer de dépôt plus tard : `LD_PUBLISH_REMOTE` dans `docker/app-compose.yml`.
|
||||
3. **Ouvrir trustinWallet** avec le lien administrateur initial (remis hors conversation, un
|
||||
seul usage : il crée l'identité administratrice). Pour l'équipe : un **lien d'équipe**
|
||||
(`/acces` → Liens d'équipe ; chaque personne qui l'ouvre a sa propre identité), ou une
|
||||
|
||||
@@ -45,10 +45,13 @@ pub async fn overview(State(state): State<AppState>, User(user): User) -> AppRes
|
||||
.bind(util::now())
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
let publication = json!({
|
||||
let mut publication = json!({
|
||||
"remote": state.cfg.publish.as_ref().map(|p| p.remote.clone()),
|
||||
"publicKey": crate::publish::public_key(&state),
|
||||
});
|
||||
if state.cfg.publish.is_some() {
|
||||
publication["status"] = crate::publish::status(&state).await?;
|
||||
}
|
||||
Ok(Json(json!({
|
||||
"publication": publication,
|
||||
"groups": groups.iter().map(|(slug, name, policy, hidden)| json!({
|
||||
@@ -165,6 +168,18 @@ pub async fn cancel_invitation(
|
||||
Ok(Json(json!({ "ok": true })))
|
||||
}
|
||||
|
||||
/// « Publier maintenant » : a key just added, a remote just created — no need
|
||||
/// to wait for the backoff.
|
||||
pub async fn publish_now(State(state): State<AppState>, User(user): User) -> AppResult<Json<serde_json::Value>> {
|
||||
admin_only(&user)?;
|
||||
if state.cfg.publish.is_none() {
|
||||
return Err(AppError::Conflict("publication_off"));
|
||||
}
|
||||
crate::publish::retry_now(&state).await?;
|
||||
tracing::info!(by = %user.id, "publication retried now");
|
||||
Ok(Json(json!({ "ok": true })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AdminBody {
|
||||
admin: bool,
|
||||
|
||||
@@ -123,6 +123,7 @@ pub fn api_router() -> Router<AppState> {
|
||||
.route("/access/groups/{slug}/members/{identity}", delete(access::revoke))
|
||||
.route("/access/invitations", post(access::invite))
|
||||
.route("/access/invitations/{hash}", delete(access::cancel_invitation))
|
||||
.route("/access/publication/retry", post(access::publish_now))
|
||||
.route("/access/team-links", post(team::create))
|
||||
.route("/access/team-links/{hash}", delete(team::revoke))
|
||||
.route("/access/identities/{id}/admin", post(access::set_admin))
|
||||
|
||||
@@ -137,8 +137,9 @@ async fn git(dir: &Path, state: &AppState, args: &[&str]) -> anyhow::Result<Stri
|
||||
}
|
||||
|
||||
/// The server's own clone: a cache of the remote, realigned on it before each
|
||||
/// commit so a push never needs a merge.
|
||||
async fn checkout(state: &AppState) -> anyhow::Result<PathBuf> {
|
||||
/// commit so a push never needs a merge. Returns the clone and the commit the
|
||||
/// remote branch points to, if it exists.
|
||||
async fn checkout(state: &AppState) -> anyhow::Result<(PathBuf, Option<String>)> {
|
||||
let publish = state.cfg.publish.as_ref().ok_or_else(|| anyhow::anyhow!("publication not configured"))?;
|
||||
let dir = state.cfg.data_dir.join("textes");
|
||||
if !dir.join(".git").exists() {
|
||||
@@ -148,7 +149,8 @@ async fn checkout(state: &AppState) -> anyhow::Result<PathBuf> {
|
||||
}
|
||||
git(&dir, state, &["remote", "set-url", "origin", &publish.remote]).await?;
|
||||
let has_remote_branch = git(&dir, state, &["ls-remote", "--heads", "origin", &publish.branch]).await?;
|
||||
if has_remote_branch.is_empty() {
|
||||
let remote_head = has_remote_branch.split_whitespace().next().map(str::to_owned);
|
||||
if remote_head.is_none() {
|
||||
git(&dir, state, &["checkout", "-q", "-f", "-B", &publish.branch]).await?;
|
||||
} else {
|
||||
// This clone is a cache: whatever a failed pass left is re-rendered from the database.
|
||||
@@ -156,14 +158,15 @@ async fn checkout(state: &AppState) -> anyhow::Result<PathBuf> {
|
||||
git(&dir, state, &["checkout", "-q", "-f", "-B", &publish.branch, &format!("origin/{}", publish.branch)])
|
||||
.await?;
|
||||
}
|
||||
Ok(dir)
|
||||
Ok((dir, remote_head))
|
||||
}
|
||||
|
||||
async fn publish_doc(state: &AppState, doc_id: &str) -> anyhow::Result<Option<String>> {
|
||||
let st = docs::load_state(state, doc_id).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let Some(path) = st.doc.publish_path.clone() else { return Ok(None) };
|
||||
let rendered = render(state, &st).await?;
|
||||
let dir = checkout(state).await?;
|
||||
let (dir, remote_head) = checkout(state).await?;
|
||||
let branch = state.cfg.publish.as_ref().map(|p| p.branch.clone()).unwrap_or_else(|| "main".into());
|
||||
let target = dir.join(&path);
|
||||
tokio::fs::create_dir_all(&target).await?;
|
||||
for (name, content) in &rendered {
|
||||
@@ -176,7 +179,15 @@ async fn publish_doc(state: &AppState, doc_id: &str) -> anyhow::Result<Option<St
|
||||
git(&dir, state, &["add", "-A", "--", &path, "README.md"]).await?;
|
||||
let staged = git(&dir, state, &["diff", "--cached", "--name-only"]).await?;
|
||||
if staged.is_empty() {
|
||||
return Ok(None);
|
||||
// Nothing new — but a commit the remote never received still goes out: a
|
||||
// first push refused (a key without write access), or a new, empty
|
||||
// repository after a change of remote.
|
||||
let Ok(head) = git(&dir, state, &["rev-parse", "--verify", "-q", "HEAD"]).await else { return Ok(None) };
|
||||
if remote_head.as_deref() == Some(head.as_str()) {
|
||||
return Ok(None);
|
||||
}
|
||||
git(&dir, state, &["push", "-q", "origin", &format!("HEAD:{branch}")]).await?;
|
||||
return Ok(Some(head));
|
||||
}
|
||||
let reasons: Vec<String> =
|
||||
sqlx::query_scalar("SELECT reason FROM outbox WHERE doc_id = ? AND done_at IS NULL ORDER BY id")
|
||||
@@ -192,7 +203,6 @@ async fn publish_doc(state: &AppState, doc_id: &str) -> anyhow::Result<Option<St
|
||||
}
|
||||
}
|
||||
git(&dir, state, &["commit", "-q", "-m", &message]).await?;
|
||||
let branch = state.cfg.publish.as_ref().map(|p| p.branch.clone()).unwrap_or_else(|| "main".into());
|
||||
git(&dir, state, &["push", "-q", "origin", &format!("HEAD:{branch}")]).await?;
|
||||
Ok(Some(git(&dir, state, &["rev-parse", "HEAD"]).await?))
|
||||
}
|
||||
@@ -225,11 +235,13 @@ async fn last_change(state: &AppState, doc_id: &str) -> anyhow::Result<String> {
|
||||
|
||||
/// On start, every published document is queued once more: a change that
|
||||
/// entered into force just before a restart, or a new rendering format, reaches
|
||||
/// the repository. Unchanged documents produce no commit.
|
||||
/// the repository. Unchanged documents produce no commit. What was waiting
|
||||
/// after failures is tried again at once (a key just added, a new remote).
|
||||
pub async fn resync(state: &AppState) -> anyhow::Result<()> {
|
||||
if state.cfg.publish.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
retry_now(state).await?;
|
||||
let now = util::now();
|
||||
sqlx::query(
|
||||
"INSERT INTO outbox (doc_id, reason, created_at, next_at)
|
||||
@@ -244,6 +256,39 @@ pub async fn resync(state: &AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Everything still waiting goes out at the next pass, backoff forgotten.
|
||||
pub async fn retry_now(state: &AppState) -> anyhow::Result<()> {
|
||||
sqlx::query("UPDATE outbox SET next_at = ?, attempts = 0 WHERE done_at IS NULL")
|
||||
.bind(util::now())
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
state.publish_wake.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What administrators see of the publication in trustinWallet.
|
||||
pub async fn status(state: &AppState) -> anyhow::Result<serde_json::Value> {
|
||||
let pending: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM outbox WHERE done_at IS NULL").fetch_one(&state.db).await?;
|
||||
let error: Option<String> = sqlx::query_scalar(
|
||||
"SELECT last_error FROM outbox WHERE done_at IS NULL AND last_error IS NOT NULL ORDER BY id DESC LIMIT 1",
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
let last: Option<(String, Option<String>)> = sqlx::query_as(
|
||||
"SELECT done_at, commit_sha FROM outbox WHERE done_at IS NOT NULL AND commit_sha IS NOT NULL
|
||||
ORDER BY done_at DESC LIMIT 1",
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
Ok(json!({
|
||||
"pending": pending,
|
||||
"lastError": error,
|
||||
"lastPublishedAt": last.as_ref().map(|(at, _)| at.clone()),
|
||||
"lastCommit": last.and_then(|(_, sha)| sha),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Markdown per language + JSON. Only the units in force, in order.
|
||||
pub async fn render(state: &AppState, st: &DocState) -> anyhow::Result<Vec<(String, String)>> {
|
||||
let titles = util::parse_lang(&st.doc.title);
|
||||
|
||||
@@ -463,6 +463,11 @@ async fn access_management_is_for_administrators_only() {
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let (_, ov) = admin.call("GET", "/api/access", None).await;
|
||||
assert!(ov["identities"].as_array().expect("i").iter().any(|i| i["displayName"] == "Chiara"));
|
||||
// « Publier maintenant » is for administrators, and needs a configured publication.
|
||||
let (s, _) = user.call("POST", "/api/access/publication/retry", None).await;
|
||||
assert_eq!(s, StatusCode::FORBIDDEN);
|
||||
let (s, v) = admin.call("POST", "/api/access/publication/retry", None).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("publication_off")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -682,3 +687,77 @@ async fn publication_writes_the_texts_in_force_to_the_repository() {
|
||||
assert_eq!(pending, 0);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_refused_first_push_goes_out_again_and_a_new_empty_repository_receives_it() {
|
||||
// Tenth independent review (850f9c4), 2026-10-21: a first push refused (a key
|
||||
// without write access) left the commit in the server's clone, and the next pass,
|
||||
// finding nothing to commit, marked the queue done with the repository still empty.
|
||||
use std::process::Command;
|
||||
let dir = std::env::temp_dir().join(format!("ld-pub-retry-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("dir");
|
||||
let bare = |name: &str| {
|
||||
let path = dir.join(name);
|
||||
assert!(Command::new("git").args(["init", "-q", "--bare"]).arg(&path).status().expect("git").success());
|
||||
path
|
||||
};
|
||||
let config = |remote: &std::path::Path| {
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.data_dir = dir.join("data");
|
||||
cfg.publish = Some(ld_server::config::Publish {
|
||||
remote: remote.display().to_string(),
|
||||
branch: "main".into(),
|
||||
key: dir.join("data/keys/publish_ed25519"),
|
||||
author_name: "libreDecision".into(),
|
||||
author_email: "decision@example.org".into(),
|
||||
});
|
||||
cfg
|
||||
};
|
||||
let has_main = |remote: &std::path::Path| {
|
||||
Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(remote)
|
||||
.args(["rev-parse", "--verify", "-q", "main"])
|
||||
.output()
|
||||
.expect("git")
|
||||
.status
|
||||
.success()
|
||||
};
|
||||
let pending = |db: &sqlx::SqlitePool| {
|
||||
let db = db.clone();
|
||||
async move {
|
||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM outbox WHERE done_at IS NULL")
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.expect("count")
|
||||
}
|
||||
};
|
||||
|
||||
let first = bare("first.git");
|
||||
let hook = first.join("hooks/pre-receive");
|
||||
std::fs::write(&hook, "#!/bin/sh\nexit 1\n").expect("hook");
|
||||
assert!(Command::new("chmod").arg("755").arg(&hook).status().expect("chmod").success());
|
||||
let db = open_db("sqlite::memory:").await.expect("db");
|
||||
let state = AppState::new(config(&first), db.clone()).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
ld_server::publish::pass(&state).await.expect("pass");
|
||||
assert!(!has_main(&first), "the push was refused");
|
||||
assert_eq!(pending(&db).await, 1, "the document still waits");
|
||||
|
||||
// The key gets its write access: the next start sends what waited, at once.
|
||||
std::fs::remove_file(&hook).expect("unhook");
|
||||
ld_server::publish::resync(&state).await.expect("resync");
|
||||
ld_server::publish::pass(&state).await.expect("pass");
|
||||
assert!(has_main(&first), "the commit kept in the clone went out");
|
||||
assert_eq!(pending(&db).await, 0);
|
||||
|
||||
// A change of remote to a new, empty repository: it receives the history.
|
||||
let second = bare("second.git");
|
||||
let moved = AppState::new(config(&second), db.clone()).await.expect("state");
|
||||
ld_server::publish::resync(&moved).await.expect("resync");
|
||||
ld_server::publish::pass(&moved).await.expect("pass");
|
||||
assert!(has_main(&second), "the new repository received the texts");
|
||||
assert_eq!(pending(&db).await, 0);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ générée par le serveur (sa partie publique s'affiche dans trustinWallet), et
|
||||
délai croissant. Dépôt : `git.open.us.org/yvv/decision-depot-textes-reference`, un dossier par groupe et
|
||||
par document — provisoire (Yvv, 2026-10-21) : viendront un dépôt pour l'ouverture, par
|
||||
transfert, puis le dépôt officiel pour les textes en vigueur ; changer de dépôt tient en une
|
||||
ligne (`LD_PUBLISH_REMOTE`), le serveur réaligne son clone. Le français fait foi ; les traductions sont marquées éditoriales.
|
||||
ligne (`LD_PUBLISH_REMOTE`) : le serveur réaligne son clone, et un dépôt neuf et vide reçoit
|
||||
l'historique. Un commit que le dépôt n'a jamais reçu (premier envoi refusé, clé sans droit
|
||||
d'écriture) repart au passage suivant ; au démarrage, ce qui attendait repart aussitôt ;
|
||||
trustinWallet montre l'état (en attente, dernier échec, dernier commit) et offre « Publier
|
||||
maintenant ». Le français fait foi ; les traductions sont marquées éditoriales.
|
||||
|
||||
**Coût.** La publication est asynchrone (quelques secondes, plus en cas de panne) ; l'état
|
||||
de la file se lit dans trustinWallet.
|
||||
|
||||
@@ -316,6 +316,13 @@ export const en: Messages = {
|
||||
'access.publication.remote': "Repository: {remote}",
|
||||
'access.publication.key': "The server's deploy key. Add it with write access on this repository (Settings › Repository › Deploy keys): until then, publications wait.",
|
||||
'access.publication.none': "Publishing is not configured on this server.",
|
||||
'access.publication.pending.one': '{n} sending waiting',
|
||||
'access.publication.pending.other': '{n} sendings waiting',
|
||||
'access.publication.last': 'Last publication: {date} (commit {commit})',
|
||||
'access.publication.never': 'Nothing has been published to this repository yet.',
|
||||
'access.publication.error': 'Last failure: {error}',
|
||||
'access.publication.now': 'Publish now',
|
||||
'access.publication.queued': 'Publication started again.',
|
||||
'access.denied': 'Access management is for trustinWallet administrators only.',
|
||||
|
||||
'soon.title': 'Coming soon',
|
||||
@@ -344,6 +351,7 @@ export const en: Messages = {
|
||||
'error.name_taken': 'This name is already taken. Choose another one, adding an initial for instance.',
|
||||
'error.team_limits': 'A team link lasts 1 to 90 days, for 1 to 500 people.',
|
||||
'error.groups': 'Choose at least one group.',
|
||||
'error.publication_off': 'Publication is not configured on this server.',
|
||||
'error.retry': 'Try again',
|
||||
|
||||
'common.cancel': 'Cancel',
|
||||
|
||||
@@ -316,6 +316,13 @@ export const es: Messages = {
|
||||
'access.publication.remote': "Repositorio: {remote}",
|
||||
'access.publication.key': "Clave de despliegue del servidor. Añádela con permiso de escritura en este repositorio (Configuración › Repositorio › Claves de despliegue): sin ella, las publicaciones esperan.",
|
||||
'access.publication.none': "La publicación no está configurada en este servidor.",
|
||||
'access.publication.pending.one': '{n} envío en espera',
|
||||
'access.publication.pending.other': '{n} envíos en espera',
|
||||
'access.publication.last': 'Última publicación: {date} (commit {commit})',
|
||||
'access.publication.never': 'Todavía no se ha publicado nada en este repositorio.',
|
||||
'access.publication.error': 'Último fallo: {error}',
|
||||
'access.publication.now': 'Publicar ahora',
|
||||
'access.publication.queued': 'Publicación relanzada.',
|
||||
'access.denied': 'La gestión de accesos está reservada a los administradores de trustinWallet.',
|
||||
|
||||
'soon.title': 'Próximamente',
|
||||
@@ -344,6 +351,7 @@ export const es: Messages = {
|
||||
'error.name_taken': 'Este nombre ya está ocupado. Elige otro, añadiendo una inicial por ejemplo.',
|
||||
'error.team_limits': 'Un enlace de equipo vale de 1 a 90 días, para 1 a 500 personas.',
|
||||
'error.groups': 'Elige al menos un grupo.',
|
||||
'error.publication_off': 'La publicación no está configurada en este servidor.',
|
||||
'error.retry': 'Reintentar',
|
||||
|
||||
'common.cancel': 'Cancelar',
|
||||
|
||||
@@ -319,6 +319,13 @@ export const fr = {
|
||||
'access.publication.remote': "Dépôt : {remote}",
|
||||
'access.publication.key': "Clé de déploiement du serveur. Ajoute-la avec le droit d'écriture sur ce dépôt (Paramètres › Dépôt › Clés de déploiement) : sans elle, les publications attendent.",
|
||||
'access.publication.none': "La publication n'est pas configurée sur ce serveur.",
|
||||
'access.publication.pending.one': '{n} envoi en attente',
|
||||
'access.publication.pending.other': '{n} envois en attente',
|
||||
'access.publication.last': 'Dernière publication : {date} (commit {commit})',
|
||||
'access.publication.never': "Rien n'a encore été publié sur ce dépôt.",
|
||||
'access.publication.error': 'Dernier échec : {error}',
|
||||
'access.publication.now': 'Publier maintenant',
|
||||
'access.publication.queued': 'Publication relancée.',
|
||||
'access.denied': 'La gestion des accès est réservée aux administrateurs de trustinWallet.',
|
||||
|
||||
'soon.title': 'À venir',
|
||||
@@ -347,6 +354,7 @@ export const fr = {
|
||||
'error.name_taken': 'Ce nom est déjà pris. Choisis-en un autre, en ajoutant une initiale par exemple.',
|
||||
'error.team_limits': "Un lien d'équipe vaut de 1 à 90 jours, pour 1 à 500 personnes.",
|
||||
'error.groups': 'Choisis au moins un groupe.',
|
||||
'error.publication_off': "La publication n'est pas configurée sur ce serveur.",
|
||||
'error.retry': 'Réessayer',
|
||||
|
||||
'common.cancel': 'Annuler',
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||
import { Remote } from '$lib/load.svelte';
|
||||
import { session } from '$lib/session.svelte';
|
||||
import { toasts } from '$lib/toast.svelte';
|
||||
|
||||
interface Overview {
|
||||
publication: { remote: string | null; publicKey: string | null };
|
||||
publication: {
|
||||
remote: string | null;
|
||||
publicKey: string | null;
|
||||
status?: { pending: number; lastError: string | null; lastPublishedAt: string | null; lastCommit: string | null };
|
||||
};
|
||||
groups: { slug: string; name: string; policy: string; hidden: boolean; members: { identityId: string; grantedAt: string }[] }[];
|
||||
identities: { id: string; kind: 'g1' | 'invite' | 'dev'; displayName: string; g1Member: boolean; admin: boolean; lastSeenAt: string | null; teamLink: string | null }[];
|
||||
invitations: { id: string; label: string; admin: boolean; groups: string[]; expiresAt: string }[];
|
||||
@@ -57,6 +62,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function publishNow() {
|
||||
await act(async () => {
|
||||
await api.post('/access/publication/retry');
|
||||
toasts.show(t('access.publication.queued'));
|
||||
});
|
||||
}
|
||||
|
||||
// A new sign-in link for an invited person (another device, a lost session).
|
||||
async function relink(id: string, name: string) {
|
||||
await act(async () => {
|
||||
@@ -166,6 +178,15 @@
|
||||
<div class="card block">
|
||||
{#if data.value.publication.remote}
|
||||
<p class="small">{t('access.publication.remote', { remote: data.value.publication.remote })}</p>
|
||||
{#if data.value.publication.status}
|
||||
{@const st = data.value.publication.status}
|
||||
<p class="small">
|
||||
{#if st.lastPublishedAt}{t('access.publication.last', { date: i18n.date(st.lastPublishedAt), commit: (st.lastCommit ?? '').slice(0, 7) })}{:else}{t('access.publication.never')}{/if}
|
||||
{#if st.pending} · {i18n.plural('access.publication.pending', st.pending)}{/if}
|
||||
</p>
|
||||
{#if st.pending && st.lastError}<p class="small faint err">{t('access.publication.error', { error: st.lastError })}</p>{/if}
|
||||
{#if st.pending}<button class="btn small" type="button" onclick={publishNow}>{t('access.publication.now')}</button>{/if}
|
||||
{/if}
|
||||
{#if data.value.publication.publicKey}
|
||||
<p class="small muted">{t('access.publication.key')}</p>
|
||||
<code class="key">{data.value.publication.publicKey}</code>
|
||||
@@ -277,6 +298,9 @@
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: var(--r-input);
|
||||
}
|
||||
.err {
|
||||
word-break: break-word;
|
||||
}
|
||||
.fb .body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user