Major rebranding, i18n, dashboard, shared folders, role capabilities

- Rebrand: new app/agent icons, SKALD.md, warm "paper" CSS palette
  (terracotta accent, --radius tokens, WCAG contrast, reduced-motion),
  updated favicon, tray icon, skaldkonur asset
- i18n: backend crate (i18n.rs, locale column, ui_locale config),
  frontend library (web/lib/i18n.js, I18nMixin, t(key)),
  translation files (web/i18n/), every component wired
- Dashboard: <dashboard-page> replaces old home-page content;
  <app-copilot> becomes the landing page (full/dock layout modes)
- Shared folders: API endpoints (shared_folders.rs), frontend page,
  can_write membership, container mount topology, user_fs routing
- Role capabilities: new db table & authorization seam (data not enums),
  roles.attrs JSON for ui_mode / interface select
- Setup: skald-setup prompts for language + password, sets ui_locale
- General: components migrated to CSS variables, Lit conventions cleanup,
  connectors/catalog/marketplace/approval refactoring
This commit is contained in:
2026-07-18 21:38:42 +01:00
parent 2b35312abd
commit 126886e309
109 changed files with 6228 additions and 1390 deletions
+24 -1
View File
@@ -65,6 +65,8 @@ pub struct User {
pub role_id: String,
pub credentials: Credentials,
pub active: bool,
/// UI locale override (NULL = follow the instance default).
pub locale: Option<String>,
pub created_at: String,
pub updated_at: String,
}
@@ -78,6 +80,7 @@ pub struct UserSummary {
pub role_id: String,
pub encrypted: bool,
pub active: bool,
pub locale: Option<String>,
pub created_at: String,
pub updated_at: String,
}
@@ -95,6 +98,7 @@ impl User {
role_id: self.role_id.clone(),
encrypted: self.is_encrypted(),
active: self.active,
locale: self.locale.clone(),
created_at: self.created_at.clone(),
updated_at: self.updated_at.clone(),
}
@@ -140,6 +144,7 @@ struct Row {
database_password: Option<Vec<u8>>,
password_hash: Option<Vec<u8>>,
active: bool,
locale: Option<String>,
created_at: String,
updated_at: String,
}
@@ -150,7 +155,7 @@ macro_rules! select {
($tail:literal) => {
concat!(
"SELECT id, username, display_name, role_id, encrypted, kdf_params, kdf_salt, ",
"database_password, password_hash, active, created_at, updated_at FROM users ",
"database_password, password_hash, active, locale, created_at, updated_at FROM users ",
$tail
)
};
@@ -185,6 +190,7 @@ impl TryFrom<Row> for User {
role_id: r.role_id,
credentials,
active: r.active,
locale: r.locale,
created_at: r.created_at,
updated_at: r.updated_at,
})
@@ -370,6 +376,22 @@ pub async fn rename(pool: &SqlitePool, id: &str, username: &str, display_name: O
Ok(())
}
/// Sets (or clears, with `None`) the user's UI locale override.
pub async fn set_locale(pool: &SqlitePool, id: &str, locale: Option<&str>) -> Result<()> {
let n = sqlx::query(
"UPDATE users SET locale = ?2, updated_at = datetime('now') WHERE id = ?1",
)
.bind(id)
.bind(locale)
.execute(pool)
.await?
.rows_affected();
if n == 0 {
bail!("no such user: {id}");
}
Ok(())
}
/// Removes the directory row only. The caller still owns `database/{id}.db`:
/// erasing a user means deleting that file too.
pub async fn delete(pool: &SqlitePool, id: &str) -> Result<()> {
@@ -556,6 +578,7 @@ mod tests {
role_id: "admin".into(),
credentials: encrypted(),
active: true,
locale: None,
created_at: "now".into(),
updated_at: "now".into(),
};