Files
MeshTender/internal/core/templates/console.html
T
2026-07-30 08:46:19 -04:00

149 lines
7.8 KiB
HTML

{{define "title"}}Console · {{.Repeater.Name}} · MeshTender{{end}}
{{define "header"}}
<div class="row g-2 align-items-center">
<div class="col">
<div class="page-pretitle">Console</div>
<h1 class="page-title fs-1">{{.Repeater.Name}}</h1>
</div>
</div>
{{end}}
{{define "content"}}
<div class="alert alert-info" role="alert">
Connect a MeshCore KISS modem and send commands to this repeater. You can run the commands in the
Allowed commands list; the server enforces what you're allowed to send.
</div>
{{/* Confirm/location prompts. Connecting the modem sends a signed login that
confirms access (server-side). These reflect the state at page load; console.js
then updates them live — hiding the confirm prompt and revealing the location
prompt once a "confirmed" status arrives, and hiding the location prompt once a
"location" status arrives. The location prompt is always rendered when no
location is stored (hidden until the repeater is confirmed with admin) so the
swap needs no new markup. The flex row is nested so the [hidden] attribute wins
(Bootstrap's .d-flex is display:flex !important and would otherwise show it). */}}
{{if not .Repeater.Confirmed}}
<div class="alert alert-warning" role="alert" data-testid="confirm-banner">
This repeater hasn't been confirmed yet. Connect your modem below — MeshTender will send a signed login
to verify it has access. Confirming happens automatically as soon as the repeater replies.
</div>
{{end}}
{{if not .Repeater.Latitude}}
<div class="alert alert-info" role="alert" data-testid="location-banner"{{if not (and .Repeater.Confirmed .Repeater.IsAdmin)}} hidden{{end}}>
<div class="d-flex align-items-center justify-content-between">
<div class="me-3">This repeater's location isn't known yet.</div>
<button type="button" class="btn btn-sm btn-primary flex-shrink-0" data-fetch-location data-testid="fetch-location">{{template "icon-map-pin" "me-1"}}Fetch location</button>
</div>
</div>
{{end}}
{{if and .Repeater.Confirmed .Repeater.AccessKnown (not .Repeater.IsAdmin)}}
<div class="alert alert-warning" role="alert" data-testid="guest-banner">
MeshTender only has <strong>guest</strong> access here. Guest is available to anyone with a blank password,
so there's no point operating at this level — re-run <code>setperm &lt;your key&gt; 3</code> on the repeater
to grant admin, then reconnect.
</div>
{{end}}
<div class="row row-cards">
<div class="col-12 col-lg-8">
<div class="card">
<div class="card-header"><h2 class="card-title">Console</h2></div>
<div class="card-body">
<div id="unsupported" class="alert alert-danger" role="alert" hidden>
This browser does not support WebSerial. Use Chrome, Edge, or another Chromium-based browser over HTTPS or on localhost.
</div>
<div class="mb-3">
<label class="form-label" for="path">Path <span class="form-label-description">optional</span></label>
<input type="text" class="form-control font-monospace" id="path" placeholder="e.g. 11, 22, 33" autocomplete="off" spellcheck="false">
<small class="form-hint">Hex hops from your modem to the repeater, in order, same length each (e.g. <code>11, 22, 33</code>). Leave blank to reach it by flood.</small>
</div>
<div class="btn-list">
<button id="connect" class="btn btn-primary" type="button">{{template "icon-plug" "me-1"}}Connect modem</button>
{{if .ShowConfig}}
<button type="button" class="btn btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#config-panel" aria-expanded="false" aria-controls="config-panel" data-testid="apply-config">{{template "icon-settings" "me-1"}}Apply organization configuration…</button>
{{end}}
</div>
<ul id="log" class="evlog console mt-3" role="log" aria-live="polite" aria-label="Console activity"></ul>
<form id="cmdform" class="input-group mt-3" autocomplete="off" hidden>
<input type="text" id="cmdinput" class="form-control font-monospace" placeholder="type a command, e.g. ver" aria-label="Command to send">
<button type="submit" id="cmdsend" class="btn btn-primary">Send</button>
</form>
</div>
</div>
{{if .ShowConfig}}
<div class="collapse mt-3" id="config-panel"
data-config-url="/repeaters/{{.Repeater.PublicID}}/config.json"
data-location-url="/repeaters/{{.Repeater.PublicID}}/location">
<div class="card">
<div class="card-header"><h2 class="card-title">Apply organization configuration</h2></div>
<div class="card-body">
<p class="text-secondary">
Pick an organization and profile to see its recommended configuration for this repeater. Every command is
listed; the ones you're allowed to send show a Run button. Connect the modem to run them — results appear in
the console log above and next to each command.
</p>
<div class="row g-2 mb-3">
<div class="col-sm-6">
<label class="form-label" for="cfg-org">Organization</label>
<select id="cfg-org" class="form-select" data-cfg="org"></select>
</div>
<div class="col-sm-6">
<label class="form-label" for="cfg-profile">Profile</label>
<select id="cfg-profile" class="form-select" data-cfg="profile"></select>
</div>
</div>
<div data-cfg="location"></div>
<div id="config-map" data-cfg="map" style="height:220px;display:none" class="mb-3 rounded" role="region" aria-label="Pick the repeater's location"></div>
<div data-cfg="commands" data-testid="config-commands"></div>
</div>
<div class="card-footer d-flex align-items-center">
<span data-cfg="hint" class="text-secondary small" data-testid="config-hint"></span>
<div class="btn-list ms-auto">
<button type="button" class="btn btn-primary" data-cfg="connect" data-testid="config-connect">{{template "icon-plug" "me-1"}}<span data-cfg="connect-label">Connect modem</span></button>
<button type="button" class="btn btn-primary" data-cfg="run-all" data-testid="config-run-all">Run all</button>
</div>
</div>
</div>
</div>
{{end}}
</div>
<div class="col-12 col-lg-4">
<div class="card" data-testid="allowed-commands">
<div class="card-header"><h2 class="card-title">Allowed commands</h2></div>
<div class="card-body">
{{if .Commands}}
<div class="d-flex flex-wrap gap-1" style="max-height:360px;overflow-y:auto">
{{range .Commands}}
<button type="button" class="chip btn btn-sm{{if .Risky}} btn-outline-danger{{end}}" data-template="{{.Template}}" title="{{.Args}}">{{.Template}}</button>
{{end}}
</div>
{{else}}
<p class="text-secondary mb-0">No commands allowed here yet.</p>
{{end}}
</div>
</div>
</div>
</div>
<div class="mt-3 d-flex align-items-center">
<a class="back-link" href="/">{{template "icon-arrow-left" "me-1"}}Back to dashboard</a>
{{if .Debug}}
<a class="text-secondary ms-2" href="/repeaters/{{.Repeater.PublicID}}/console">Disable debug</a>
{{else}}
<a class="text-secondary ms-2" href="/repeaters/{{.Repeater.PublicID}}/console?debug=1">Debug: show raw frames</a>
{{end}}
</div>
{{if .ShowConfig}}
<link rel="stylesheet" href="{{ asset "/static/leaflet.css" }}">
<script src="{{ asset "/static/leaflet.js" }}"></script>
<script src="{{ asset "/static/regionmap.js" }}"></script>
<script src="{{ asset "/static/console-config.js" }}"></script>
{{end}}
<script nonce="{{.Nonce}}">
window.MESHTENDER_WS = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/repeaters/{{.Repeater.PublicID}}/console/ws{{if .Debug}}?debug=1{{end}}";
</script>
<script src="{{ asset "/static/console.js" }}"></script>
{{end}}