diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index bc2b390..de152d7 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -56,7 +56,8 @@ modules: - id: project-tools name: 项目工具链 area: frontend - status: done + status: abandoned + note: "2026-10-04 feature-pruning B1 撤销项目级工具(project_tools 0 行);全局快启保留为 global_tools" progress: 100 position: [500, 150] @@ -271,7 +272,8 @@ modules: - id: user-project-todos name: 项目个人备忘 area: frontend - status: done + status: abandoned + note: "2026-10-04 feature-pruning B1 撤销(1 条,表保留不删)" progress: 100 position: [2000, 150] @@ -329,14 +331,16 @@ modules: - id: deploy-info name: 项目部署信息 area: frontend - status: done + status: abandoned + note: "2026-10-04 feature-pruning B1 撤销部署命令(deploy_commands 0 行)与死文件;生产/测试地址展示保留" progress: 100 position: [3000, 150] - id: host-apps name: 宿主机应用管理 area: frontend - status: done + status: abandoned + note: "2026-10-04 feature-pruning B1 撤销(server_host_apps 0 行)" progress: 100 position: [2750, 600] @@ -534,6 +538,15 @@ modules: note: "两层模型(人持有的意图目录 .blueprint/catalog.md + 蓝图正文),书装意图、指向事实;卡侧 intent 钉版,agent 编译可判定 /goal,确认卡兼作最小任务简报,回执写进提交。IC1 设计已确认(最小文件优先方案);IC2–IC4 只新增文档、可与 P8 并行,门控与接线等 P8 收口后做,IC13 按实测数据决定去留。" position: [4000, 950] + - id: feature-pruning + name: 功能留撤盘点 + area: infra + status: in_progress + progress: 40 + updated: 2026-10-04 + note: "按管家准入与退出阶段性过滤冗余功能;台账 docs/tasks/2026-10-04-feature-pruning.md。B1 撤零数据/死代码 agent_verify 通过待人终验(PR),B2 产品组退役待设计。" + position: [4250, 950] + edges: - from: project-resource-broker to: project-dev-variables diff --git a/.blueprint/modules/feature-pruning.md b/.blueprint/modules/feature-pruning.md new file mode 100644 index 0000000..738bb56 --- /dev/null +++ b/.blueprint/modules/feature-pruning.md @@ -0,0 +1,28 @@ +--- +id: feature-pruning +name: 功能留撤盘点 +status: in_progress +progress: 40 +--- + +## 概述 + +项目管理改用多维表 + 子主题之前,先按「管家准入与退出」阶段性过滤冗余功能。 +台账(证据、价值评估、批次状态)单一来源:`docs/tasks/2026-10-04-feature-pruning.md`,本文件只登记任务卡。 + +## 任务卡 + +### 🔵 B1 批次 1:撤销零数据 / 死代码功能 +- complexity: M +- status: in_progress +- files: src-tauri/src/commands/{host_apps,activity,user_todos,deploy_info,spaces,servers,projects,templates,mod}.rs, src-tauri/src/commands/project_tools.rs → global_tools.rs, src-tauri/src/commands/publish.rs → local_git.rs, src-tauri/src/models.rs, src-tauri/src/{db,lib}.rs, src/components/dashboard/{ProjectToolsModal → GlobalToolsModal,DeployModal,HostAppsPanel,UserTodoPanel,DeployInfoPanel,Dashboard,ProjectCard,ServerManagerPanel}.tsx, src/components/network/NetworkPage.tsx, src/lib/commands.ts +- acceptance: + - agent_verify: [cargo test] 无 FAILED;[pnpm typecheck] exit 0;[grep] 全仓无 get_project_tools / get_deploy_commands / get_host_apps / get_user_todos / add_activity 残留(get_server_network_config 核查后撤回:在用,移出本卡);[旧库副本] 迁移启动不报错 + - human_verify: 项目卡与服务器面板不再出现工具链、部署、宿主机应用、备忘入口,其余功能正常 +- 数据原则:不 DROP 表、不删已有行,只移除建表与读写代码 +- 进展(2026-10-04):agent_verify 前三项通过;旧库副本迁移未实测;server_network_config 核查后撤回(在用);待 PR 与人终验 + +### 📋 B2 批次 2:产品组退役 + 旧 projects 表(L,先设计) +- complexity: L +- status: todo +- 前置:接入判定改为项目 token;组→标签迁移(仅 ≥2 成员组);`list_group_projects` MCP 契约撤销(T1) diff --git a/docs/tasks/2026-10-04-feature-pruning.md b/docs/tasks/2026-10-04-feature-pruning.md new file mode 100644 index 0000000..b2033c4 --- /dev/null +++ b/docs/tasks/2026-10-04-feature-pruning.md @@ -0,0 +1,95 @@ +# 炼境功能留撤盘点(持续维护) + +> 起因(2026-10-04 用户):项目管理改用 enterprise-system 风格多维表,务实功能收进项目详情「子主题」,冗余功能去掉。 +> 本文是**阶段性过滤的单一台账**:每批处理完更新「状态」列与「处理记录」,不另开文档。 +> 数据证据是 2026-10-04 快照(本机库只读计数),易变——复核时重新计数,不信本表旧数。 + +## 0. 评估口径 + +- **A|agent 价值**:管家主线程度 / 是否有 MCP 投影。 +- **H|人用价值**:以本机库真实行数为准(只说明「有无数据」,不说明使用频率;只读查看型功能可能被低估)。 +- **C|维护成本**:代码体量与耦合面。 +- 判据对齐 AGENTS.md「管家准入与退出」:实测无收益、被替代、或维护成本超过收益 → 撤。 + +状态取值:`待处理` / `进行中` / `已完成` / `待用户判断` / `转后续`。 + +## 1. 核心保留(管家主线) + +| 模块 | A | H | C | 证据 | +|---|---|---|---|---| +| mcp-server / mcp-single-server | 高 | — | 高 | 所有 agent 能力的入口 | +| mcp-config-inject | 高 | — | 高 | 三端注入本体(mcp_inject.rs 约 4300 行) | +| onboarding-pack | 高 | 中 | 高 | 一键接入主线 | +| agent-client-capability-packs | 高 | — | 中 | 能力包 + 使者回传(envoy_dispositions 63 条) | +| toolchain-capability-pack | 高 | — | 中 | get_toolchain_status / prepare_toolchain | +| agent-access-reliability | 高 | 中 | 中 | 设置页 readiness | +| project-resource-broker | 高 | 中 | 高 | 凭证代办与交付状态 | +| gitea-integration | 高 | 中 | 高 | pr_events 362 条 | +| cicd-workflow | 高 | 中 | 中 | 4 个 MCP 工具 | +| blueprint-reader / view / governance | 高 | 高 | 高 | 任务上下文主载体 | +| blueprint-flywheel / flywheel-intelligence | 高 | 中 | 中 | commit_metrics 1840、commit_files 19105 | +| blueprint-feedback / project-mentor | 高 | 中 | 中 | project_notes 199、project_snapshots 2036 | +| runtime-diagnostics / health-center | 高 | 中 | 中 | runtime_logs 517 | +| project-mgmt / workspace-mgmt | 中 | 高 | 高 | 28 个项目、2 个空间;多维表数据源 | +| devtools-scan / project-env-scan | 中 | 中 | 中 | 工具链能力包输入(dev_tools 33) | +| git-ops / shell-launch | 低 | 高 | 低 | 开编辑器/终端等高频小功能 | +| sqlite-db / tray-service / auto-update / settings-page | — | 高 | 中 | 基础设施 | + +## 2. 合并(能力保留,换落点) + +| 模块 | 落点 | 理由 | 状态 | +|---|---|---|---| +| project-dashboard(看板) | 多维表 | 看板与管理两套视图合一;看板「分组/独立」依赖产品组 | 待处理 | +| tags-groups 中的标签 | 多维表可选字段 | 仅 1 个标签 1 条映射,留着成本低 | 待处理 | +| repo-registry | 项目档案 repo_url + Gitea | git_repos 仅 2 条,已被 gitea_repos(7)替代 | 待处理 | +| server-registry + ssh-config-sync + vscode-remote + server-software + service-config | 资源页「服务器」一页 + 项目详情子主题「部署」 | 5 个模块是同一件事(servers 2 / server_software 9 / service_configs 5) | 待处理 | +| cloud-products + cloud-docker-apps + cloud-db-instances | 「云资源」一页 | 同上(7 / 1 / 5) | 待处理 | +| agent-infrastructure | 并入接入包 | 与一键接入重叠,入口藏在蓝图弹窗 | 待处理 | +| blueprint-onboarding(提示词) | 并入接入面板 | 被一键接入取代,MCP 工具保留 | 待处理 | +| api-keys | 并入 credential_store 统一凭证存储 | 11 条真实使用;**需先核实是否明文存储** | 待处理 | + +## 3. 撤销 + +### 3a. 零数据 / 死代码(批次 1) + +| 项 | 证据 | 改动范围 | 状态 | +|---|---|---|---| +| project-tools 项目工具链 | project_tools 0 行 | 项目级 command 删除;文件改名 global_tools.rs / GlobalToolsModal(全局快启 global_tools 有 4 条,保留) | 已完成 | +| 部署命令 deploy_commands / deploy-info | deploy_commands 0 行;deploy_info.rs 未编译、DeployInfoPanel 为 `export {}` | spaces.rs 的 4 个部署 command、DeployModal 与「部署配置」按钮、两个死文件(生产/测试地址展示保留) | 已完成 | +| host-apps 宿主机应用 | server_host_apps 0 行 | host_apps.rs、HostAppsPanel、ServerManagerPanel 入口、删服务器时的级联清理 | 已完成 | +| ~~server_network_config~~ | 0 行,但 set_server_mirror 写、安装命令生成读、ServerSoftwareBlock 在调 | **撤回:属在用的镜像选择功能**,归入「合并 → 服务器」一并考虑 | 不处理 | +| activity_log / relations | 均 0 行;add_activity 无调用点,get_activity 恒为空 | activity.rs、ProjectCard「动态」按钮与活动区、get_project 去掉 recent_activities、projects.rs 删除清理 | 已完成 | +| user-project-todos 个人备忘 | 1 条(表与该行保留在库中,界面不再可见) | user_todos.rs、UserTodoPanel、ProjectCard 入口 | 已完成 | +| github-auth / github-publish 残留 | git_accounts 仅 db.rs 建表、无引用;github_client_* 设置种子 | db.rs 建表、索引与种子删除;publish.rs 改名 local_git.rs(内容是本地 git 与 WSL 路径辅助) | 已完成 | +| NetworkPage 孤儿文件 | `export {}` | 删文件 | 已完成 | + +批次 1 数据原则:**不 DROP 表、不删已有行**(可逆);只移除建表语句与读写代码,旧库残留表无害。 + +### 3b. 需先设计(批次 2,L 级) + +| 项 | 为什么不能直接删 | 状态 | +|---|---|---| +| 产品组本体 + product-group-mcp | 约 150 处引用;被当作「已接入」标记(MCP 重注入判断、健康统计、一键接入 product_group_buff 步骤);`list_group_projects` 是 MCP 契约(T1)。须先把接入判定改为项目 token、组→标签迁移(仅 ≥2 成员的组) | 转后续 | +| 旧 `projects` 表 | commit_metrics.rs 仍 LEFT JOIN;deploy_commands 外键指向它;credential_store 测试插入它 | 转后续 | + +## 4. 待用户判断 + +| 项 | 证据 | C | 倾向 | 状态 | +|---|---|---|---|---| +| wsl-proxy + v2rayn-import | 受监控工具 1 个、network_mode_log 0 | 约 1050 行 | 撤或移出炼境(与管家定位无关) | 待用户判断 | +| source-library 源码库 | 2 个项目、4 个版本、0 个分类 | 630 行 | 撤 | 待用户判断 | +| project-templates 项目模板 | 15 个模板 / 151 个文件(未区分是否为种子) | 约 2460 行 | 视实际使用 | 待用户判断 | +| GroupResourceCanvas 资源画布 | canvas_states 6 条 | 约 1370 行 | 随产品组撤 | 待用户判断 | +| claude-autonomy-config | 只读展示 beast-mode「狂暴模式」安装状态(初判「绑 Claude 违反跨 agent」不准确,已从撤销移来) | 约 80 行 + 弹窗 | 视是否还看 | 待用户判断 | + +## 5. 对多维表方案的影响 + +- 标签几乎未用(1 条),预置视图不宜以标签分组为主;优先用已有数据的字段:空间、状态、类型、技术栈、接入状态。 +- 项目详情抽屉的子主题 tab 草案:概要 / 蓝图与导师 / 交付(CI、Gitea、PR)/ 环境(工具链、开发变量)/ 部署(服务器)。 + +## 6. 处理记录 + +| 日期 | 批次 | 内容 | 证据 | +|---|---|---|---| +| 2026-10-04 | — | 建立台账;核查后更正三处初判(claude-autonomy-config 移入待判断;旧 projects 表、产品组移入批次 2) | 本会话盘点 | +| 2026-10-04 | B1 | 3a 七项完成、server_network_config 撤回;分支 feat/feature-pruning。db.rs 移除 relations/activity_log/project_tools/git_accounts/deploy_commands/user_project_todos/server_host_apps 建表及两段 FK 迁移,不 DROP 旧表 | cargo test 408 通过 0 失败;pnpm typecheck exit 0;被撤 command 全仓 grep 零残留。**未做**:旧库副本迁移实测(复制真实库的命令被拒),待人终验 | diff --git a/src-tauri/src/commands/activity.rs b/src-tauri/src/commands/activity.rs deleted file mode 100644 index 2e39acc..0000000 --- a/src-tauri/src/commands/activity.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::db::pool; -use crate::models::ActivityLog; -use rusqlite::params; - -#[tauri::command] -pub fn get_activity(project_id: String, limit: Option) -> Result, String> { - let conn = pool().get().map_err(|e| e.to_string())?; - let lim = limit.unwrap_or(20); - let mut stmt = conn - .prepare( - "SELECT * FROM activity_log WHERE project_id = ?1 ORDER BY created_at DESC LIMIT ?2", - ) - .map_err(|e| e.to_string())?; - let result = stmt - .query_map(params![project_id, lim], ActivityLog::from_row) - .map_err(|e| e.to_string())? - .collect::, _>>() - .map_err(|e| e.to_string()); - result -} - -#[tauri::command] -pub fn add_activity(project_id: String, summary: String) -> Result<(), String> { - let conn = pool().get().map_err(|e| e.to_string())?; - let exists: bool = conn - .query_row( - "SELECT 1 FROM project_workspaces WHERE id = ?1", - params![project_id], - |_| Ok(true), - ) - .unwrap_or(false); - if !exists { - return Err(format!("Project not found: {project_id}")); - } - conn.execute( - "INSERT INTO activity_log (project_id, summary, created_at) VALUES (?1, ?2, datetime('now'))", - params![project_id, summary], - ) - .map_err(|e| e.to_string())?; - conn.execute( - "UPDATE project_workspaces SET updated_at = datetime('now') WHERE id = ?1", - params![project_id], - ) - .map_err(|e| e.to_string())?; - Ok(()) -} diff --git a/src-tauri/src/commands/deploy_info.rs b/src-tauri/src/commands/deploy_info.rs deleted file mode 100644 index 6b03a30..0000000 --- a/src-tauri/src/commands/deploy_info.rs +++ /dev/null @@ -1,194 +0,0 @@ -use rusqlite::{params, Connection}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct DeployInfo { - pub id: String, - pub project_id: String, - pub server_host: Option, - pub server_port: Option, - pub username: Option, - pub password: Option, - pub ssh_key_path: Option, - pub quick_links: Option, // JSON: [{label, url}] - pub notes: Option, - pub updated_at: Option, -} - -#[derive(Debug, Deserialize)] -pub struct SaveDeployInfoInput { - pub server_host: Option, - pub server_port: Option, - pub username: Option, - pub password: Option, - pub ssh_key_path: Option, - pub quick_links: Option, - pub notes: Option, -} - -// ── 业务函数 ────────────────────────────────────────────────────────────────── - -pub fn get_info(conn: &Connection, project_id: &str) -> Option { - conn.query_row( - "SELECT id, project_id, server_host, server_port, username, password, - ssh_key_path, quick_links, notes, updated_at - FROM project_deploy_info WHERE project_id = ?1", - params![project_id], - |row| { - Ok(DeployInfo { - id: row.get(0)?, - project_id: row.get(1)?, - server_host: row.get(2)?, - server_port: row.get(3)?, - username: row.get(4)?, - password: row.get(5)?, - ssh_key_path: row.get(6)?, - quick_links: row.get(7)?, - notes: row.get(8)?, - updated_at: row.get(9)?, - }) - }, - ) - .ok() -} - -pub fn save_info( - conn: &Connection, - project_id: &str, - input: &SaveDeployInfoInput, -) -> Result { - let existing = get_info(conn, project_id); - let id = existing - .as_ref() - .map(|e| e.id.clone()) - .unwrap_or_else(|| format!("deploy-{}", uuid::Uuid::new_v4())); - - conn.execute( - "INSERT INTO project_deploy_info - (id, project_id, server_host, server_port, username, password, - ssh_key_path, quick_links, notes, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, datetime('now')) - ON CONFLICT(project_id) DO UPDATE SET - server_host = ?3, - server_port = ?4, - username = ?5, - password = ?6, - ssh_key_path = ?7, - quick_links = ?8, - notes = ?9, - updated_at = datetime('now')", - params![ - id, - project_id, - input.server_host, - input.server_port, - input.username, - input.password, - input.ssh_key_path, - input.quick_links, - input.notes, - ], - ) - .map_err(|e| e.to_string())?; - - get_info(conn, project_id).ok_or_else(|| "保存后查询失败".to_string()) -} - -// ── Tauri command 薄包装层 ──────────────────────────────────────────────────── - -#[tauri::command] -pub fn get_deploy_info(project_id: String) -> Result, String> { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - Ok(get_info(&conn, &project_id)) -} - -#[tauri::command] -pub fn save_deploy_info( - project_id: String, - input: SaveDeployInfoInput, -) -> Result { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - save_info(&conn, &project_id, &input) -} - -// ── 单元测试 ────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - use rusqlite::Connection; - - fn setup() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - conn.execute_batch( - "CREATE TABLE project_deploy_info ( - id TEXT PRIMARY KEY, - project_id TEXT NOT NULL UNIQUE, - server_host TEXT, - server_port TEXT DEFAULT '22', - username TEXT, - password TEXT, - ssh_key_path TEXT, - quick_links TEXT, - notes TEXT, - updated_at TEXT DEFAULT (datetime('now')) - );", - ) - .unwrap(); - conn - } - - #[test] - fn test_get_empty() { - let conn = setup(); - assert!(get_info(&conn, "proj-1").is_none()); - } - - #[test] - fn test_save_and_get() { - let conn = setup(); - let input = SaveDeployInfoInput { - server_host: Some("192.168.1.100".to_string()), - server_port: Some("22".to_string()), - username: Some("root".to_string()), - password: Some("secret".to_string()), - ssh_key_path: None, - quick_links: Some(r#"[{"label":"控制台","url":"https://example.com"}]"#.to_string()), - notes: Some("测试服务器".to_string()), - }; - let info = save_info(&conn, "proj-1", &input).unwrap(); - assert_eq!(info.server_host.as_deref(), Some("192.168.1.100")); - assert_eq!(info.username.as_deref(), Some("root")); - - let fetched = get_info(&conn, "proj-1").unwrap(); - assert_eq!(fetched.password.as_deref(), Some("secret")); - } - - #[test] - fn test_upsert_overwrites() { - let conn = setup(); - let input1 = SaveDeployInfoInput { - server_host: Some("1.1.1.1".to_string()), - server_port: Some("22".to_string()), - username: Some("admin".to_string()), - password: None, - ssh_key_path: None, - quick_links: None, - notes: None, - }; - save_info(&conn, "proj-1", &input1).unwrap(); - - let input2 = SaveDeployInfoInput { - server_host: Some("2.2.2.2".to_string()), - server_port: Some("2222".to_string()), - username: Some("deploy".to_string()), - password: Some("new-pass".to_string()), - ssh_key_path: None, - quick_links: None, - notes: None, - }; - let info = save_info(&conn, "proj-1", &input2).unwrap(); - assert_eq!(info.server_host.as_deref(), Some("2.2.2.2")); - assert_eq!(info.username.as_deref(), Some("deploy")); - } -} diff --git a/src-tauri/src/commands/project_tools.rs b/src-tauri/src/commands/global_tools.rs similarity index 52% rename from src-tauri/src/commands/project_tools.rs rename to src-tauri/src/commands/global_tools.rs index 4cbee09..17d3ca8 100644 --- a/src-tauri/src/commands/project_tools.rs +++ b/src-tauri/src/commands/global_tools.rs @@ -6,9 +6,8 @@ use std::process::Command as StdCommand; // ── 数据结构 ────────────────────────────────────────────────────────────────── #[derive(Debug, Serialize)] -pub struct ProjectTool { +pub struct GlobalTool { pub id: String, - pub project_id: String, pub name: String, pub icon: String, pub exe_path: String, @@ -17,7 +16,7 @@ pub struct ProjectTool { } #[derive(Debug, Deserialize)] -pub struct ProjectToolInput { +pub struct GlobalToolInput { pub name: String, pub icon: String, pub exe_path: String, @@ -25,94 +24,10 @@ pub struct ProjectToolInput { pub sort_order: Option, } -// ── Tauri 命令 ──────────────────────────────────────────────────────────────── - -/// 获取某个项目的所有工具 -#[tauri::command] -pub fn get_project_tools(project_id: String) -> Result, String> { - let conn = pool().get().map_err(|e| e.to_string())?; - let mut stmt = conn - .prepare( - "SELECT id, project_id, name, icon, exe_path, args, sort_order - FROM project_tools - WHERE project_id = ?1 - ORDER BY sort_order ASC, rowid ASC", - ) - .map_err(|e| e.to_string())?; - let rows = stmt - .query_map(params![project_id], |row| { - Ok(ProjectTool { - id: row.get(0)?, - project_id: row.get(1)?, - name: row.get(2)?, - icon: row.get(3)?, - exe_path: row.get(4)?, - args: row.get(5)?, - sort_order: row.get(6)?, - }) - }) - .map_err(|e| e.to_string())? - .collect::, _>>() - .map_err(|e| e.to_string()); - rows -} - -/// 添加工具到项目 -#[tauri::command] -pub fn add_project_tool(project_id: String, input: ProjectToolInput) -> Result { - let conn = pool().get().map_err(|e| e.to_string())?; - let id = uuid::Uuid::new_v4().to_string(); - conn.execute( - "INSERT INTO project_tools (id, project_id, name, icon, exe_path, args, sort_order) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![ - id, - project_id, - input.name, - input.icon, - input.exe_path, - input.args, - input.sort_order.unwrap_or(0), - ], - ) - .map_err(|e| e.to_string())?; - Ok(id) -} - -/// 更新工具信息 -#[tauri::command] -pub fn update_project_tool(id: String, input: ProjectToolInput) -> Result<(), String> { - let conn = pool().get().map_err(|e| e.to_string())?; - conn.execute( - "UPDATE project_tools - SET name = ?1, icon = ?2, exe_path = ?3, args = ?4, sort_order = ?5 - WHERE id = ?6", - params![ - input.name, - input.icon, - input.exe_path, - input.args, - input.sort_order.unwrap_or(0), - id, - ], - ) - .map_err(|e| e.to_string())?; - Ok(()) -} - -/// 删除项目工具 -#[tauri::command] -pub fn remove_project_tool(id: String) -> Result<(), String> { - let conn = pool().get().map_err(|e| e.to_string())?; - conn.execute("DELETE FROM project_tools WHERE id = ?1", params![id]) - .map_err(|e| e.to_string())?; - Ok(()) -} - -// ── 全局快启命令(写入 global_tools 表,无项目 FK 约束)──────────────────────── +// ── 全局快启命令(写入 global_tools 表)──────────────────────────────────────── #[tauri::command] -pub fn get_global_tools() -> Result, String> { +pub fn get_global_tools() -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -123,9 +38,8 @@ pub fn get_global_tools() -> Result, String> { .map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { - Ok(ProjectTool { + Ok(GlobalTool { id: row.get(0)?, - project_id: "__global__".to_string(), name: row.get(1)?, icon: row.get(2)?, exe_path: row.get(3)?, @@ -140,7 +54,7 @@ pub fn get_global_tools() -> Result, String> { } #[tauri::command] -pub fn add_global_tool(input: ProjectToolInput) -> Result { +pub fn add_global_tool(input: GlobalToolInput) -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let id = uuid::Uuid::new_v4().to_string(); conn.execute( @@ -160,7 +74,7 @@ pub fn add_global_tool(input: ProjectToolInput) -> Result { } #[tauri::command] -pub fn update_global_tool(id: String, input: ProjectToolInput) -> Result<(), String> { +pub fn update_global_tool(id: String, input: GlobalToolInput) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute( "UPDATE global_tools diff --git a/src-tauri/src/commands/host_apps.rs b/src-tauri/src/commands/host_apps.rs deleted file mode 100644 index 4c5f50c..0000000 --- a/src-tauri/src/commands/host_apps.rs +++ /dev/null @@ -1,171 +0,0 @@ -use rusqlite::{params, Connection}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct HostApp { - pub id: String, - pub server_id: String, - pub name: String, - pub deploy_type: String, - pub service_name: Option, - pub binary_path: Option, - pub config_path: Option, - pub log_path: Option, - pub port: Option, - pub start_cmd: Option, - pub stop_cmd: Option, - pub restart_cmd: Option, - pub status_cmd: Option, - pub notes: Option, - pub sort_order: Option, - pub created_at: Option, - pub updated_at: Option, -} - -#[derive(Debug, Deserialize)] -pub struct SaveHostAppInput { - pub name: String, - pub deploy_type: String, - pub service_name: Option, - pub binary_path: Option, - pub config_path: Option, - pub log_path: Option, - pub port: Option, - pub start_cmd: Option, - pub stop_cmd: Option, - pub restart_cmd: Option, - pub status_cmd: Option, - pub notes: Option, - pub sort_order: Option, -} - -const COLS: &str = "id, server_id, name, deploy_type, service_name, binary_path, config_path, log_path, port, start_cmd, stop_cmd, restart_cmd, status_cmd, notes, sort_order, created_at, updated_at"; - -fn row_to_app(row: &rusqlite::Row<'_>) -> rusqlite::Result { - Ok(HostApp { - id: row.get(0)?, - server_id: row.get(1)?, - name: row.get(2)?, - deploy_type: row.get(3)?, - service_name: row.get(4)?, - binary_path: row.get(5)?, - config_path: row.get(6)?, - log_path: row.get(7)?, - port: row.get(8)?, - start_cmd: row.get(9)?, - stop_cmd: row.get(10)?, - restart_cmd: row.get(11)?, - status_cmd: row.get(12)?, - notes: row.get(13)?, - sort_order: row.get(14)?, - created_at: row.get(15)?, - updated_at: row.get(16)?, - }) -} - -pub fn list_host_apps(conn: &Connection, server_id: &str) -> Vec { - let sql = format!("SELECT {COLS} FROM server_host_apps WHERE server_id = ?1 ORDER BY sort_order ASC, created_at ASC"); - let mut stmt = match conn.prepare(&sql) { - Ok(s) => s, - Err(_) => return vec![], - }; - stmt.query_map(params![server_id], row_to_app) - .map(|iter| iter.filter_map(|r| r.ok()).collect()) - .unwrap_or_default() -} - -pub fn get_host_app(conn: &Connection, id: &str) -> Option { - let sql = format!("SELECT {COLS} FROM server_host_apps WHERE id = ?1"); - conn.query_row(&sql, params![id], row_to_app).ok() -} - -pub fn create_host_app( - conn: &Connection, - server_id: &str, - input: &SaveHostAppInput, -) -> Result { - let id = format!("ha-{}", uuid::Uuid::new_v4()); - conn.execute( - "INSERT INTO server_host_apps (id, server_id, name, deploy_type, service_name, binary_path, config_path, log_path, port, start_cmd, stop_cmd, restart_cmd, status_cmd, notes, sort_order) - VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)", - params![id, server_id, input.name, input.deploy_type, input.service_name, input.binary_path, - input.config_path, input.log_path, input.port, input.start_cmd, input.stop_cmd, - input.restart_cmd, input.status_cmd, input.notes, input.sort_order.unwrap_or(0)], - ).map_err(|e| e.to_string())?; - get_host_app(conn, &id).ok_or_else(|| "创建后查询失败".into()) -} - -pub fn update_host_app( - conn: &Connection, - id: &str, - input: &SaveHostAppInput, -) -> Result { - let rows = conn - .execute( - "UPDATE server_host_apps SET name=?1, deploy_type=?2, service_name=?3, binary_path=?4, - config_path=?5, log_path=?6, port=?7, start_cmd=?8, stop_cmd=?9, restart_cmd=?10, - status_cmd=?11, notes=?12, sort_order=?13, updated_at=datetime('now') WHERE id=?14", - params![ - input.name, - input.deploy_type, - input.service_name, - input.binary_path, - input.config_path, - input.log_path, - input.port, - input.start_cmd, - input.stop_cmd, - input.restart_cmd, - input.status_cmd, - input.notes, - input.sort_order.unwrap_or(0), - id - ], - ) - .map_err(|e| e.to_string())?; - if rows == 0 { - return Err(format!("应用 {} 不存在", id)); - } - get_host_app(conn, id).ok_or_else(|| "更新后查询失败".into()) -} - -pub fn delete_host_app_fn(conn: &Connection, id: &str) -> Result<(), String> { - conn.execute("DELETE FROM server_host_apps WHERE id=?1", params![id]) - .map_err(|e| e.to_string())?; - Ok(()) -} - -pub fn delete_apps_by_server(conn: &Connection, server_id: &str) -> Result<(), String> { - conn.execute( - "DELETE FROM server_host_apps WHERE server_id=?1", - params![server_id], - ) - .map_err(|e| e.to_string())?; - Ok(()) -} - -// ── Tauri commands ─────────────────────────────────────────────────────────── - -#[tauri::command] -pub fn get_host_apps(server_id: String) -> Result, String> { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - Ok(list_host_apps(&conn, &server_id)) -} - -#[tauri::command] -pub fn add_host_app(server_id: String, input: SaveHostAppInput) -> Result { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - create_host_app(&conn, &server_id, &input) -} - -#[tauri::command] -pub fn edit_host_app(id: String, input: SaveHostAppInput) -> Result { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - update_host_app(&conn, &id, &input) -} - -#[tauri::command] -pub fn delete_host_app(id: String) -> Result<(), String> { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - delete_host_app_fn(&conn, &id) -} diff --git a/src-tauri/src/commands/publish.rs b/src-tauri/src/commands/local_git.rs similarity index 100% rename from src-tauri/src/commands/publish.rs rename to src-tauri/src/commands/local_git.rs diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index cdf11b5..a690c18 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,4 +1,3 @@ -pub mod activity; pub mod agent_infra; pub mod api_keys; pub mod blueprint; @@ -16,16 +15,15 @@ pub mod docker_apps; pub mod git_ops; pub mod git_repos; pub mod gitea; +pub mod global_tools; pub mod groups; -pub mod host_apps; +pub mod local_git; pub mod mentor; pub mod network; pub mod onboarding; pub mod project_scan; -pub mod project_tools; pub mod projects; pub mod proxy; -pub mod publish; pub mod risk; pub mod server_software; pub mod servers; @@ -38,4 +36,3 @@ pub mod ssh_config; pub mod tags; pub mod templates; pub mod toolchain; -pub mod user_todos; diff --git a/src-tauri/src/commands/projects.rs b/src-tauri/src/commands/projects.rs index b92d672..c91788f 100644 --- a/src-tauri/src/commands/projects.rs +++ b/src-tauri/src/commands/projects.rs @@ -1,7 +1,7 @@ use crate::commands::gitea::{self, RemovedRepoLink}; use crate::db::pool; use crate::mcp_inject; -use crate::models::{ActivityLog, Project}; +use crate::models::Project; use rusqlite::{params, OptionalExtension}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -93,13 +93,6 @@ pub struct UpdateProjectInput { pub startup_commands: Option, } -#[derive(Debug, Serialize)] -pub struct ProjectDetail { - #[serde(flatten)] - pub project: Project, - pub recent_activities: Vec, -} - #[tauri::command] pub fn get_projects( status: Option, @@ -156,27 +149,11 @@ pub fn get_projects( } #[tauri::command] -pub fn get_project(id: String) -> Result { +pub fn get_project(id: String) -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let sql = format!("{PROJECT_VIEW_SQL} WHERE pw.id = ?1"); - let project = conn - .query_row(&sql, params![id], Project::from_row) - .map_err(|_| format!("Project not found: {id}"))?; - let mut stmt = conn - .prepare( - "SELECT * FROM activity_log WHERE project_id = ?1 ORDER BY created_at DESC LIMIT 5", - ) - .map_err(|e| e.to_string())?; - let activities: Vec = stmt - .query_map(params![id], ActivityLog::from_row) - .map_err(|e| e.to_string())? - .collect::, _>>() - .map_err(|e| e.to_string())?; - drop(stmt); - Ok(ProjectDetail { - project, - recent_activities: activities, - }) + conn.query_row(&sql, params![id], Project::from_row) + .map_err(|_| format!("Project not found: {id}")) } // ── 登记本体(P8f)───────────────────────────────────────────────────────────── @@ -681,24 +658,13 @@ pub(crate) fn delete_project_records( let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?; // 清理关联数据(旧 FK 指向 projects 表,需手动清理) - for table in [ - "activity_log", - "project_tools", - "project_group_map", - "project_env_tools", - "deploy_commands", - ] { + for table in ["project_group_map", "project_env_tools"] { tx.execute( &format!("DELETE FROM {table} WHERE project_id = ?1"), params![id], ) .map_err(|e| e.to_string())?; } - tx.execute( - "DELETE FROM relations WHERE from_project = ?1 OR to_project = ?1", - params![id], - ) - .map_err(|e| e.to_string())?; // 不解绑的话,后台 Gitea 轮询会继续把 commit 与 PR 事件写到已删除的项目 ID 下 let removed_link = gitea::remove_repo_link_records(&tx, id)?; diff --git a/src-tauri/src/commands/servers.rs b/src-tauri/src/commands/servers.rs index 1b886af..da9386c 100644 --- a/src-tauri/src/commands/servers.rs +++ b/src-tauri/src/commands/servers.rs @@ -174,9 +174,8 @@ pub fn delete_server_fn(conn: &Connection, id: &str) -> Result<(), String> { params![id], ) .map_err(|e| e.to_string())?; - // 级联删除服务连接配置 + 宿主机应用 + // 级联删除服务连接配置(server_host_apps 已撤销,旧库残留行无害) let _ = super::service_config::delete_configs_by_server(conn, id); - let _ = super::host_apps::delete_apps_by_server(conn, id); conn.execute("DELETE FROM servers WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) diff --git a/src-tauri/src/commands/spaces.rs b/src-tauri/src/commands/spaces.rs index 02a7f43..f77dfb4 100644 --- a/src-tauri/src/commands/spaces.rs +++ b/src-tauri/src/commands/spaces.rs @@ -1,5 +1,5 @@ use crate::db; -use crate::models::{DeployCommand, Space}; +use crate::models::Space; use uuid::Uuid; // ── 空间管理 ──────────────────────────────────────────────── @@ -81,93 +81,3 @@ pub fn set_current_space(id: String) -> Result<(), String> { .map_err(|e| e.to_string())?; Ok(()) } - -// ── 部署命令管理 ──────────────────────────────────────────── - -#[tauri::command] -pub fn get_deploy_commands(project_id: String) -> Result, String> { - let conn = db::pool().get().map_err(|e| e.to_string())?; - let mut stmt = conn - .prepare("SELECT id, project_id, name, command, run_in FROM deploy_commands WHERE project_id = ?1") - .map_err(|e| e.to_string())?; - let rows = stmt - .query_map(rusqlite::params![project_id], DeployCommand::from_row) - .map_err(|e| e.to_string())?; - rows.map(|r| r.map_err(|e| e.to_string())).collect() -} - -#[tauri::command] -pub fn add_deploy_command( - project_id: String, - name: String, - command: String, - run_in: String, -) -> Result { - let conn = db::pool().get().map_err(|e| e.to_string())?; - let id = Uuid::new_v4().to_string(); - conn.execute( - "INSERT INTO deploy_commands (id, project_id, name, command, run_in) VALUES (?1, ?2, ?3, ?4, ?5)", - rusqlite::params![id, project_id, name, command, run_in], - ) - .map_err(|e| e.to_string())?; - let cmd = conn - .query_row( - "SELECT id, project_id, name, command, run_in FROM deploy_commands WHERE id = ?1", - rusqlite::params![id], - DeployCommand::from_row, - ) - .map_err(|e| e.to_string())?; - Ok(cmd) -} - -#[tauri::command] -pub fn remove_deploy_command(id: String) -> Result<(), String> { - let conn = db::pool().get().map_err(|e| e.to_string())?; - conn.execute( - "DELETE FROM deploy_commands WHERE id = ?1", - rusqlite::params![id], - ) - .map_err(|e| e.to_string())?; - Ok(()) -} - -/// 执行部署命令,返回输出内容 -#[tauri::command] -pub fn execute_deploy( - command: String, - run_in: String, - cwd: Option, -) -> Result { - let mut cmd = if run_in == "wsl" { - let mut c = crate::silent_cmd("wsl"); - c.args(["-e", "bash", "-c", &command]); - c - } else { - let mut c = crate::silent_cmd("cmd"); - c.args(["/c", &command]); - c - }; - - if let Some(dir) = cwd { - cmd.current_dir(&dir); - } - - let output = cmd.output(); - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout).to_string(); - let stderr = String::from_utf8_lossy(&out.stderr).to_string(); - if out.status.success() { - Ok(if stdout.is_empty() { - "执行完成".to_string() - } else { - stdout - }) - } else { - Err(if !stderr.is_empty() { stderr } else { stdout }) - } - } - Err(e) => Err(format!("启动进程失败: {}", e)), - } -} diff --git a/src-tauri/src/commands/templates.rs b/src-tauri/src/commands/templates.rs index 58c779a..42b9c84 100644 --- a/src-tauri/src/commands/templates.rs +++ b/src-tauri/src/commands/templates.rs @@ -220,7 +220,7 @@ pub fn create_project_from_template( } // ── 1. 计算项目路径 ─────────────────────────────────────────────────────── - let win_parent = crate::commands::publish::wsl_to_win(&parent_path); + let win_parent = crate::commands::local_git::wsl_to_win(&parent_path); let project_win_path = format!("{}\\{}", win_parent.trim_end_matches('\\'), project_name); let project_root = Path::new(&project_win_path); @@ -259,7 +259,7 @@ pub fn create_project_from_template( if let Some(cmds) = &tmpl.init_commands { let linux_project_path = if platform == "wsl" { - crate::commands::publish::extract_linux_path(&parent_path, &win_parent) + crate::commands::local_git::extract_linux_path(&parent_path, &win_parent) .map(|p| format!("{}/{}", p.trim_end_matches('/'), project_name)) } else { None diff --git a/src-tauri/src/commands/user_todos.rs b/src-tauri/src/commands/user_todos.rs deleted file mode 100644 index bc09bb2..0000000 --- a/src-tauri/src/commands/user_todos.rs +++ /dev/null @@ -1,198 +0,0 @@ -use rusqlite::{params, Connection}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserTodo { - pub id: i64, - pub project_id: String, - pub content: String, - pub done: bool, - pub sort_order: i64, - pub created_at: String, -} - -// ── 业务函数(依赖注入,可单元测试)──────────────────────────────────────────── - -pub fn get_todos(conn: &Connection, project_id: &str) -> Vec { - let mut stmt = match conn.prepare( - "SELECT id, project_id, content, done, sort_order, created_at - FROM user_project_todos - WHERE project_id = ?1 - ORDER BY sort_order ASC, id ASC", - ) { - Ok(s) => s, - Err(_) => return vec![], - }; - stmt.query_map(params![project_id], |row| { - Ok(UserTodo { - id: row.get(0)?, - project_id: row.get(1)?, - content: row.get(2)?, - done: row.get::<_, i64>(3)? != 0, - sort_order: row.get(4)?, - created_at: row.get(5)?, - }) - }) - .map(|iter| iter.filter_map(|r| r.ok()).collect()) - .unwrap_or_default() -} - -pub fn add_todo(conn: &Connection, project_id: &str, content: &str) -> Result { - let content = content.trim(); - if content.is_empty() { - return Err("内容不能为空".to_string()); - } - // sort_order = 当前最大值 + 1 - let max_order: i64 = conn - .query_row( - "SELECT COALESCE(MAX(sort_order), -1) FROM user_project_todos WHERE project_id = ?1", - params![project_id], - |row| row.get(0), - ) - .unwrap_or(-1); - - conn.execute( - "INSERT INTO user_project_todos (project_id, content, sort_order) VALUES (?1, ?2, ?3)", - params![project_id, content, max_order + 1], - ) - .map_err(|e| e.to_string())?; - - let id = conn.last_insert_rowid(); - conn.query_row( - "SELECT id, project_id, content, done, sort_order, created_at - FROM user_project_todos WHERE id = ?1", - params![id], - |row| { - Ok(UserTodo { - id: row.get(0)?, - project_id: row.get(1)?, - content: row.get(2)?, - done: row.get::<_, i64>(3)? != 0, - sort_order: row.get(4)?, - created_at: row.get(5)?, - }) - }, - ) - .map_err(|e| e.to_string()) -} - -pub fn toggle_todo(conn: &Connection, id: i64) -> Result<(), String> { - let rows = conn - .execute( - "UPDATE user_project_todos SET done = CASE WHEN done = 0 THEN 1 ELSE 0 END WHERE id = ?1", - params![id], - ) - .map_err(|e| e.to_string())?; - if rows == 0 { - return Err(format!("todo {} 不存在", id)); - } - Ok(()) -} - -pub fn delete_todo(conn: &Connection, id: i64) -> Result<(), String> { - conn.execute("DELETE FROM user_project_todos WHERE id = ?1", params![id]) - .map_err(|e| e.to_string())?; - Ok(()) -} - -// ── Tauri command 薄包装层(仅负责获取连接)──────────────────────────────────── - -#[tauri::command] -pub fn get_user_todos(project_id: String) -> Result, String> { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - Ok(get_todos(&conn, &project_id)) -} - -#[tauri::command] -pub fn add_user_todo(project_id: String, content: String) -> Result { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - add_todo(&conn, &project_id, &content) -} - -#[tauri::command] -pub fn toggle_user_todo(id: i64) -> Result<(), String> { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - toggle_todo(&conn, id) -} - -#[tauri::command] -pub fn delete_user_todo(id: i64) -> Result<(), String> { - let conn = crate::db::pool().get().map_err(|e| e.to_string())?; - delete_todo(&conn, id) -} - -// ── 单元测试 ─────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - use rusqlite::Connection; - - fn setup() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - conn.execute_batch( - "CREATE TABLE user_project_todos ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id TEXT NOT NULL, - content TEXT NOT NULL, - done INTEGER NOT NULL DEFAULT 0, - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT DEFAULT (datetime('now', 'localtime')) - );", - ) - .unwrap(); - conn - } - - #[test] - fn test_add_and_get() { - let conn = setup(); - add_todo(&conn, "proj-1", "买牛奶").unwrap(); - add_todo(&conn, "proj-1", "写测试").unwrap(); - let todos = get_todos(&conn, "proj-1"); - assert_eq!(todos.len(), 2); - assert_eq!(todos[0].content, "买牛奶"); - assert!(!todos[0].done); - } - - #[test] - fn test_empty_content_rejected() { - let conn = setup(); - assert!(add_todo(&conn, "proj-1", " ").is_err()); - } - - #[test] - fn test_toggle() { - let conn = setup(); - let todo = add_todo(&conn, "proj-1", "测试条目").unwrap(); - toggle_todo(&conn, todo.id).unwrap(); - let todos = get_todos(&conn, "proj-1"); - assert!(todos[0].done); - toggle_todo(&conn, todo.id).unwrap(); - let todos = get_todos(&conn, "proj-1"); - assert!(!todos[0].done); - } - - #[test] - fn test_delete() { - let conn = setup(); - let todo = add_todo(&conn, "proj-1", "待删除").unwrap(); - delete_todo(&conn, todo.id).unwrap(); - assert!(get_todos(&conn, "proj-1").is_empty()); - } - - #[test] - fn test_get_empty_returns_empty() { - let conn = setup(); - assert!(get_todos(&conn, "不存在的项目").is_empty()); - } - - #[test] - fn test_sort_order_increments() { - let conn = setup(); - add_todo(&conn, "proj-1", "第一").unwrap(); - add_todo(&conn, "proj-1", "第二").unwrap(); - let todos = get_todos(&conn, "proj-1"); - assert!(todos[0].sort_order < todos[1].sort_order); - } -} diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index ba8c40c..97907da 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -63,36 +63,11 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { PRIMARY KEY (project_id, group_id) ); - CREATE TABLE IF NOT EXISTS relations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_project TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - to_project TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - relation_type TEXT, - note TEXT - ); - - CREATE TABLE IF NOT EXISTS activity_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - summary TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')) - ); - CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS project_tools ( - id TEXT PRIMARY KEY, - project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - name TEXT NOT NULL, - icon TEXT NOT NULL DEFAULT '🔧', - exe_path TEXT NOT NULL, - args TEXT, - sort_order INTEGER DEFAULT 0 - ); - CREATE TABLE IF NOT EXISTS dev_tools ( id TEXT PRIMARY KEY, name TEXT NOT NULL, @@ -131,8 +106,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { INSERT OR IGNORE INTO settings (key, value) VALUES ('editor', 'cursor'); INSERT OR IGNORE INTO settings (key, value) VALUES ('wsl_distro', 'Ubuntu'); INSERT OR IGNORE INTO settings (key, value) VALUES ('dev_mode', '0'); - INSERT OR IGNORE INTO settings (key, value) VALUES ('github_client_id', ''); - INSERT OR IGNORE INTO settings (key, value) VALUES ('github_client_secret', ''); INSERT OR IGNORE INTO settings (key, value) VALUES ('mcp_port', '27190'); ", ) @@ -147,14 +120,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { // 增量迁移:Git 多空间功能所需新表 conn.execute_batch( " - CREATE TABLE IF NOT EXISTS git_accounts ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - avatar_url TEXT, - provider TEXT NOT NULL DEFAULT 'github', - created_at TEXT DEFAULT (datetime('now')) - ); - CREATE TABLE IF NOT EXISTS spaces ( id TEXT PRIMARY KEY, name TEXT NOT NULL, @@ -162,24 +127,10 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { is_current INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); - - CREATE TABLE IF NOT EXISTS deploy_commands ( - id TEXT PRIMARY KEY, - project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - name TEXT NOT NULL, - command TEXT NOT NULL, - run_in TEXT NOT NULL DEFAULT 'windows' - ); ", ) .expect("migrate git spaces"); - // 增量迁移:git_accounts 补 UNIQUE 索引(已存在则忽略) - let _ = conn.execute( - "CREATE UNIQUE INDEX IF NOT EXISTS idx_git_accounts_username ON git_accounts(username)", - [], - ); - // 增量迁移:projects 表新增 Git 相关字段(已存在则忽略) let _ = conn.execute("ALTER TABLE projects ADD COLUMN space_id TEXT", []); let _ = conn.execute("ALTER TABLE projects ADD COLUMN repo_url TEXT", []); @@ -341,77 +292,7 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { .expect("migrate project_group_map FK to project_workspaces"); } - // 迁移 project_tools / activity_log / project_env_tools: - // 将外键从 projects(id) 改为 project_workspaces(id) - let pt_sql: Option = conn - .query_row( - "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_tools'", - [], - |row| row.get(0), - ) - .ok(); - if pt_sql - .as_deref() - .map(|s| s.contains("REFERENCES projects")) - .unwrap_or(false) - { - conn.execute_batch( - " - PRAGMA foreign_keys=OFF; - CREATE TABLE project_tools_new ( - id TEXT PRIMARY KEY, - project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE, - name TEXT NOT NULL, - icon TEXT NOT NULL DEFAULT '🔧', - exe_path TEXT NOT NULL, - args TEXT, - sort_order INTEGER DEFAULT 0 - ); - INSERT OR IGNORE INTO project_tools_new - SELECT t.id, t.project_id, t.name, t.icon, t.exe_path, t.args, t.sort_order - FROM project_tools t - INNER JOIN project_workspaces pw ON pw.id = t.project_id; - DROP TABLE project_tools; - ALTER TABLE project_tools_new RENAME TO project_tools; - PRAGMA foreign_keys=ON; - ", - ) - .expect("migrate project_tools FK to project_workspaces"); - } - - let al_sql: Option = conn - .query_row( - "SELECT sql FROM sqlite_master WHERE type='table' AND name='activity_log'", - [], - |row| row.get(0), - ) - .ok(); - if al_sql - .as_deref() - .map(|s| s.contains("REFERENCES projects")) - .unwrap_or(false) - { - conn.execute_batch( - " - PRAGMA foreign_keys=OFF; - CREATE TABLE activity_log_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE, - summary TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')) - ); - INSERT OR IGNORE INTO activity_log_new (id, project_id, summary, created_at) - SELECT a.id, a.project_id, a.summary, a.created_at - FROM activity_log a - INNER JOIN project_workspaces pw ON pw.id = a.project_id; - DROP TABLE activity_log; - ALTER TABLE activity_log_new RENAME TO activity_log; - PRAGMA foreign_keys=ON; - ", - ) - .expect("migrate activity_log FK to project_workspaces"); - } - + // 迁移 project_env_tools:将外键从 projects(id) 改为 project_workspaces(id) let pet_sql: Option = conn .query_row( "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_env_tools'", @@ -574,21 +455,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { ) .expect("create envoy_dispositions table"); - // 用户项目个人备忘(自由文本待办,Claude 不感知) - conn.execute_batch( - " - CREATE TABLE IF NOT EXISTS user_project_todos ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id TEXT NOT NULL, - content TEXT NOT NULL, - done INTEGER NOT NULL DEFAULT 0, - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT DEFAULT (datetime('now', 'localtime')) - ); - ", - ) - .expect("create user_project_todos table"); - // 蓝图 Buff:记录哪些项目施加了 buff conn.execute_batch( " @@ -876,32 +742,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) { .expect("create api_keys table"); let _ = conn.execute("ALTER TABLE api_keys ADD COLUMN credential_ref TEXT", []); - // 增量迁移:宿主机部署应用 - conn.execute_batch( - " - CREATE TABLE IF NOT EXISTS server_host_apps ( - id TEXT PRIMARY KEY, - server_id TEXT NOT NULL, - name TEXT NOT NULL, - deploy_type TEXT NOT NULL DEFAULT 'systemd', - service_name TEXT, - binary_path TEXT, - config_path TEXT, - log_path TEXT, - port TEXT, - start_cmd TEXT, - stop_cmd TEXT, - restart_cmd TEXT, - status_cmd TEXT, - notes TEXT, - sort_order INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - ", - ) - .expect("create server_host_apps table"); - // ── 源码库 ────────────────────────────────────────────────────────── conn.execute_batch( " diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5b162fd..391dcb7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -120,7 +120,6 @@ pub(crate) fn silent_cmd(program: &str) -> std::process::Command { } use commands::{ - activity::*, blueprint::*, buff::{apply_blueprint_buff, get_buff_status, list_buffed_projects, remove_blueprint_buff}, canvas::*, @@ -129,7 +128,9 @@ use commands::{ devtools::*, git_ops::*, git_repos::*, + global_tools::*, groups::*, + local_git::*, mentor::{ add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes, get_startup_alerts, run_startup_health_scan, @@ -139,13 +140,11 @@ use commands::{ test_wsl_proxy_ip, }, project_scan::*, - project_tools::*, projects::{ batch_create_projects, create_project, delete_project, get_project, get_projects, scan_subfolders, update_project, update_project_tags, }, proxy::*, - publish::*, settings::{ check_mcp_health, detect_editors, get_agent_autostart_status, get_agent_runtime_status, get_settings, run_health_check, search_editor_path, set_agent_autostart, update_settings, @@ -306,9 +305,6 @@ pub fn run() { delete_project, scan_subfolders, batch_create_projects, - // activity - get_activity, - add_activity, // groups get_groups, create_group, @@ -328,13 +324,8 @@ pub fn run() { get_agent_autostart_status, set_agent_autostart, run_health_check, - // project tools - get_project_tools, - add_project_tool, - update_project_tool, - remove_project_tool, + // global quick launch tools launch_tool, - // global quick launch get_global_tools, add_global_tool, update_global_tool, @@ -383,10 +374,6 @@ pub fn run() { update_space, delete_space, set_current_space, - get_deploy_commands, - add_deploy_command, - remove_deploy_command, - execute_deploy, // git repos registry get_git_repos, import_git_repos, @@ -462,11 +449,6 @@ pub fn run() { add_project_note, get_group_mentor_report, get_startup_alerts, - // user todos - commands::user_todos::get_user_todos, - commands::user_todos::add_user_todo, - commands::user_todos::toggle_user_todo, - commands::user_todos::delete_user_todo, // servers commands::servers::get_servers, commands::servers::add_server, @@ -512,11 +494,6 @@ pub fn run() { commands::dev_variables::save_project_test_account, commands::dev_variables::reveal_project_dev_variable, commands::dev_variables::revoke_project_dev_variable, - // host apps - commands::host_apps::get_host_apps, - commands::host_apps::add_host_app, - commands::host_apps::edit_host_app, - commands::host_apps::delete_host_app, // server service configs commands::service_config::get_service_configs, commands::service_config::add_service_config, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 8de3d9a..77524f5 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -70,25 +70,6 @@ impl Project { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ActivityLog { - pub id: i64, - pub project_id: String, - pub summary: String, - pub created_at: Option, -} - -impl ActivityLog { - pub fn from_row(row: &Row<'_>) -> rusqlite::Result { - Ok(Self { - id: row.get("id")?, - project_id: row.get("project_id")?, - summary: row.get("summary")?, - created_at: row.get("created_at")?, - }) - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ProductGroup { pub id: String, @@ -146,27 +127,6 @@ impl Space { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct DeployCommand { - pub id: String, - pub project_id: String, - pub name: String, - pub command: String, - pub run_in: String, -} - -impl DeployCommand { - pub fn from_row(row: &Row<'_>) -> rusqlite::Result { - Ok(Self { - id: row.get("id")?, - project_id: row.get("project_id")?, - name: row.get("name")?, - command: row.get("command")?, - run_in: row.get("run_in")?, - }) - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CicdConfig { pub id: String, diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index f955af6..105f178 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -1,14 +1,13 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query"; -import { getGroups, getGroupMaps, getProjects, getSpaces, getTags, getAllProjectTagMaps, getGlobalTools, addGlobalTool, updateGlobalTool, removeGlobalTool, launchTool, type ProductGroup, type Project, type ProjectToolInput } from "../../lib/commands"; +import { getGroups, getGroupMaps, getProjects, getSpaces, getTags, getAllProjectTagMaps, getGlobalTools, launchTool, type ProductGroup, type Project } from "../../lib/commands"; import { ProjectCard } from "./ProjectCard"; -import { DeployModal } from "./DeployModal"; import { GroupResourceCanvas } from "./GroupResourceCanvas"; import { ManagePage } from "../manage/ManagePage"; import { BatchAddModal } from "./BatchAddModal"; import { MountRepoModal } from "./MountRepoModal"; -import { ProjectToolsModal } from "./ProjectToolsModal"; +import { GlobalToolsModal, GLOBAL_TOOLS_QUERY_KEY } from "./GlobalToolsModal"; import { StatusBadge } from "../ui/Badge"; import { openUrl } from "@tauri-apps/plugin-opener"; import { useUIStore } from "../../store/ui"; @@ -197,7 +196,6 @@ interface GroupBoardSectionProps { } function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) { - const [deployTarget, setDeployTarget] = useState(null); const [groupView, setGroupView] = useState("default"); const winMembers = members.filter(({ project: p }) => p.platform !== "wsl"); @@ -266,7 +264,6 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) { key={project.id} project={project} role={role ?? undefined} - onDeploy={setDeployTarget} /> )) :

暂无

@@ -283,7 +280,6 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) { key={project.id} project={project} role={role ?? undefined} - onDeploy={setDeployTarget} /> )) :

暂无

@@ -292,15 +288,6 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) { )} - {deployTarget && ( - setDeployTarget(null)} - /> - )} ); } @@ -308,11 +295,10 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) { /* ── ProjectRow ────────────────────────────────────────────────────────────── */ function ProjectRow({ - project: p, role, onDeploy, + project: p, role, }: { project: Project; role?: string; - onDeploy: (p: Project) => void; }) { return (
@@ -388,16 +374,6 @@ function ProjectRow({ {!p.prod_url && !p.staging_url && !p.server_note && !p.recent_focus && (

未配置部署信息

)} - - {/* 部署配置按钮 */} -
- -
); @@ -405,22 +381,13 @@ function ProjectRow({ /* ── GlobalQuickLaunchBar ──────────────────────────────────────────────────── */ -const GLOBAL_QUERY_KEY = ["globalTools"]; - -const globalToolCmds = { - getTools: () => getGlobalTools(), - addTool: (input: ProjectToolInput) => addGlobalTool(input), - updateTool: (id: string, input: ProjectToolInput) => updateGlobalTool(id, input), - removeTool: (id: string) => removeGlobalTool(id), -}; - function GlobalQuickLaunchBar() { const showToast = useUIStore((s) => s.showToast); const [showMgr, setShowMgr] = useState(false); const [mgrDefaultAdd, setMgrDefaultAdd] = useState(false); const { data: tools = [] } = useQuery({ - queryKey: GLOBAL_QUERY_KEY, + queryKey: GLOBAL_TOOLS_QUERY_KEY, queryFn: () => getGlobalTools(), staleTime: 30000, }); @@ -477,12 +444,8 @@ function GlobalQuickLaunchBar() { {showMgr && ( - { setShowMgr(false); setMgrDefaultAdd(false); }} /> )} @@ -497,7 +460,6 @@ function StandaloneBoardSection({ }: { standalone: Project[]; }) { - const [deployTarget, setDeployTarget] = useState(null); const [selectedTagIds, setSelectedTagIds] = useState>(new Set()); const [search, setSearch] = useState(""); @@ -585,7 +547,7 @@ function StandaloneBoardSection({ {winProjects.length > 0 ? winProjects.map((p) => ( - + )) :

暂无

} @@ -597,21 +559,12 @@ function StandaloneBoardSection({ {wslProjects.length > 0 ? wslProjects.map((p) => ( - + )) :

暂无

} - {deployTarget && ( - setDeployTarget(null)} - /> - )} ); } diff --git a/src/components/dashboard/DeployInfoPanel.tsx b/src/components/dashboard/DeployInfoPanel.tsx deleted file mode 100644 index 05ca560..0000000 --- a/src/components/dashboard/DeployInfoPanel.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// 已废弃,部署信息已迁移到 ServerManagerPanel + ProjectServerPanel -export {}; diff --git a/src/components/dashboard/DeployModal.tsx b/src/components/dashboard/DeployModal.tsx deleted file mode 100644 index 1a463ff..0000000 --- a/src/components/dashboard/DeployModal.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useState } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { - getDeployCommands, - addDeployCommand, - removeDeployCommand, - executeDeploy, - type DeployCommand, -} from "../../lib/commands"; -import { useUIStore } from "../../store/ui"; - -interface Props { - projectId: string; - projectName: string; - projectPath?: string; // Windows 路径(win_path) - projectWslPath?: string; // WSL 路径(wsl_path),作为 WSL 命令的 fallback cwd - onClose: () => void; -} - -export function DeployModal({ projectId, projectName, projectPath, projectWslPath, onClose }: Props) { - const queryClient = useQueryClient(); - const showToast = useUIStore((s) => s.showToast); - - const [newName, setNewName] = useState(""); - const [newCmd, setNewCmd] = useState(""); - const [newRunIn, setNewRunIn] = useState<"windows" | "wsl">("windows"); - - // 正在执行的命令 id - const [runningId, setRunningId] = useState(null); - // 执行输出 - const [output, setOutput] = useState<{ id: string; text: string; ok: boolean } | null>(null); - - const { data: commands = [] } = useQuery({ - queryKey: ["deployCommands", projectId], - queryFn: () => getDeployCommands(projectId), - }); - - const addMutation = useMutation({ - mutationFn: () => addDeployCommand(projectId, newName, newCmd, newRunIn), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["deployCommands", projectId] }); - setNewName(""); - setNewCmd(""); - }, - onError: (e) => showToast(`添加失败: ${e}`), - }); - - const removeMutation = useMutation({ - mutationFn: (id: string) => removeDeployCommand(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["deployCommands", projectId] }), - }); - - async function handleRun(cmd: DeployCommand) { - setRunningId(cmd.id); - setOutput(null); - try { - // WSL 命令优先用 projectPath(Windows 路径,WSL 自动映射 /mnt/...) - // 若无 win_path,则将 wsl_path 通过 `cd && ` 前缀传入命令 - let effectiveCommand = cmd.command; - let cwd: string | undefined = projectPath; - if (cmd.run_in === "wsl" && !projectPath && projectWslPath) { - effectiveCommand = `cd ${JSON.stringify(projectWslPath)} && ${cmd.command}`; - cwd = undefined; - } - const result = await executeDeploy(effectiveCommand, cmd.run_in, cwd); - setOutput({ id: cmd.id, text: result, ok: true }); - } catch (e) { - setOutput({ id: cmd.id, text: String(e), ok: false }); - } finally { - setRunningId(null); - } - } - - const canAdd = newName.trim() && newCmd.trim(); - - return ( -
-
- {/* 标题 */} -
-
-

部署命令

-

{projectName}

-
- -
- -
- {/* 已有命令列表 */} - {commands.length === 0 && ( -

暂无部署命令

- )} - {commands.map((cmd) => ( -
-
- {cmd.name} - - {cmd.run_in.toUpperCase()} - - - -
- - {cmd.command} - - {/* 执行输出 */} - {output?.id === cmd.id && ( -
- {output.text} -
- )} -
- ))} -
- - {/* 添加新命令 */} -
-

添加命令

-
- setNewName(e.target.value)} - className="flex-1 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-600" - /> - -
-
- setNewCmd(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && canAdd && addMutation.mutate()} - className="flex-1 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-600 font-mono" - /> - -
-
-
-
- ); -} diff --git a/src/components/dashboard/ProjectToolsModal.tsx b/src/components/dashboard/GlobalToolsModal.tsx similarity index 84% rename from src/components/dashboard/ProjectToolsModal.tsx rename to src/components/dashboard/GlobalToolsModal.tsx index 61898e5..a6911ab 100644 --- a/src/components/dashboard/ProjectToolsModal.tsx +++ b/src/components/dashboard/GlobalToolsModal.tsx @@ -2,56 +2,38 @@ import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { open } from "@tauri-apps/plugin-dialog"; import { - getProjectTools, - addProjectTool, - updateProjectTool, - removeProjectTool, - type ProjectTool, - type ProjectToolInput, + getGlobalTools, + addGlobalTool, + updateGlobalTool, + removeGlobalTool, + type GlobalTool, + type GlobalToolInput, } from "../../lib/commands"; - -export interface ToolCommands { - getTools: () => Promise; - addTool: (input: ProjectToolInput) => Promise; - updateTool: (id: string, input: ProjectToolInput) => Promise; - removeTool: (id: string) => Promise; -} import { useUIStore } from "../../store/ui"; import { Dialog } from "../ui/Dialog"; +export const GLOBAL_TOOLS_QUERY_KEY = ["globalTools"]; + const COMMON_ICONS = ["🔧", "🎬", "🎨", "📷", "🗄️", "📮", "🌐", "📊", "🎵", "📐", "🔬", "⚙️", "📁", "🖨️", "🎮"]; -const EMPTY: ProjectToolInput = { name: "", icon: "🔧", exe_path: "", args: "" }; +const EMPTY: GlobalToolInput = { name: "", icon: "🔧", exe_path: "", args: "" }; interface Props { - projectId: string; - projectName: string; onClose: () => void; defaultOpenAdd?: boolean; - commands?: ToolCommands; - queryKey?: unknown[]; } -export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpenAdd, commands, queryKey }: Props) { +export function GlobalToolsModal({ onClose, defaultOpenAdd }: Props) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); - const cmds: ToolCommands = commands ?? { - getTools: () => getProjectTools(projectId), - addTool: (input) => addProjectTool(projectId, input), - updateTool: (id, input) => updateProjectTool(id, input), - removeTool: (id) => removeProjectTool(id), - }; - - const resolvedKey = queryKey ?? ["projectTools", projectId]; - const { data: tools = [], refetch } = useQuery({ - queryKey: resolvedKey, - queryFn: () => cmds.getTools(), + queryKey: GLOBAL_TOOLS_QUERY_KEY, + queryFn: () => getGlobalTools(), }); - const [editing, setEditing] = useState(null); // null = 新增模式 - const [form, setForm] = useState(EMPTY); + const [editing, setEditing] = useState(null); // null = 新增模式 + const [form, setForm] = useState(EMPTY); const [showForm, setShowForm] = useState(defaultOpenAdd ?? false); const [saving, setSaving] = useState(false); @@ -61,13 +43,13 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen setShowForm(true); }; - const openEdit = (tool: ProjectTool) => { + const openEdit = (tool: GlobalTool) => { setEditing(tool); setForm({ name: tool.name, icon: tool.icon, exe_path: tool.exe_path, args: tool.args ?? "" }); setShowForm(true); }; - const set = (k: keyof ProjectToolInput, v: string) => + const set = (k: keyof GlobalToolInput, v: string) => setForm((f) => ({ ...f, [k]: v })); const pickExe = async () => { @@ -93,18 +75,18 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen if (!form.exe_path.trim()) { showToast("❌ 程序路径不能为空"); return; } setSaving(true); try { - const input: ProjectToolInput = { + const input: GlobalToolInput = { ...form, args: form.args?.trim() || undefined, sort_order: editing?.sort_order ?? tools.length, }; if (editing) { - await cmds.updateTool(editing.id, input); + await updateGlobalTool(editing.id, input); } else { - await cmds.addTool(input); + await addGlobalTool(input); } await refetch(); - qc.invalidateQueries({ queryKey: resolvedKey }); + qc.invalidateQueries({ queryKey: GLOBAL_TOOLS_QUERY_KEY }); setShowForm(false); showToast(editing ? "已更新 ✓" : "已添加 ✓"); } catch (e) { @@ -116,9 +98,9 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen const handleDelete = async (id: string) => { try { - await cmds.removeTool(id); + await removeGlobalTool(id); await refetch(); - qc.invalidateQueries({ queryKey: resolvedKey }); + qc.invalidateQueries({ queryKey: GLOBAL_TOOLS_QUERY_KEY }); } catch (e) { showToast(`❌ ${e}`); } @@ -127,7 +109,7 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200"; return ( - +
{/* 工具列表 */} diff --git a/src/components/dashboard/HostAppsPanel.tsx b/src/components/dashboard/HostAppsPanel.tsx deleted file mode 100644 index 4c7937b..0000000 --- a/src/components/dashboard/HostAppsPanel.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import { useState } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { - getHostApps, addHostApp, editHostApp, deleteHostApp, - type HostApp, type SaveHostAppInput, -} from "../../lib/commands"; - -const DEPLOY_TYPES = [ - { value: "systemd", label: "systemd 服务", color: "bg-green-100 text-green-700 border-green-200" }, - { value: "pm2", label: "PM2", color: "bg-blue-100 text-blue-700 border-blue-200" }, - { value: "supervisor", label: "Supervisor", color: "bg-purple-100 text-purple-700 border-purple-200" }, - { value: "screen", label: "Screen/Tmux", color: "bg-cyan-100 text-cyan-700 border-cyan-200" }, - { value: "manual", label: "手动启动", color: "bg-gray-100 text-gray-600 border-gray-200" }, -]; - -function getTypeStyle(t: string) { - return DEPLOY_TYPES.find((d) => d.value === t)?.color ?? DEPLOY_TYPES[DEPLOY_TYPES.length - 1].color; -} -function getTypeLabel(t: string) { - return DEPLOY_TYPES.find((d) => d.value === t)?.label ?? t; -} - -function CopyBtn({ value }: { value: string }) { - const [copied, setCopied] = useState(false); - const handleCopy = async () => { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - }; - return ( - - ); -} - -function CmdRow({ label, value }: { label: string; value: string }) { - return ( -
- {label} - $ {value} - - - -
- ); -} - -// ── 模态框表单 ──────────────────────────────────────────────────────────────── - -function HostAppModal({ initial, onSave, onClose, saving }: { - initial?: HostApp; - onSave: (input: SaveHostAppInput) => void; - onClose: () => void; - saving: boolean; -}) { - const [name, setName] = useState(initial?.name ?? ""); - const [deployType, setDeployType] = useState(initial?.deploy_type ?? "systemd"); - const [serviceName, setServiceName] = useState(initial?.service_name ?? ""); - const [binaryPath, setBinaryPath] = useState(initial?.binary_path ?? ""); - const [configPath, setConfigPath] = useState(initial?.config_path ?? ""); - const [logPath, setLogPath] = useState(initial?.log_path ?? ""); - const [port, setPort] = useState(initial?.port ?? ""); - const [startCmd, setStartCmd] = useState(initial?.start_cmd ?? ""); - const [stopCmd, setStopCmd] = useState(initial?.stop_cmd ?? ""); - const [restartCmd, setRestartCmd] = useState(initial?.restart_cmd ?? ""); - const [statusCmd, setStatusCmd] = useState(initial?.status_cmd ?? ""); - const [notes, setNotes] = useState(initial?.notes ?? ""); - - // systemd 时自动填充命令 - const autoFillSystemd = (svc: string) => { - setServiceName(svc); - if (deployType === "systemd" && svc.trim()) { - const s = svc.trim(); - if (!startCmd) setStartCmd(`systemctl start ${s}`); - if (!stopCmd) setStopCmd(`systemctl stop ${s}`); - if (!restartCmd) setRestartCmd(`systemctl restart ${s}`); - if (!statusCmd) setStatusCmd(`systemctl status ${s}`); - } - }; - - const handleSubmit = () => { - if (!name.trim()) return; - onSave({ - name: name.trim(), - deploy_type: deployType, - service_name: serviceName.trim() || undefined, - binary_path: binaryPath.trim() || undefined, - config_path: configPath.trim() || undefined, - log_path: logPath.trim() || undefined, - port: port.trim() || undefined, - start_cmd: startCmd.trim() || undefined, - stop_cmd: stopCmd.trim() || undefined, - restart_cmd: restartCmd.trim() || undefined, - status_cmd: statusCmd.trim() || undefined, - notes: notes.trim() || undefined, - }); - }; - - const isEdit = !!initial; - - return ( -
-
e.stopPropagation()}> -
-

{isEdit ? "编辑宿主机应用" : "添加宿主机应用"}

- -
- -
-
- - setName(e.target.value)} placeholder="例: Nginx" - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300" /> - - - - autoFillSystemd(e.target.value)} - placeholder={deployType === "systemd" ? "例: nginx" : "例: app-name"} - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> - - setPort(e.target.value)} placeholder="例: 8080" - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono w-28" /> - - setBinaryPath(e.target.value)} placeholder="/usr/bin/..." - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> - - setConfigPath(e.target.value)} placeholder="/etc/..." - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> - - setLogPath(e.target.value)} placeholder="/var/log/..." - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> -
- - {/* 管理命令 */} -
- 管理命令 -
- - setStartCmd(e.target.value)} placeholder="systemctl start ..." - className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> - - setStopCmd(e.target.value)} placeholder="systemctl stop ..." - className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> - - setRestartCmd(e.target.value)} placeholder="systemctl restart ..." - className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> - - setStatusCmd(e.target.value)} placeholder="systemctl status ..." - className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" /> -
-
- -
- - setNotes(e.target.value)} placeholder="用途说明" - className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300" /> -
-
- -
- - -
-
-
- ); -} - -// ── 应用卡片 ────────────────────────────────────────────────────────────────── - -function HostAppCard({ app, onEdit, onDelete }: { - app: HostApp; - onEdit: () => void; - onDelete: () => void; -}) { - const [confirmDelete, setConfirmDelete] = useState(false); - - const infos = [ - app.service_name && { label: "服务名", value: app.service_name }, - app.port && { label: "端口", value: app.port }, - app.binary_path && { label: "程序", value: app.binary_path }, - app.config_path && { label: "配置", value: app.config_path }, - app.log_path && { label: "日志", value: app.log_path }, - ].filter(Boolean) as { label: string; value: string }[]; - - const cmds = [ - app.start_cmd && { label: "启动", value: app.start_cmd }, - app.stop_cmd && { label: "停止", value: app.stop_cmd }, - app.restart_cmd && { label: "重启", value: app.restart_cmd }, - app.status_cmd && { label: "状态", value: app.status_cmd }, - ].filter(Boolean) as { label: string; value: string }[]; - - return ( -
- {/* 标题 */} -
- - {getTypeLabel(app.deploy_type)} - - {app.name} - {app.port && :{app.port}} - {app.notes && {app.notes}} - - {!confirmDelete ? ( - - ) : ( -
- - -
- )} -
- -
- {/* 基础信息 */} - {infos.length > 0 && ( -
- {infos.map((info, idx) => ( -
- {info.label} - {info.value} - - - -
- ))} -
- )} - - {/* 管理命令 */} - {cmds.length > 0 && ( -
- {cmds.map((cmd, idx) => ( - - ))} -
- )} -
-
- ); -} - -// ── 主面板 ──────────────────────────────────────────────────────────────────── - -export function HostAppsPanel({ serverId }: { serverId: string }) { - const qc = useQueryClient(); - const [showModal, setShowModal] = useState(false); - const [editingApp, setEditingApp] = useState(null); - - const { data: apps = [] } = useQuery({ - queryKey: ["host-apps", serverId], - queryFn: () => getHostApps(serverId), - }); - - const addMut = useMutation({ - mutationFn: (input: SaveHostAppInput) => addHostApp(serverId, input), - onSuccess: () => { qc.invalidateQueries({ queryKey: ["host-apps", serverId] }); setShowModal(false); }, - }); - - const editMut = useMutation({ - mutationFn: ({ id, input }: { id: string; input: SaveHostAppInput }) => editHostApp(id, input), - onSuccess: () => { qc.invalidateQueries({ queryKey: ["host-apps", serverId] }); setEditingApp(null); }, - }); - - const deleteMut = useMutation({ - mutationFn: (id: string) => deleteHostApp(id), - onSuccess: () => qc.invalidateQueries({ queryKey: ["host-apps", serverId] }), - }); - - return ( -
-
-

宿主机应用

- -
- -
- {apps.map((app) => ( - setEditingApp(app)} - onDelete={() => deleteMut.mutate(app.id)} - /> - ))} - - {apps.length === 0 && ( -

- 暂无宿主机应用,点击「+ 添加」记录 systemd 服务、PM2 进程等 -

- )} -
- - {showModal && ( - addMut.mutate(input)} - onClose={() => setShowModal(false)} - saving={addMut.isPending} - /> - )} - - {editingApp && ( - editMut.mutate({ id: editingApp.id, input })} - onClose={() => setEditingApp(null)} - saving={editMut.isPending} - /> - )} -
- ); -} diff --git a/src/components/dashboard/ProjectCard.tsx b/src/components/dashboard/ProjectCard.tsx index 3636503..172aa02 100644 --- a/src/components/dashboard/ProjectCard.tsx +++ b/src/components/dashboard/ProjectCard.tsx @@ -1,15 +1,13 @@ import { useState, useRef, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { getActivity, getSettings, getProjectTools, openEditor, openTerminal, launchTool, gitStatus, gitFetch, gitPull, gitPush, gitUpstreamBehind, gitCreateBranch, gitListBranches, gitCheckoutBranch, getBlueprintStatus, getBuffStatus, getDocFreshnessCache, type Project, type BlueprintStatus, type BuffStatus, type CachedFreshness } from "../../lib/commands"; +import { getSettings, openEditor, openTerminal, gitStatus, gitFetch, gitPull, gitPush, gitUpstreamBehind, gitCreateBranch, gitListBranches, gitCheckoutBranch, getBlueprintStatus, getBuffStatus, getDocFreshnessCache, type Project, type BlueprintStatus, type BuffStatus, type CachedFreshness } from "../../lib/commands"; import { useUIStore } from "../../store/ui"; import { PriorityBadge, StatusBadge } from "../ui/Badge"; -import { ProjectToolsModal } from "./ProjectToolsModal"; import { ProjectEnvModal } from "./ProjectEnvModal"; import { CicdModal } from "./CicdModal"; import { BlueprintModal } from "./BlueprintModal"; import { BlueprintPromptModal } from "./BlueprintPromptModal"; import { MentorPanel } from "../mentor/MentorPanel"; -import { UserTodoPanel } from "./UserTodoPanel"; import { ProjectServerPanel } from "./ProjectServerPanel"; import { ClaudeConfigModal } from "./ClaudeConfigModal"; import { ProjectDevVariablesPanel } from "./ProjectDevVariablesPanel"; @@ -58,9 +56,6 @@ interface Props { } export function ProjectCard({ project: p }: Props) { - const [expanded, setExpanded] = useState(false); - const [showToolsMgr, setShowToolsMgr] = useState(false); - const [toolsMgrDefaultAdd, setToolsMgrDefaultAdd] = useState(false); const [showEnvScan, setShowEnvScan] = useState(false); const [showCicd, setShowCicd] = useState(false); const [showBlueprint, setShowBlueprint] = useState(false); @@ -68,7 +63,6 @@ export function ProjectCard({ project: p }: Props) { const [showMentor, setShowMentor] = useState(false); const [showClaudeConfig, setShowClaudeConfig] = useState(false); const [showDevVariables, setShowDevVariables] = useState(false); - const [showTodos, setShowTodos] = useState(false); const [showServers, setShowServers] = useState(false); const [showBranchMenu, setShowBranchMenu] = useState(false); const [newBranchName, setNewBranchName] = useState(""); @@ -191,17 +185,6 @@ export function ProjectCard({ project: p }: Props) { staleTime: 30000, }); - const { data: projectTools = [] } = useQuery({ - queryKey: ["projectTools", p.id], - queryFn: () => getProjectTools(p.id), - }); - - const { data: acts } = useQuery({ - queryKey: ["activity", p.id], - queryFn: () => getActivity(p.id, 5), - enabled: expanded, - }); - // 当前已启用的编辑器列表 const activeEditors: { id: string; name: string; path: string }[] = (() => { try { @@ -238,15 +221,6 @@ export function ProjectCard({ project: p }: Props) { } }; - const handleLaunchTool = async (exePath: string, args?: string | null, name?: string) => { - try { - await launchTool(exePath, args ?? undefined); - showToast(`已启动 ${name ?? ""} ✓`); - } catch (e) { - showToast(`❌ ${e}`); - } - }; - const copy = (text: string) => { navigator.clipboard.writeText(text).catch(() => {}); showToast("已复制 ✓"); @@ -555,32 +529,6 @@ export function ProjectCard({ project: p }: Props) {
)} -
- 快启 - {projectTools.length > 0 ? ( - <> - {projectTools.map((tool) => ( - - ))} - - - ) : ( - - )} -
)} @@ -603,7 +551,7 @@ export function ProjectCard({ project: p }: Props) { - - {showToolsMgr && ( - { setShowToolsMgr(false); setToolsMgrDefaultAdd(false); }} - /> - )} - {showEnvScan && ( )} - {/* 个人备忘 */} - {showTodos && } - {/* 关联服务器 */} {showServers && } - - {/* Expanded activity */} - {expanded && ( -
-

最近动态

- {!acts ? ( -

加载中…

- ) : acts.length === 0 ? ( -

暂无记录

- ) : ( -
    - {acts.map((a) => ( -
  • - {(a.created_at ?? "").slice(5, 10)} - {a.summary} -
  • - ))} -
- )} -
- )} ); } diff --git a/src/components/dashboard/ServerManagerPanel.tsx b/src/components/dashboard/ServerManagerPanel.tsx index dea2ecc..420bc9c 100644 --- a/src/components/dashboard/ServerManagerPanel.tsx +++ b/src/components/dashboard/ServerManagerPanel.tsx @@ -11,7 +11,6 @@ import { import { useUIStore } from "../../store/ui"; import { ServiceConfigPanel } from "./ServiceConfigBlock"; -import { HostAppsPanel } from "./HostAppsPanel"; function CopyBtn({ value }: { value: string }) { const [copied, setCopied] = useState(false); @@ -472,9 +471,6 @@ function ServerCard({ server, onEdit, onDelete, globalKeyPath }: { {/* VS Code Remote */} - {/* 宿主机应用 */} - - {/* 服务连接配置(Linux 客户端层) */} diff --git a/src/components/dashboard/UserTodoPanel.tsx b/src/components/dashboard/UserTodoPanel.tsx deleted file mode 100644 index 370ea1f..0000000 --- a/src/components/dashboard/UserTodoPanel.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { getUserTodos, addUserTodo, toggleUserTodo, deleteUserTodo } from "../../lib/commands"; - -export function UserTodoPanel({ projectId }: { projectId: string }) { - const qc = useQueryClient(); - const [input, setInput] = useState(""); - const inputRef = useRef(null); - - const { data: todos = [] } = useQuery({ - queryKey: ["user-todos", projectId], - queryFn: () => getUserTodos(projectId), - }); - - const addMut = useMutation({ - mutationFn: (content: string) => addUserTodo(projectId, content), - onSuccess: () => { - qc.invalidateQueries({ queryKey: ["user-todos", projectId] }); - setInput(""); - }, - }); - - const toggleMut = useMutation({ - mutationFn: (id: number) => toggleUserTodo(id), - onSuccess: () => qc.invalidateQueries({ queryKey: ["user-todos", projectId] }), - }); - - const deleteMut = useMutation({ - mutationFn: (id: number) => deleteUserTodo(id), - onSuccess: () => qc.invalidateQueries({ queryKey: ["user-todos", projectId] }), - }); - - useEffect(() => { - inputRef.current?.focus(); - }, []); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && input.trim()) { - addMut.mutate(input.trim()); - } - }; - - const pending = todos.filter((t) => !t.done); - const done = todos.filter((t) => t.done); - - return ( -
- {/* 输入框 */} - setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="记点什么… 回车确认" - className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 bg-gray-50 - placeholder-gray-300 focus:outline-none focus:border-gray-400 mb-2" - /> - - {/* 待办列表 */} - {pending.length === 0 && done.length === 0 && ( -

暂无备忘

- )} - -
    - {pending.map((t) => ( -
  • - -
  • - ))} - - {/* 已完成的条目折叠在末尾 */} - {done.map((t) => ( -
  • - - {t.content} - -
  • - ))} -
-
- ); -} diff --git a/src/components/network/NetworkPage.tsx b/src/components/network/NetworkPage.tsx deleted file mode 100644 index cb0ff5c..0000000 --- a/src/components/network/NetworkPage.tsx +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 64006b9..b01e837 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -34,17 +34,6 @@ export interface Project { updated_at?: string; } -export interface ProjectDetail extends Project { - recent_activities: ActivityLog[]; -} - -export interface ActivityLog { - id: number; - project_id: string; - summary: string; - created_at?: string; -} - export interface ProductGroup { id: string; name: string; @@ -72,7 +61,7 @@ export const getProjects = (status?: string, spaceId?: string) => invoke("get_projects", { status, spaceId }); export const getProject = (id: string) => - invoke("get_project", { id }); + invoke("get_project", { id }); export const createProject = (input: Omit) => invoke("create_project", { input }); @@ -100,14 +89,6 @@ export interface BatchProjectInput { export const batchCreateProjects = (items: BatchProjectInput[]) => invoke("batch_create_projects", { items }); -// ── Activity ────────────────────────────────────────────────────────────────── - -export const getActivity = (projectId: string, limit?: number) => - invoke("get_activity", { projectId, limit }); - -export const addActivity = (projectId: string, summary: string) => - invoke("add_activity", { projectId, summary }); - // ── Groups ──────────────────────────────────────────────────────────────────── export const getGroups = (spaceId?: string) => invoke("get_groups", { spaceId }); @@ -222,11 +203,10 @@ export interface HealthReport { export const runHealthCheck = () => invoke("run_health_check"); -// ── Project Tools ───────────────────────────────────────────────────────────── +// ── Global Quick Launch ─────────────────────────────────────────────────────── -export interface ProjectTool { +export interface GlobalTool { id: string; - project_id: string; name: string; icon: string; exe_path: string; @@ -234,7 +214,7 @@ export interface ProjectTool { sort_order: number; } -export interface ProjectToolInput { +export interface GlobalToolInput { name: string; icon: string; exe_path: string; @@ -242,30 +222,16 @@ export interface ProjectToolInput { sort_order?: number; } -export const getProjectTools = (projectId: string) => - invoke("get_project_tools", { projectId }); - -export const addProjectTool = (projectId: string, input: ProjectToolInput) => - invoke("add_project_tool", { projectId, input }); - -export const updateProjectTool = (id: string, input: ProjectToolInput) => - invoke("update_project_tool", { id, input }); - -export const removeProjectTool = (id: string) => - invoke("remove_project_tool", { id }); - export const launchTool = (exePath: string, args?: string) => invoke("launch_tool", { exePath, args }); -// ── Global Quick Launch ─────────────────────────────────────────────────────── - export const getGlobalTools = () => - invoke("get_global_tools"); + invoke("get_global_tools"); -export const addGlobalTool = (input: ProjectToolInput) => +export const addGlobalTool = (input: GlobalToolInput) => invoke("add_global_tool", { input }); -export const updateGlobalTool = (id: string, input: ProjectToolInput) => +export const updateGlobalTool = (id: string, input: GlobalToolInput) => invoke("update_global_tool", { id, input }); export const removeGlobalTool = (id: string) => @@ -414,14 +380,6 @@ export interface Space { created_at?: string; } -export interface DeployCommand { - id: string; - project_id: string; - name: string; - command: string; - run_in: "windows" | "wsl"; -} - export const getSpaces = () => invoke("get_spaces"); @@ -437,22 +395,6 @@ export const deleteSpace = (id: string) => export const setCurrentSpace = (id: string) => invoke("set_current_space", { id }); -export const getDeployCommands = (projectId: string) => - invoke("get_deploy_commands", { projectId }); - -export const addDeployCommand = ( - projectId: string, - name: string, - command: string, - runIn: "windows" | "wsl" -) => invoke("add_deploy_command", { projectId, name, command, runIn }); - -export const removeDeployCommand = (id: string) => - invoke("remove_deploy_command", { id }); - -export const executeDeploy = (command: string, runIn: "windows" | "wsl", cwd?: string) => - invoke("execute_deploy", { command, runIn, cwd }); - // ── Git 操作 ────────────────────────────────────────────────────────────────── export interface GitStatus { @@ -1171,29 +1113,6 @@ export const getProjectNotes = (projectId: string) => export const addProjectNote = (projectId: string, content: string, source: string = "manual") => invoke("add_project_note", { projectId, content, source }); -// ── 用户项目个人备忘 ────────────────────────────────────────────────────────── - -export interface UserTodo { - id: number; - project_id: string; - content: string; - done: boolean; - sort_order: number; - created_at: string; -} - -export const getUserTodos = (projectId: string) => - invoke("get_user_todos", { projectId }); - -export const addUserTodo = (projectId: string, content: string) => - invoke("add_user_todo", { projectId, content }); - -export const toggleUserTodo = (id: number) => - invoke("toggle_user_todo", { id }); - -export const deleteUserTodo = (id: number) => - invoke("delete_user_todo", { id }); - // ── Servers ────────────────────────────────────────────────────────────────── export interface QuickLink { @@ -1318,56 +1237,6 @@ export const editServiceConfig = (id: string, input: SaveServiceConfigInput) => export const deleteServiceConfig = (id: string) => invoke("delete_service_config", { id }); -// ── Host Apps(宿主机部署应用)──────────────────────────────────────────────── - -export interface HostApp { - id: string; - server_id: string; - name: string; - deploy_type: string; - service_name?: string; - binary_path?: string; - config_path?: string; - log_path?: string; - port?: string; - start_cmd?: string; - stop_cmd?: string; - restart_cmd?: string; - status_cmd?: string; - notes?: string; - sort_order?: number; - created_at?: string; - updated_at?: string; -} - -export interface SaveHostAppInput { - name: string; - deploy_type: string; - service_name?: string; - binary_path?: string; - config_path?: string; - log_path?: string; - port?: string; - start_cmd?: string; - stop_cmd?: string; - restart_cmd?: string; - status_cmd?: string; - notes?: string; - sort_order?: number; -} - -export const getHostApps = (serverId: string) => - invoke("get_host_apps", { serverId }); - -export const addHostApp = (serverId: string, input: SaveHostAppInput) => - invoke("add_host_app", { serverId, input }); - -export const editHostApp = (id: string, input: SaveHostAppInput) => - invoke("edit_host_app", { id, input }); - -export const deleteHostApp = (id: string) => - invoke("delete_host_app", { id }); - // ── API Keys ───────────────────────────────────────────────────────────────── export interface ApiKey {