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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user