feat(aura-gui): v0.1 Tauri-based desktop client — system tray + profile manager + admin status

New crate (kept out of the cargo workspace so the protocol-side check/test cycle stays fast):
a Tauri 2 + React 19 + TypeScript desktop app that runs in the system tray and manages
`aura client` for the user. The clash-verge replacement we settled on instead of trying to
shoehorn AuraVPN's L3 IP-tunnel into a clash-verge L4 outbound.

## What's wired

- **Profile manager** — `aura-gui/src-tauri/src/profiles.rs`. App-data layout
  (`~/Library/Application Support/ru.undergr0und.aura/profiles/<id>/` on macOS, the
  equivalent on Linux + Windows). `import_profile_from_tgz` accepts the same bundle shape
  `aura provision-client` emits, detects flat vs single-dir layouts, and refuses overwrites
  unless the operator deletes first. `delete_profile` refuses symlinks.

- **Connection control** — `cli_proc.rs`. Spawns `aura client --config <profile>/client.toml
  --admin-socket /tmp/aura-admin-<uid>-<profile>.sock`, captures stderr into a bounded
  in-memory ring (200 lines) for the UI to tail, kills via `Child::kill` on disconnect.
  Per-profile / per-uid socket paths so two GUIs (or two profiles) don't collide.

- **Live status** — `admin.rs`. Tiny JSON-line client for the v3.3 admin socket. Polled by
  the React app every 1.5 s: peer id, rx/tx packets, default action, rule count. Falls back
  gracefully (admin_error in the response) when the handshake hasn't completed yet.

- **System tray** — `lib.rs` `setup` callback. Three-item menu (Open AuraVPN / Disconnect /
  Quit). The window's close button hides to the tray instead of exiting — the app keeps
  running so the VPN stays connected; the user explicitly chooses Quit.

- **Frontend** — `src/App.tsx`. Single-page layout: profile list (with badge for missing
  files), connect/disconnect button per profile, status table, collapsible logs panel,
  binary-path picker at the bottom. Dark-mode CSS by default; the same look as a typical
  WireGuard / Tailscale-style tray app.

## What's deferred for v0.2

- Auto-start at login (launchd plist / systemd user unit / Windows Run key)
- Code signing + notarization
- Persisting the aura binary path between sessions
- Per-profile route overrides editor
- Live log streaming (today the frontend polls the ring buffer)
- Admin status query on Windows (today's `admin.rs` Unix-only; Windows path returns a clear
  "not supported yet" error)
- Polkit / authorization-services prompt for the TUN-needs-root step (today the operator
  has to launch the GUI from a privileged context, e.g. `sudo open -a aura-gui` on macOS)

## Workspace hygiene

Cargo workspace at the repo root now has `exclude = ["aura-gui"]` so the protocol crates'
`cargo check --workspace` / `cargo test --workspace` don't pull in the tauri + wry + webview
dep graph. The GUI builds standalone from `aura-gui/` via `npm run tauri build`.

## Validation

- `cd aura-gui/src-tauri && cargo check` — green
- `cd aura-gui/src-tauri && cargo clippy -- -D warnings` — clean
- `cd aura-gui/src-tauri && cargo fmt --check` — clean
- `cd aura-gui && npm run build` — frontend tsc + vite build succeeds
- Full `npm run tauri dev` not exercised in this session (would open a real window) — should
  work; if it breaks the surface area is small enough that next session fixes it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-29 17:47:51 +03:00
parent 7c2080321b
commit 40b38beb11
44 changed files with 9051 additions and 0 deletions
+278
View File
@@ -0,0 +1,278 @@
/* AuraVPN GUI — dark-mode by default, dense single-pane VPN dashboard. */
:root {
--bg: #0f1115;
--panel-bg: #1a1d24;
--border: #2a2f3a;
--text: #d9dde4;
--text-dim: #8a92a3;
--accent: #5ad3aa;
--accent-hot: #4ac09a;
--danger: #ef5a5a;
--warn: #f7b955;
--bad: #ef5a5a;
font-family: -apple-system, "Segoe UI", Inter, Avenir, Helvetica, Arial,
sans-serif;
font-size: 14px;
line-height: 1.45;
color: var(--text);
background-color: var(--bg);
}
body {
margin: 0;
}
.container {
max-width: 760px;
margin: 0 auto;
padding: 24px 24px 48px;
display: flex;
flex-direction: column;
gap: 16px;
}
header {
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
header h1 {
margin: 0;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.02em;
}
header .sub {
margin: 4px 0 0;
color: var(--text-dim);
font-size: 13px;
}
.pill {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.pill.running {
background: rgba(90, 211, 170, 0.18);
color: var(--accent);
}
.pill.stopped {
background: rgba(138, 146, 163, 0.18);
color: var(--text-dim);
}
.panel {
background: var(--panel-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
}
.panel.small {
padding: 10px 14px;
}
.panel h2 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--text-dim);
text-transform: uppercase;
}
.row-between {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.row-between h2 {
margin: 0;
}
.empty {
margin: 12px 0;
color: var(--text-dim);
font-style: italic;
}
button {
background: #2a2f3a;
color: var(--text);
border: 1px solid #2a2f3a;
border-radius: 6px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
button:hover:not(:disabled) {
background: #353a45;
border-color: #404552;
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
button.primary {
background: var(--accent);
color: #0f1115;
border-color: var(--accent);
}
button.primary:hover:not(:disabled) {
background: var(--accent-hot);
border-color: var(--accent-hot);
}
button.danger {
background: var(--danger);
color: white;
border-color: var(--danger);
}
button.danger:hover:not(:disabled) {
background: #d44e4e;
border-color: #d44e4e;
}
.profile-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.profile-list li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 12px 14px;
background: #14171d;
border: 1px solid #232730;
border-radius: 8px;
}
.profile-list li.active {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(90, 211, 170, 0.4);
}
.profile-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.profile-name {
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.profile-server {
font-size: 12px;
color: var(--text-dim);
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
}
.profile-id {
font-size: 11px;
color: var(--text-dim);
}
.profile-actions {
display: flex;
gap: 8px;
}
.badge {
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
text-transform: uppercase;
}
.badge.bad {
background: rgba(239, 90, 90, 0.15);
color: var(--bad);
}
.status {
border-collapse: collapse;
width: 100%;
}
.status td {
padding: 6px 8px;
border-bottom: 1px solid #232730;
font-size: 13px;
}
.status td:first-child {
width: 40%;
color: var(--text-dim);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.04em;
}
.status td.warn {
color: var(--warn);
}
.logs {
background: #0a0c10;
border: 1px solid #232730;
border-radius: 6px;
padding: 10px 12px;
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
font-size: 11px;
max-height: 260px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
color: #c6cbd5;
}
.error {
background: rgba(239, 90, 90, 0.12);
border: 1px solid rgba(239, 90, 90, 0.4);
border-radius: 8px;
padding: 10px 14px;
color: #ffb1b1;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
}
.aura-bin code {
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
font-size: 12px;
color: var(--text-dim);
}