Add Web Push, guided start screen, SVG icons, chunked uploads, rename

- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished
  notifications while no app window is visible; settings with test button
- Guided new-session flow: task, craft target (website/app/game/old
  software/fresh), clean room or deep, description, model, folder
  (new, existing or cloned git repository); originals upload to original/
- Craft phase bar (craft_phase tool) for study/specify/build/check/ready
- Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB)
- All UI glyphs and emojis replaced by themeable SVG icons
- Rename dialog with manual name or model-generated suggestion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 21:03:26 +01:00
1 parent 013695722a
commit a0d0e707f2
48 files changed
+2724 -228

No files matched your search

+22 -3
View File
@@ -373,16 +373,35 @@ pub async fn remove(Extension(user): Extension<User>, State(app): State<AppState
pub struct UploadQuery {
/// Target file path (absolute).
path: String,
/// Chunked upload: byte offset of this chunk (0 = first chunk, creates the file).
offset: Option<u64>,
}
/// Upload one file as the raw request body.
/// Upload a file as the raw request body – whole, or in chunks with `offset`
/// (large files: each chunk stays below the reverse proxy's body limit).
pub async fn upload(Extension(user): Extension<User>, State(app): State<AppState>, Query(q): Query<UploadQuery>, body: Bytes) -> ApiResult {
let p = checked_path(&app, &user, &q.path, false)?;
if p.is_dir() {
return Err(bad("is a directory"));
}
tokio::fs::write(&p, &body).await?;
Ok(Json(json!({"ok": true, "path": p, "size": body.len()})))
let max = app.config().server.max_upload_mb.saturating_mul(1024 * 1024);
let offset = q.offset.unwrap_or(0);
if offset + body.len() as u64 > max {
return Err(ApiError(StatusCode::PAYLOAD_TOO_LARGE, format!("file is larger than {} MB", max / 1024 / 1024)));
}
if offset == 0 {
tokio::fs::write(&p, &body).await?;
} else {
use tokio::io::AsyncWriteExt;
let current = tokio::fs::metadata(&p).await.map(|m| m.len()).unwrap_or(0);
if current != offset {
return Err(ApiError(StatusCode::CONFLICT, format!("chunk offset {offset} does not match the uploaded size {current}")));
}
let mut f = tokio::fs::OpenOptions::new().append(true).open(&p).await?;
f.write_all(&body).await?;
f.flush().await?;
}
Ok(Json(json!({"ok": true, "path": p, "size": offset + body.len() as u64})))
}
#[derive(Deserialize)]