Registering a machine meant reading a multi-step page, installing Node, and running three commands in the right order. Now it is one line per platform. The hub serves scripts/install.sh and scripts/install.ps1 with its own address and the enrolment token substituted in, so the published command carries everything and there is nothing to fill in: curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh irm https://support.freakma.com/install.ps1?token=TOKEN | iex That output is piped straight into a shell, so the token — the only untrusted value in either file — is refused unless it matches the base64url shape that randomToken produces. Each script checks for a usable runtime and stops with instructions rather than guessing, warns when nothing is serving RFB on the loopback, installs per-user with no root or administrator, and registers a login-scoped service: launchd on macOS, a lingering systemd user service on Linux, a logon task on Windows. The Windows script uses Node 22 when it is present and falls back to the bundled executable otherwise, which is what lets one command cover both Windows 11 and Server 2008 R2. It registers a logon task rather than a service on purpose: services run in session 0 and cannot draw on the interactive desktop, so the "ask first" consent prompt would never appear. Also adds /docs — a per-OS setup guide with service management and a troubleshooting table — and reworks the enrolment page into OS tabs that open on whichever platform the reader is sitting at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
5.2 KiB
HTML
167 lines
5.2 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="color-scheme" content="dark">
|
|
<title>Remote Support</title>
|
|
<link rel="stylesheet" href="/styles.css">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ambient particle field: login screen full strength, console dimmed ------ -->
|
|
<div class="bg-field" id="bg-field" aria-hidden="true"></div>
|
|
|
|
<!-- login ---------------------------------------------------------------- -->
|
|
<div id="login" class="login-shell hidden">
|
|
|
|
<section class="login-aside">
|
|
<div class="brand">
|
|
<div class="brand-mark">RC</div>
|
|
<div>
|
|
<h1>Remote Support</h1>
|
|
<span class="brand-sub">Operator console</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="login-headline">Sit down at any desk<br>on the <em>network</em>.</h2>
|
|
<p class="login-lede">
|
|
Screen sharing and remote control for the machines you look after — running
|
|
on your own hardware, on your own wire. Nothing leaves the building.
|
|
</p>
|
|
|
|
<ul class="login-facts">
|
|
<li>Self-hosted · no cloud relay</li>
|
|
<li>Consent prompt on the remote desk</li>
|
|
<li>Every session recorded in the audit log</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<form class="login-card" id="login-form">
|
|
<div class="brand card-brand">
|
|
<div class="brand-mark">RC</div>
|
|
<div>
|
|
<h1>Remote Support</h1>
|
|
<span class="brand-sub">Operator console</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="form-title">Sign in</h2>
|
|
<p class="faint form-note">Use your usual account.</p>
|
|
|
|
<div id="login-error" class="notice error hidden"></div>
|
|
|
|
<div class="field">
|
|
<label for="username">Username</label>
|
|
<input id="username" name="username" autocomplete="username" required autofocus>
|
|
</div>
|
|
<div class="field">
|
|
<label for="password">Password</label>
|
|
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
|
</div>
|
|
|
|
<button class="primary" type="submit" id="login-button">Sign in</button>
|
|
|
|
<p class="login-foot">Sessions expire after one hour</p>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- app ------------------------------------------------------------------ -->
|
|
<div id="app" class="hidden">
|
|
<header class="topbar">
|
|
<div class="brand">
|
|
<div class="brand-mark">RC</div>
|
|
<div>
|
|
<h1>Remote Support</h1>
|
|
<span class="brand-sub">Operator console</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="topbar-divider"></div>
|
|
|
|
<nav class="tabs" id="tabs">
|
|
<button data-tab="machines" class="active">Machines</button>
|
|
<button data-tab="invites">Invites</button>
|
|
<button data-tab="sessions">Sessions</button>
|
|
<button data-tab="audit">Audit</button>
|
|
</nav>
|
|
|
|
<div class="spacer"></div>
|
|
<a class="btn ghost small" href="/docs" target="_blank" rel="noopener">Setup guide</a>
|
|
<span class="faint" id="whoami"></span>
|
|
<button class="ghost small" id="logout">Sign out</button>
|
|
</header>
|
|
|
|
<main>
|
|
<!-- machines -->
|
|
<section id="tab-machines">
|
|
<div class="panel-head">
|
|
<h2>Machines</h2>
|
|
<span class="faint" id="machines-count"></span>
|
|
<div class="spacer"></div>
|
|
<button class="small" id="btn-enroll-invite">Invite a machine</button>
|
|
<button class="primary small" id="btn-add-client">Add machine</button>
|
|
</div>
|
|
<div id="machines" class="grid"></div>
|
|
</section>
|
|
|
|
<!-- invites -->
|
|
<section id="tab-invites" class="hidden">
|
|
<div class="panel-head">
|
|
<h2>Invite links</h2>
|
|
<div class="spacer"></div>
|
|
<button class="small" id="btn-new-enroll">New enrollment link</button>
|
|
<button class="primary small" id="btn-new-share">New support link</button>
|
|
</div>
|
|
<div id="invites"></div>
|
|
</section>
|
|
|
|
<!-- sessions -->
|
|
<section id="tab-sessions" class="hidden">
|
|
<div class="panel-head">
|
|
<h2>Live now</h2>
|
|
<div class="spacer"></div>
|
|
<button class="ghost small" id="btn-refresh-sessions">Refresh</button>
|
|
</div>
|
|
<div id="live-sessions"></div>
|
|
|
|
<div class="panel-head" style="margin-top:34px">
|
|
<h2>History</h2>
|
|
</div>
|
|
<div id="session-history"></div>
|
|
</section>
|
|
|
|
<!-- audit -->
|
|
<section id="tab-audit" class="hidden">
|
|
<div class="panel-head"><h2>Audit log</h2></div>
|
|
<div id="audit"></div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="modal-root"></div>
|
|
|
|
<script src="/app.js"></script>
|
|
|
|
<!-- Particle field. Purely decorative; app.js is untouched by this. -->
|
|
<script type="module">
|
|
import { mountParticles } from '/particles.js';
|
|
|
|
const layer = document.getElementById('bg-field');
|
|
const field = mountParticles(layer);
|
|
|
|
// app.js flips .hidden on #login / #app. Mirror that onto <body data-screen>
|
|
// so the field can dim itself, and freeze the animation inside the console.
|
|
const app = document.getElementById('app');
|
|
const sync = () => {
|
|
const inConsole = !app.classList.contains('hidden');
|
|
document.body.dataset.screen = inConsole ? 'console' : 'login';
|
|
field.setMode(inConsole ? 'static' : 'animate');
|
|
};
|
|
|
|
new MutationObserver(sync).observe(app, { attributes: true, attributeFilter: ['class'] });
|
|
sync();
|
|
</script>
|
|
</body>
|
|
</html>
|