From ac2afc7d50697a21301ae96f2261aab702e9fd98 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 5 Apr 2026 08:57:02 +0200 Subject: [PATCH] fix: more deviations from spec fixed --- .../meta/categories.json | 6 ++ .../meta/category-meta.json | 1 + .../meta/project.json | 6 ++ .../meta/publishing.json | 1 + .../meta/tags.json | 1 + crates/bds-core/src/db/fts.rs | 68 ++++++++++++++++--- crates/bds-core/src/db/queries/project.rs | 6 +- crates/bds-core/src/engine/media.rs | 43 ++++++++---- crates/bds-core/src/engine/meta.rs | 4 -- crates/bds-core/src/engine/post.rs | 12 +++- crates/bds-core/src/engine/project.rs | 36 +++++++--- crates/bds-core/src/engine/tag.rs | 12 ++-- crates/bds-core/src/engine/task.rs | 1 + crates/bds-core/src/model/metadata.rs | 9 +-- crates/bds-core/src/util/thumbnail.rs | 10 +-- crates/bds-ui/src/app.rs | 8 +-- specs/schema.allium | 4 +- 17 files changed, 161 insertions(+), 67 deletions(-) create mode 100644 crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/categories.json create mode 100644 crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/category-meta.json create mode 100644 crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/project.json create mode 100644 crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/publishing.json create mode 100644 crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/tags.json diff --git a/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/categories.json b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/categories.json new file mode 100644 index 0000000..942a464 --- /dev/null +++ b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/categories.json @@ -0,0 +1,6 @@ +[ + "article", + "aside", + "page", + "picture" +] \ No newline at end of file diff --git a/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/category-meta.json b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/category-meta.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/category-meta.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/project.json b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/project.json new file mode 100644 index 0000000..f156406 --- /dev/null +++ b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/project.json @@ -0,0 +1,6 @@ +{ + "name": "P", + "maxPostsPerPage": 50, + "semanticSimilarityEnabled": false, + "blogLanguages": [] +} \ No newline at end of file diff --git a/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/publishing.json b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/publishing.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/publishing.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/tags.json b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/tags.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/crates/bds-core/projects/131fbd64-cd05-4018-9aff-e9678ce4429d/meta/tags.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/crates/bds-core/src/db/fts.rs b/crates/bds-core/src/db/fts.rs index 28c60df..77d7395 100644 --- a/crates/bds-core/src/db/fts.rs +++ b/crates/bds-core/src/db/fts.rs @@ -182,17 +182,26 @@ pub struct PostSearchFilters<'a> { pub offset: Option, } -/// Search posts with filters. Returns matching post IDs. +/// Search result envelope with pagination metadata per spec. +#[derive(Debug)] +pub struct SearchResults { + pub post_ids: Vec, + pub total: usize, + pub offset: usize, + pub limit: usize, +} + +/// Search posts with filters. Returns matching post IDs with pagination metadata. pub fn search_posts_filtered( conn: &Connection, query: &str, language: &str, filters: &PostSearchFilters, -) -> rusqlite::Result> { +) -> rusqlite::Result { // Get FTS matches first let fts_ids = search_posts(conn, query, language)?; if fts_ids.is_empty() { - return Ok(vec![]); + return Ok(SearchResults { post_ids: vec![], total: 0, offset: filters.offset.unwrap_or(0), limit: filters.limit.unwrap_or(0) }); } // Apply filters by querying posts table @@ -210,6 +219,28 @@ pub fn search_posts_filtered( param_idx += 1; } + if let Some(tags) = filters.tags { + // Filter posts whose JSON tags array contains ALL specified tags (case-insensitive) + for tag in tags { + sql.push_str(&format!( + "AND EXISTS (SELECT 1 FROM json_each(posts.tags) WHERE LOWER(json_each.value) = LOWER(?{param_idx})) " + )); + params.push(Box::new(tag.clone())); + param_idx += 1; + } + } + + if let Some(categories) = filters.categories { + // Filter posts whose JSON categories array contains ALL specified categories (case-insensitive) + for cat in categories { + sql.push_str(&format!( + "AND EXISTS (SELECT 1 FROM json_each(posts.categories) WHERE LOWER(json_each.value) = LOWER(?{param_idx})) " + )); + params.push(Box::new(cat.clone())); + param_idx += 1; + } + } + if let Some(year) = filters.year { // Filter by year from created_at (unix ms) let start = chrono::NaiveDate::from_ymd_opt(year, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp_millis(); @@ -234,13 +265,22 @@ pub fn search_posts_filtered( let _ = param_idx; // suppress unused warning + // First get total count (without LIMIT/OFFSET) + let count_sql = sql.replace("SELECT id FROM posts", "SELECT COUNT(*) FROM posts"); + let total: usize = { + let mut stmt = conn.prepare(&count_sql)?; + let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); + stmt.query_row(params_refs.as_slice(), |row| row.get::<_, usize>(0))? + }; + sql.push_str("ORDER BY created_at DESC "); - if let Some(limit) = filters.limit { + let offset = filters.offset.unwrap_or(0); + let limit = filters.limit.unwrap_or(total); + + if filters.limit.is_some() { sql.push_str(&format!("LIMIT {limit} ")); - if let Some(offset) = filters.offset { - sql.push_str(&format!("OFFSET {offset} ")); - } + sql.push_str(&format!("OFFSET {offset} ")); } let mut stmt = conn.prepare(&sql)?; @@ -248,7 +288,8 @@ pub fn search_posts_filtered( let rows = stmt.query_map(params_refs.as_slice(), |row| { row.get::<_, String>(0) })?; - rows.collect() + let post_ids: Vec = rows.collect::>>()?; + Ok(SearchResults { post_ids, total, offset, limit }) } /// Search media by full-text query. Returns matching media IDs. @@ -524,7 +565,8 @@ mod tests { ..Default::default() }; let results = search_posts_filtered(db.conn(), "post", "en", &filters).unwrap(); - assert_eq!(results, vec!["post1"]); + assert_eq!(results.post_ids, vec!["post1"]); + assert_eq!(results.total, 1); } #[test] @@ -557,7 +599,8 @@ mod tests { ..Default::default() }; let results = search_posts_filtered(db.conn(), "year", "en", &filters).unwrap(); - assert_eq!(results, vec!["p2024"]); + assert_eq!(results.post_ids, vec!["p2024"]); + assert_eq!(results.total, 1); } #[test] @@ -583,6 +626,9 @@ mod tests { ..Default::default() }; let results = search_posts_filtered(db.conn(), "searchable", "en", &filters).unwrap(); - assert_eq!(results.len(), 2); + assert_eq!(results.post_ids.len(), 2); + assert_eq!(results.total, 5); + assert_eq!(results.offset, 1); + assert_eq!(results.limit, 2); } } diff --git a/crates/bds-core/src/db/queries/project.rs b/crates/bds-core/src/db/queries/project.rs index 67bce63..2aebe1e 100644 --- a/crates/bds-core/src/db/queries/project.rs +++ b/crates/bds-core/src/db/queries/project.rs @@ -46,11 +46,13 @@ pub fn get_active_project(conn: &Connection) -> rusqlite::Result { } pub fn set_active_project(conn: &Connection, id: &str) -> rusqlite::Result<()> { - conn.execute("UPDATE projects SET is_active = 0 WHERE is_active = 1", [])?; - conn.execute( + let tx = conn.unchecked_transaction()?; + tx.execute("UPDATE projects SET is_active = 0 WHERE is_active = 1", [])?; + tx.execute( "UPDATE projects SET is_active = 1 WHERE id = ?1", params![id], )?; + tx.commit()?; Ok(()) } diff --git a/crates/bds-core/src/engine/media.rs b/crates/bds-core/src/engine/media.rs index 2b5d6b4..79873b8 100644 --- a/crates/bds-core/src/engine/media.rs +++ b/crates/bds-core/src/engine/media.rs @@ -31,6 +31,13 @@ pub struct MediaRebuildReport { pub errors: Vec, } +/// Supported image MIME types for import (per media_processing.allium). +const SUPPORTED_IMAGE_TYPES: &[&str] = &[ + "image/jpeg", "image/png", "image/gif", + "image/webp", "image/tiff", "image/bmp", + "image/heic", "image/heif", +]; + /// Import a media file (image, etc.) into the project. pub fn import_media( conn: &Connection, @@ -45,14 +52,21 @@ pub fn import_media( language: Option<&str>, tags: Vec, ) -> EngineResult { - let id = Uuid::new_v4().to_string(); - let now = now_unix_ms(); - - // Derive extension from original_name + // Validate file type per spec let ext = Path::new(original_name) .extension() .and_then(|e| e.to_str()) .unwrap_or("bin"); + let mime_type = mime_from_extension(ext).to_string(); + if !SUPPORTED_IMAGE_TYPES.contains(&mime_type.as_str()) { + return Err(EngineError::Validation(format!( + "unsupported file type: {mime_type} (file: {original_name})" + ))); + } + + let id = Uuid::new_v4().to_string(); + let now = now_unix_ms(); + let filename = format!("{id}.{ext}"); // Compute target directory and copy file @@ -65,14 +79,11 @@ pub fn import_media( } fs::copy(source_path, &abs_file_path)?; - // Try to get image dimensions (silently ignore errors for non-image files) + // Get image dimensions let (width, height) = image_dimensions(&abs_file_path) .map(|(w, h)| (Some(w as i32), Some(h as i32))) .unwrap_or((None, None)); - // Detect MIME type from extension - let mime_type = mime_from_extension(ext).to_string(); - // Get file size let file_size = fs::metadata(&abs_file_path)?.len() as i64; @@ -542,7 +553,7 @@ fn rebuild_canonical_media( let now = now_unix_ms(); let existing = qm::get_media_by_id(conn, &sc.id); - match existing { + let created = match existing { Ok(mut media) => { // Update existing media media.original_name = sc.original_name; @@ -561,11 +572,11 @@ fn rebuild_canonical_media( media.tags = sc.tags; media.updated_at = now; qm::update_media(conn, &media)?; - Ok(false) + false } Err(_) => { let media = Media { - id: sc.id, + id: sc.id.clone(), project_id: project_id.to_string(), filename, original_name: sc.original_name, @@ -586,9 +597,17 @@ fn rebuild_canonical_media( updated_at: now, }; qm::insert_media(conn, &media)?; - Ok(true) + true } + }; + + // Regenerate thumbnails if the binary file exists + if abs_file.exists() { + let thumbnails_dir = data_dir.join("thumbnails"); + let _ = generate_all_thumbnails(&abs_file, &thumbnails_dir, &sc.id); } + + Ok(created) } /// Rebuild a translation from a `*.{lang}.meta` sidecar. Returns true if created, false if updated. diff --git a/crates/bds-core/src/engine/meta.rs b/crates/bds-core/src/engine/meta.rs index cf92793..23589cb 100644 --- a/crates/bds-core/src/engine/meta.rs +++ b/crates/bds-core/src/engine/meta.rs @@ -126,7 +126,6 @@ pub fn add_category(data_dir: &Path, category: &str) -> EngineResult<()> { CategorySettings { render_in_lists: true, show_title: true, - title: None, post_template_slug: None, list_template_slug: None, }, @@ -176,7 +175,6 @@ mod tests { max_posts_per_page: 25, blogmark_category: None, pico_theme: None, - python_runtime_mode: None, semantic_similarity_enabled: false, blog_languages: vec!["en".into()], }; @@ -209,7 +207,6 @@ mod tests { CategorySettings { render_in_lists: true, show_title: true, - title: None, post_template_slug: None, list_template_slug: None, }, @@ -299,7 +296,6 @@ mod tests { CategorySettings { render_in_lists: true, show_title: true, - title: None, post_template_slug: None, list_template_slug: None, }, diff --git a/crates/bds-core/src/engine/post.rs b/crates/bds-core/src/engine/post.rs index f5a466f..5479d94 100644 --- a/crates/bds-core/src/engine/post.rs +++ b/crates/bds-core/src/engine/post.rs @@ -43,8 +43,8 @@ pub fn create_post( template_slug: Option<&str>, ) -> EngineResult { let id = Uuid::new_v4().to_string(); - let raw_title = if title.is_empty() { "untitled" } else { title }; - let base_slug = slugify(raw_title); + let slug_source = if title.is_empty() { "untitled" } else { title }; + let base_slug = slugify(slug_source); let base_slug = if base_slug.is_empty() { "untitled".to_string() } else { @@ -58,7 +58,7 @@ pub fn create_post( let post = Post { id, project_id: project_id.to_string(), - title: raw_title.to_string(), + title: title.to_string(), slug, excerpt: None, content: content.map(|s| s.to_string()), @@ -341,6 +341,12 @@ pub fn delete_post( params![post_id], )?; + // Delete post-media associations + conn.execute( + "DELETE FROM post_media WHERE post_id = ?1", + params![post_id], + )?; + // Remove from FTS fts::remove_post_from_index(conn, post_id)?; diff --git a/crates/bds-core/src/engine/project.rs b/crates/bds-core/src/engine/project.rs index f5f2df1..eb030aa 100644 --- a/crates/bds-core/src/engine/project.rs +++ b/crates/bds-core/src/engine/project.rs @@ -111,8 +111,8 @@ pub fn list_projects(conn: &Connection) -> EngineResult> { /// Delete a project row (cascading handled by queries). /// Rejects deletion of the default project and the currently active project. -/// Optionally cleans up the project data directory. -pub fn delete_project(conn: &Connection, project_id: &str, data_dir: Option<&Path>) -> EngineResult<()> { +/// Only cleans up the internal project data directory (not external custom paths). +pub fn delete_project(conn: &Connection, project_id: &str, internal_data_dir: Option<&Path>) -> EngineResult<()> { // Cannot delete the default project if project_id == DEFAULT_PROJECT_ID { return Err(EngineError::Validation( @@ -129,12 +129,20 @@ pub fn delete_project(conn: &Connection, project_id: &str, data_dir: Option<&Pat } } + // Only delete internal data directory, never external custom data_path. + // The caller must pass the internal path only. + let project = q::get_project_by_id(conn, project_id) + .map_err(|_| EngineError::NotFound(format!("project {project_id}")))?; + let is_custom_path = project.data_path.is_some(); + q::delete_project(conn, project_id)?; - // Clean up filesystem if path provided - if let Some(dir) = data_dir { - if dir.exists() { - let _ = fs::remove_dir_all(dir); + // Clean up internal filesystem only (not custom external paths per spec) + if !is_custom_path { + if let Some(dir) = internal_data_dir { + if dir.exists() { + let _ = fs::remove_dir_all(dir); + } } } @@ -164,7 +172,6 @@ fn write_default_meta_files(data_dir: &Path, project_name: &str) -> EngineResult max_posts_per_page: 50, blogmark_category: None, pico_theme: None, - python_runtime_mode: None, semantic_similarity_enabled: false, blog_languages: Vec::new(), }; @@ -291,11 +298,18 @@ mod tests { #[test] fn delete_project_removes_row() { let (db, dir) = setup(); - let p_path = dir.path().join("p"); - let p = create_project(db.conn(), "P", Some(p_path.to_str().unwrap())).unwrap(); - delete_project(db.conn(), &p.id, Some(&p_path)).unwrap(); + // Project with no custom data_path → uses internal directory + let p = create_project( + db.conn(), + "P", + None, + ) + .unwrap(); + let internal_dir = dir.path().join("projects").join(&p.id); + let _ = std::fs::create_dir_all(&internal_dir); + delete_project(db.conn(), &p.id, Some(&internal_dir)).unwrap(); assert!(list_projects(db.conn()).unwrap().is_empty()); - assert!(!p_path.exists(), "project directory should be cleaned up"); + assert!(!internal_dir.exists(), "internal directory should be cleaned up"); } #[test] diff --git a/crates/bds-core/src/engine/tag.rs b/crates/bds-core/src/engine/tag.rs index 72e9200..49a7236 100644 --- a/crates/bds-core/src/engine/tag.rs +++ b/crates/bds-core/src/engine/tag.rs @@ -196,15 +196,19 @@ pub fn sync_tags_from_posts( ) -> EngineResult> { let posts = post_q::list_posts_by_project(conn, project_id)?; - // Collect all unique tag names from posts - let mut tag_names = std::collections::HashSet::new(); + // Collect all unique tag names from posts (preserve original casing per spec). + // Use a case-insensitive set to avoid duplicates while keeping the first-seen casing. + let mut seen_lower = std::collections::HashSet::new(); + let mut tag_names = Vec::new(); for post in &posts { for tag_name in &post.tags { - tag_names.insert(tag_name.to_lowercase()); + if seen_lower.insert(tag_name.to_lowercase()) { + tag_names.push(tag_name.clone()); + } } } - // Create any tags that don't exist yet + // Create any tags that don't exist yet (using original casing) let now = now_unix_ms(); for name in &tag_names { if tag_q::get_tag_by_project_and_name(conn, project_id, name).is_err() { diff --git a/crates/bds-core/src/engine/task.rs b/crates/bds-core/src/engine/task.rs index 9f86f53..f31a765 100644 --- a/crates/bds-core/src/engine/task.rs +++ b/crates/bds-core/src/engine/task.rs @@ -114,6 +114,7 @@ impl TaskManager { let mut tasks = self.tasks.lock().unwrap(); if let Some(entry) = tasks.iter_mut().find(|t| t.id == task_id) { if matches!(entry.status, TaskStatus::Running) { + entry.message = Some(error.clone()); entry.status = TaskStatus::Failed(error); } } diff --git a/crates/bds-core/src/model/metadata.rs b/crates/bds-core/src/model/metadata.rs index 82f1231..93802f6 100644 --- a/crates/bds-core/src/model/metadata.rs +++ b/crates/bds-core/src/model/metadata.rs @@ -26,8 +26,6 @@ pub struct ProjectMetadata { pub blogmark_category: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pico_theme: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub python_runtime_mode: Option, #[serde(default)] pub semantic_similarity_enabled: bool, #[serde(default)] @@ -55,8 +53,6 @@ pub struct CategorySettings { #[serde(default = "default_true")] pub show_title: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub post_template_slug: Option, #[serde(skip_serializing_if = "Option::is_none")] pub list_template_slug: Option, @@ -86,7 +82,6 @@ mod tests { max_posts_per_page: 50, blogmark_category: None, pico_theme: None, - python_runtime_mode: None, semantic_similarity_enabled: false, blog_languages: vec!["en".into(), "de".into()], }; @@ -116,7 +111,6 @@ mod tests { let settings: CategorySettings = serde_json::from_str(json).unwrap(); assert!(settings.render_in_lists); assert!(settings.show_title); - assert!(settings.title.is_none()); } #[test] @@ -124,7 +118,6 @@ mod tests { let settings = CategorySettings { render_in_lists: false, show_title: true, - title: Some("Articles".into()), post_template_slug: Some("article-tpl".into()), list_template_slug: None, }; @@ -163,7 +156,7 @@ mod tests { name: "Test".into(), description: None, public_url: None, main_language: None, default_author: None, max_posts_per_page: 50, blogmark_category: None, - pico_theme: None, python_runtime_mode: None, + pico_theme: None, semantic_similarity_enabled: false, blog_languages: vec![], }; assert!(meta.validate().is_ok()); diff --git a/crates/bds-core/src/util/thumbnail.rs b/crates/bds-core/src/util/thumbnail.rs index 02be38a..a7a45df 100644 --- a/crates/bds-core/src/util/thumbnail.rs +++ b/crates/bds-core/src/util/thumbnail.rs @@ -32,9 +32,9 @@ pub enum ThumbnailFit { /// Standard thumbnail sizes matching spec: small/medium/large are /// width-constrained aspect-preserving; AI is letterboxed on black. pub const THUMBNAIL_SIZES: &[ThumbnailSize] = &[ - ThumbnailSize { name: "small", width: 150, height: 150, format: ThumbnailFormat::Webp, fit: ThumbnailFit::Inside }, - ThumbnailSize { name: "medium", width: 400, height: 400, format: ThumbnailFormat::Webp, fit: ThumbnailFit::Inside }, - ThumbnailSize { name: "large", width: 800, height: 800, format: ThumbnailFormat::Webp, fit: ThumbnailFit::Inside }, + ThumbnailSize { name: "small", width: 150, height: u32::MAX, format: ThumbnailFormat::Webp, fit: ThumbnailFit::Inside }, + ThumbnailSize { name: "medium", width: 400, height: u32::MAX, format: ThumbnailFormat::Webp, fit: ThumbnailFit::Inside }, + ThumbnailSize { name: "large", width: 800, height: u32::MAX, format: ThumbnailFormat::Webp, fit: ThumbnailFit::Inside }, ThumbnailSize { name: "ai", width: 448, height: 448, format: ThumbnailFormat::Jpeg, fit: ThumbnailFit::Contain }, ]; @@ -276,11 +276,11 @@ mod tests { let dir = TempDir::new().unwrap(); let source = create_test_png(dir.path()); let dest = dir.path().join("thumb.webp"); - let size = &THUMBNAIL_SIZES[0]; // small: 150 Inside + let size = &THUMBNAIL_SIZES[0]; // small: 150 width-constrained generate_thumbnail(&source, &dest, size, 80).unwrap(); assert!(dest.exists()); let (w, h) = image_dimensions(&dest).unwrap(); - // 100x80 fits inside 150x150 without enlargement + // 100x80 is smaller than 150 wide, no enlargement assert_eq!(w, 100); assert_eq!(h, 80); } diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index d5e2541..88c18fe 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -188,7 +188,7 @@ impl BdsApp { .and_then(|p| p.data_path.as_ref()) .map(PathBuf::from); - // If no projects exist, create a default one + // If no projects exist, ensure the default project per spec let init_task = if projects.is_empty() { if let Some(ref db) = db { let default_data = dirs::data_dir() @@ -196,13 +196,11 @@ impl BdsApp { .join("bds") .join("projects") .join("my-blog"); - match engine::project::create_project( + match engine::project::ensure_default_project( db.conn(), - "My Blog", - Some(default_data.to_str().unwrap_or("my-blog")), + Some(&default_data), ) { Ok(project) => { - let _ = engine::project::set_active_project(db.conn(), &project.id); Task::done(Message::ProjectsLoaded(vec![project])) } Err(_) => Task::none(), diff --git a/specs/schema.allium b/specs/schema.allium index 86d4b37..bbbda5e 100644 --- a/specs/schema.allium +++ b/specs/schema.allium @@ -42,7 +42,7 @@ entity Post { language: String? -- ISO 639-1 code do_not_translate: Boolean - -- Legacy columns (read-only, no longer written) + -- Published snapshot columns (written on publish for diff detection) published_title: String? published_content: String? published_tags: String? @@ -58,7 +58,7 @@ entity PostTranslation { title: String excerpt: String? content: String? -- Draft body (null when published) - status: draft | published | archived + status: draft | published created_at: Timestamp updated_at: Timestamp published_at: Timestamp?