${esc(snap.name)}
${esc(summary)}
diff --git a/.github/workflows/os-sanity.yml b/.github/workflows/os-sanity.yml index 341579080d9..84d2e369544 100644 --- a/.github/workflows/os-sanity.yml +++ b/.github/workflows/os-sanity.yml @@ -57,21 +57,19 @@ jobs: # 只做可读性检查,不 update/install(不触网) ./scripts/feeds list >/dev/null - - name: CapOS webdesktop and capbox smoke tests + - name: CapOS WebDesktop smoke tests shell: bash run: | set -euxo pipefail - bash -n package/capos/capbox/files/capbox - g++ -std=gnu++17 -Ipackage/capos/capos-webpanel/src \ - -o /tmp/capos-api-smoke package/capos/capos-webpanel/src/api.cpp -lcrypt - g++ -std=gnu++17 -Ipackage/capos/capos-webpanel/src \ - -o /tmp/capos-app-smoke package/capos/capos-webpanel/src/app.cpp -lcrypt -lssl -lcrypto + g++ -std=gnu++17 -Ipackage/capos/capos-webdesktop/src \ + -o /tmp/capos-api-smoke package/capos/capos-webdesktop/src/api.cpp -lcrypt -lssl -lcrypto + g++ -std=gnu++17 -Ipackage/capos/capos-webdesktop/src \ + -o /tmp/capos-app-smoke package/capos/capos-webdesktop/src/app.cpp -lcrypt -lssl -lcrypto - tests/capbox_logic_tests.sh - tests/webpanel_frontend_smoke.sh - tests/webpanel_proxy_smoke.sh - tests/webpanel_runtime_config_smoke.sh + tests/webdesktop_frontend_smoke.sh + tests/webdesktop_proxy_smoke.sh + tests/webdesktop_runtime_config_smoke.sh - name: Kconfig sanity (defconfig) shell: bash @@ -79,6 +77,8 @@ jobs: set -euxo pipefail # 不跑交互 menuconfig,只验证 Kconfig/配置生成链路 make defconfig + grep -q '^CONFIG_TARGET_ROOTFS_ARGOSFS=y' .config + grep -q '^CONFIG_TARGET_ROOTFS_PERSIST_VAR=y' .config - name: Package metadata sanity (prepare-tmpinfo) shell: bash diff --git a/README.md b/README.md index 6d53f5e7eb8..b43e6ed2512 100755 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ Instead of feeling like a stripped-down build environment, CapOS aims to feel li
Runs with a small footprint and works well across a broad range of hardware, from compact devices to full PCs.
Manage the system from a browser through a more visual, approachable interface for settings, packages, and system status.
+Manage the system and install applications from the official Snap ecosystem directly in CapOS WebDesktop. Web services are discovered and integrated automatically.
${esc(summary)}
CapOS 会从 Snap 的实际运行进程自动发现监听端口并检测 HTTP/HTTPS 入口,不需要单独维护应用适配表。
+ +等待上传 CPK。
"; - if (message) { - showToast(message, "error"); - } -} - -async function api(path, options = {}) { - const res = await fetch(`/cgi-bin/cap/api${path}`, { - credentials: "same-origin", - ...options, - }); - const text = await res.text(); - let data = {}; - try { - data = text ? JSON.parse(text) : {}; - } catch (error) { - throw new ApiError(text || `HTTP ${res.status}`, res.status, "INVALID_JSON"); - } - if (res.status === 401) { - resetSessionUi(data.message || "登录状态已过期,请重新登录。"); - throw new ApiError(data.message || "unauthenticated", res.status, data.error_code || "UNAUTHENTICATED"); - } - if (!res.ok || data.ok === false) { - throw new ApiError(data.message || `HTTP ${res.status}`, res.status, data.error_code || "ERROR"); - } - return data; -} - -function setPending(key, pending) { - if (pending) { - state.pending.add(key); - } else { - state.pending.delete(key); - } - renderApps(); - nodes.loginBtn.disabled = state.pending.has("login"); - nodes.uploadBtn.disabled = state.pending.has("upload"); - nodes.installBtn.disabled = !state.upload || state.pending.has("install"); - nodes.savePortBtn.disabled = state.pending.has("port"); -} - -function isDesktopApp(appName) { - return Boolean(state.desktop && state.desktop.app === appName); -} - -function risksForApp(item) { - const manifest = item.manifest || {}; - const container = manifest.container || {}; - const privileges = container.privileges || {}; - const network = manifest.network || {}; - const host = manifest.host || {}; - const exec = host.exec || {}; - const risks = []; - if (network.host || item.permissions?.host_network) risks.push("host network"); - if (exec.enabled || item.permissions?.host_exec) risks.push("host.exec"); - if (privileges.enabled) risks.push("privileged"); - if ((privileges.capabilities || []).length) risks.push("capabilities"); - if ((container.devices || []).length) risks.push("devices"); - if ((container.volumes?.extra || []).length) risks.push("host mounts"); - if ((network.publish || []).length) risks.push("publish"); - return risks; -} - -function serviceLabel(item) { - const desktop = item.desktop || {}; - if (!desktop.port) { - return "未声明"; - } - return `${desktop.scheme || "http"}://${desktop.target_host || "-"}:${desktop.port}`; -} - -function portmapText(map) { - return `${map.proto || "tcp"} ${map.listen}->${map.target_port}`; -} - -function appMatches(item, query, filter) { - const app = item.app || {}; - const running = Boolean(item.runtime?.running); - const desktop = isDesktopApp(app.name); - const risks = risksForApp(item); - if (filter === "running" && !running) return false; - if (filter === "stopped" && running) return false; - if (filter === "desktop" && !desktop) return false; - if (filter === "risk" && risks.length === 0) return false; - if (!query) return true; - const haystack = [ - app.name, - app.nickname, - app.version, - app.description, - item.runtime?.container_name, - item.runtime?.ip, - serviceLabel(item), - ...(item.portmaps || []).map(portmapText), - ...risks, - ] - .filter(Boolean) - .join(" ") - .toLowerCase(); - return haystack.includes(query); -} - -function emptyState(title, message) { - return `${h(message)}
${h(app.description || "这个应用还没有提供描述。")}
-${h(app.description || "这个 CPK 没有提供描述。")}
-未发现 privileged、devices、host.exec、host network 等高风险能力。
' - } - `; -} - -async function uploadCpk() { - const file = nodes.cpkFile.files[0]; - if (!file) { - showToast("请先选择一个 CPK 文件。", "error"); - return; - } - setPending("upload", true); - try { - const data = await api(`/upload?filename=${encodeURIComponent(file.name)}`, { - method: "POST", - headers: { "Content-Type": "application/octet-stream" }, - body: await file.arrayBuffer(), - }); - state.upload = data.upload; - renderInspection(data.inspection || {}); - showToast("上传完成,预检通过。", "success"); - } catch (error) { - state.upload = null; - nodes.inspectionBox.innerHTML = `安装成功,可以在应用库中查看。
"; - nodes.cpkFile.value = ""; - state.upload = null; - showToast("应用已安装。", "success"); - await refreshAll(); - } finally { - setPending("install", false); - } -} - -nodes.loginForm.addEventListener("submit", async (event) => { - event.preventDefault(); - setPending("login", true); - try { - await api("/login", formBody({ username: nodes.username.value, password: nodes.password.value })); - nodes.password.value = ""; - showToast("登录成功。", "success"); - await refreshAll(); - } catch (error) { - showToast(error.message, "error"); - } finally { - setPending("login", false); - } -}); - -$("logoutBtn").addEventListener("click", async () => { - try { - await api("/logout", { method: "POST" }); - } catch (error) { - showToast(error.message, "error"); - } finally { - resetSessionUi(); - } -}); - -$("toggleDrawerBtn").addEventListener("click", () => nodes.controlDrawer.classList.toggle("open")); -$("closeDrawerBtn").addEventListener("click", () => nodes.controlDrawer.classList.remove("open")); -$("refreshBtn").addEventListener("click", () => refreshAll().catch((error) => showToast(error.message, "error"))); -$("appsRefreshBtn").addEventListener("click", () => refreshAll().catch((error) => showToast(error.message, "error"))); -nodes.appSearch.addEventListener("input", renderApps); -nodes.appFilter.addEventListener("change", renderApps); -window.addEventListener("resize", updateViewportOffsets); - -nodes.appsList.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-action]"); - if (!button) return; - const { action, app } = button.dataset; - try { - if (action === "remove-port") { - await removePortMapping(button); - return; - } - if (action === "portmap") { - openPortModal(app); - return; - } - if (action === "desktop") { - await api("/desktop", formBody({ app })); - showToast(`已将 ${app} 设为桌面应用。`, "success"); - await refreshAll(); - return; - } - await appAction(app, action); - } catch (error) { - showToast(error.message, "error"); - } -}); - -nodes.portForm.addEventListener("submit", async (event) => { - if (event.submitter?.value === "cancel") { - return; - } - event.preventDefault(); - try { - await savePortMapping(); - } catch (error) { - showToast(error.message, "error"); - } -}); - -nodes.uploadBtn.addEventListener("click", () => uploadCpk().catch((error) => showToast(error.message, "error"))); -nodes.installBtn.addEventListener("click", () => installUpload().catch((error) => showToast(error.message, "error"))); - -refreshAll().catch((error) => { - resetSessionUi(); - showToast(error.message, "error"); -}); -updateViewportOffsets(); diff --git a/package/capos/capos-webpanel/htdocs/assets/styles.css b/package/capos/capos-webpanel/htdocs/assets/styles.css deleted file mode 100644 index 339561d4da2..00000000000 --- a/package/capos/capos-webpanel/htdocs/assets/styles.css +++ /dev/null @@ -1,596 +0,0 @@ -:root { - color-scheme: light; - --bg: #e8ebef; - --surface: rgba(255, 255, 255, 0.94); - --surface-strong: #ffffff; - --ink: #18222c; - --muted: #62717f; - --line: #d7dde4; - --accent: #276a8f; - --accent-strong: #164f72; - --danger: #b23b36; - --warning: #9a6517; - --success: #1f7a54; - --shadow: 0 16px 38px rgba(19, 31, 43, 0.16); -} - -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - min-height: 100%; - font-family: "Segoe UI", "PingFang SC", "Noto Sans CJK SC", sans-serif; - color: var(--ink); - background: - linear-gradient(135deg, rgba(39, 106, 143, 0.12), transparent 38%), - linear-gradient(180deg, #f6f7f8 0%, var(--bg) 100%); -} - -button, -.link-button, -input, -select { - font: inherit; -} - -button, -.link-button { - border: none; - border-radius: 8px; - padding: 8px 12px; - cursor: pointer; - text-decoration: none; - transition: transform 0.14s ease, opacity 0.14s ease, background 0.14s ease; - white-space: nowrap; -} - -button:hover, -.link-button:hover { - transform: translateY(-1px); -} - -button:disabled { - cursor: not-allowed; - opacity: 0.52; - transform: none; -} - -.primary { - background: var(--accent); - color: #fff; -} - -.primary:hover { - background: var(--accent-strong); -} - -.secondary { - background: #fff; - color: var(--ink); - border: 1px solid var(--line); -} - -.danger { - background: rgba(178, 59, 54, 0.12); - color: var(--danger); -} - -.small { - padding: 6px 9px; - font-size: 12px; -} - -.wide { - width: 100%; -} - -.icon-button { - width: 34px; - height: 34px; - padding: 0; -} - -label { - display: grid; - gap: 6px; - color: var(--muted); - font-size: 12px; - font-weight: 700; -} - -input, -select { - width: 100%; - min-width: 0; - padding: 10px 11px; - border-radius: 8px; - border: 1px solid var(--line); - background: #fff; - color: var(--ink); -} - -.login { - display: grid; - place-items: center; - min-height: 100vh; - padding: 20px; -} - -.login-card { - width: min(430px, 100%); - border: 1px solid rgba(255, 255, 255, 0.62); - border-radius: 16px; - background: var(--surface); - box-shadow: var(--shadow); - padding: 24px; - display: grid; - gap: 14px; -} - -.brand-mark { - justify-self: start; - border: 1px solid var(--line); - border-radius: 8px; - padding: 7px 10px; - color: var(--accent); - font-weight: 800; - background: #fff; -} - -.login-card h1, -.login-card p, -.panel h3, -.panel p, -.modal h3, -.modal p, -.empty-state h4, -.empty-state p { - margin: 0; -} - -.login-card h1 { - font-size: 28px; -} - -.login-card p, -.panel p, -.modal p, -.empty-state p { - color: var(--muted); - line-height: 1.45; -} - -.app { - display: none; - min-height: 100vh; -} - -.app.ready { - display: block; -} - -.workspace { - position: relative; - min-height: 100vh; - padding-top: var(--workspace-top, 86px); -} - -.desktop { - position: relative; - height: calc(100vh - var(--workspace-top, 86px)); - min-height: 360px; - background: #f7f8f8; -} - -.desktop-frame { - width: 100%; - height: 100%; - border: none; - display: block; - background: #f7f8f8; -} - -.desktop-status { - position: absolute; - top: 12px; - left: 12px; - z-index: 5; - display: none; - max-width: min(560px, calc(100% - 24px)); - border: 1px solid var(--line); - border-radius: 10px; - background: rgba(255, 255, 255, 0.93); - padding: 10px 12px; - box-shadow: 0 12px 26px rgba(19, 31, 43, 0.1); - color: var(--muted); - font-size: 13px; -} - -.desktop-status.visible { - display: block; -} - -.topbar { - position: fixed; - z-index: 20; - top: 12px; - left: 12px; - right: 12px; - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - padding: 10px 12px; - border-radius: 12px; - border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(10px); - box-shadow: var(--shadow); -} - -.topbar-main { - display: grid; - min-width: 0; -} - -.topbar-main strong { - font-size: 14px; -} - -.topbar-main span { - color: var(--muted); - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.topbar-actions { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; -} - -.pill, -.chip { - display: inline-flex; - align-items: center; - border-radius: 999px; - font-weight: 700; -} - -.pill { - padding: 7px 11px; - font-size: 13px; - background: rgba(39, 106, 143, 0.12); - color: var(--accent); -} - -.control-drawer { - position: fixed; - top: var(--workspace-top, 86px); - right: 12px; - bottom: 12px; - width: min(520px, calc(100vw - 24px)); - border: 1px solid var(--line); - border-radius: 14px; - background: var(--surface); - box-shadow: var(--shadow); - padding: 14px; - display: grid; - grid-template-rows: auto 1fr; - gap: 12px; - z-index: 18; - transform: translateX(calc(100% + 18px)); - transition: transform 0.2s ease; -} - -.control-drawer.open { - transform: translateX(0); -} - -.drawer-head, -.panel-head, -.modal-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.drawer-head h2, -.drawer-head p { - margin: 0; -} - -.drawer-head h2 { - font-size: 18px; -} - -.drawer-head p { - margin-top: 3px; - color: var(--muted); - font-size: 12px; -} - -.drawer-content { - overflow: auto; - display: grid; - gap: 12px; - padding-right: 4px; -} - -.panel { - border: 1px solid var(--line); - border-radius: 12px; - background: rgba(255, 255, 255, 0.78); - padding: 12px; - display: grid; - gap: 10px; -} - -.panel h3 { - font-size: 15px; -} - -.filters { - display: grid; - grid-template-columns: minmax(0, 1fr) 132px; - gap: 8px; -} - -.apps { - display: grid; - gap: 10px; -} - -.app-card { - border: 1px solid var(--line); - border-radius: 8px; - background: #fff; - padding: 12px; - display: grid; - gap: 10px; -} - -.app-card.is-desktop { - border-color: rgba(39, 106, 143, 0.45); - box-shadow: inset 3px 0 0 var(--accent); -} - -.app-title { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 10px; -} - -.app-title h4, -.app-title p { - margin: 0; -} - -.app-title h4 { - font-size: 15px; -} - -.app-title p { - margin-top: 3px; - font-size: 12px; - color: var(--muted); - line-height: 1.4; -} - -.app-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} - -.fact { - border: 1px solid var(--line); - border-radius: 8px; - padding: 7px 8px; - background: #fbfcfd; - min-width: 0; -} - -.fact strong { - display: block; - color: var(--muted); - font-size: 11px; - margin-bottom: 3px; -} - -.fact span { - display: block; - overflow-wrap: anywhere; - font-size: 12px; -} - -.chips, -.actions, -.port-list { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.chip { - border: 1px solid var(--line); - padding: 4px 8px; - background: #fff; - color: var(--muted); - font-size: 11px; -} - -.chip.running { - color: var(--success); -} - -.chip.stopped, -.chip.risk { - color: var(--danger); -} - -.chip.warning { - color: var(--warning); -} - -.chip.desktop-chip { - color: var(--accent); - border-color: rgba(39, 106, 143, 0.42); - background: rgba(39, 106, 143, 0.08); -} - -.port-pill { - border: 1px solid var(--line); - border-radius: 999px; - padding: 5px 8px; - background: #fff; - color: var(--ink); - font-size: 11px; - cursor: pointer; -} - -.actions button { - font-size: 12px; - padding: 6px 9px; -} - -.upload-box { - display: grid; - gap: 9px; -} - -.review { - border: 1px solid var(--line); - border-radius: 8px; - background: #fbfcfd; - padding: 10px; - display: grid; - gap: 9px; - max-height: 300px; - overflow: auto; -} - -.review h4 { - margin: 0; - font-size: 14px; -} - -.review p { - margin: 0; - color: var(--muted); - font-size: 12px; - line-height: 1.4; -} - -.review-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} - -.risk-list { - display: grid; - gap: 6px; -} - -.risk-item { - border: 1px solid rgba(178, 59, 54, 0.24); - border-radius: 8px; - background: rgba(178, 59, 54, 0.06); - padding: 8px; - color: var(--danger); - font-size: 12px; -} - -.empty-state { - border: 1px dashed var(--line); - border-radius: 8px; - background: rgba(255, 255, 255, 0.68); - padding: 14px; - display: grid; - gap: 5px; -} - -.toast-stack { - position: fixed; - right: 14px; - bottom: 14px; - z-index: 50; - display: grid; - gap: 8px; - width: min(360px, calc(100vw - 28px)); -} - -.toast { - padding: 11px 12px; - border-radius: 10px; - background: rgba(22, 32, 40, 0.94); - color: #fff; - box-shadow: var(--shadow); - font-size: 13px; -} - -.toast.error { - background: rgba(178, 59, 54, 0.96); -} - -.toast.success { - background: rgba(31, 122, 84, 0.96); -} - -.modal { - border: none; - padding: 0; - background: transparent; -} - -.modal::backdrop { - background: rgba(24, 34, 44, 0.36); -} - -.modal-card { - width: min(420px, calc(100vw - 28px)); - border: 1px solid var(--line); - border-radius: 12px; - background: #fff; - box-shadow: var(--shadow); - padding: 14px; - display: grid; - gap: 12px; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 8px; -} - -@media (max-width: 760px) { - .topbar { - align-items: flex-start; - flex-wrap: wrap; - } - - .topbar-main { - width: 100%; - } - - .topbar-actions { - width: 100%; - } - - .filters, - .app-grid, - .review-grid { - grid-template-columns: 1fr; - } -} diff --git a/package/capos/capos-webpanel/htdocs/index.html b/package/capos/capos-webpanel/htdocs/index.html deleted file mode 100644 index e0cc399221c..00000000000 --- a/package/capos/capos-webpanel/htdocs/index.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - -