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,105 @@
|
||||
// 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, "'");
|
||||
}
|
||||
Reference in New Issue
Block a user