From ab7658c1f2af397f81baa21fcdbce5c8b1ba0e1f Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 21 Jun 2026 16:34:36 +0900 Subject: [PATCH] feat: bootstrap workspace web control plane --- Cargo.lock | 148 +++ Cargo.toml | 7 + crates/workspace-server/Cargo.toml | 24 + crates/workspace-server/src/lib.rs | 35 + crates/workspace-server/src/records.rs | 341 +++++ crates/workspace-server/src/server.rs | 530 ++++++++ crates/workspace-server/src/store.rs | 341 +++++ package.nix | 5 +- web/workspace/.gitignore | 5 + web/workspace/README.md | 21 + web/workspace/package-lock.json | 1673 ++++++++++++++++++++++++ web/workspace/package.json | 21 + web/workspace/src/app.html | 11 + web/workspace/src/routes/+layout.ts | 2 + web/workspace/src/routes/+page.svelte | 176 +++ web/workspace/svelte.config.js | 18 + web/workspace/tsconfig.json | 14 + web/workspace/vite.config.ts | 6 + 18 files changed, 3377 insertions(+), 1 deletion(-) create mode 100644 crates/workspace-server/Cargo.toml create mode 100644 crates/workspace-server/src/lib.rs create mode 100644 crates/workspace-server/src/records.rs create mode 100644 crates/workspace-server/src/server.rs create mode 100644 crates/workspace-server/src/store.rs create mode 100644 web/workspace/.gitignore create mode 100644 web/workspace/README.md create mode 100644 web/workspace/package-lock.json create mode 100644 web/workspace/package.json create mode 100644 web/workspace/src/app.html create mode 100644 web/workspace/src/routes/+layout.ts create mode 100644 web/workspace/src/routes/+page.svelte create mode 100644 web/workspace/svelte.config.js create mode 100644 web/workspace/tsconfig.json create mode 100644 web/workspace/vite.config.ts diff --git a/Cargo.lock b/Cargo.lock index cd96cb82..ed21e162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,58 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" @@ -1039,6 +1091,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1429,6 +1493,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -1968,6 +2041,17 @@ dependencies = [ "redox_syscall 0.7.4", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "line-clipping" version = "0.3.7" @@ -2201,6 +2285,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "mcp" version = "0.1.0" @@ -3376,6 +3466,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -3692,6 +3796,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -3701,6 +3816,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -4419,6 +4546,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -4457,6 +4585,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5839,6 +5968,25 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "yoi-workspace-server" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "project-record", + "rusqlite", + "serde", + "serde_json", + "serde_yaml", + "tempfile", + "thiserror 2.0.18", + "ticket", + "tokio", + "tower", + "tracing", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 210f5bc2..044d3131 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "crates/ticket", "crates/project-record", "crates/workflow", + "crates/workspace-server", "tests/e2e", ] default-members = [ @@ -50,6 +51,7 @@ default-members = [ "crates/ticket", "crates/project-record", "crates/workflow", + "crates/workspace-server", ] [workspace.package] @@ -80,21 +82,26 @@ session-store = { path = "crates/session-store" } secrets = { path = "crates/secrets" } tools = { path = "crates/tools" } tui = { path = "crates/tui" } +yoi-workspace-server = { path = "crates/workspace-server" } # External # Note: `reqwest` and `chrono` are not aggregated here because some crates # need `default-features = false`, which workspace inheritance cannot override. async-trait = "0.1" +axum = "0.8" fs4 = "0.13" futures = "0.3" libc = "0.2" schemars = "1.2" serde = "1.0" serde_json = "1.0" +serde_yaml = "0.9.34" +rusqlite = { version = "0.37", features = ["bundled"] } sha2 = "0.11" tempfile = "3.27" thiserror = "2.0" tokio = "1.52" +tower = "0.5" toml = "1.1" tracing = "0.1" uuid = "1.23" diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml new file mode 100644 index 00000000..e7a6d8d8 --- /dev/null +++ b/crates/workspace-server/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "yoi-workspace-server" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +async-trait.workspace = true +axum.workspace = true +project-record.workspace = true +rusqlite.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true +ticket.workspace = true +tokio = { workspace = true, features = ["fs", "net", "rt", "sync"] } +tracing.workspace = true + +[dev-dependencies] +tempfile.workspace = true +tower = { workspace = true, features = ["util"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs new file mode 100644 index 00000000..502ca47d --- /dev/null +++ b/crates/workspace-server/src/lib.rs @@ -0,0 +1,35 @@ +//! Local workspace web control plane backend bootstrap. +//! +//! This crate deliberately provides backend building blocks and an HTTP router; +//! it is not the product CLI facade. Existing `.yoi` Ticket and Objective files +//! remain the canonical project records and are read through bounded bridge APIs. + +pub mod records; +pub mod server; +pub mod store; + +pub use records::{ + LocalProjectRecordReader, ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary, +}; +pub use server::{AuthConfig, ServerConfig, WorkspaceApi, build_router, serve}; +pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("sqlite error: {0}")] + Sqlite(#[from] rusqlite::Error), + #[error("ticket error: {0}")] + Ticket(#[from] ticket::TicketError), + #[error("yaml error: {0}")] + Yaml(#[from] serde_yaml::Error), + #[error("invalid project record id `{0}`")] + InvalidRecordId(String), + #[error("record `{0}` is missing frontmatter")] + MissingFrontmatter(String), + #[error("store error: {0}")] + Store(String), +} diff --git a/crates/workspace-server/src/records.rs b/crates/workspace-server/src/records.rs new file mode 100644 index 00000000..fe3e0d12 --- /dev/null +++ b/crates/workspace-server/src/records.rs @@ -0,0 +1,341 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use project_record::validate_record_id; +use serde::{Deserialize, Serialize}; +use ticket::{LocalTicketBackend, TicketFilter, TicketIdOrSlug}; + +use crate::{Error, Result}; + +const DETAIL_BODY_LIMIT: usize = 64 * 1024; + +#[derive(Debug, Clone)] +pub struct LocalProjectRecordReader { + workspace_root: PathBuf, + ticket_backend: LocalTicketBackend, +} + +impl LocalProjectRecordReader { + pub fn new(workspace_root: impl Into) -> Self { + let workspace_root = workspace_root.into(); + let ticket_root = workspace_root.join(".yoi/tickets"); + Self { + workspace_root, + ticket_backend: LocalTicketBackend::new(ticket_root), + } + } + + pub fn workspace_root(&self) -> &Path { + self.workspace_root.as_path() + } + + pub fn list_tickets(&self, limit: usize) -> Result> { + let partial = self.ticket_backend.list_partial(TicketFilter::all())?; + let mut items = partial + .tickets + .into_iter() + .map(|item| TicketSummary { + id: item.id, + title: item.title, + state: item.workflow_state.as_str().to_string(), + priority: item.priority, + updated_at: item.updated_at, + queued_by: item.queued_by, + queued_at: item.queued_at, + record_source: "local_yoi_ticket".to_string(), + }) + .collect::>(); + items.sort_by(|a, b| { + b.updated_at + .cmp(&a.updated_at) + .then_with(|| a.id.cmp(&b.id)) + }); + items.truncate(limit.min(200)); + Ok(ProjectRecordList { + items, + invalid_records: partial + .invalid_records + .into_iter() + .map(|record| InvalidProjectRecord { + label: record.label, + reason: record.reason, + }) + .collect(), + record_authority: "local_yoi_project_records".to_string(), + }) + } + + pub fn ticket(&self, id: &str) -> Result { + validate_project_id(id)?; + let partial = self + .ticket_backend + .show_partial(TicketIdOrSlug::Id(id.to_string()))?; + let ticket = partial.ticket; + let (body, body_truncated) = + truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT); + Ok(TicketDetail { + id: ticket.meta.id, + title: ticket.meta.title, + state: ticket.meta.workflow_state.as_str().to_string(), + priority: ticket.meta.priority, + created_at: ticket.meta.created_at, + updated_at: ticket.meta.updated_at, + queued_by: ticket.meta.queued_by, + queued_at: ticket.meta.queued_at, + risk_flags: ticket.meta.risk_flags, + body, + body_truncated, + event_count: ticket.events.len(), + artifact_count: ticket.artifacts.len(), + record_source: "local_yoi_ticket".to_string(), + }) + } + + pub fn list_objectives(&self, limit: usize) -> Result> { + let mut items = Vec::new(); + let mut invalid_records = Vec::new(); + let root = self.workspace_root.join(".yoi/objectives"); + if !root.exists() { + return Ok(ProjectRecordList { + items, + invalid_records, + record_authority: "local_yoi_project_records".to_string(), + }); + } + + for entry in fs::read_dir(&root)? { + let entry = entry?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + let id = entry.file_name().to_string_lossy().to_string(); + match read_objective_summary(&path, &id) { + Ok(item) => items.push(item), + Err(error) => invalid_records.push(InvalidProjectRecord { + label: id, + reason: error.to_string(), + }), + } + } + items.sort_by(|a, b| { + b.updated_at + .cmp(&a.updated_at) + .then_with(|| a.id.cmp(&b.id)) + }); + items.truncate(limit.min(200)); + Ok(ProjectRecordList { + items, + invalid_records, + record_authority: "local_yoi_project_records".to_string(), + }) + } + + pub fn objective(&self, id: &str) -> Result { + validate_project_id(id)?; + let path = self.workspace_root.join(".yoi/objectives").join(id); + let raw = fs::read_to_string(path.join("item.md"))?; + let (frontmatter, body) = split_frontmatter(&raw, id)?; + let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?; + let (body, body_truncated) = truncate_body(body, DETAIL_BODY_LIMIT); + Ok(ObjectiveDetail { + id: id.to_string(), + title: meta.title, + state: meta.state, + created_at: meta.created_at, + updated_at: meta.updated_at, + linked_tickets: meta.linked_tickets, + body, + body_truncated, + record_source: "local_yoi_objective".to_string(), + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProjectRecordList { + pub items: Vec, + pub invalid_records: Vec, + pub record_authority: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InvalidProjectRecord { + pub label: String, + pub reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TicketSummary { + pub id: String, + pub title: String, + pub state: String, + pub priority: String, + pub updated_at: Option, + pub queued_by: Option, + pub queued_at: Option, + pub record_source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TicketDetail { + pub id: String, + pub title: String, + pub state: String, + pub priority: String, + pub created_at: Option, + pub updated_at: Option, + pub queued_by: Option, + pub queued_at: Option, + pub risk_flags: Vec, + pub body: String, + pub body_truncated: bool, + pub event_count: usize, + pub artifact_count: usize, + pub record_source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveSummary { + pub id: String, + pub title: String, + pub state: String, + pub updated_at: Option, + pub linked_tickets: Vec, + pub record_source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ObjectiveDetail { + pub id: String, + pub title: String, + pub state: String, + pub created_at: Option, + pub updated_at: Option, + pub linked_tickets: Vec, + pub body: String, + pub body_truncated: bool, + pub record_source: String, +} + +#[derive(Debug, Deserialize)] +struct ObjectiveFrontmatter { + title: String, + state: String, + #[serde(default)] + created_at: Option, + #[serde(default)] + updated_at: Option, + #[serde(default)] + linked_tickets: Vec, +} + +fn read_objective_summary(path: &Path, id: &str) -> Result { + validate_project_id(id)?; + let raw = fs::read_to_string(path.join("item.md"))?; + let (frontmatter, _) = split_frontmatter(&raw, id)?; + let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?; + Ok(ObjectiveSummary { + id: id.to_string(), + title: meta.title, + state: meta.state, + updated_at: meta.updated_at, + linked_tickets: meta.linked_tickets, + record_source: "local_yoi_objective".to_string(), + }) +} + +fn split_frontmatter<'a>(raw: &'a str, label: &str) -> Result<(&'a str, &'a str)> { + let rest = raw + .strip_prefix("---\n") + .ok_or_else(|| Error::MissingFrontmatter(label.to_string()))?; + let Some((frontmatter, body)) = rest.split_once("\n---\n") else { + return Err(Error::MissingFrontmatter(label.to_string())); + }; + Ok((frontmatter, body)) +} + +fn validate_project_id(id: &str) -> Result<()> { + validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string())) +} + +fn truncate_body(body: &str, limit: usize) -> (String, bool) { + if body.len() <= limit { + return (body.to_string(), false); + } + let mut end = limit; + while !body.is_char_boundary(end) { + end -= 1; + } + (body[..end].to_string(), true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reads_local_yoi_ticket_and_objective_records_without_migration() { + let dir = tempfile::tempdir().unwrap(); + write_ticket(dir.path(), "00000000001J2", "Read bridge", "ready"); + write_objective(dir.path(), "00000000001J3", "Control plane", "active"); + + let reader = LocalProjectRecordReader::new(dir.path()); + let tickets = reader.list_tickets(20).unwrap(); + assert_eq!(tickets.record_authority, "local_yoi_project_records"); + assert_eq!(tickets.items[0].id, "00000000001J2"); + assert_eq!(tickets.items[0].state, "ready"); + + let ticket = reader.ticket("00000000001J2").unwrap(); + assert!(ticket.body.contains("Ticket body")); + + let objectives = reader.list_objectives(20).unwrap(); + assert_eq!(objectives.items[0].id, "00000000001J3"); + assert_eq!(objectives.items[0].linked_tickets, vec!["00000000001J2"]); + + let objective = reader.objective("00000000001J3").unwrap(); + assert!(objective.body.contains("Objective body")); + } + + fn write_ticket(root: &Path, id: &str, title: &str, state: &str) { + let ticket_dir = root.join(".yoi/tickets").join(id); + fs::create_dir_all(&ticket_dir).unwrap(); + fs::write( + ticket_dir.join("item.md"), + format!( + r#"--- +title: "{title}" +state: "{state}" +created_at: "2026-01-01T00:00:00Z" +updated_at: "2026-01-02T00:00:00Z" +--- + +Ticket body. +"#, + ), + ) + .unwrap(); + fs::write(ticket_dir.join("thread.md"), "").unwrap(); + } + + fn write_objective(root: &Path, id: &str, title: &str, state: &str) { + let objective_dir = root.join(".yoi/objectives").join(id); + fs::create_dir_all(&objective_dir).unwrap(); + fs::write( + objective_dir.join("item.md"), + format!( + r#"--- +title: "{title}" +state: "{state}" +created_at: "2026-01-01T00:00:00Z" +updated_at: "2026-01-02T00:00:00Z" +linked_tickets: ["00000000001J2"] +--- + +Objective body. +"#, + ), + ) + .unwrap(); + } +} diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs new file mode 100644 index 00000000..19d5f27d --- /dev/null +++ b/crates/workspace-server/src/server.rs @@ -0,0 +1,530 @@ +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; + +use axum::extract::{Path as AxumPath, State}; +use axum::http::header::CONTENT_TYPE; +use axum::http::{StatusCode, Uri}; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use tokio::net::TcpListener; + +use crate::records::{LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail}; +use crate::store::{ControlPlaneStore, RunSummary, RunnerSummary, WorkspaceRecord}; +use crate::{Error, Result}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum AuthConfig { + /// Local/dev-only mode. If a token is configured by a future entrypoint, it + /// is a development guard only and not a production SaaS auth model. + LocalDevToken { token_configured: bool }, +} + +#[derive(Clone)] +pub struct ServerConfig { + pub workspace_id: String, + pub workspace_root: PathBuf, + pub static_assets_dir: Option, + pub auth: AuthConfig, + pub max_records: usize, +} + +impl ServerConfig { + pub fn local_dev(workspace_root: impl Into) -> Self { + let workspace_root = workspace_root.into(); + let display = workspace_root + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("workspace"); + Self { + workspace_id: format!("local:{display}"), + workspace_root, + static_assets_dir: None, + auth: AuthConfig::LocalDevToken { + token_configured: false, + }, + max_records: 200, + } + } +} + +#[derive(Clone)] +pub struct WorkspaceApi { + config: ServerConfig, + store: Arc, + records: LocalProjectRecordReader, +} + +impl WorkspaceApi { + pub async fn new(config: ServerConfig, store: Arc) -> Result { + let display_name = config + .workspace_root + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("workspace") + .to_string(); + store + .upsert_workspace(&WorkspaceRecord { + workspace_id: config.workspace_id.clone(), + display_name, + local_root: config.workspace_root.clone(), + record_authority: "local_yoi_project_records".to_string(), + created_at: "1970-01-01T00:00:00Z".to_string(), + updated_at: "1970-01-01T00:00:00Z".to_string(), + }) + .await?; + Ok(Self { + records: LocalProjectRecordReader::new(config.workspace_root.clone()), + config, + store, + }) + } + + pub fn workspace_id(&self) -> &str { + self.config.workspace_id.as_str() + } +} + +pub fn build_router(api: WorkspaceApi) -> Router { + Router::new() + .route("/api/workspace", get(get_workspace)) + .route("/api/tickets", get(list_tickets)) + .route("/api/tickets/{id}", get(get_ticket)) + .route("/api/objectives", get(list_objectives)) + .route("/api/objectives/{id}", get(get_objective)) + .route("/api/runs", get(list_runs)) + .route("/api/runners", get(list_runners)) + .fallback(get(static_or_spa_fallback)) + .with_state(api) +} + +pub async fn serve( + config: ServerConfig, + store: Arc, + listener: TcpListener, +) -> Result<()> { + let api = WorkspaceApi::new(config, store).await?; + axum::serve(listener, build_router(api)).await?; + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WorkspaceResponse { + pub workspace_id: String, + pub display_name: String, + pub local_root: PathBuf, + pub record_authority: String, + pub schema_version: i64, + pub auth: AuthConfig, + pub extension_points: ExtensionPoints, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExtensionPoints { + pub store: String, + pub event_stream: ExtensionPointState, + pub runner_connection: ExtensionPointState, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExtensionPointState { + pub status: String, + pub note: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ListResponse { + pub workspace_id: String, + pub limit: usize, + pub items: Vec, + pub invalid_records: Vec, + pub record_authority: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RuntimeListResponse { + pub workspace_id: String, + pub limit: usize, + pub items: Vec, + pub source: String, +} + +async fn get_workspace(State(api): State) -> ApiResult> { + let schema_version = api.store.schema_version().await?; + let stored = api.store.get_workspace(api.workspace_id()).await?; + let display_name = stored + .as_ref() + .map(|record| record.display_name.clone()) + .or_else(|| { + api.config + .workspace_root + .file_name() + .and_then(|name| name.to_str()) + .map(str::to_string) + }) + .unwrap_or_else(|| "workspace".to_string()); + Ok(Json(WorkspaceResponse { + workspace_id: api.config.workspace_id.clone(), + display_name, + local_root: api.config.workspace_root.clone(), + record_authority: "local_yoi_project_records".to_string(), + schema_version, + auth: api.config.auth.clone(), + extension_points: ExtensionPoints { + store: "sqlite".to_string(), + event_stream: ExtensionPointState { + status: "reserved".to_string(), + note: "No event stream is exposed in this bootstrap; route/state seams are reserved.".to_string(), + }, + runner_connection: ExtensionPointState { + status: "reserved".to_string(), + note: "Runner connections are modeled, but no job dispatch or scheduler is implemented.".to_string(), + }, + }, + })) +} + +async fn list_tickets( + State(api): State, +) -> ApiResult>> { + let limit = api.config.max_records.min(200); + let ProjectRecordList { + items, + invalid_records, + record_authority, + } = api.records.list_tickets(limit)?; + Ok(Json(ListResponse { + workspace_id: api.config.workspace_id, + limit, + items, + invalid_records, + record_authority, + })) +} + +async fn get_ticket( + State(api): State, + AxumPath(id): AxumPath, +) -> ApiResult> { + Ok(Json(api.records.ticket(&id)?)) +} + +async fn list_objectives( + State(api): State, +) -> ApiResult>> { + let limit = api.config.max_records.min(200); + let ProjectRecordList { + items, + invalid_records, + record_authority, + } = api.records.list_objectives(limit)?; + Ok(Json(ListResponse { + workspace_id: api.config.workspace_id, + limit, + items, + invalid_records, + record_authority, + })) +} + +async fn get_objective( + State(api): State, + AxumPath(id): AxumPath, +) -> ApiResult> { + Ok(Json(api.records.objective(&id)?)) +} + +async fn list_runs( + State(api): State, +) -> ApiResult>> { + let limit = api.config.max_records.min(200); + let items = api.store.list_runs(api.workspace_id(), limit).await?; + Ok(Json(RuntimeListResponse { + workspace_id: api.config.workspace_id, + limit, + items, + source: "sqlite_runtime_tables".to_string(), + })) +} + +async fn list_runners( + State(api): State, +) -> ApiResult>> { + let limit = api.config.max_records.min(200); + let items = api.store.list_runners(api.workspace_id(), limit).await?; + Ok(Json(RuntimeListResponse { + workspace_id: api.config.workspace_id, + limit, + items, + source: "sqlite_runtime_tables".to_string(), + })) +} + +async fn static_or_spa_fallback(State(api): State, uri: Uri) -> Response { + if uri.path().starts_with("/api/") || uri.path() == "/api" { + return ( + StatusCode::NOT_FOUND, + [(CONTENT_TYPE, "application/json")], + Json(serde_json::json!({ + "error": "not_found", + "message": "unknown api route" + })) + .to_string(), + ) + .into_response(); + } + + let Some(static_root) = api.config.static_assets_dir.as_ref() else { + return StatusCode::NOT_FOUND.into_response(); + }; + + match read_static_or_index(static_root, uri.path()).await { + Ok(StaticAsset { + bytes, + content_type, + }) => (StatusCode::OK, [(CONTENT_TYPE, content_type)], bytes).into_response(), + Err(error) => { + tracing::debug!(%error, path = %uri.path(), "failed to serve static asset"); + StatusCode::NOT_FOUND.into_response() + } + } +} + +struct StaticAsset { + bytes: Vec, + content_type: &'static str, +} + +async fn read_static_or_index(root: &Path, request_path: &str) -> Result { + let candidate = safe_static_candidate(root, request_path)?; + let file = if tokio::fs::metadata(&candidate) + .await + .map(|m| m.is_file()) + .unwrap_or(false) + { + candidate + } else { + root.join("index.html") + }; + let content_type = content_type_for(&file); + let bytes = tokio::fs::read(file).await?; + Ok(StaticAsset { + bytes, + content_type, + }) +} + +fn safe_static_candidate(root: &Path, request_path: &str) -> Result { + let mut path = root.to_path_buf(); + let clean = request_path.trim_start_matches('/'); + if clean.is_empty() { + path.push("index.html"); + return Ok(path); + } + for component in Path::new(clean).components() { + match component { + Component::Normal(part) => path.push(part), + Component::CurDir => {} + _ => return Err(Error::Store("static path escape rejected".to_string())), + } + } + Ok(path) +} + +fn content_type_for(path: &Path) -> &'static str { + match path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or_default() + { + "css" => "text/css; charset=utf-8", + "js" => "text/javascript; charset=utf-8", + "json" => "application/json", + "svg" => "image/svg+xml", + "html" | "" => "text/html; charset=utf-8", + _ => "application/octet-stream", + } +} + +type ApiResult = std::result::Result; + +struct ApiError(Error); + +impl From for ApiError { + fn from(error: Error) -> Self { + Self(error) + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = match &self.0 { + Error::InvalidRecordId(_) | Error::MissingFrontmatter(_) => StatusCode::NOT_FOUND, + Error::Ticket(_) => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + ( + status, + [(CONTENT_TYPE, "application/json")], + Json(serde_json::json!({ + "error": status.canonical_reason().unwrap_or("error"), + "message": self.0.to_string(), + })) + .to_string(), + ) + .into_response() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::{Body, to_bytes}; + use axum::http::Request; + use serde_json::Value; + use tower::ServiceExt; + + use crate::store::SqliteWorkspaceStore; + + #[tokio::test] + async fn serves_bounded_read_apis_and_static_spa_separately() { + let dir = tempfile::tempdir().unwrap(); + write_ticket(dir.path(), "00000000001J2", "API Ticket", "ready"); + write_objective(dir.path(), "00000000001J3", "API Objective", "active"); + let static_dir = dir.path().join("static"); + std::fs::create_dir_all(static_dir.join("assets")).unwrap(); + std::fs::write(static_dir.join("index.html"), "
Yoi Workspace
").unwrap(); + std::fs::write(static_dir.join("assets/app.js"), "console.log('yoi');").unwrap(); + + let store = SqliteWorkspaceStore::in_memory().unwrap(); + let mut config = ServerConfig::local_dev(dir.path()); + config.workspace_id = "local:test".to_string(); + config.static_assets_dir = Some(static_dir); + let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap(); + let app = build_router(api); + + let workspace = get_json(app.clone(), "/api/workspace").await; + assert_eq!(workspace["workspace_id"], "local:test"); + assert_eq!(workspace["record_authority"], "local_yoi_project_records"); + assert_eq!( + workspace["extension_points"]["runner_connection"]["status"], + "reserved" + ); + + let tickets = get_json(app.clone(), "/api/tickets").await; + assert_eq!(tickets["items"][0]["id"], "00000000001J2"); + assert_eq!(tickets["items"][0]["state"], "ready"); + + let objectives = get_json(app.clone(), "/api/objectives").await; + assert_eq!(objectives["items"][0]["id"], "00000000001J3"); + + let runners = get_json(app.clone(), "/api/runners").await; + assert!(runners["items"].as_array().unwrap().is_empty()); + + let static_response = app + .clone() + .oneshot( + Request::builder() + .uri("/assets/app.js") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(static_response.status(), StatusCode::OK); + assert_eq!( + static_response.headers().get(CONTENT_TYPE).unwrap(), + "text/javascript; charset=utf-8" + ); + + let spa_response = app + .clone() + .oneshot( + Request::builder() + .uri("/tickets/00000000001J2") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(spa_response.status(), StatusCode::OK); + let bytes = to_bytes(spa_response.into_body(), usize::MAX) + .await + .unwrap(); + assert!( + String::from_utf8(bytes.to_vec()) + .unwrap() + .contains("Yoi Workspace") + ); + + let api_miss = app + .oneshot( + Request::builder() + .uri("/api/nope") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(api_miss.status(), StatusCode::NOT_FOUND); + let bytes = to_bytes(api_miss.into_body(), usize::MAX).await.unwrap(); + assert!( + !String::from_utf8(bytes.to_vec()) + .unwrap() + .contains("Yoi Workspace") + ); + } + + async fn get_json(app: Router, uri: &str) -> Value { + let response = app + .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK, "{uri}"); + let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + serde_json::from_slice(&bytes).unwrap() + } + + fn write_ticket(root: &Path, id: &str, title: &str, state: &str) { + let ticket_dir = root.join(".yoi/tickets").join(id); + std::fs::create_dir_all(&ticket_dir).unwrap(); + std::fs::write( + ticket_dir.join("item.md"), + format!( + r#"--- +title: "{title}" +state: "{state}" +created_at: "2026-01-01T00:00:00Z" +updated_at: "2026-01-02T00:00:00Z" +--- + +Ticket body. +"#, + ), + ) + .unwrap(); + std::fs::write(ticket_dir.join("thread.md"), "").unwrap(); + } + + fn write_objective(root: &Path, id: &str, title: &str, state: &str) { + let objective_dir = root.join(".yoi/objectives").join(id); + std::fs::create_dir_all(&objective_dir).unwrap(); + std::fs::write( + objective_dir.join("item.md"), + format!( + r#"--- +title: "{title}" +state: "{state}" +created_at: "2026-01-01T00:00:00Z" +updated_at: "2026-01-02T00:00:00Z" +linked_tickets: ["00000000001J2"] +--- + +Objective body. +"#, + ), + ) + .unwrap(); + } +} diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs new file mode 100644 index 00000000..3b4a6334 --- /dev/null +++ b/crates/workspace-server/src/store.rs @@ -0,0 +1,341 @@ +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use async_trait::async_trait; +use rusqlite::{Connection, OptionalExtension, params}; +use serde::{Deserialize, Serialize}; + +use crate::{Error, Result}; + +const MIGRATIONS: &[Migration] = &[Migration { + version: 1, + name: "bootstrap workspace control plane", + sql: r#" +CREATE TABLE IF NOT EXISTS workspaces ( + workspace_id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + local_root TEXT NOT NULL, + record_authority TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS repositories ( + repository_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + local_root TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT NOT NULL +); + +-- Projection tables are intentionally empty in this bootstrap: `.yoi/tickets` +-- and `.yoi/objectives` remain canonical, but the tables reserve a future +-- projection/cache seam without migrating authority. +CREATE TABLE IF NOT EXISTS ticket_projections ( + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + ticket_id TEXT NOT NULL, + title TEXT NOT NULL, + state TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (workspace_id, ticket_id) +); + +CREATE TABLE IF NOT EXISTS objective_projections ( + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + objective_id TEXT NOT NULL, + title TEXT NOT NULL, + state TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (workspace_id, objective_id) +); + +CREATE TABLE IF NOT EXISTS runners ( + runner_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + label TEXT NOT NULL, + status TEXT NOT NULL, + last_seen_at TEXT +); + +CREATE TABLE IF NOT EXISTS runs ( + run_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + subject_kind TEXT NOT NULL, + subject_id TEXT NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS artifacts ( + artifact_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + run_id TEXT REFERENCES runs(run_id) ON DELETE SET NULL, + path TEXT NOT NULL, + content_type TEXT, + created_at TEXT NOT NULL +); +"#, +}]; + +struct Migration { + version: i64, + name: &'static str, + sql: &'static str, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct WorkspaceRecord { + pub workspace_id: String, + pub display_name: String, + pub local_root: PathBuf, + pub record_authority: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RunSummary { + pub run_id: String, + pub workspace_id: String, + pub subject_kind: String, + pub subject_id: String, + pub status: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RunnerSummary { + pub runner_id: String, + pub workspace_id: String, + pub label: String, + pub status: String, + pub last_seen_at: Option, +} + +#[async_trait] +pub trait ControlPlaneStore: Send + Sync { + async fn schema_version(&self) -> Result; + async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>; + async fn get_workspace(&self, workspace_id: &str) -> Result>; + async fn list_runs(&self, workspace_id: &str, limit: usize) -> Result>; + async fn list_runners(&self, workspace_id: &str, limit: usize) -> Result>; +} + +#[derive(Clone)] +pub struct SqliteWorkspaceStore { + conn: Arc>, +} + +impl SqliteWorkspaceStore { + pub fn open(path: impl AsRef) -> Result { + let conn = Connection::open(path)?; + Self::from_connection(conn) + } + + pub fn in_memory() -> Result { + Self::from_connection(Connection::open_in_memory()?) + } + + pub fn from_connection(conn: Connection) -> Result { + configure_sqlite(&conn)?; + apply_migrations(&conn)?; + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } + + fn with_conn(&self, f: impl FnOnce(&Connection) -> Result) -> Result { + let conn = self + .conn + .lock() + .map_err(|_| Error::Store("sqlite connection lock poisoned".to_string()))?; + f(&conn) + } +} + +#[async_trait] +impl ControlPlaneStore for SqliteWorkspaceStore { + async fn schema_version(&self) -> Result { + self.with_conn(current_schema_version) + } + + async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()> { + self.with_conn(|conn| { + conn.execute( + r#"INSERT INTO workspaces ( + workspace_id, display_name, local_root, record_authority, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(workspace_id) DO UPDATE SET + display_name = excluded.display_name, + local_root = excluded.local_root, + record_authority = excluded.record_authority, + updated_at = excluded.updated_at"#, + params![ + record.workspace_id, + record.display_name, + record.local_root.to_string_lossy(), + record.record_authority, + record.created_at, + record.updated_at, + ], + )?; + Ok(()) + }) + } + + async fn get_workspace(&self, workspace_id: &str) -> Result> { + self.with_conn(|conn| { + conn.query_row( + r#"SELECT workspace_id, display_name, local_root, record_authority, created_at, updated_at + FROM workspaces WHERE workspace_id = ?1"#, + params![workspace_id], + |row| { + Ok(WorkspaceRecord { + workspace_id: row.get(0)?, + display_name: row.get(1)?, + local_root: PathBuf::from(row.get::<_, String>(2)?), + record_authority: row.get(3)?, + created_at: row.get(4)?, + updated_at: row.get(5)?, + }) + }, + ) + .optional() + .map_err(Error::from) + }) + } + + async fn list_runs(&self, workspace_id: &str, limit: usize) -> Result> { + self.with_conn(|conn| { + let limit = limit.min(200) as i64; + let mut stmt = conn.prepare( + r#"SELECT run_id, workspace_id, subject_kind, subject_id, status, created_at, updated_at + FROM runs WHERE workspace_id = ?1 ORDER BY updated_at DESC, run_id DESC LIMIT ?2"#, + )?; + let rows = stmt.query_map(params![workspace_id, limit], |row| { + Ok(RunSummary { + run_id: row.get(0)?, + workspace_id: row.get(1)?, + subject_kind: row.get(2)?, + subject_id: row.get(3)?, + status: row.get(4)?, + created_at: row.get(5)?, + updated_at: row.get(6)?, + }) + })?; + rows.collect::>>().map_err(Error::from) + }) + } + + async fn list_runners(&self, workspace_id: &str, limit: usize) -> Result> { + self.with_conn(|conn| { + let limit = limit.min(200) as i64; + let mut stmt = conn.prepare( + r#"SELECT runner_id, workspace_id, label, status, last_seen_at + FROM runners WHERE workspace_id = ?1 ORDER BY runner_id ASC LIMIT ?2"#, + )?; + let rows = stmt.query_map(params![workspace_id, limit], |row| { + Ok(RunnerSummary { + runner_id: row.get(0)?, + workspace_id: row.get(1)?, + label: row.get(2)?, + status: row.get(3)?, + last_seen_at: row.get(4)?, + }) + })?; + rows.collect::>>() + .map_err(Error::from) + }) + } +} + +fn configure_sqlite(conn: &Connection) -> Result<()> { + conn.busy_timeout(Duration::from_millis(5_000))?; + conn.execute_batch( + r#" +PRAGMA foreign_keys = ON; +PRAGMA journal_mode = WAL; +PRAGMA busy_timeout = 5000; +CREATE TABLE IF NOT EXISTS __yoi_schema_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); +"#, + )?; + Ok(()) +} + +fn current_schema_version(conn: &Connection) -> Result { + conn.query_row( + "SELECT COALESCE(MAX(version), 0) FROM __yoi_schema_migrations", + [], + |row| row.get(0), + ) + .map_err(Error::from) +} + +fn apply_migrations(conn: &Connection) -> Result<()> { + let current = current_schema_version(conn)?; + for migration in MIGRATIONS + .iter() + .filter(|migration| migration.version > current) + { + let tx = conn.unchecked_transaction()?; + tx.execute_batch(migration.sql)?; + tx.execute( + "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", + params![migration.version, migration.name], + )?; + tx.commit()?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn migrates_sqlite_and_preserves_workspace_record() { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join("control-plane.sqlite"); + let store = SqliteWorkspaceStore::open(&db).unwrap(); + + assert_eq!(store.schema_version().await.unwrap(), 1); + + let record = WorkspaceRecord { + workspace_id: "local-dev".to_string(), + display_name: "Yoi Dev".to_string(), + local_root: dir.path().to_path_buf(), + record_authority: "local_yoi_project_records".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), + }; + store.upsert_workspace(&record).await.unwrap(); + + let reopened = SqliteWorkspaceStore::open(&db).unwrap(); + assert_eq!(reopened.schema_version().await.unwrap(), 1); + assert_eq!( + reopened.get_workspace("local-dev").await.unwrap(), + Some(record) + ); + assert!( + reopened + .list_runs("local-dev", 20) + .await + .unwrap() + .is_empty() + ); + assert!( + reopened + .list_runners("local-dev", 20) + .await + .unwrap() + .is_empty() + ); + } +} diff --git a/package.nix b/package.nix index 35743036..7ac7830b 100644 --- a/package.nix +++ b/package.nix @@ -29,6 +29,9 @@ let || isExcludedTree ".worktree" || isExcludedTree "work-items" || isExcludedTree "docs/report" + || isExcludedTree "web/workspace/node_modules" + || isExcludedTree "web/workspace/.svelte-kit" + || isExcludedTree "web/workspace/build" ); in rustPlatform.buildRustPackage rec { @@ -40,7 +43,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-GUqhvq+JhJokk1R4VVeVz5cZe/6oSrVMyKjcltZEWqE="; + cargoHash = "sha256-RER/UXd74C2VhPHAeF36u6ruNBg0oLnR4YeQ/zLag88="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint, diff --git a/web/workspace/.gitignore b/web/workspace/.gitignore new file mode 100644 index 00000000..89e541ca --- /dev/null +++ b/web/workspace/.gitignore @@ -0,0 +1,5 @@ +node_modules +.svelte-kit +build +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/web/workspace/README.md b/web/workspace/README.md new file mode 100644 index 00000000..3fc3b885 --- /dev/null +++ b/web/workspace/README.md @@ -0,0 +1,21 @@ +# Workspace web SPA + +This is the static SvelteKit shell for the local Yoi Workspace control plane. +It is intentionally a read-only UI bootstrap: `.yoi/tickets` and +`.yoi/objectives` remain canonical, and the Rust backend owns all business/API +semantics. + +Package manager: npm with `package-lock.json` committed. + +Commands: + +```sh +npm install +npm run check +npm run build +``` + +Build output is `web/workspace/build/` and is not checked in. Point the Rust +backend `ServerConfig.static_assets_dir` at that directory (or another static +asset directory) to serve the SPA. `node_modules/`, `.svelte-kit/`, and `build/` +are generated local state and must remain ignored/excluded from package sources. diff --git a/web/workspace/package-lock.json b/web/workspace/package-lock.json new file mode 100644 index 00000000..a218a11b --- /dev/null +++ b/web/workspace/package-lock.json @@ -0,0 +1,1673 @@ +{ + "name": "@yoi/workspace-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@yoi/workspace-web", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.9", + "@sveltejs/kit": "^2.49.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "vite": "^7.2.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.66.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.66.0.tgz", + "integrity": "sha512-7nN4Ur4+nofZ36DVo83JbRe02m61Vc+I441mML/DYa1pUTZ/x26+lbrdqPen8gjmsUc6flMtHEqAtn0UfmfvAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.1.1.tgz", + "integrity": "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.12.tgz", + "integrity": "sha512-On0QbLyaiAkVC4eXtgnXK9Kh2opit+3rcUSOc45DqJ2s/X2eXAHsGOKRSJ6IDagQEW5vPyivANfXUiqgXC67Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.56.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", + "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.11", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.6.0.tgz", + "integrity": "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "0.1.1", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/workspace/package.json b/web/workspace/package.json new file mode 100644 index 00000000..1f29694d --- /dev/null +++ b/web/workspace/package.json @@ -0,0 +1,21 @@ +{ + "name": "@yoi/workspace-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.9", + "@sveltejs/kit": "^2.49.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "vite": "^7.2.7" + } +} diff --git a/web/workspace/src/app.html b/web/workspace/src/app.html new file mode 100644 index 00000000..adf8bd87 --- /dev/null +++ b/web/workspace/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/web/workspace/src/routes/+layout.ts b/web/workspace/src/routes/+layout.ts new file mode 100644 index 00000000..89da957b --- /dev/null +++ b/web/workspace/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false; +export const prerender = true; diff --git a/web/workspace/src/routes/+page.svelte b/web/workspace/src/routes/+page.svelte new file mode 100644 index 00000000..d905974c --- /dev/null +++ b/web/workspace/src/routes/+page.svelte @@ -0,0 +1,176 @@ + + + + Yoi Workspace Control Plane + + + +
+
+

Local / single-workspace bootstrap

+

Yoi Workspace Control Plane

+

+ Static SPA shell for reading canonical .yoi project records + through bounded backend APIs. Ticket and Objective lifecycle authority stays + in the existing local record workflow. +

+
+ +
+

Workspace

+ {#if workspace} +
+
+
ID
+
{workspace.workspace_id}
+
+
+
Name
+
{workspace.display_name}
+
+
+
Record authority
+
{workspace.record_authority}
+
+
+ {:else if loadError} +

{loadError}

+ {:else} +

Waiting for /api/workspace

+ {/if} +
+ +
+
+

Read API surface

+
    + {#each endpoints as endpoint} +
  • {endpoint.path} — {endpoint.label}
  • + {/each} +
+
+ +
+

Reserved seams

+

+ Event streams and runner connections are represented as extension-point + state in the backend response, but no scheduler, write API, or hosted + multi-tenant behavior is implemented in this slice. +

+
+
+
+ + diff --git a/web/workspace/svelte.config.js b/web/workspace/svelte.config.js new file mode 100644 index 00000000..cb64985a --- /dev/null +++ b/web/workspace/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true + }) + } +}; + +export default config; diff --git a/web/workspace/tsconfig.json b/web/workspace/tsconfig.json new file mode 100644 index 00000000..43447105 --- /dev/null +++ b/web/workspace/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/web/workspace/vite.config.ts b/web/workspace/vite.config.ts new file mode 100644 index 00000000..3406f32d --- /dev/null +++ b/web/workspace/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});