full sync

This commit is contained in:
larssand
2026-03-14 09:51:35 +01:00
commit 3b0af41466
41 changed files with 102303 additions and 0 deletions

174
windows/README.md Normal file
View File

@@ -0,0 +1,174 @@
# JMK RB Live Event
RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika heat/finaler), AMMC WebSocket och lokal SQLite-lagring på Windows.
## Vad som ingår
- Event-lägen:
- `Race (driver transponders)`
- `Track Event (shared cars)`
- UI-separering:
- `Event` = sponsor-event med delade bilar/transpondrar
- `Race Setup` = riktiga race med personlig transponder per förare
- `Race Setup` innehåller nu även:
- välj exakt vilka förare som är med i racet
- practice-ranking
- kval-ranking med `poäng` eller `bästa resultat`
- inbyggd guide för hur man skapar race steg för steg
- beskrivningar direkt i alla fält under `Raceformat`
- sessionstyp `Free Practice` för löpande varvtider utan seedning
- auto-generering av kvalheat från practice-ranking eller klasslista
- reseeding av kommande kvalheat från aktuell ranking
- auto-generering av `A/B/C...` finaler från ranking
- sparad manuell grid per session via dragbar grid-editor
- final-ranking över flera leg med räknade finalheat
- valbar `bump-up` mellan finaler
- reserverade bump-platser i högre finaler
- visuell finalmatris med reserverade bump-platser
- dragbar grid-editor för positionsstart
- utskrift/export av heatsheets per kval/final
- overlay-vy för extern leaderboard-skärm
- flera overlay-lägen: leaderboard, speaker och results
- utskrift av startlistor och resultat
- genererade kval/finaler ärver tid och starttyp från raceformatet
- finish-ljud som siren i stället för browser-röst
- Sessioner: `practice`, `qualification`, `heat`, `final`
- Sponsor-verktyg:
- Skapa rundor automatiskt (`qualification`, `heat`, `final`)
- Auto-assign förare -> bil per session
- Live timing från AMMC WebSocket (`msg: "PASSING"`)
- Hanterad AMMC från webbgränssnittet:
- backend kan starta/stoppa lokal `ammc-amb` på Windows, Linux och macOS
- läser bundlade binärer från `AMMC/windows64`, `AMMC/linux_x86-64`, `AMMC/apple_m`
- Redigering i UI:
- Klasser, eventnamn/datum, förare och bilar kan redigeras direkt
- Live race-kontroll:
- Nedräkning under pågående session
- Auto-finish vid tidslut med status `Race is finished`
- Leaderboard-sortering: varv först, därefter närmast måltid för sessionen (t.ex. 5 min = 600s)
- Browserljud för passing (`blipp` eller tala förarnamn) och målgång
- Sessioninställningar för `Mass start`, `Position start`, `Staggered`
- `Timing` visar grid/startordning för aktiv `Position start`-session
- leaderboard visar både `gap till ledaren`, `gap till bilen framför` och `eget delta` mot förra varvet
- Practice/Kval kan seedas på bästa `2` eller `3` varv i sessionsinställningar
- Persistens:
- Frontend state i browser (`localStorage`)
- Samma state + passeringar sparas i lokal SQLite via Node-backend
- Inbyggd `Guide`-meny i appen med steg-för-steg för:
- Sponsor-event (10 personer / 4 bilar)
- Vanligt race
- AMMC + npm setup på Windows och Linux
- Språkval i UI: `SV` / `EN`
## Windows installation
Kör i PowerShell i projektmappen.
1. Installera Node.js LTS (18+).
2. Installera dependencies:
```powershell
npm install
```
3. Starta servern i bakgrunden:
```powershell
npm start
```
4. Öppna:
- `http://localhost:8081`
- eller från annan dator: `http://<server-ip>:8081`
Vanliga kommandon:
```powershell
npm start
npm stop
npm restart
npm run status
npm run start:fg
```
- `npm start` startar `live_event` i bakgrunden
- `npm stop` stoppar processen via `data/server.pid`
- `npm restart` startar om backend
- `npm run status` visar om backend kör
- `npm run start:fg` kör i foreground för felsökning
### Windows scripts (bakgrundsstart)
Det finns färdiga `.bat`-filer i mappen `windows/`:
- `windows\\start_ammc.bat` startar AMMC i bakgrunden
- `windows\\start_backend.bat` startar `npm start` i bakgrunden (logg: `logs\\backend.log`)
- `windows\\start_all.bat` startar både AMMC + backend och öppnar webbsidan
- `windows\\stop_all.bat` stoppar backend på port `8081` och `ammc-amb.exe`
SQLite-filen skapas automatiskt här:
- `data\\rc_timing.sqlite`
## Koppla mot AMMC
Det finns nu två sätt:
### A. Hanterad AMMC i webbgränssnittet
Viktigt:
- AMMC körs på samma host där `npm start` / `node server.js` körs.
- Om du öppnar sidan från en annan laptop startas ingen AMMC där.
- Fältet `AMMC binär` i `Settings` är en sökväg på backend-hosten, inte på klienten som surfar in.
1. Lägg AMMC-binärerna i projektmappen `AMMC/` (redan gjort i denna repo).
2. Öppna `Settings`.
3. Aktivera `Hanterad AMMC / Managed AMMC`.
4. Sätt `Decoder IP / host`, kontrollera port `9000`, spara.
5. Klicka `Starta AMMC / Start AMMC`.
6. Klicka `Använd serverns WS-url / Use server WS URL` så sätts klienten till t.ex. `ws://<server-ip>:9000`.
Standardbinärer:
- Linux-host: `AMMC/linux_x86-64/ammc-amb`
- Windows-host: `AMMC/windows64/ammc-amb.exe`
- macOS-host: `AMMC/apple_m/ammc-amb`
### B. Manuell start
Starta AMMC med WebSocket (exempel):
```powershell
ammc-amb.exe -w 9000 192.168.1.11
```
I appen:
1. `Settings`
2. Sätt `WebSocket URL` till t.ex. `ws://127.0.0.1:9000`
3. Sätt `Backend URL` till `http://127.0.0.1:8081`
4. Klicka `Test Backend`
5. Gå till `Timing` och klicka `Connect Decoder`
Om du kör Linux-brandvägg (UFW), öppna porten:
```bash
sudo ufw allow 8081/tcp
```
## Auto reload vid uppdatering
- Servern bevakar `index.html`, `src/app.js` och `src/styles.css`.
- När du uppdaterar filer i `live_event` och sparar, laddar klienten om sidan automatiskt.
- Om backendkoden ändras, kör `npm restart`.
## Verifiera att SQLite sparar
Hämta senaste passeringar via API:
- `http://localhost:8081/api/passings`
Hämta sparad app-state:
- `http://localhost:8081/api/state`
## Viktig regel för sponsor-event
- I **samma pågående session** måste varje aktiv bil ha unikt transponder-ID.
- Samma transponder-ID kan återanvändas i **nästa session** (Heat 1 -> Heat 2 -> Heat 3 -> Final 1 ...).
## Referens AMMC JSON
Officiell quick-start:
- https://www.ammconverter.eu/docs/intro/quick-start/
Exempelmeddelande:
```json
{
"msg": "PASSING",
"passing_number": 1,
"transponder": 232323,
"rtc_time": "2022-10-11T22:57:36.099+02:00",
"strength": 0.0,
"resend": false,
"tran_code": "ID:232323",
"loop_id": "55"
}
```

5171
windows/src/app.js Normal file

File diff suppressed because it is too large Load Diff

873
windows/src/styles.css Normal file
View File

@@ -0,0 +1,873 @@
:root {
--bg: #07090e;
--bg-soft: #0f1420;
--panel: #131a28;
--panel-2: #0c1220;
--line: #273149;
--text: #f3f7ff;
--muted: #98a7c8;
--accent: #e10600;
--accent-2: #ff3b30;
--ok: #26c281;
--warn: #f5a623;
--shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Barlow, "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 15% 0%, rgba(225, 6, 0, 0.18), transparent 30%),
radial-gradient(circle at 100% 80%, rgba(37, 59, 103, 0.22), transparent 30%),
linear-gradient(160deg, #05070b 0%, #090d16 55%, #07090e 100%);
min-height: 100vh;
}
.app-shell {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
}
.sidebar {
border-right: 1px solid var(--line);
background:
linear-gradient(180deg, rgba(225, 6, 0, 0.1), transparent 25%),
repeating-linear-gradient(
-32deg,
rgba(255, 255, 255, 0.02) 0,
rgba(255, 255, 255, 0.02) 6px,
transparent 6px,
transparent 18px
),
var(--panel-2);
padding: 20px 16px;
position: sticky;
top: 0;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
margin-bottom: 12px;
}
.brand-mark {
width: 46px;
height: 46px;
border-radius: 10px;
display: grid;
place-items: center;
font-family: Orbitron, sans-serif;
font-weight: 800;
background: linear-gradient(135deg, #ff574f 0%, var(--accent) 55%, #8b0000 100%);
box-shadow: 0 8px 20px rgba(225, 6, 0, 0.5);
}
.brand h1 {
margin: 0;
font-family: Orbitron, sans-serif;
font-size: 1.05rem;
letter-spacing: 0.5px;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 0.84rem;
}
.nav {
display: grid;
gap: 8px;
padding-top: 6px;
}
.nav-item {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.02);
color: var(--text);
border-radius: 10px;
padding: 10px 12px;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
}
.nav-item:hover {
border-color: #405076;
transform: translateX(3px);
}
.nav-item.active {
border-color: rgba(255, 88, 79, 0.55);
background: linear-gradient(90deg, rgba(225, 6, 0, 0.3), rgba(225, 6, 0, 0.1));
}
.sidebar-footer {
position: absolute;
left: 16px;
right: 16px;
bottom: 16px;
color: var(--muted);
}
.badge {
display: inline-block;
border-radius: 999px;
padding: 5px 10px;
font-size: 0.8rem;
font-weight: 700;
border: 1px solid;
}
.badge-online {
color: #b8ffd6;
border-color: rgba(38, 194, 129, 0.7);
background: rgba(38, 194, 129, 0.18);
}
.badge-offline {
color: #ffd2cf;
border-color: rgba(225, 6, 0, 0.7);
background: rgba(225, 6, 0, 0.15);
}
.content {
padding: 20px 24px 26px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--line);
padding-bottom: 14px;
}
.topbar h2 {
margin: 0;
font-family: Orbitron, sans-serif;
letter-spacing: 0.4px;
}
.topbar p {
margin: 6px 0 0;
color: var(--muted);
}
.chip {
margin: 0;
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 12px;
font-weight: 600;
background: rgba(255, 255, 255, 0.02);
}
.topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.lang-wrap {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 0.86rem;
}
.lang-select {
width: 70px;
padding: 6px 8px;
border-radius: 8px;
}
.view {
margin-top: 16px;
}
.grid {
display: grid;
gap: 14px;
}
.cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.stat-card {
background:
linear-gradient(170deg, rgba(225, 6, 0, 0.12), transparent 40%),
linear-gradient(180deg, #151d2d 0%, #121927 100%);
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
box-shadow: var(--shadow);
}
.stat-card p {
margin: 0;
color: var(--muted);
}
.stat-card h3 {
margin: 6px 0;
font-family: Orbitron, sans-serif;
font-size: 1.5rem;
}
.panel-row {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 14px;
margin-top: 14px;
}
.panel {
background: linear-gradient(180deg, #131a28 0%, #101724 100%);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-header {
padding: 12px 14px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 {
margin: 0;
font-size: 1rem;
letter-spacing: 0.3px;
}
.panel-body {
padding: 12px 14px;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
border: 1px solid #425273;
background: linear-gradient(180deg, #1a2334 0%, #141c2b 100%);
color: var(--text);
border-radius: 10px;
font-weight: 700;
padding: 9px 12px;
cursor: pointer;
}
.btn:hover {
border-color: #60739b;
}
.btn-primary {
border-color: #b11714;
background: linear-gradient(180deg, #f20d07 0%, #d30702 52%, #8e0603 100%);
}
.btn-danger {
border-color: #843137;
background: linear-gradient(180deg, #8f222a, #66181e);
}
.btn-mini {
padding: 3px 8px;
font-size: 0.76rem;
}
.form-grid {
display: grid;
gap: 10px;
}
.field-card {
display: grid;
gap: 8px;
align-content: start;
padding: 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: rgba(255, 255, 255, 0.025);
}
.field-card-checkbox {
grid-column: span 2;
}
.field-label {
font-weight: 700;
}
.field-hint {
color: var(--muted);
font-size: 0.84rem;
line-height: 1.4;
}
.cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
input,
select {
width: 100%;
background: #0f1522;
border: 1px solid var(--line);
border-radius: 10px;
color: var(--text);
padding: 9px 10px;
}
input:focus,
select:focus {
outline: none;
border-color: #6578a4;
box-shadow: 0 0 0 3px rgba(225, 6, 0, 0.18);
}
.log-box {
margin: 10px 0 0;
min-height: 120px;
max-height: 260px;
overflow: auto;
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px;
background: #0b1019;
color: #c8d6f5;
white-space: pre-wrap;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.82rem;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table thead {
background: rgba(255, 255, 255, 0.03);
}
.data-table th,
.data-table td {
text-align: left;
border-bottom: 1px solid var(--line);
padding: 9px 8px;
font-size: 0.95rem;
}
.data-table th {
color: #bdc8e3;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.55px;
}
.data-table tbody tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.simple-list {
margin: 0;
padding: 0;
list-style: none;
}
.simple-list li {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 8px 0;
}
.assignment-group {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px;
margin-bottom: 10px;
}
.assignment-group h4 {
margin: 0 0 8px;
}
.assignment-group ul {
margin: 0;
padding-left: 18px;
}
.assignment-group li {
margin-bottom: 6px;
}
.actions-inline {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.check-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.check-card {
display: flex;
gap: 10px;
align-items: center;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(255, 255, 255, 0.02);
}
.check-card input {
width: auto;
}
.grid-editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.drag-list {
display: grid;
gap: 10px;
}
.drag-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
cursor: grab;
}
.drag-item:hover {
border-color: #60739b;
}
.drag-item-active {
opacity: 0.5;
}
.drag-item-over {
border-color: rgba(225, 6, 0, 0.8);
background: rgba(225, 6, 0, 0.12);
}
.position-grid h4,
.final-card h4 {
margin: 0;
}
.position-grid-list,
.matrix-slots {
display: grid;
gap: 10px;
margin-top: 12px;
}
.position-grid-list {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.position-grid-item,
.matrix-slot,
.matrix-session-row,
.final-card {
border: 1px solid var(--line);
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
}
.position-grid-item,
.matrix-slot,
.matrix-session-row {
display: flex;
gap: 10px;
align-items: center;
padding: 10px 12px;
}
.final-matrix {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 12px;
}
.final-card {
padding: 12px;
}
.final-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.matrix-slot {
justify-content: space-between;
}
.matrix-slot-reserved {
border-color: rgba(245, 166, 35, 0.6);
background: rgba(245, 166, 35, 0.12);
}
.matrix-session-list {
display: grid;
gap: 8px;
margin-top: 12px;
}
.matrix-session-row {
justify-content: space-between;
}
.overlay-mode .sidebar,
.overlay-mode .topbar {
display: none;
}
.overlay-mode .app-shell {
display: block;
}
.overlay-mode .content {
padding: 0;
}
.overlay-shell {
min-height: 100vh;
padding: 24px;
background:
radial-gradient(circle at 15% 0%, rgba(225, 6, 0, 0.18), transparent 30%),
radial-gradient(circle at 100% 80%, rgba(37, 59, 103, 0.22), transparent 30%),
linear-gradient(160deg, #05070b 0%, #090d16 55%, #07090e 100%);
}
.overlay-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-end;
margin-bottom: 18px;
}
.overlay-header h1 {
margin: 4px 0;
font-family: Orbitron, sans-serif;
font-size: clamp(2rem, 4vw, 3.8rem);
}
.overlay-kicker {
margin: 0;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.overlay-meta {
text-align: right;
}
.overlay-clock {
font-family: Orbitron, sans-serif;
font-size: clamp(2.6rem, 5vw, 4.8rem);
font-weight: 800;
}
.overlay-status {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.overlay-board {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.7fr);
gap: 18px;
}
.overlay-speaker {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
gap: 18px;
}
.overlay-speaker-main {
border: 1px solid var(--line);
border-radius: 18px;
padding: 28px;
background: rgba(7, 12, 20, 0.82);
box-shadow: var(--shadow);
}
.overlay-speaker-label {
font-family: Orbitron, sans-serif;
font-size: 1rem;
color: var(--muted);
letter-spacing: 0.1em;
}
.overlay-speaker-main h2 {
margin: 12px 0;
font-family: Orbitron, sans-serif;
font-size: clamp(2.8rem, 6vw, 5rem);
}
.overlay-speaker-side,
.overlay-results {
display: grid;
gap: 16px;
}
.overlay-table-wrap,
.overlay-side-card,
.overlay-empty {
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(7, 12, 20, 0.82);
box-shadow: var(--shadow);
}
.overlay-table-wrap {
padding: 8px 12px 12px;
}
.overlay-side {
display: grid;
gap: 16px;
}
.overlay-side-card {
padding: 14px;
}
.overlay-side-card h3 {
margin: 0 0 10px;
}
.overlay-passing {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid var(--line);
}
.overlay-passing:last-child {
border-bottom: 0;
}
.overlay-empty {
display: grid;
place-items: center;
min-height: calc(100vh - 48px);
text-align: center;
padding: 24px;
}
.pill {
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.78rem;
padding: 3px 8px;
color: var(--muted);
}
.pill-green {
border-color: rgba(38, 194, 129, 0.6);
color: #b7ffd4;
}
.pos-pill {
display: inline-grid;
place-items: center;
width: 32px;
height: 22px;
border-radius: 999px;
border: 1px solid var(--line);
font-weight: 800;
}
.pos-1 {
background: linear-gradient(180deg, #ffd95a, #d3a80e);
color: #1a1400;
border-color: #ffd95a;
}
.pos-2 {
background: linear-gradient(180deg, #dce4ef, #99a7b9);
color: #0f141b;
border-color: #dce4ef;
}
.pos-3 {
background: linear-gradient(180deg, #d19562, #8c5d34);
color: #1a0f05;
border-color: #d19562;
}
.best {
color: #dfb9ff;
font-weight: 700;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 8px;
}
.error {
color: #ffb5b5;
min-height: 18px;
}
.finish-banner {
margin-top: 10px;
display: inline-block;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid rgba(225, 6, 0, 0.7);
background: rgba(225, 6, 0, 0.2);
color: #ffd6d4;
font-weight: 700;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(4, 7, 12, 0.76);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
z-index: 40;
}
.modal-card {
width: min(760px, 100%);
max-height: 88vh;
overflow: auto;
border: 1px solid var(--line);
border-radius: 16px;
background:
linear-gradient(180deg, rgba(225, 6, 0, 0.08), transparent 20%),
linear-gradient(180deg, #131a28 0%, #101724 100%);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
}
.hint {
margin: 10px 0 0;
color: var(--muted);
font-size: 0.9rem;
}
.mt-16 {
margin-top: 16px;
}
@media (max-width: 1200px) {
.cols-4,
.cols-5 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 820px) {
.app-shell {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
height: auto;
}
.sidebar-footer {
position: static;
margin-top: 14px;
}
.cols-3,
.cols-4,
.cols-5 {
grid-template-columns: 1fr;
}
.topbar {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.field-card-checkbox {
grid-column: span 1;
}
.overlay-board {
grid-template-columns: 1fr;
}
.overlay-speaker {
grid-template-columns: 1fr;
}
.overlay-header {
align-items: flex-start;
flex-direction: column;
}
.modal-overlay {
padding: 12px;
}
}

10
windows/start_all.bat Normal file
View File

@@ -0,0 +1,10 @@
@echo off
setlocal
call "%~dp0start_ammc.bat"
call "%~dp0start_backend.bat"
start "" "http://localhost:8081"
echo [OK] AMMC + backend started
endlocal

23
windows/start_ammc.bat Normal file
View File

@@ -0,0 +1,23 @@
@echo off
setlocal
if "%AMMC_EXE%"=="" set "AMMC_EXE=%~dp0ammc-amb.exe"
if not exist "%AMMC_EXE%" if exist "%~dp0..\ammc-amb.exe" set "AMMC_EXE=%~dp0..\ammc-amb.exe"
if "%DECODER_IP%"=="" set "DECODER_IP=192.168.1.11"
if "%WS_PORT%"=="" set "WS_PORT=9000"
if not exist "%AMMC_EXE%" (
echo [ERROR] Could not find AMMC executable:
echo %AMMC_EXE%
echo Set AMMC_EXE in this file or as environment variable.
exit /b 1
)
start "AMMC" /min cmd /c "\"%AMMC_EXE%\" -w %WS_PORT% %DECODER_IP%"
echo [OK] AMMC started in background
echo Decoder IP: %DECODER_IP%
echo WebSocket: ws://127.0.0.1:%WS_PORT%
endlocal

15
windows/start_backend.bat Normal file
View File

@@ -0,0 +1,15 @@
@echo off
setlocal
for %%I in ("%~dp0..") do set "APP_DIR=%%~fI"
set "LOG_DIR=%APP_DIR%\logs"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
start "RC Timing Backend" /min cmd /c "cd /d \"%APP_DIR%\" && npm start >> \"%LOG_DIR%\backend.log\" 2>&1"
echo [OK] Backend started in background
echo URL: http://localhost:8081
echo Log: %LOG_DIR%\backend.log
endlocal

11
windows/stop_all.bat Normal file
View File

@@ -0,0 +1,11 @@
@echo off
setlocal
for /f "tokens=5" %%p in ('netstat -ano ^| findstr ":8081" ^| findstr "LISTENING"') do (
taskkill /PID %%p /F >nul 2>&1
)
taskkill /IM ammc-amb.exe /F >nul 2>&1
echo [OK] Stopped backend on :8081 and ammc-amb.exe (if running)
endlocal