init: 初始化 rclone-webgui 项目结构
外层仓库管理 webgui 源码、Docker 编排与项目文档;rclone 作为
git submodule 锁定在上游 master HEAD(59c86b01b),不携带任何
我们的改动。
- webgui/: webgui 源码(原本位于 rclone/cmd/webgui/)
- web/: 原生 HTML/CSS/JS 静态前端(Anthropic 设计语言)
- webgui.go: Go 子命令源码,仅当自行构建 rclone 二进制时需要
- rclone-cmd-all-add-webgui-import.patch: 把 webgui 注册进
rclone 的 cmd/all/all.go 的补丁,留作 fork 时使用
- rclone/: submodule → github.com/rclone/rclone,纯净不改动
- Dockerfile.webgui: 基于 nginx:1.27-alpine,从 ./webgui/web/
COPY 静态资源
- docker/nginx.conf: SPA 静态托管 + 反向代理 RC API
(/config/、/operations/、/sync/、/job/ 等) 与文件下载
(/<remote>:<path>) 到 rclone rcd 容器,前端同源访问无 CORS
- docker-compose.yml: rclone (官方镜像 + rcd --rc-no-auth
--rc-serve) + gui (nginx) 双服务编排,config 走 bind mount
持久化
- DESIGN.md / CLAUDE.md / README.md: 文档
- .gitignore / .dockerignore: 排除 rclone.conf 等敏感文件,
Docker 构建上下文只剩 webgui/web/ + nginx 配置(几十 KB)
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// app.js — entry point. Routes hash changes to view renderers and
|
||||
// keeps the top-nav active state in sync.
|
||||
|
||||
import { onRoute } from "./state.js";
|
||||
import { renderRemotes } from "./views/remotes.js";
|
||||
import { renderBrowse } from "./views/browser.js";
|
||||
import { renderJobs, renderNewJob, stopJobPolling } from "./views/jobs.js";
|
||||
import {
|
||||
renderConfigureNew,
|
||||
renderConfigureEdit,
|
||||
} from "./views/configure.js";
|
||||
|
||||
const views = {
|
||||
remotes: renderRemotes,
|
||||
browse: renderBrowse,
|
||||
jobs: renderJobs,
|
||||
"jobs-new": renderNewJob,
|
||||
"configure-new": renderConfigureNew,
|
||||
"configure-edit": renderConfigureEdit,
|
||||
};
|
||||
|
||||
function setActiveNav(routeName) {
|
||||
const links = document.querySelectorAll("#nav-links a");
|
||||
for (const a of links) {
|
||||
const target = a.dataset.route;
|
||||
let isActive = target === routeName;
|
||||
// "Configure" is active on both configure-new and configure-edit
|
||||
if (target === "configure" && routeName.startsWith("configure-")) {
|
||||
isActive = true;
|
||||
}
|
||||
a.classList.toggle("active", isActive);
|
||||
}
|
||||
}
|
||||
|
||||
onRoute(async (route) => {
|
||||
setActiveNav(route.name);
|
||||
|
||||
// Stop jobs polling when leaving the jobs view
|
||||
if (route.name !== "jobs") {
|
||||
stopJobPolling();
|
||||
}
|
||||
|
||||
const renderer = views[route.name] || renderRemotes;
|
||||
try {
|
||||
await renderer(route.params);
|
||||
} catch (e) {
|
||||
console.error("view error", e);
|
||||
const app = document.getElementById("app");
|
||||
if (app) {
|
||||
app.innerHTML = `<div class="empty"><h3>Something went wrong</h3><p>${escapeHtml(e.message)}</p></div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
Reference in New Issue
Block a user