23dcfb393f
Implements the design in claude.md as a building skeleton: pure Go, no cgo, cross-compiles to a single Windows .exe from Linux. Architecture follows the spec's deliberate split. The fast path is native — tray icon, grouped submenus, a Win32 MessageBox showing a live before/after diff, then netsh. The slow path is an embedded web editor served on a random loopback port and opened in the default browser. Two decisions worth recording: Reads use GetAdaptersAddresses, writes use netsh. The spec left the enumeration mechanism open; parsing `netsh show config` breaks on a non-English Windows because the output is localised. DNS static-vs-DHCP origin is not exposed by that API, so it comes from one registry read. The netsh command plan is built in portable code. That puts the delete-every- existing-address step — the one that stops secondary addresses leaking across switches — under test without needing a Windows box. The editor requires the session token in a header for mutations, not just the cookie: any page in the browser can make it POST to 127.0.0.1 with the cookie attached, but it cannot set a header. The updater refuses to install a release that publishes no SHA256. Not yet done: no group picker for export (the API supports it), no single-instance guard, and internal/server/web/app.css is reconstructed from the description in claude.md rather than the canonical apointless.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
194 lines
6.5 KiB
HTML
194 lines
6.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ipswap — presets</title>
|
|
<link rel="stylesheet" href="/app.css">
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header class="top">
|
|
<div>
|
|
<h1>ipswap</h1>
|
|
<div class="dim">Preset editor · this page closes itself when you stop using it</div>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button id="theme-toggle" class="ghost small">Light</button>
|
|
<button id="btn-settings" class="ghost small">Settings</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div id="alerts"></div>
|
|
|
|
<div class="stat-row" id="stats"></div>
|
|
|
|
<div class="card">
|
|
<div class="btn-row" style="justify-content: space-between;">
|
|
<h2 style="margin:0">Presets</h2>
|
|
<div class="btn-row">
|
|
<button id="btn-import">Import…</button>
|
|
<button id="btn-export">Export</button>
|
|
<button id="btn-new" class="primary">New preset</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="groups"></div>
|
|
|
|
<div class="card">
|
|
<h2>Adapters on this machine</h2>
|
|
<div class="table-scroll">
|
|
<table>
|
|
<thead>
|
|
<tr><th>Name</th><th>Description</th><th>Current IPv4</th><th>State</th></tr>
|
|
</thead>
|
|
<tbody id="adapters"><tr><td colspan="4"><span class="spinner"></span></td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- preset editor -->
|
|
<dialog id="editor">
|
|
<form method="dialog" id="editor-form">
|
|
<h2 id="editor-title">New preset</h2>
|
|
<div id="editor-error"></div>
|
|
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="f-name">Name</label>
|
|
<input id="f-name" required placeholder="Artist frame — control">
|
|
</div>
|
|
<div class="field">
|
|
<label for="f-group">Group</label>
|
|
<input id="f-group" list="group-list" placeholder="Riedel">
|
|
<datalist id="group-list"></datalist>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="f-adapter">Adapter</label>
|
|
<select id="f-adapter"></select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="f-mode">Mode</label>
|
|
<select id="f-mode">
|
|
<option value="static">Static</option>
|
|
<option value="dhcp">DHCP</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="static-fields">
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="f-address">Address</label>
|
|
<input id="f-address" placeholder="192.168.42.100">
|
|
</div>
|
|
<div class="field">
|
|
<label for="f-prefix">Mask <span class="dim">(/24 or 255.255.255.0)</span></label>
|
|
<input id="f-prefix" placeholder="/24">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="f-gateway">Gateway <span class="dim">(optional)</span></label>
|
|
<input id="f-gateway" placeholder="192.168.42.1">
|
|
</div>
|
|
<div class="field">
|
|
<label for="f-metric">Gateway metric <span class="dim">(0 = automatic)</span></label>
|
|
<input id="f-metric" type="number" min="0" value="0">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Secondary addresses</label>
|
|
<div class="sec-list" id="secondaries"></div>
|
|
<button type="button" id="btn-add-sec" class="small" style="margin-top:8px">Add secondary</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="f-dns-mode">DNS</label>
|
|
<select id="f-dns-mode">
|
|
<option value="static">Static</option>
|
|
<option value="dhcp">From DHCP</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="field" id="dns-servers-field">
|
|
<label for="f-dns">DNS servers <span class="dim">(comma separated, order matters)</span></label>
|
|
<input id="f-dns" placeholder="192.168.42.1, 1.1.1.1">
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="f-notes">Notes</label>
|
|
<input id="f-notes" placeholder="Frame A, rack 3">
|
|
</div>
|
|
|
|
<div class="btn-row" style="justify-content: flex-end; margin-top: 16px;">
|
|
<button type="button" id="btn-cancel" class="ghost">Cancel</button>
|
|
<button type="button" id="btn-save" class="primary">Save</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
|
|
<!-- settings -->
|
|
<dialog id="settings">
|
|
<h2>Settings</h2>
|
|
<div class="field">
|
|
<label for="s-mask">Mask display</label>
|
|
<select id="s-mask">
|
|
<option value="prefix">Prefix (/24)</option>
|
|
<option value="dotted">Dotted (255.255.255.0)</option>
|
|
</select>
|
|
</div>
|
|
<div class="field check">
|
|
<input type="checkbox" id="s-startup">
|
|
<label for="s-startup">Start ipswap when Windows starts</label>
|
|
</div>
|
|
<div class="field check">
|
|
<input type="checkbox" id="s-updates">
|
|
<label for="s-updates">Check for updates on startup</label>
|
|
</div>
|
|
<div class="field">
|
|
<label for="s-repo">Update repository <span class="dim">(https://host/owner/repo — blank disables)</span></label>
|
|
<input id="s-repo" placeholder="https://gitea.apointless.space/bsncubed/ipswap">
|
|
</div>
|
|
<div class="btn-row" style="justify-content: flex-end; margin-top: 16px;">
|
|
<button type="button" id="btn-settings-cancel" class="ghost">Cancel</button>
|
|
<button type="button" id="btn-settings-save" class="primary">Save</button>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- import -->
|
|
<dialog id="import">
|
|
<h2>Import presets</h2>
|
|
<div class="alert info">
|
|
The file is validated in full before anything is written. If it is rejected, nothing changes.
|
|
</div>
|
|
<div class="field">
|
|
<label for="i-file">Preset pack (.json)</label>
|
|
<input type="file" id="i-file" accept="application/json,.json">
|
|
</div>
|
|
<div class="field">
|
|
<label for="i-mode">Mode</label>
|
|
<select id="i-mode">
|
|
<option value="merge">Merge — keep what I have, add these</option>
|
|
<option value="replace">Replace — discard my existing presets</option>
|
|
</select>
|
|
</div>
|
|
<div id="import-error"></div>
|
|
<div class="btn-row" style="justify-content: flex-end; margin-top: 16px;">
|
|
<button type="button" id="btn-import-cancel" class="ghost">Cancel</button>
|
|
<button type="button" id="btn-import-go" class="primary">Import</button>
|
|
</div>
|
|
</dialog>
|
|
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|