eb32fd5e22
外层仓库管理 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)
106 lines
3.2 KiB
JavaScript
106 lines
3.2 KiB
JavaScript
// views/remotes.js — connector-tile grid of configured remotes with CRUD.
|
||
|
||
import { post } from "../rc.js";
|
||
import { getState, setState, toast } from "../state.js";
|
||
|
||
export async function renderRemotes() {
|
||
const app = document.getElementById("app");
|
||
if (!app) return;
|
||
|
||
app.innerHTML = `
|
||
<div class="section-head">
|
||
<div>
|
||
<h2>Remotes</h2>
|
||
<p class="subtitle">Configured cloud storage providers. Click a tile to browse, hover for edit/delete.</p>
|
||
</div>
|
||
<a class="btn btn-primary btn-sm" href="#/configure/new">New remote</a>
|
||
</div>
|
||
<div id="remote-grid" class="connector-grid">
|
||
<p class="empty" style="grid-column:1/-1">Loading remotes…</p>
|
||
</div>
|
||
`;
|
||
|
||
const grid = document.getElementById("remote-grid");
|
||
|
||
try {
|
||
const [listRes, dumpRes] = await Promise.all([
|
||
post("config/listremotes"),
|
||
post("config/dump"),
|
||
]);
|
||
const names = (listRes && listRes.remotes) || [];
|
||
const dump = dumpRes || {};
|
||
const remotes = names.map((name) => ({
|
||
name,
|
||
type: (dump[name] && dump[name].type) || "unknown",
|
||
}));
|
||
|
||
setState({ remotes });
|
||
|
||
if (remotes.length === 0) {
|
||
grid.innerHTML = `
|
||
<div class="empty" style="grid-column:1/-1">
|
||
<h3>No remotes configured</h3>
|
||
<p>Click <strong>New remote</strong> above to add one from this GUI.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = remotes
|
||
.map(
|
||
(r) => `
|
||
<a class="connector-tile tile-with-actions" href="#/browse/${encodeURIComponent(r.name)}">
|
||
<span class="tile-name">${escapeHtml(r.name)}</span>
|
||
<span class="badge">${escapeHtml(r.type)}</span>
|
||
<div class="tile-actions">
|
||
<button type="button" title="Edit" data-edit="${escapeHtml(r.name)}">✎</button>
|
||
<button type="button" title="Delete" class="danger" data-delete="${escapeHtml(r.name)}">🗑</button>
|
||
</div>
|
||
</a>
|
||
`,
|
||
)
|
||
.join("");
|
||
|
||
// Wire action buttons
|
||
grid.querySelectorAll("[data-edit]").forEach((btn) => {
|
||
btn.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
location.hash = `#/configure/edit/${encodeURIComponent(btn.dataset.edit)}`;
|
||
});
|
||
});
|
||
grid.querySelectorAll("[data-delete]").forEach((btn) => {
|
||
btn.addEventListener("click", async (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
const name = btn.dataset.delete;
|
||
if (!confirm(`Delete remote ${name}? This cannot be undone.`)) return;
|
||
try {
|
||
await post("config/delete", { name });
|
||
toast(`Deleted ${name}`, "success");
|
||
await renderRemotes();
|
||
} catch (err) {
|
||
toast(`Delete failed: ${err.message}`, "error");
|
||
}
|
||
});
|
||
});
|
||
} catch (e) {
|
||
grid.innerHTML = `
|
||
<div class="empty" style="grid-column:1/-1">
|
||
<h3>Couldn’t reach rclone</h3>
|
||
<p>${escapeHtml(e.message)}</p>
|
||
</div>
|
||
`;
|
||
toast(`Failed to load remotes: ${e.message}`, "error");
|
||
}
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|