mirror of
https://github.com/renorris/openfsd
synced 2026-08-12 20:47:41 +08:00
web: add sweatbox instructor user manual
Serve a full HTML manual at /sweatbox/manual (Administrator, no FSD dependency) and link it from the control panel titlebar in a new tab without changing the dense operator layout.
This commit is contained in:
@@ -35,6 +35,19 @@ func (s *Server) handleFrontendSweatbox(c *gin.Context) {
|
||||
s.writeTemplate(c, "sweatbox", page)
|
||||
}
|
||||
|
||||
// handleFrontendSweatboxManual GET /sweatbox/manual — full instructor user manual.
|
||||
// Same Administrator gate as the control panel. Static HTML (no FSD dependency).
|
||||
func (s *Server) handleFrontendSweatboxManual(c *gin.Context) {
|
||||
claims, ok := requireJwtContext(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.writeTemplate(c, "sweatbox_manual", basePage{
|
||||
User: pageUserFromClaims(claims),
|
||||
CSRFToken: s.issueCSRFToken(c),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) newSweatboxPage(c *gin.Context) sweatboxPage {
|
||||
claims, ok := requireJwtContext(c)
|
||||
if !ok {
|
||||
|
||||
@@ -125,6 +125,84 @@ func TestSweatboxPOSTRequiresCSRF(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweatboxManualUnauthRedirect(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/sweatbox/manual", nil)
|
||||
w := httptest.NewRecorder()
|
||||
ts.engine.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status %d want 303", w.Code)
|
||||
}
|
||||
if loc := w.Header().Get("Location"); loc != "/login" {
|
||||
t.Fatalf("Location=%q want /login", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweatboxManualObserverRedirect(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
obs := createTestUser(t, ts, "pw", int(protocol.NetworkRatingObserver))
|
||||
cookies := formLogin(t, ts, obs.CID, "pw")
|
||||
|
||||
w, _ := authedGET(t, ts, "/sweatbox/manual", cookies)
|
||||
if w.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status %d want 303", w.Code)
|
||||
}
|
||||
if loc := w.Header().Get("Location"); loc != "/dashboard" {
|
||||
t.Fatalf("Location=%q want /dashboard", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweatboxManualAdminOK(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
admin := createTestUser(t, ts, "admin-pass", int(protocol.NetworkRatingAdministator))
|
||||
cookies := formLogin(t, ts, admin.CID, "admin-pass")
|
||||
|
||||
w, _ := authedGET(t, ts, "/sweatbox/manual", cookies)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
for _, want := range []string{
|
||||
"Sweatbox instructor manual",
|
||||
"Command language",
|
||||
"SWEATBOX_ENABLED",
|
||||
`href="/sweatbox"`,
|
||||
"add rules weight engine",
|
||||
"Pattern & arrival",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("manual missing %q, body=%s", want, clip(body, 600))
|
||||
}
|
||||
}
|
||||
// Manual is static HTML — no FSD dependency required.
|
||||
if strings.Contains(body, "Unavailable.") {
|
||||
t.Fatal("manual must not depend on FSD availability banner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweatboxPageLinksManualNewTab(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
admin := createTestUser(t, ts, "admin-pass", int(protocol.NetworkRatingAdministator))
|
||||
cookies := formLogin(t, ts, admin.CID, "admin-pass")
|
||||
|
||||
w, _ := authedGET(t, ts, "/sweatbox", cookies)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
// Non-invasive titlebar link; opens in a new tab.
|
||||
if !strings.Contains(body, `href="/sweatbox/manual"`) {
|
||||
t.Fatal("expected /sweatbox/manual link on control panel")
|
||||
}
|
||||
if !strings.Contains(body, `target="_blank"`) || !strings.Contains(body, `rel="noopener noreferrer"`) {
|
||||
t.Fatalf("manual link should open in new tab with noopener, body=%s", clip(body, 500))
|
||||
}
|
||||
// Link text should stay quiet (not a primary action button).
|
||||
if !strings.Contains(body, ">Manual</a>") {
|
||||
t.Fatal("expected muted Manual link text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweatboxCommandEmptyRedirectsErrorFlash(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
admin := createTestUser(t, ts, "pw", int(protocol.NetworkRatingAdministator))
|
||||
|
||||
@@ -136,6 +136,7 @@ func (s *Server) setupFrontendRoutes(parent *gin.RouterGroup) {
|
||||
|
||||
// Sweatbox instructor UI: server-rendered forms; proxies FSD /sweatbox/* service HTTP.
|
||||
admin.GET("/sweatbox", s.handleFrontendSweatbox)
|
||||
admin.GET("/sweatbox/manual", s.handleFrontendSweatboxManual)
|
||||
admin.POST("/sweatbox/airport", s.handleFrontendSweatboxAirport)
|
||||
admin.POST("/sweatbox/scenario", s.handleFrontendSweatboxScenario)
|
||||
admin.POST("/sweatbox/command", s.handleFrontendSweatboxCommand)
|
||||
|
||||
281
internal/web/static/css/openfsd/sweatbox-manual.css
Normal file
281
internal/web/static/css/openfsd/sweatbox-manual.css
Normal file
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Sweatbox instructor manual — readable document layout.
|
||||
* Separate from the dense operator console (sweatbox.css).
|
||||
*/
|
||||
|
||||
.sbxm {
|
||||
--sbxm-max: 52rem;
|
||||
--sbxm-toc-w: 14rem;
|
||||
--sbxm-gap: 1.5rem;
|
||||
--sbxm-border: #d0d4d9;
|
||||
--sbxm-muted: #5c636a;
|
||||
--sbxm-bg: #f8f9fa;
|
||||
--sbxm-panel: #fff;
|
||||
--sbxm-link: #0b57d0;
|
||||
--sbxm-mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
--sbxm-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
box-sizing: border-box;
|
||||
font-family: var(--sbxm-font);
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.55;
|
||||
color: #1a1a1a;
|
||||
background: var(--sbxm-bg);
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 1rem 1.25rem 3rem;
|
||||
}
|
||||
|
||||
.sbxm *,
|
||||
.sbxm *::before,
|
||||
.sbxm *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sbxm-header {
|
||||
max-width: calc(var(--sbxm-max) + var(--sbxm-toc-w) + var(--sbxm-gap));
|
||||
margin: 0 auto 1.25rem;
|
||||
}
|
||||
|
||||
.sbxm-kicker {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.sbxm-kicker a {
|
||||
color: var(--sbxm-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sbxm-kicker a:hover,
|
||||
.sbxm-kicker a:focus-visible {
|
||||
color: var(--sbxm-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sbxm-header h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.sbxm-lead {
|
||||
margin: 0;
|
||||
max-width: 42rem;
|
||||
color: var(--sbxm-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sbxm-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--sbxm-gap);
|
||||
max-width: calc(var(--sbxm-max) + var(--sbxm-toc-w) + var(--sbxm-gap));
|
||||
margin: 0 auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (min-width: 56rem) {
|
||||
.sbxm-layout {
|
||||
grid-template-columns: var(--sbxm-toc-w) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sbxm-toc {
|
||||
position: sticky;
|
||||
top: 0.75rem;
|
||||
max-height: calc(100dvh - 1.5rem);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sbxm-toc {
|
||||
background: var(--sbxm-panel);
|
||||
border: 1px solid var(--sbxm-border);
|
||||
border-radius: 0.35rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.sbxm-toc h2 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--sbxm-muted);
|
||||
}
|
||||
|
||||
.sbxm-toc ol {
|
||||
margin: 0;
|
||||
padding-left: 1.15rem;
|
||||
}
|
||||
|
||||
.sbxm-toc li {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
.sbxm-toc a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sbxm-toc a:hover,
|
||||
.sbxm-toc a:focus-visible {
|
||||
color: var(--sbxm-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sbxm-doc {
|
||||
background: var(--sbxm-panel);
|
||||
border: 1px solid var(--sbxm-border);
|
||||
border-radius: 0.35rem;
|
||||
padding: 1.25rem 1.35rem 2rem;
|
||||
max-width: var(--sbxm-max);
|
||||
}
|
||||
|
||||
.sbxm-doc section {
|
||||
margin: 0 0 1.75rem;
|
||||
scroll-margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.sbxm-doc section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sbxm-doc h2 {
|
||||
margin: 0 0 0.65rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid var(--sbxm-border);
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.sbxm-doc h3 {
|
||||
margin: 1rem 0 0.4rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.sbxm-doc p,
|
||||
.sbxm-doc ul,
|
||||
.sbxm-doc ol {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.sbxm-doc ul,
|
||||
.sbxm-doc ol {
|
||||
padding-left: 1.35rem;
|
||||
}
|
||||
|
||||
.sbxm-doc li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.sbxm-doc code {
|
||||
font-family: var(--sbxm-mono);
|
||||
font-size: 0.88em;
|
||||
background: #f0f2f4;
|
||||
padding: 0.08em 0.28em;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.sbxm-doc a {
|
||||
color: var(--sbxm-link);
|
||||
}
|
||||
|
||||
.sbxm-doc a:focus-visible {
|
||||
outline: 2px solid var(--sbxm-link);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sbxm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 0.85rem;
|
||||
}
|
||||
|
||||
.sbxm-table th,
|
||||
.sbxm-table td {
|
||||
border: 1px solid var(--sbxm-border);
|
||||
padding: 0.4rem 0.55rem;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sbxm-table th {
|
||||
background: #eef1f4;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.sbxm-cmd td:first-child {
|
||||
white-space: nowrap;
|
||||
font-family: var(--sbxm-mono);
|
||||
font-size: 0.82em;
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.sbxm-dl {
|
||||
margin: 0 0 0.85rem;
|
||||
}
|
||||
|
||||
.sbxm-dl dt {
|
||||
font-weight: 650;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sbxm-dl dd {
|
||||
margin: 0.15rem 0 0.35rem 0;
|
||||
color: #2a2a2a;
|
||||
}
|
||||
|
||||
.sbxm-note {
|
||||
font-size: 0.875rem;
|
||||
color: var(--sbxm-muted);
|
||||
border-left: 3px solid var(--sbxm-border);
|
||||
padding-left: 0.65rem;
|
||||
}
|
||||
|
||||
.sbxm-cols {
|
||||
columns: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.sbxm-cols {
|
||||
columns: 2;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sbxm-foot {
|
||||
margin-top: 1.25rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--sbxm-muted);
|
||||
}
|
||||
|
||||
@media print {
|
||||
.sbxm {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sbxm-toc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sbxm-doc {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sbxm-kicker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sbxm-doc section {
|
||||
scroll-margin-top: 0;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ var pageTemplateKeys = []string{
|
||||
"usereditor",
|
||||
"configeditor",
|
||||
"sweatbox",
|
||||
"sweatbox_manual",
|
||||
"airport_editor",
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<div class="sbx-titlebar">
|
||||
<h1>Sweatbox</h1>
|
||||
<p class="sbx-hint">Control panel · forms work without JS · <a href="/sweatbox">Refresh</a> reloads snapshot</p>
|
||||
<p class="sbx-hint">Control panel · forms work without JS · <a href="/sweatbox">Refresh</a> reloads snapshot · <a href="/sweatbox/manual" target="_blank" rel="noopener noreferrer">Manual</a></p>
|
||||
</div>
|
||||
|
||||
{{ if .FlashSuccess }}
|
||||
|
||||
460
internal/web/templates/sweatbox_manual.html
Normal file
460
internal/web/templates/sweatbox_manual.html
Normal file
@@ -0,0 +1,460 @@
|
||||
{{ define "title" }}Sweatbox manual{{ end }}
|
||||
|
||||
{{ define "body" }}
|
||||
<link href="/static/css/openfsd/sweatbox-manual.css" rel="stylesheet">
|
||||
|
||||
<main class="sbxm container-fluid">
|
||||
<header class="sbxm-header">
|
||||
<p class="sbxm-kicker"><a href="/sweatbox">← Sweatbox control panel</a></p>
|
||||
<h1>Sweatbox instructor manual</h1>
|
||||
<p class="sbxm-lead">
|
||||
How to run openfsd’s in-process traffic simulator for tower training.
|
||||
Synthetic aircraft appear on the FSD network as normal pilots so ATC clients
|
||||
(vSTARS, Euroscope, vatSys, etc.) see realistic traffic without N pilot logins.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="sbxm-layout">
|
||||
<nav class="sbxm-toc" aria-label="Manual contents">
|
||||
<h2>Contents</h2>
|
||||
<ol>
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#requirements">Requirements & enablement</a></li>
|
||||
<li><a href="#ui">Control panel UI</a></li>
|
||||
<li><a href="#workflow">Typical workflow</a></li>
|
||||
<li><a href="#files">Airport & scenario files</a></li>
|
||||
<li><a href="#commands">Command language</a></li>
|
||||
<li><a href="#cmd-spawn">Spawn & session</a></li>
|
||||
<li><a href="#cmd-ground">Ground</a></li>
|
||||
<li><a href="#cmd-air">Airborne</a></li>
|
||||
<li><a href="#cmd-pattern">Pattern & arrival</a></li>
|
||||
<li><a href="#cmd-xpdr-fp">Transponder & flight plan</a></li>
|
||||
<li><a href="#status">Status vocabulary</a></li>
|
||||
<li><a href="#students">What students see</a></li>
|
||||
<li><a href="#ops">Ops notes & troubleshooting</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<article class="sbxm-doc">
|
||||
<section id="overview">
|
||||
<h2>1. Overview</h2>
|
||||
<p>
|
||||
The sweatbox is a <strong>native, in-process simulator</strong> inside the FSD process.
|
||||
Aircraft are synthetic registry participants (no TCP pilot sockets). You drive them
|
||||
from the Administrator web page at <code>/sweatbox</code> with text commands that
|
||||
follow TWRTrainer-style vocabulary.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Control plane:</strong> web forms → openfsd web → FSD service HTTP (<code>/sweatbox/*</code>).</li>
|
||||
<li><strong>Wire:</strong> synthetics inject <code>#AP</code>, position <code>@</code>, flight plans, and <code>#DP</code> like real pilots.</li>
|
||||
<li><strong>Primary path:</strong> HTML forms work with JavaScript disabled. JS only refreshes the table live.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="requirements">
|
||||
<h2>2. Requirements & enablement</h2>
|
||||
<table class="sbxm-table">
|
||||
<thead>
|
||||
<tr><th>Need</th><th>Detail</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Administrator network rating (same as Config / Airport Editor).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FSD process</td>
|
||||
<td>Running and reachable by the web process over service HTTP (default <code>:13618</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feature flag</td>
|
||||
<td><code>SWEATBOX_ENABLED=true</code> on the FSD process (default is <strong>off</strong>). Restart after changing.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Optional env</td>
|
||||
<td><code>SWEATBOX_CID</code> (default <code>900001</code>) — synthetic pilot CID shared by all sweatbox aircraft.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
If the page says <strong>Disabled</strong>, sweatbox routes are absent on FSD.
|
||||
If it says <strong>Unavailable</strong>, the web UI cannot reach FSD service HTTP.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="ui">
|
||||
<h2>3. Control panel UI</h2>
|
||||
<p>Dense 16:9 operator layout at <code>/sweatbox</code>.</p>
|
||||
<dl class="sbxm-dl">
|
||||
<dt>Status strip</dt>
|
||||
<dd>ICAO, pause state, elapsed sim time, arrival/departure counts. Pause / Unpause / Refresh.</dd>
|
||||
<dt>Aircraft table</dt>
|
||||
<dd>
|
||||
Callsign, type, rules, squawk, heading, altitude, speed, status, instruction.
|
||||
Click a row (or Tab + Enter with JS) to copy the callsign into <strong>Selected CS</strong>.
|
||||
Per-row <strong>Del</strong> removes that synthetic; <strong>Delete all</strong> needs the confirm checkbox.
|
||||
</dd>
|
||||
<dt>Command rail</dt>
|
||||
<dd>
|
||||
<strong>Selected CS</strong> + command <strong>Line</strong> + <strong>Run</strong>.
|
||||
Target forms: embed callsign in the line (<code>AAL123, taxi A</code>) or fill Selected CS and type only the verb.
|
||||
</dd>
|
||||
<dt>Airport / scenario</dt>
|
||||
<dd>Load <code>.apt</code> and <code>.air</code> via file upload or paste. Scenario load auto-pauses the sim.</dd>
|
||||
</dl>
|
||||
<p>
|
||||
With JavaScript enabled, the table polls about every 1.5 s. Draft fields may persist in browser storage.
|
||||
<strong>Commands always execute on the server</strong> — client JS never authorizes mutations.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="workflow">
|
||||
<h2>4. Typical workflow</h2>
|
||||
<ol>
|
||||
<li>Enable sweatbox on FSD and open <code>/sweatbox</code> as Administrator.</li>
|
||||
<li>
|
||||
Load an <strong>airport</strong> (<code>.apt</code>). Use
|
||||
<a href="/airport-editor">Airport Editor</a> to author/download files if needed
|
||||
(editor never injects into a live session — you load files here).
|
||||
</li>
|
||||
<li>Optionally load a <strong>scenario</strong> (<code>.air</code>). Sim pauses automatically.</li>
|
||||
<li>Unpause when ready for motion.</li>
|
||||
<li>
|
||||
Spawn or select aircraft, issue taxi / takeoff / pattern / vector commands while students
|
||||
control as normal ATC on the FSD port.
|
||||
</li>
|
||||
<li>Delete aircraft when finished, or use Delete all.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section id="files">
|
||||
<h2>5. Airport & scenario files</h2>
|
||||
<h3>Airport (<code>.apt</code>)</h3>
|
||||
<p>
|
||||
TWRTrainer-compatible airport geometry: ICAO, magnetic variation, parking spots,
|
||||
runways, taxiways, holds. Defines the graph used for taxi routing (~100 ft intersection snap).
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Replace</strong> checkbox clears existing aircraft when loading a new airport.</li>
|
||||
<li>Max upload/paste size is 2 MiB.</li>
|
||||
<li>
|
||||
Bulk XP12 conversion: operator-fetched <code>apt.dat</code> via
|
||||
<code>go run ./cmd/aptdat2apt …</code> (see repo docs on X-Plane airport data).
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Scenario (<code>.air</code>)</h3>
|
||||
<p>
|
||||
Colon-delimited aircraft snapshots (callsign, type, engine, rules, position, plan fields, …).
|
||||
Load is best-effort: soft parse issues may be reported while accepted aircraft still spawn.
|
||||
Loading a scenario <strong>auto-pauses</strong> the engine.
|
||||
</p>
|
||||
<h3>Airport Editor</h3>
|
||||
<p>
|
||||
<code>/airport-editor</code> is map-first authoring with Blob download only (no server save).
|
||||
Workflow: edit → download <code>.apt</code>/<code>.air</code> → load on Sweatbox.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="commands">
|
||||
<h2>6. Command language</h2>
|
||||
<p>Commands are plain text. Soft failures return a message (flash); success is often silent motion.</p>
|
||||
<h3>Targeting</h3>
|
||||
<ul>
|
||||
<li><strong>Global</strong> (no aircraft): <code>add</code>, <code>p</code>/<code>pause</code>, <code>un</code>/<code>unpause</code>, <code>ops</code>/<code>stats</code>.</li>
|
||||
<li><strong>Callsign prefix:</strong> <code>AAL123, taxi A B hs 33</code> or <code>AAL123 taxi A B hs 33</code> (comma optional).</li>
|
||||
<li><strong>Selected CS:</strong> fill Selected CS, then <code>taxi A B hs 33</code>.</li>
|
||||
</ul>
|
||||
<h3>Aliases</h3>
|
||||
<p>
|
||||
Many verbs accept short forms (e.g. <code>p</code>=pause, <code>ph</code>=pos, <code>cancel</code>=ctoc,
|
||||
<code>speed</code>/<code>slow</code>=spd, <code>tcn</code>=tc). Prefer the canonical names below when teaching.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="cmd-spawn">
|
||||
<h2>7. Spawn & session commands</h2>
|
||||
<table class="sbxm-table sbxm-cmd">
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Meaning</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>add rules weight engine runway distance [type]</code></td>
|
||||
<td>
|
||||
Place on approach to <em>runway</em> at <em>distance</em> NM.
|
||||
<em>rules</em>: <code>v</code>/<code>i</code> (VFR/IFR).
|
||||
<em>weight</em>: <code>s</code>/<code>m</code>/<code>l</code>/<code>h</code>.
|
||||
<em>engine</em>: <code>p</code>/<code>t</code>/<code>j</code>/<code>h</code> (prop / turboprop / jet / helo).
|
||||
Optional ICAO type override.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add rules weight engine @parking [type]</code></td>
|
||||
<td>Parked at named parking space (e.g. <code>@GA1</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add rules weight engine -bearing distance altitude [type]</code></td>
|
||||
<td>Airborne on radial from field: negative bearing, distance NM, altitude feet.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>del</code></td>
|
||||
<td>Delete selected / targeted aircraft (disconnect synthetic).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>p</code> / <code>pause</code></td>
|
||||
<td>Freeze motion; aircraft remain online.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>un</code> / <code>unpause</code></td>
|
||||
<td>Resume motion.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ops</code> / <code>stats</code></td>
|
||||
<td>Session stats: elapsed, arrivals, departures, rates.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="sbxm-note">
|
||||
Example approach spawn: <code>add i h j 33 8</code> — IFR heavy jet on 8 NM final for runway 33.
|
||||
Example parking: <code>add v s p @GA1 C172</code>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="cmd-ground">
|
||||
<h2>8. Ground commands</h2>
|
||||
<table class="sbxm-table sbxm-cmd">
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Meaning</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>taxi path… [hs name…]</code></td>
|
||||
<td>
|
||||
Taxi via taxiway/runway names. Optional <code>hs</code> tokens insert hold-shorts
|
||||
(e.g. <code>taxi K C D 33 hs 15</code>). First step must intersect current surface.
|
||||
Max path length is limited (on the order of 100 steps).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>hold</code></td>
|
||||
<td>Hold present position (stop taxi).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>res</code></td>
|
||||
<td>Resume after hold / hold-short / cross wait.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cross name</code></td>
|
||||
<td>Cross the named surface currently holding short of.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pos</code> / <code>ph</code></td>
|
||||
<td>Line up and wait (position and hold) on departure runway.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cto [hdg]</code></td>
|
||||
<td>Cleared for takeoff; optional departure heading.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ctoc</code> / <code>cancel</code></td>
|
||||
<td>Cancel takeoff clearance.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ctomlt</code> / <code>ctomrt</code></td>
|
||||
<td>Cleared takeoff into left / right closed traffic.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>nostop</code> / <code>nohold</code></td>
|
||||
<td>Cancel planned stop/hold-short behavior (e.g. during rollout taxi intent).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="cmd-air">
|
||||
<h2>9. Airborne / vector commands</h2>
|
||||
<table class="sbxm-table sbxm-cmd">
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Meaning</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>fh hdg</code></td><td>Fly heading (shortest turn).</td></tr>
|
||||
<tr><td><code>fhn hdg</code></td><td>Fly heading now (snap / immediate).</td></tr>
|
||||
<tr><td><code>tr hdg</code> / <code>tl hdg</code></td><td>Turn right / left to heading (forced direction, may long-arc).</td></tr>
|
||||
<tr><td><code>fph</code></td><td>Fly present heading.</td></tr>
|
||||
<tr><td><code>cm alt</code></td><td>Climb/maintain altitude (feet).</td></tr>
|
||||
<tr><td><code>spd n</code></td><td>Speed control (knots). Aliases include <code>speed</code>, <code>slow</code>.</td></tr>
|
||||
<tr><td><code>ga</code></td><td>Go around from approach/pattern/landing context.</td></tr>
|
||||
<tr><td><code>go</code></td><td>Continue after stop-and-go wait on the runway.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Kinematics are training-oriented (constant turn/climb rates), not a full flight model.
|
||||
There is no collision AI between aircraft.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="cmd-pattern">
|
||||
<h2>10. Pattern & arrival commands</h2>
|
||||
<table class="sbxm-table sbxm-cmd">
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Meaning</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>erc</code> / <code>erd</code> / <code>erb</code> <em>rwy</em></td>
|
||||
<td>Enter right crosswind / downwind / base for runway.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>elc</code> / <code>eld</code> / <code>elb</code> <em>rwy</em></td>
|
||||
<td>Enter left crosswind / downwind / base.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ef</code> <em>rwy</em></td>
|
||||
<td>Enter final.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tg</code></td>
|
||||
<td>Touch and go.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sg [sec]</code></td>
|
||||
<td>Stop and go; optional seconds to hold, else wait for <code>go</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>la</code></td>
|
||||
<td>Low approach.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fs</code></td>
|
||||
<td>Full stop landing.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ext</code></td>
|
||||
<td>Extend current pattern leg (upwind/crosswind/downwind).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mlt</code> / <code>mrt</code></td>
|
||||
<td>Make left / right traffic.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>msa</code> / <code>mna</code></td>
|
||||
<td>Make short approach / normal approach (short cuts base from downwind).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ps</code> <em>nm</em></td>
|
||||
<td>Pattern size in nautical miles.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tc</code> / <code>td</code> / <code>tb</code></td>
|
||||
<td>Turn crosswind / downwind / base now (end extend / advance leg).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="sbxm-note">
|
||||
Closed-traffic takeoff (<code>ctomlt</code>/<code>ctomrt</code>) joins the circuit after climb.
|
||||
Midfield downwind is reflected in the Instruction column when crossed.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="cmd-xpdr-fp">
|
||||
<h2>11. Transponder & flight plan</h2>
|
||||
<table class="sbxm-table sbxm-cmd">
|
||||
<thead>
|
||||
<tr><th>Command</th><th>Meaning</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>sq code</code></td><td>Squawk code.</td></tr>
|
||||
<tr><td><code>sqi code</code></td><td>Squawk code and ident.</td></tr>
|
||||
<tr><td><code>sn</code></td><td>Squawk normal (Mode C).</td></tr>
|
||||
<tr><td><code>ss</code></td><td>Squawk standby.</td></tr>
|
||||
<tr><td><code>id</code></td><td>Ident.</td></tr>
|
||||
<tr><td><code>fp type alt route…</code></td><td>IFR flight plan fields (type, cruise alt, route tokens).</td></tr>
|
||||
<tr><td><code>vp type alt route…</code></td><td>VFR flight plan fields.</td></tr>
|
||||
<tr><td><code>remarks text…</code></td><td>Flight plan remarks.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
ATC clients can query and amend flight plans on the wire. The instructor table merges
|
||||
live session plan fields after ATC amends, so the UI stays honest about what controllers see.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="status">
|
||||
<h2>12. Status vocabulary</h2>
|
||||
<p>Common Status column values (aligned with TWRTrainer where practical):</p>
|
||||
<ul class="sbxm-cols">
|
||||
<li>Parked, Taxiing, Holding Short, Holding in Position, Holding</li>
|
||||
<li>Takeoff, Departing, On Approach, Airborne, Landed</li>
|
||||
<li>Upwind, Crosswind, Downwind, Base, Final</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Instruction</strong> is a short human-readable cue (taxi plan, pattern leg,
|
||||
midfield report, landing type, etc.). Use it together with Status when briefing or debugging.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="students">
|
||||
<h2>13. What students see</h2>
|
||||
<ul>
|
||||
<li>Normal pilot targets: callsigns, positions, flight plans, squawk.</li>
|
||||
<li>Late-joining ATC does <strong>not</strong> receive historical <code>#AP</code> for already-online traffic (same as real pilots); they see subsequent positions in range and can query plans.</li>
|
||||
<li>Direct text / CPDLC-like traffic <em>to</em> synthetics is not answered (drain-and-drop). Instructors use the web command box, not the radio, to control aircraft.</li>
|
||||
<li>Dashboard may badge synthetic connections; public datafeed can include them when sweatbox is enabled—know your ops policy.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="ops">
|
||||
<h2>14. Ops notes & troubleshooting</h2>
|
||||
<table class="sbxm-table">
|
||||
<thead>
|
||||
<tr><th>Symptom</th><th>Check</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Page shows Disabled</td>
|
||||
<td><code>SWEATBOX_ENABLED=true</code> on FSD; restart FSD/binary.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Page shows Unavailable</td>
|
||||
<td>Web can reach FSD service HTTP; shared JWT secret/config; FSD process up.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Command soft-fails</td>
|
||||
<td>Read flash message: missing CS, not taxiing, runway not found, no intersection, etc.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>No motion</td>
|
||||
<td>Sim paused (scenario load pauses). Unpause.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Callsign conflict</td>
|
||||
<td>Shared registry with human pilots; pick another CS or remove the human connection.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxi “do not intersect”</td>
|
||||
<td>Path steps must chain via ~100 ft graph snap; fix airport geometry in Airport Editor.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Limits (defaults)</h3>
|
||||
<ul>
|
||||
<li>Max aircraft on the order of 64 (clamped; see server config).</li>
|
||||
<li>Upload bodies capped at 2 MiB for airport/scenario.</li>
|
||||
<li>One active airport / scenario namespace per FSD process.</li>
|
||||
</ul>
|
||||
<h3>Related tools</h3>
|
||||
<ul>
|
||||
<li><a href="/airport-editor">Airport Editor</a> — author <code>.apt</code> / <code>.air</code></li>
|
||||
<li><a href="/dashboard">Dashboard</a> — online users including synthetics</li>
|
||||
<li><a href="/sweatbox">Sweatbox control panel</a> — live session</li>
|
||||
</ul>
|
||||
<p class="sbxm-foot">
|
||||
This manual describes the openfsd instructor control plane. It is not a substitute for
|
||||
local training procedures or client-specific ATC software docs.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
||||
@@ -18,6 +18,7 @@ Operator documentation for [openfsd](https://github.com/renorris/openfsd).
|
||||
| URL | Who | Notes |
|
||||
|-----|-----|--------|
|
||||
| `/sweatbox` | Administrator | Live ground/taxi simulator control (needs FSD + sweatbox enabled) |
|
||||
| `/sweatbox/manual` | Administrator | Instructor user manual (also linked from the control panel, opens in a new tab) |
|
||||
| `/airport-editor` | Administrator | Map-first `.apt` / `.air` authoring; **download only** (no server save). Validate tab has live client checks + optional server confirm. Download files, then load them on `/sweatbox`. Design: repo `docs/design/apt-air-editor.md` |
|
||||
|
||||
## Quick links
|
||||
|
||||
Reference in New Issue
Block a user