Skip to content

Releases: Cmochance/mochan-linux

v1.0.2

06 May 16:53
646e80c

Choose a tag to compare

What's Changed

  • docs: fetch tags before VPS version build by @Cmochance in #8
  • Complete app backend persistence queue by @Cmochance in #9

Full Changelog: v1.0.1...v1.0.2

v1.0.1

06 May 08:18
7365aa5

Choose a tag to compare

mochan-linux v1.0.1

v1.0.1 is a documentation and release-readiness patch for the current
server-backed desktop state.

Added

  • README preview screenshots generated from the local app preview at
    http://127.0.0.1:3001/ against the local Go backend.
  • Screenshot coverage for the desktop, app launcher, File Manager, Terminal,
    Settings, and Browser start page under docs/img/.
  • README backend completion summary that states which applications are already
    backed by real server APIs or server-side persistence.
  • Clear README separation between applications still being migrated to backend
    support and local-only tools or games that do not yet need dedicated backend
    APIs.

Backend Status Snapshot

Implemented server-backed applications through P15:

  • System and desktop: login gate, File Manager, Terminal, Trash, System
    Monitor, Task Manager, Audit Log, Settings, and Browser.
  • File and network tools: Text Editor, Markdown Editor, Image Viewer, Download
    Manager, API Tester, RSS Reader, Git Client, SSH Client, FTP/SFTP Client,
    Bookmarks, Weather, and Email Client.
  • Personal data apps: Chat App, Notes, Calendar, and Notebook.

Still queued for backend migration:

  • Spreadsheet, Mind Map, Presentation, Pomodoro, Habit Tracker, Dictionary,
    Translator, Photo Album, Camera, Voice Recorder, Music Player, Video Player,
    PDF Reader, and Paint.

Deferred until there is a concrete sync, history, export, preset, or
leaderboard requirement:

  • Calculator, Clock, Color Picker, Base64 Tool, QR Code Generator, Password
    Generator, Regex Tester, JSON Editor, board games, puzzle games, White Noise,
    and Metronome.

Validation

  • Generated and checked README preview screenshots locally.
  • npm run build passed.
  • GOCACHE=/Users/alysechen/alysechen/github/mochan-linux/.tmp/go-cache make build VERSION="v1.0.1-docs" passed.
  • The GitHub Release workflow is triggered by pushing the v1.0.1 tag.

Notes

  • This patch does not change runtime application code.
  • The in-app Browser remains a server-side proxy feature: authenticated browser
    requests are fetched by the mochan-linux backend, so it can reach addresses
    available from the server host.

v0.9.0

04 May 04:04

Choose a tag to compare

mochan-linux v0.9.0

本版本主线: 设置真接入。Settings 不再是纯 localStorage 的 stub —— 主题 / 语言 / 桌面壁纸由后端 /api/settings 持久化(写到 /var/lib/mochan/settings.json),换浏览器也保留。新增完整的壁纸桶 /var/lib/mochan/wallpapers/,可以从设置面板里直接上传图片做壁纸。Settings 应用整个重写成多页签:外观 / 语言 / 关于,关于页接 /api/sys/stat 实时显示主机信息。

中文

后端 internal/settings

两个组件:

  • Store: 单文件 JSON 持久化。原子写入(写到 .tmp 然后 os.Rename),0640 权限,只接受 enum 值合法的 patch(theme ∈ {ink, dark, light},language ∈ {zh, en})。
  • Bucket: 用户上传的壁纸放在 <DataDir>/wallpapers/。文件名校验拦截 .. / / / NUL,扩展名白名单(.jpg / .jpeg / .png / .webp / .gif / .bmp / .avif)。

端点

方法 路径 用途
GET /api/settings/ 返回 {theme, language, wallpaper}
PATCH /api/settings/ 合并 patch,持久化,返回新状态
GET /api/settings/wallpapers/ 列出 bundled + user 壁纸,返回 {name, url, size, source}
POST /api/settings/wallpapers/ multipart 上传,字段名 file
GET /api/settings/wallpapers/{name} 流式返回用户上传的壁纸文件(带 5min Cache-Control)
DELETE /api/settings/wallpapers/{name} 删除用户壁纸

前端 apps/Settings.tsx 完全重写

旧 stub 是 372 行 localStorage UI 但实际不生效(theme 在 useSystemStore,Settings 自己又写一份),改成 200 行的三页签:

  • 外观: 主题三按钮(水墨 / 夜色 / 宣纸)+ 壁纸 4 列网格(bundled 走 ./wallpaper-xxx.jpg,user 走 /api/settings/wallpapers/xxx),user 壁纸 hover 出现红色删除按钮。点选 = 立即生效(zustand 改 → useSettingsSync 钩子 debounce 300ms 写回服务器)。上传按钮调 /api/settings/wallpapers/ POST。
  • 语言: 中文 / English 二选一。
  • 关于: 水墨 Linux 品牌区 + GitHub / Releases 外链 + 实时主机信息卡(主机名 / OS / 内核 / 架构 / 已运行 / CPU 核数 / 内存 / 磁盘挂载数 / 负载 / 当前用户)。

useSettingsSync 钩子

// 在 Layout 里挂上,每次 booted 都跑一次
useSettingsSync()

逻辑:

  1. 进入 Layout(已登录) → fetch /api/settings/ → 拿到 {theme, language, wallpaper} → 用 setTheme/setLanguage/setWallpaper 灌进 zustand。服务端是真理之源,把 localStorage 作为暖启动缓存——先用 localStorage 渲染屏幕,再用服务端值覆盖,体感不闪烁。
  2. zustand subscribe 监听 theme / language / wallpaper 变化,300ms 防抖后 PATCH 服务器。
  3. lastWritten 记忆最近一次同步内容,避免回声(服务器响应触发的 setState 不应再 PATCH)。

副作用:StatusBar 上那个右上角的语言切换按钮现在也会真持久化到服务端——任何路径改的 zustand 都会被 hook 同步,Settings 应用不需要垄断这个能力。

桌面壁纸打通真 FS

Desktop / LockScreen / AuthGate 三处之前都硬编码 url(./${wallpaper}.jpg)。新增 wallpaperUrl(id) 帮手:

export function wallpaperUrl(id: string): string {
  if (id.startsWith('wallpaper-')) return `./${id}.jpg`;
  return `/api/settings/wallpapers/${encodeURIComponent(id)}`;
}

useDesktopStore.WallpaperId 类型从五个字面量 union 放宽到 string,所以用户上传的任意图名都能作为合法 wallpaper id 存。

试一下

  1. Settings → 外观 → 上传新壁纸:选张本机图片 → 网格里立刻出现 → 点它 → 桌面 / 锁屏 / 登录页背景同步切换。
  2. 服务器端能验证: ssh dochenmo 'cat /var/lib/mochan/settings.json && ls /var/lib/mochan/wallpapers/' 看到 {"theme":"ink","language":"zh","wallpaper":"<your.png>"} 和实际文件。
  3. 在另一台机器(或匿名隐私窗口)登录:壁纸已经是你刚才设的——服务端拿回来的不是 localStorage。

故意不做的

  • 端口设置: 改 MOCHAN_LISTEN 涉及 systemd 重启 + 反向代理上游联动,不该是 in-app 一键。仍然走 /etc/mochan/config.env + systemctl restart mochan
  • 密码修改: 暂未做。理由同上(密码哈希在 config.env,需要原子改文件 + 重启)。Stage 11 之前应当补,但本版本不投入。
  • 2FA / OAuth: 仍是单密码,记忆里硬规则警告未变。

English

Backend

internal/settings adds a Store (atomic JSON write to <DataDir>/settings.json, 0640) and a Bucket for user-uploaded wallpapers under <DataDir>/wallpapers/. Endpoints under /api/settings/: GET / PATCH for the document, GET / POST / GET {name} / DELETE {name} for the wallpaper bucket. Patch values are validated against enums; uploads are filename-guarded against traversal and extension-whitelisted.

Frontend

apps/Settings.tsx rewritten as a tabbed page (Appearance / Language / About). Theme buttons, language toggle, wallpaper grid with hover-to-delete and an upload button. About tab renders live host info via /api/sys/stat.

A new useSettingsSync() hook runs in Layout. It bootstraps zustand stores from the server on entry, then debounce-writes back any local mutation (300 ms) — so the StatusBar language toggle persists too, not just the Settings app.

Desktop / LockScreen / AuthGate resolve wallpaper URLs through a new wallpaperUrl(id) helper that routes bundled IDs to the static bundle and any other ID to /api/settings/wallpapers/<name>.

Verify

ssh user@host 'cat /var/lib/mochan/settings.json && ls /var/lib/mochan/wallpapers/'

After uploading a wallpaper from Settings, you'll see the file on disk and the JSON updated. Logging in from a fresh browser shows the same theme/wallpaper because the server is the source of truth — localStorage is just a warm cache.

Out of scope

  • Listen port is intentionally not in the UI; changing it requires editing /etc/mochan/config.env plus a service restart and a reverse-proxy update.
  • Password change is not in this release — same constraint (password hash lives in config.env).
  • 2FA / OAuth still out of scope; this is single-user by design.

v0.8.0

04 May 03:34

Choose a tag to compare

mochan-linux v0.8.0

本版本主线: 终端不再因为网络抖一下就死。WebSocket 断了 PTY 不死,客户端用同一 session ID 重连即接回原 shell,服务端 256 KiB 环形缓冲会重放最近输出,xterm 看到的就像 tmux 一样无缝。htop / vim / 编译输出在地铁信号断、笔记本短暂休眠、CF 间歇性掐 WebSocket 时都不丢。

中文

后端: 会话与 WebSocket 解耦

internal/pty 重构,新增 session.go:

  • Session: 一个长寿命的 PTY 包装。持有 *os.File(PTY) + *exec.Cmd(bash) + ringBuffer (256 KiB) + 订阅者集合。一个独立 goroutine 持续从 PTY 读,每块同时 (a) 写入环形缓冲、(b) 广播给所有订阅者通道。
  • Manager: 进程级单例,持有 map[string]*Session,带 30 秒一次的 reaper goroutine。reaper 把"订阅者数 == 0 且 lastIdle 超过 idleTTL"的 session 标记关闭(默认 idleTTL = 5 分钟)。
  • /ws/pty: 不再每次 WebSocket 进来就 pty.StartWithSize 起新 shell。改为 Manager.GetOrCreate(id, opts) —— 已存在就附加(共用 PTY + 缓冲),不存在就用这个 ID 创建新 session。

协议: 握手控制帧 + 二进制重放

升级成功后,服务端现在先发一个 JSON 文本帧:

{
  "type": "attached",
  "session_id": "abc123…",
  "cols": 80,
  "rows": 24,
  "buffer_len": 12345
}

紧接着发 buffer_len 字节的二进制帧,内容是环形缓冲的完整快照(可以是 0 字节)。

之后流量回到 v0.2.0 起就有的格式:

  • 二进制帧 = PTY 输出/输入
  • 文本帧 = JSON 控制消息(目前只有 {type:"resize", cols, rows})

前端: apps/Terminal.tsx 自动重连

  • 组件挂载时用 crypto.randomUUID() 生成 session ID,放在闭包里(不写 localStorage,见下)。
  • WebSocket URL 带 ?session=<id>,断开后跑指数退避重连(初始 600 ms,倍率 1.6,封顶 8 s,加随机抖动)。
  • 重连成功在终端里打印 [已重新连接](绿色,2 行,不打扰主流)。
  • 第一次连接收到 attached 控制帧后,缓冲重放只是写进 xterm,xterm 自己根据 ANSI 序列(光标定位 / 清屏 / 重绘)正确恢复界面——TUI 比如 vim/htop 在 SIGWINCH 后也会自己全屏重绘,所以"重连后画面正确"是协议自然产物。

为什么 session ID 不持久化

刻意的:

  1. 多窗口需要独立 shell。如果 ID 写 localStorage,同一浏览器开两个终端窗口会争抢同一个 PTY,鼠标在哪个窗口打字都会"穿越"。
  2. 浏览器硬刷新后想要"接续昨天的会话"是 tmux / screen 的工作,不该让本工具承担。tmux 已经做得很好,直接 tmux new -s work + tmux attach -t work 即可。
  3. 当 session 被 reaper 回收后,继续用同一 ID 重连服务端会新建一个同 ID 的 session(因为 ID 不可达 → 当作新建),静默切到一个空 shell,用户看到"重连成功但是 htop 没了"会很困惑。强制每次新组件挂载就新 ID,行为一致。

空闲回收: 5 分钟无客户端 → kill PTY

Manager.reapLoop 每 30 秒扫一次:订阅者数 == 0 且 lastIdle 超过 5 分钟的 session 调用 markClosed(),关 PTY、process.Kill + Wait、关所有订阅者通道、从 map 移除、写日志 pty: reaping idle session <id>

正常场景:

  • 你打开一个终端窗口 → 创建 session A → 你点关闭按钮(window 关闭) → WebSocket 断 → reaper 5 分钟后清掉 A。中间这 5 分钟你又打开一个新终端就是新 session(因为新组件 = 新 ID),A 自然死。
  • 网络断 30 秒重连 → 你的客户端用同 ID 重连 → reaper 不会动这个 session,因为 lastIdle 在 30 秒前被刷新成 now,但更重要的是你重连后订阅者数从 0 变 1,reaper 条件不满足。

已知边界

  • 服务进程崩溃 / 重启 → 所有 session 死。这是单进程内存型 session,不持久。systemd 重启服务后 PTY 全没,前端会看到重连失败一段时间然后 reconnect 时 server 给一个新 session ID(空 shell)。要扛进程崩溃就得用 tmux,这是一句话能解的事所以本版本不投入 disk-backed session。
  • 共享一个 ID 的多客户端: 协议支持(同时广播给所有订阅者),但前端没暴露这个能力,每个 Terminal 组件实例都有独立 ID。
  • 环形缓冲 256 KiB: 大概是 80×24 全屏 ANSI 重绘 5–8 次。tmux / vim 这类自带屏幕状态的应用没问题;tail -f /var/log/big.log 这种纯 append 流断 1 分钟可能丢失一些早期行(屏幕重绘可恢复但是 xterm scrollback 里只剩缓冲长度)。

English

Backend: PTY lifetime decoupled from WebSocket

internal/pty now has a Session/Manager pair. A Session wraps the PTY file, the *exec.Cmd, a 256 KiB ring buffer, and a subscriber map; a goroutine streams every PTY read into the buffer and fans it out to subscriber channels. A process-wide Manager owns the named-session map and runs a 30 s reaper that kills sessions whose subscriber count has been zero for >5 minutes.

Protocol: handshake control frame + binary replay

After upgrade, the server sends a JSON text frame {"type":"attached", "session_id":..., "cols":..., "rows":..., "buffer_len":N} followed by N bytes of binary replay. The rest of the protocol is unchanged.

Frontend: random session ID + exponential backoff

apps/Terminal.tsx mints a random session ID per component instance, attaches it to the WebSocket URL, and on close auto-reconnects with jittered exponential backoff (600 ms → 8 s, factor 1.6). After a successful reconnect, the terminal prints [已重新连接] inline.

Intentional non-features

  • Session ID is not persisted to localStorage. Two terminal windows in the same browser get independent shells; a hard refresh starts a fresh shell. Use tmux for "want my shell back tomorrow."
  • Sessions die with the server process — they live in memory. systemd restart kills all PTYs.
  • Buffer is 256 KiB (~5–8 full-screen redraws); long pure-append streams may lose early lines on a 1-minute disconnect.

v0.6.0

03 May 18:19

Choose a tag to compare

mochan-linux v0.6.0

本版本主线: 应用之间真的串起来了。FileManager 双击文件不再永远落到 CodeMirror 模态框,按扩展名路由到对应的桌面 App: .md 进 MarkdownEditor、图片进 ImageViewer、源码 / 文本进 TextEditor。这些 App 在被这样打开时会直接走 /api/fs/read(或图片的 downloadURL)从主机文件系统加载,保存按钮也直接写回真文件——不再是浏览器下载。

中文

路由表 (lib/openFile.ts)

按扩展名分派,文件管理器双击触发:

扩展名 目标 App 行为
.md / .markdown MarkdownEditor /api/fs/read 加载,保存按钮 → /api/fs/write
.jpg / .jpeg / .png / .gif / .webp / .svg / .bmp / .ico / .avif ImageViewer /api/fs/download 直链作为 <img src>,自动测尺寸
.txt / .log / .conf / .cfg / .ini / .env / .sh / .bash / .zsh / .go / .py / .rs / .js / .jsx / .ts / .tsx / .json / .yaml / .yml / .toml / .html / .css / .sql / .rb / .php / .java / .kt / .c / .cpp / .h / .hpp / .cs TextEditor /api/fs/read 加载,Ctrl+S → /api/fs/write
其它文本(≤8 MiB) FileManager CodeMirror 模态 兜底,与 v0.3.0 一样
二进制 / 大文件 下载 兜底,与 v0.3.0 一样

数据通道: WindowData.payload

useWindowStoreWindowData 加了 payload?: Record<string, unknown> 字段。openWindow(appId, title, { payload: { path, source: 'filemanager' } }) 把上下文塞进去,App 用 usePayloadPath(windowId) 取出。

这是一个可选通道——没 payload 启动的窗口(开始菜单 / Dock 点击)走原来的本地存储行为完全不变。

三个被改造的 App

  • TextEditor: 收到 payload 时,丢掉 localStorage 的草稿,改成显示远端文件内容。Ctrl+S 和 "保存" 按钮直接写 /api/fs/write。无 payload 时保留原来的"下载到浏览器" 行为。
  • MarkdownEditor: 同上。继续支持实时 Markdown 预览。
  • ImageViewer: 收到 payload 时,把 /api/fs/download?path=... URL 当作 <img src> 直接渲染,顺便用 Image() 探测自然尺寸填进 images 数组。其它已有功能(缩放 / 旋转 / 翻转 / 拖放上传 / 幻灯片)不变。

为什么不路由 .json 到 JSONEditor?

JSONEditor 还是个本地 stub,没有 fs-aware 模式。.json 在路由表里被归到 TextEditor(CodeMirror 自带 JSON 高亮),后续可以单独把 JSONEditor 改造好再切。

未做

  • 反向操作不存在: TextEditor 内置的 "新建/打开本地文件 / 下载" 按钮没有"保存到主机文件系统"的快捷入口。临时方案是从 FileManager 启动以拿到 payload。
  • 没有 "用其它 App 打开" 的右键菜单——只有默认路由,改路由要改 lib/openFile.ts
  • ImageViewer 的"下一张/上一张"在 payload 模式下只有一张图,翻页无效;需要改成支持远端目录列表。

English

Extension routing (lib/openFile.ts)

Double-clicking a file in FileManager now routes by extension:

Ext Target App Behaviour
.md / .markdown MarkdownEditor loads via /api/fs/read, save → /api/fs/write
common image extensions ImageViewer uses /api/fs/download as the <img> source, probes natural size
common text / code extensions TextEditor loads via /api/fs/read, Ctrl+S → /api/fs/write
other text (≤8 MiB) FileManager CodeMirror modal fallback (unchanged)
binary / oversize download fallback (unchanged)

Data channel: WindowData.payload

useWindowStore.WindowData gained payload?: Record<string, unknown>. The opener writes openWindow(appId, title, { payload: { path, source: 'filemanager' } }); consumers read usePayloadPath(windowId). Apps launched without a payload keep their original local-storage behaviour.

Apps modified

TextEditor, MarkdownEditor, ImageViewer now opt in to the payload protocol. When launched with a path, they fetch the file from /api/fs and override the default content; save buttons write back to /api/fs/write instead of triggering a browser download.

Not yet

  • TextEditor's stand-alone "save" still downloads to the browser unless it was launched with a payload.
  • No right-click "open with…" menu yet — defaults are hard-coded in lib/openFile.ts.
  • ImageViewer in payload mode shows a single image; the prev/next paging assumes a directory walk, which we haven't wired yet.

v0.7.0

03 May 18:19

Choose a tag to compare

mochan-linux v0.7.0

本版本主线: 手机能用了。窄屏 (< 768 px) 下窗口自动覆盖整屏(只在 StatusBar 与 Dock 之间留必要空间),拖动 / 调整大小 / 调整边把全部禁掉,标题栏的 12 px 红黄绿三个圆点放大到 22 px 以满足触屏点击区域。Dock 改成 56 px 高、横向滚动,图标列再多也能滑出来。

中文

WindowFrame 改造

新加 useIsMobile() 检测视口 < 768 px。命中时对每个窗口套一组 mobile-only 样式覆写:

字段 桌面 移动
left win.x 0
top win.y 28 (StatusBar 高度)
width win.width 100vw
height win.height calc(100vh - 84px) (减去 StatusBar 28 + Dock 56)
borderRadius 8px 0
拖动 标题栏 cursor=move handleMouseDown 早退,无任何拖动反应
调整大小 渲染 8 个 resize handle 不渲染
红黄绿圆点尺寸 12 × 12 px 22 × 22 px

无 payload 桌面行为完全不变。

Dock 改造

字段 桌面 移动
高度 64 px 56 px
圆角 16px 16px 0 0 12px 12px 0 0
最大宽度 90vw 100vw
横向滚动 不允许 overflow-x-auto

验证

把 Chrome devtools 切到 iPhone 14(390 × 844)分辨率,逐个 App 试:

  • 登录页:已经是 max-w-sm 居中表单,直接 OK,密码框 / 按钮 tap 命中正常。
  • 终端:xterm.js 5.x 触屏点击会 term.focus() 唤起系统软键盘,选择 / 长按复制原生支持。
  • 文件管理器:列表自适应 100% 宽,操作按钮(下载 / 重命名 / 删除)tap 命中正常,文件名长会截断 + 悬浮 title。
  • 系统监控 / 审计日志 / 任务管理器:卡片 / 表格在窄屏会横向滚,功能不残废,但视觉密度偏大。
  • StatusBar:右上角图标在窄屏上会被语言开关 / 用户名挤压,但都还点得到。

没做的

  • Desktop 桌面图标仍按桌面 drag-and-drop 设计,长按拖动暂未实现。
  • AppLauncher 分类网格在 < 768 px 下会单列偏挤,未做专门列宽适配。
  • 没有"手势返回"(滑动关闭窗口) / "三指切换" 这类原生体验。
  • 没有 PWA manifest.json / Service Worker——本版本不离线、不可加到主屏。

这是一次可用性验收,不是移动端 UX 终态。后续 Stage 10(Settings 实页)会把"主题密度 / 触屏模式"这类系统级设置打通,届时一并优化。

English

WindowFrame mobile overrides

useIsMobile() detects viewport < 768 px. When true:

  • left=0, top=28 (StatusBar), width=100vw, height=calc(100vh - 84px) (StatusBar 28 + Dock 56).
  • Drag (title bar) and resize (8 edge/corner handles) are disabled.
  • The 12-px traffic-light buttons grow to 22 px so they exceed Apple HIG's 44-px touch target across two finger axes when combined with padding. Acceptable.
  • Border-radius 0 — fullscreen looks intentional.

Dock mobile mode

  • 56-px tall (vs 64 px desktop), 100vw wide (vs 90vw), overflow-x-auto so the long pinned + open + trash icon list can scroll horizontally.
  • Desktop behaviour unchanged.

Verified at 390 × 844

Login, terminal (xterm.js handles touch out of the box), file manager, audit log, system monitor, task manager — all reachable. Some density issues (StatusBar icons crowd the right edge, AppLauncher category grid is tight) but no functional regressions.

Not yet

  • Desktop icons still designed for mouse drag-and-drop. Long-press drag is not wired.
  • AppLauncher category grid is cramped on phone widths.
  • No PWA manifest.json or Service Worker. Offline / install-to-home-screen is out of scope here.
  • No native gesture mappings (swipe-to-close, etc.).

This is a viability pass to make the system usable from a phone in an emergency, not the final mobile UX.

v0.5.0

03 May 17:56

Choose a tag to compare

mochan-linux v0.5.0

本版本主线: 加上安全审计。任何敏感操作都会落到一份只追加的 JSONL 日志里——登录成功、登录失败、退出登录、文件写 / 删 / 移 / 上传、进程被杀,带真实客户端 IP(透过 Cloudflare 与反向代理的 X-Forwarded-For / CF-Connecting-IP)。新增 审计日志 桌面应用,可按事件类型筛选,5 秒自动刷新,顶栏统计登录成功 / 失败次数。

中文

后端 internal/audit

  • JSONL 写入: 每行一个 Event,字段为 time / type / actor / ip / outcome / detail。文件位于 <DataDir>/audit.log(默认 /var/lib/mochan/audit.log),权限 0640,只追加。
  • 轮转: 单文件超过 10 MiB 时改名为 .1 重开;最多保留一份历史,旧的 .1 会被覆盖。单用户低频场景下不需要 logrotate 那一套。
  • 静默容错: Logger.Log 是 nil-safe,审计写入失败永远不会中断底层操作(登录、文件写等)。
  • 真客户端 IP: audit.ClientIP(r) 按优先级取 CF-Connecting-IPX-Real-IPX-Forwarded-For(取首段) → RemoteAddr。在 linux.mochance.xyz(CF orange-cloud → NPM → mochan)上确认能拿到原始访问者 IP。

已捕获事件

类型 触发位置 detail 字段
auth.login.success /api/auth/login 验证通过 (无,actor 即用户名)
auth.login.fail /api/auth/login 401 前 (无,outcome=deny)
auth.logout /api/auth/logout (无)
fs.write /api/fs/write 成功后 path, size
fs.mkdir /api/fs/mkdir 成功后 path, parents
fs.delete /api/fs/?path=... DELETE 成功后 path, recursive
fs.move /api/fs/move 成功后 from, to
fs.upload /api/fs/upload 成功后 dir, files: [{name, size}]
sys.kill /api/sys/kill 成功发信号后 pid, signal

故意记录的事件:fs.list / fs.read / fs.stat / fs.download / sys.stat / sys.processes —— 这些是高频读操作,记进审计日志反而把真正可疑的事件埋掉。

查询接口

  • GET /api/sys/audit/?limit=200&type=auth.login.fail —— 返回最近 N 条事件,按时间倒序。type 可选,精确匹配。
  • 透明合并 audit.logaudit.log.1 两个文件,所以即使最近发生过轮转也能回溯。
  • 单次最多 5000 条;有截断时返回 more: true

前端 apps/AuditLog.tsx

  • 注册在"系统工具"分类,图标 ScrollText
  • 顶栏:事件类型下拉(所有事件 / 9 种事件类型)、自动刷新开关(默认开,5 秒一次)、手动刷新按钮、清除筛选按钮。
  • 表格:时间(本地时区) / 事件(带颜色的 chip) / 用户 / IP / 结果(ok 灰,deny 红,error 橙) / 详情(把 detailkey=value 拍平)。
  • 顶栏右侧实时统计当前视图里登录成功 / 失败次数,方便一眼看出爆破。

端到端验证

部署到 https://linux.mochance.xyz 后用 /tmp/mochan-deploy/credentials.txt 跑了一次完整流程:

auth.login.fail   admin   38.150.4.233   deny   (wrong password)
auth.login.success admin  38.150.4.233   ok
fs.mkdir          admin   38.150.4.233   ok     {path:"/tmp/audit-test", parents:false}
fs.write          admin   38.150.4.233   ok     {path:"/tmp/audit-test/x.txt", size:2}
fs.delete         admin   38.150.4.233   ok     {path:"/tmp/audit-test", recursive:true}
auth.logout       admin   38.150.4.233   ok
auth.login.success admin  38.150.4.233   ok     (re-login)

仓库整理(同版本附赠)

  • 删掉了 v0.1.0 → v0.4.0 三条 commit 里残留的 Co-Authored-By: Claude trailer,https://github.com/Cmochance/mochan-linux/graphs/contributors 现在只剩 Cmochance。

已知未做

  • 暂未提供"导出审计日志为 CSV / 下载原始 JSONL 文件"的按钮 —— 现阶段直接 cat /var/lib/mochan/audit.log 或走 /api/sys/audit/?limit=5000 拿就可以。
  • 没有"按 IP 自动锁定"逻辑(类似 fail2ban) —— 仍然推荐在反向代理或操作系统层做。审计日志的目的是事后可追溯,不是 IPS。
  • 终端会话结束 / WebSocket 断开尚未审计;/ws/pty 内部的 shell 命令也不会被审计(终端字面上是"shell 直连",审计意义有限,且记录所有键盘输入会冲掉真正的安全事件)。

English

Backend internal/audit

  • JSONL append-only writer at <DataDir>/audit.log (default /var/lib/mochan/audit.log), perms 0640. Each row carries time, type, actor, ip, outcome, detail.
  • Rotates to .1 when the active file exceeds 10 MiB; we keep one rotation. Adequate for single-user, low-volume audit; no logrotate dependency.
  • Logger.Log is nil-safe — an audit-write failure never breaks the underlying operation (login, file write, etc.).
  • Real client IP: audit.ClientIP(r) prefers CF-Connecting-IPX-Real-IP → first hop of X-Forwarded-ForRemoteAddr. Verified end-to-end on the linux.mochance.xyz deployment (Cloudflare → NPM → mochan).

Captured events

auth.login.success, auth.login.fail, auth.logout, fs.write, fs.mkdir, fs.delete, fs.move, fs.upload, sys.kill. Each event records actor, IP, outcome (ok / deny / error) and event-specific detail (path, size, recursive, signal, etc.).

Deliberately not recorded: fs.list / fs.read / fs.stat / fs.download / sys.stat / sys.processes. High-frequency reads drown out the actually suspicious events.

Query endpoint

  • GET /api/sys/audit/?limit=200&type=auth.login.fail — newest-first, transparently merges audit.log and audit.log.1 so a recent rotation does not lose events.
  • Single response max 5000 rows; more: true indicates truncation.

Frontend apps/AuditLog.tsx

Registered as "审计日志" / "Audit Log" under system tools (ScrollText icon). Type-filter dropdown, 5 s auto-refresh toggle, manual refresh, clear-filter. Table renders time / typed-and-colored badge / actor / IP / outcome / flattened detail. Toolbar shows live login-success and login-fail counts.

Repo housekeeping (same release)

  • Stripped Co-Authored-By: Claude trailer from the v0.1.0 → v0.4.0 commits. The contributor graph now correctly shows only Cmochance.

Known gaps

  • No CSV / raw-JSONL export button yet — the file at /var/lib/mochan/audit.log is plain JSONL, and /api/sys/audit/?limit=5000 already returns the same content.
  • No IP-based auto-blocking (no fail2ban-equivalent). Audit is for forensic trail, not IPS — keep IP throttling at the reverse-proxy / OS layer.
  • PTY sessions are not audited at the keystroke level — the keyboard stream goes straight into a real shell, and recording every keystroke would drown the security signal we actually care about.

Verify and install

curl -LO https://github.com/Cmochance/mochan-linux/releases/download/v0.5.0/mochan-linux-amd64.tar.gz
curl -LO https://github.com/Cmochance/mochan-linux/releases/download/v0.5.0/mochan-linux-amd64.tar.gz.sha256
sha256sum -c mochan-linux-amd64.tar.gz.sha256
tar xzf mochan-linux-amd64.tar.gz

The release was built by GitHub Actions (.github/workflows/release.yml) on tag push; both linux/amd64 and linux/arm64 artifacts are signed by their per-file .sha256.

v0.4.0 — Phase 3

03 May 17:12

Choose a tag to compare

mochan-linux v0.4.0

本版本主线:首次公开,把"浏览器即 Linux 桌面"从概念做到端到端可用。一周内连续推完 Phase 0–3,单 Go 二进制(13 MB,前端 embed.FS 注入)就能部署一个让你从任何设备打开 https://your.domain 直接拿到真终端、真文件系统、真进程管理、真系统监控的水墨风工作站,专门用来测试自己开发的 Linux 应用。

中文

一、登录与认证 (v0.1.0 → v0.1.1)

  • 登录门: bcrypt 密码 + HS256 JWT,Token 写到 HttpOnly Secure SameSite=Lax cookie。前端 AuthGate 启动时调 /api/me,未登录显示水墨风登录屏,未通过认证看不到桌面任何内容
  • 退出登录: StatusBar 右上角用户下拉,点击调 /api/auth/logout 清 cookie,立刻回到登录屏。
  • CLI 子命令: mochan hash-password 从 stdin 读密码出 bcrypt 哈希,mochan gen-secret 出 48 字节十六进制 JWT 密钥,避免明文密码进 shell history。
  • Phase 0 安全修复: 早期前端的桌面在未认证时直接渲染(后端鉴权对,前端没 gate);这一版补齐前端 gate,把"看不到 UI 就是没访问权"做成硬保证。

二、真终端 (v0.2.0)

  • 后端 /ws/pty: 基于 coder/websocket + creack/pty,通过 cookie 或 ?token= 传 JWT,握手时验证后调 pty.StartWithSizebash -l。运行身份是服务用户(mochan),有 NOPASSWD sudo 时浏览器里直接 sudo apt install … 测自己应用,无需 SSH。
  • 前端 apps/Terminal.tsx: @xterm/xterm v5 + xterm-addon-fit,TERM=xterm-256color + COLORTERM=truecolor + LANG=C.UTF-8,htop / vim / tmux / lazygit 全跑,窗口 resize 通过 JSON 控制帧 {type:"resize", cols, rows} 发回服务器走 TIOCSWINSZ
  • 配色: 12 位 ANSI 调色板按水墨主题重调,不抢桌面注意力。

三、真文件系统 (v0.3.0)

  • 后端 /api/fs/*: home / list / read / write / mkdir / delete / move / upload / download / stat。读 8 MiB 上限、写 32 MiB、上传 256 MiB;返回真实 OS 错误(403 / 404 / 409 / 413)。
  • 不做 chroot: 整个主机文件系统可见,权限完全由 OS 决定。mochan 用户访问 /root → 403,访问 /etc/hostname → 200 文本,访问自己 home → 全权限。
  • 前端 apps/FileManager.tsx: 替换之前的 20 行占位 stub。顶栏路径输入框 + 上一级 / 主目录 / 刷新;左侧栏快捷入口(主目录 / / / /etc / /var/log / /tmp);表格视图带图标 / 大小 / 修改时间 / 权限串 / 操作(下载 / 重命名 / 删除);软链显示 → target;双击目录进入,双击文本进编辑器,双击非文本走下载。

四、代码编辑器、系统监控、进程管理 (v0.4.0)

  • CodeMirror 6 编辑器 (components/CodeEditor.tsx): 替换 v0.3.0 的 textarea。按文件扩展名自动识别 JS / TS / JSON / Python / HTML / CSS / Markdown / YAML 加载语言扩展,one-dark 主题,行号 / 折叠 / 自动补全 / 搜索全开。FileManager 双击文本文件直接进。
  • /api/sys/stat: 基于 gopsutil/v4,单次请求拿主机名 / 内核 / OS / 架构 / uptime / 1-5-15 负载 / CPU 总占用 + 每核占用 / 内存 / Swap / 所有非 pseudo 挂载点 / 网络累积字节。
  • apps/SystemMonitor.tsx: 每 2 秒轮询,进度条颜色按占用阈值变化(≤60 % 蓝、60-85 % 橙、>85 % 红),网速通过两次采样差分本地计算。
  • /api/sys/processes + /api/sys/kill: 进程列表带 PID / PPID / 用户 / 状态 / CPU% / RSS / 线程 / cmdline;kill 端点接受 TERM / INT / HUP / KILL,拒绝 pid ≤ 1,权限不足返回 403。
  • apps/TaskManager.tsx: 替换之前的伪进程列表。搜索 PID / 名称 / 用户 / cmdline;按 CPU / RSS / PID / 名称双向排序;杀进程对话框含信号选择器和说明(TERM 优雅 / INT 等价 Ctrl+C / HUP 重载 / KILL 强杀)。

五、部署与发布

  • 单二进制: 前端 npm run buildcp -r web/dist server/internal/static/dist,再 go build 通过 embed.FS 注入。运行时只需要二进制 + /etc/mochan/config.env,无 Node 运行时、无 npm、无依赖目录
  • systemd unit: After=docker.service(因为绑 Docker bridge IP 172.17.0.1:38421,对应反向代理在容器里的常见情况) + EnvironmentFile=/etc/mochan/config.env,崩溃自动 Restart=on-failure
  • install.sh: 交互式装服务用户(默认 mochan)+ NOPASSWD sudo + 二进制 + systemd unit + 启动 + curl /healthz 自检,幂等可重跑。
  • Nginx Proxy Manager 集成: 完整字段说明在 deploy/npm-proxy-host.md已在 NPM Advanced 框踩过坑——文档明确告知只能贴完整 nginx 指令(proxy_read_timeout 86400s; 不能写 proxy_*_timeout),否则 NPM 会保存原文导致 nginx -t 失败 + SSL block 不生成 → Cloudflare 525 SSL handshake failed。
  • 多架构 release: .github/workflows/release.yml 监听 v* tag,并行构建 linux/amd64 + linux/arm64,各自打 tar.gz + sha256,上传到 Release。本版本预构建产物含 mochan-linux-amd64.tar.gzmochan-linux-arm64.tar.gzSHA256SUMS

六、生产部署示范

  • 已在 https://linux.mochance.xyz 跑通,运行宿主: DigitalOcean Droplet (Ubuntu 24.04.3, 2 vCPU, 4 GB)。
  • 反向代理: Nginx Proxy Manager 容器 + Cloudflare orange-cloud(CF SSL/TLS = Full(strict))。
  • 端口分配: mochan 绑 172.17.0.1:38421(Docker bridge IP,私网,外网不可达),NPM 反代 linux.mochance.xyzhttp://172.17.0.1:38421,WebSockets 升级开启。

已知未做

  • 桌面应用之间的 inter-app 调用(FileManager 双击 .md 跳到 MarkdownEditor 等)尚未串起来,每个 app 独立运行。
  • 多用户与会话隔离(每用户独立容器、配额)目前明确不在 scope 内——本项目就是单用户。
  • 文件管理器未做拖拽多选 / 右键菜单。
  • 终端会话不支持断线重连(暂时是切断重起)。

English

Headline of this release: first public cut, "browser as Linux desktop" taken from concept to end-to-end usable. Phases 0–3 shipped in one week. A single Go binary (13 MB, frontend embedded via embed.FS) is enough to deploy a workstation reachable as https://your.domain from any device, giving you a real terminal, real filesystem, real process manager, and real system monitor — purpose-built for testing your own Linux applications.

1. Auth and login (v0.1.0 → v0.1.1)

  • bcrypt password + HS256 JWT in an HttpOnly Secure SameSite=Lax cookie. The React AuthGate calls /api/me on mount; on 401 it shows an ink-style login screen, and the desktop renders nothing until login succeeds.
  • StatusBar user dropdown with "Log out" calls /api/auth/logout, clears the cookie, and returns to the login screen.
  • CLI: mochan hash-password reads from stdin and prints a bcrypt hash; mochan gen-secret prints a 48-byte hex JWT key. No plaintext passwords land in shell history.
  • Phase 0 frontend gap: an earlier build rendered the desktop with no auth check (the backend was already gated, but the frontend was not). Fixed in v0.1.1 — no UI without a verified token.

2. Real terminal (v0.2.0)

  • Backend /ws/pty over coder/websocket + creack/pty, JWT via cookie or ?token=. Spawns bash -l as the service user (mochan); with NOPASSWD sudo configured, you can sudo apt install … directly from the browser to test your own apps without SSH.
  • Frontend apps/Terminal.tsx: @xterm/xterm + fit addon, TERM=xterm-256color, COLORTERM=truecolor, LANG=C.UTF-8. htop / vim / tmux / lazygit all work. Window resize sends {type:"resize", cols, rows} JSON control frames; the server applies TIOCSWINSZ.
  • 12-color ANSI palette retuned to match the ink theme.

3. Real filesystem (v0.3.0)

  • Backend /api/fs/*: home, list, read, write, mkdir, delete, move, upload, download, stat. Read cap 8 MiB, write 32 MiB, upload 256 MiB. Errors surface as real OS conditions (403 / 404 / 409 / 413).
  • No chroot. The whole host FS is reachable; the OS enforces the boundary. mochan reading /root → 403; reading /etc/hostname → 200 text; reading its own home → full access.
  • Frontend apps/FileManager.tsx replaces the 20-line placeholder. Path input + up / home / refresh in the toolbar; sidebar shortcuts (Home, /, /etc, /var/log, /tmp); table view with icon / size / mtime / permission string / actions (download / rename / delete); symlinks shown with → target; double-click a folder to descend, a text file to open the editor, a binary to download.

4. Editor, system monitor, process manager (v0.4.0)

  • CodeMirror 6 editor (components/CodeEditor.tsx) replacing the v0.3.0 textarea. JS / TS / JSON / Python / HTML / CSS / Markdown / YAML language modes auto-loaded by extension, one-dark theme, line numbers / folding / autocompletion / search all on. FileManager opens text files into it.
  • /api/sys/stat backed by gopsutil/v4. One request returns hostname / kernel / OS / arch / uptime / 1-5-15 load / aggregate and per-core CPU / memory / swap / every non-pseudo mountpoint / cumulative network bytes.
  • apps/SystemMonitor.tsx polls every 2 s; bar color shifts by threshold (≤60 % blue, 60–85 % orange, >85 % red). Network rate is computed locally from two consecutive samples.
  • /api/sys/processes + /api/sys/kill. Process list includes PID / PPID / user / status / CPU% / RSS / threads / cmdline. The kill endpoint accepts TERM / INT / HUP / KILL, refuses pid ≤ 1, and surfaces permission errors as 403.
  • apps/TaskManager.tsx replaces the previous fake process list. Search across PID / name / user / cmdline; sort by CPU / RSS / PID / name; kill dialog with signal selector and short explanations (TERM graceful, INT = Ctrl+C, HUP reload, KILL non-catchable).

5. Deploy and release

  • Single binary. npm run buildcp -r web/dist server/internal/static/distgo build injects the frontend through embed.FS. The runtime needs only the binary plus /etc/mochan/config.env. No Node runtime, no npm, no node_modules.
  • systemd unit with After=docker.service (because the typical setup binds 172.17.0.1:38421 to be reachable from a containerized reverse proxy) and EnvironmentFile=/etc/mochan/config.env. Restart=on-failure for crash recovery.
  • install.sh: idempotent — creates the service user (default mochan), grants NOPASSWD sudo, installs the binary, writes the systemd unit, starts the service, curls /healthz.
  • Nginx Proxy Manager: full field-by-field setup in deploy/npm-proxy-host.md. The doc explicitly warns that the Advanced Config field is pasted verbatim — you must use complete directives like proxy_read_timeout 86400s;, not abbreviated patterns like proxy_*_timeout, or NPM saves the bad text and nginx -t fails silently, the listen 443 ssl block is suppressed, and Cloudflare returns "525 SSL handshake failed".
  • Multi-arch release. .github/workflows/release.yml triggers on v* tags, builds linux/amd64 and linux/arm64 in parallel, packages each as a .tar.gz with sha256, and uploads them to the GitHub Release.

6. Reference deployment

Production instance: https://linux.mochance.xyz. Host: DigitalOcean Droplet (Ubuntu 24.04.3, 2 vCPU, 4 GB). Reverse proxy: containerized Nginx Proxy Manager. CDN: Cloudflare proxy (orange cloud) with SSL/TLS = Full (strict). mochan binds 172.17.0.1:38421 (Docker bridge IP, never reachable externally), NPM forwards with WebSocket upgrade enabled.

Known gaps

  • Inter-app routing (e.g. double-clicking an .md in FileManager to launch MarkdownEditor) is not wired yet — apps are independent windows.
  • Multi-user is intentionally out of scope. This is single-user by design.
  • File manager has no drag-multi-select or right-click context menu yet.
  • Terminal sessions don't survive reconnect — they simply ...
Read more