diff --git a/docs/nats/nats-campaign-plan.md b/docs/nats/nats-campaign-plan.md index 205049f1..7af4831e 100644 --- a/docs/nats/nats-campaign-plan.md +++ b/docs/nats/nats-campaign-plan.md @@ -1,153 +1,543 @@ # NATS Campaign Plan -**24-Scenario Training Progression** +**24-Scenario Training Progression — Singular Reference for Parallel Scenario Builds** + +This document is the canonical plan for the NATS campaign. It exists so that scenarios 10-24 can be built in parallel without contradictions: each entry specifies premise, mechanics, NICE codes, characters, satellites, prerequisites, and what the scenario must teach beyond what prior scenarios already taught. + +Mechanical "how to build a scenario" guidance lives in the [scenario-developer](../../.claude/skills/scenario-developer/SKILL.md) skill. NATS-specific tone, character voices, and archetype templates live in the [nats-campaign-builder](../../.claude/skills/nats-campaign-builder/SKILL.md) skill. This document is **what** each scenario covers, not **how** to write one. + +--- + +## Campaign Phase Map + +| Phase | Scenarios | Theme | Status | +|-------|-----------|-------|--------| +| 1 | 1-8 | Foundations — Guided learning with Charlie Brooks | Built | +| 2 | 9-16 | Qualified Operations — Solo shifts, real customers, operational judgment | Built | +| 3 | 17-24 | Crisis Operations — High-stakes, multi-system, mentoring | Built | + +--- + +## Phase 1: Foundations (Scenarios 1-8) — Built + +*Theme: Orientation and fundamentals. Charlie Brooks guides the player through one new mechanic per scenario.* + +| Lvl | Title | Subtitle | Core mechanic introduced | Primary NICE | +|-----|-------|----------|--------------------------|--------------| +| 1 | First Day | TIDEMARK-1 Health Check | UI navigation, equipment observation, alarm reading | K0740, K0741, T0153 | +| 2 | Scheduled Maintenance | Power Down and Recovery Procedures | Power-on/off sequencing, RF safety, antenna positioning | T1567, K0770, S0421 | +| 3 | Weather Emergency Handover | Multi-Site Operations | Feed heater, AGC monitoring, ME-02 commissioning, traffic handover | T0153, K0689, S0421 | +| 4 | New Bird on the Block | Satellite Switchover Operations | IF frequency calculation, satellite acquisition, full-duplex link establishment | S0421, K1032, K0773 | +| 5 | Interference Hunt | Spectrum Analysis and Mitigation | Spectrum analysis at wide span, cross-pol interference, notch filter | T0153, K0773, S0582 | +| 6 | Old Faithful | Step-Track Operations on Inclined Orbit | Step-track mode, AURORA-7 inclined orbit, BUC TX IF calculation | K1032, S0421, K0773 | +| 7 | Uplink Validation | Transmit Enable Sequence & Power Verification | BUC loopback, post-maintenance diagnosis, HPA enable sequencing | S0077, T1313 | +| 8 | Night Shift | Solo Operations Evaluation | Multi-fault diagnosis, intermittent fault, backup modem switching | T0081, S0582, K0773 | + +End-of-Phase-1 state (pre-S9): +- Charlie Brooks has transferred to a European station; he is not on-site after S8. +- Dana Torres is the on-site supervisor. +- Catherine Vega runs ME-02 (operational since S3). +- TIDEMARK-1, TIDEMARK-2, SES-10 (visible only), AURORA-7 are in the satellite roster. + +--- + +## Phase 2: Qualified Operations (Scenarios 9-16) + +*Theme: The player is qualified. Dialog density drops sharply. Quizzes default to `Character.SYSTEM`. Per-scenario character clips capped at 3-7. Tone is operational, not instructional.* + +**Phase 2 satellite roster:** TIDEMARK-1, TIDEMARK-2, TIDEMARK-3 (new in S9), AURORA-7, SES-10 (visible only). +**Phase 2 stations:** VT-01 and ME-02 (both fully operational). +**Phase 2 arc:** Mostly standalone day-in-the-life vignettes. One 2-scenario mini-arc at S11-S12 (planned maintenance: handover out → return to service). + +--- + +### S9 — Morning Rounds | **Built** + +| Field | Value | +|-------|-------| +| Subtitle | Multi-Satellite Health Check | +| Status | Built | +| Premise | First qualified shift. Compressed health checks across three TIDEMARK birds: VT-01/TM-1, ME-02/TM-2, and a quick spot-check on the newly commissioned TIDEMARK-3. No drama. | +| Duration | 20-25 min | +| Objectives | 18 | +| Mechanics reused | S1 health-check sequence, S4 antenna repoint, S5 spectrum tuning. Multi-station navigation from S3. | +| Satellites | TIDEMARK-1, TIDEMARK-2, **TIDEMARK-3** (new), SES-10 (visible) | +| Ground stations | VT-01 (active), ME-02 (active) | +| Characters & density | Dana only. 5 clips: text-message intro, 3 phase transitions, sign-off. All quizzes are SYSTEM. | +| NICE — Primary | T0153, T0431, S0421 | +| NICE — Supporting | K0645, K0740, K0741, K0773, K1032 | +| New value beyond Phase 1 | First scenario with no instructional dialog. Demonstrates compressed checklist execution across multiple satellites. Introduces TIDEMARK-3. | +| Prerequisites | nats-level-8-night-shift | +| Arc connection | Standalone | --- -## Phase 1: Core Mechanics (Scenarios 1-8) +### S10 — Customer Pass + +| Field | Value | +|-------|-------| +| Subtitle | High-Throughput Window on AURORA-7 | +| Status | Built | +| Premise | SeaLink requests a 30-minute high-priority data window on AURORA-7. Marcus Chen is on the line throughout. Operator runs step-track, monitors C/N for sustained margin, optimizes HPA backoff for the pass duration. Customer is watching the link live. | +| Duration | 25-30 min | +| Objectives | 18-22 | +| Mechanics reused | S6 step-track on AURORA-7, S1 C/N monitoring, S2/S4 HPA backoff adjustment, S1 payload data integrity check | +| Satellites | AURORA-7 | +| Ground stations | VT-01 | +| Characters & density | Dana (intro). Marcus Chen (3-4 clips: pre-pass briefing, mid-pass throughput acknowledgment, post-pass thank-you). 5-6 clips total. | +| NICE — Primary | S0478, T1580, T0153 | +| NICE — Supporting | K1032, K0740, S0675 | +| New value beyond Phase 1 + S9 | First scenario with **active customer dialog during operations**. Pre-S10 customer interaction was bracketing (brief/debrief); S10 puts the customer on the link in real time. Throughput-vs-margin tradeoff is explicit. | +| Prerequisites | nats-scenario9 | +| Arc connection | Standalone | -*Theme: Orientation, fundamentals, guided learning with Charlie Brooks* +--- -| Lvl | Title | Mechanics Introduced | Learning Outcomes | Primary Codes | Proficiency | -|-----|-------|---------------------|-------------------|---------------|-------------| -| 1 | First Day | UI navigation, equipment observation, alarm reading | Identify system performance indicators and availability metrics | K0740, K0741 | Remember | -| 2 | Power Sequence | Power-on sequence, equipment states, safety procedures | Execute proper equipment configuration and safety protocols | T1567, K0770 | Understand | -| 3 | Weather Event | AGC, ice accumulation, traffic handover | Monitor system performance and troubleshoot degraded conditions | T0153, S0582 | Understand | -| 4 | Link Budget Basics | Calculate C/N, IF frequency translation | Apply telecommunications principles to optimize signal quality | K0773, S0675 | Apply | -| 5 | Multi-Carrier | Spectrum management, adjacent channel interference | Configure bandwidth allocation and network parameters | K0737, K0792 | Apply | -| 6 | Step-Track LEO | Non-GEO tracking, step-track mode, inclined orbit | Operate satellite tracking equipment for non-geostationary orbits | K1032, S0421 | Apply | -| 7 | Uplink Validation | Transmit enable sequence, power verification | Test network infrastructure and verify secure communications | S0077, T1313 | Apply | -| 8 | Fault Isolation | Multiple simultaneous faults, prioritization | Diagnose connectivity problems and troubleshoot equipment failures | T0081, S0815 | Analyze | +### S11 — Planned Maintenance: Hand Off (arc 1 of 2) + +| Field | Value | +|-------|-------| +| Subtitle | Coordinated Traffic Transfer to Sister Teleport | +| Status | Built | +| Premise | Scheduled 2-hour VT-01 maintenance window for HPA waveguide gasket inspection. Operator pre-coordinates with sister teleport and ME-02 (Catherine), executes planned TIDEMARK-1 handover, stows VT-01 for the maintenance crew. Unlike S3's emergency handover, there is no weather pressure. The grade is procedural cleanliness. | +| Duration | 25-30 min | +| Objectives | 18-22 | +| Mechanics reused | S3 traffic handover, S2 antenna maintenance position, S3 cross-station coordination | +| Satellites | TIDEMARK-1 | +| Ground stations | VT-01 (handing off), ME-02 (receiving control) | +| Characters & density | Dana (intro + sign-off). Catherine (2-3 clips: coordination, receive confirmation). 5-6 clips total. | +| NICE — Primary | K0718, T0129, S0593 | +| NICE — Supporting | K0645, K0741 | +| New value beyond Phase 1 + S9-S10 | S3 emergency handover under time pressure; S11 is the **same mechanic under planning discipline**. Pre-coordination protocol. Formal callouts. The lesson: the procedure does not relax just because the urgency is off. | +| Prerequisites | nats-scenario10 | +| Arc connection | Start of S11-S12 mini-arc (planned maintenance cycle) | + +--- -### Scenario 1 NICE Coverage +### S12 — Planned Maintenance: Return to Service (arc 2 of 2) + +| Field | Value | +|-------|-------| +| Subtitle | Post-Maintenance Restoration | +| Status | Built | +| Premise | Maintenance is complete. Operator brings VT-01 back: power-up sequence, RX chain validation, post-maintenance leftover sweep (a small misconfig from the maintenance crew), retake TIDEMARK-1 traffic from the sister teleport, notify customer of restoration. | +| Duration | 25-35 min | +| Objectives | 20-25 | +| Mechanics reused | S2 power-up sequence, S7 post-maintenance validation (incl. leftover detection), S1/S9 beacon spot check, S3 traffic handover (return direction) | +| Satellites | TIDEMARK-1 | +| Ground stations | VT-01 (restoring), ME-02 (releasing) | +| Characters & density | Dana (intro + sign-off). Catherine (2 clips: release coordination). Marcus or James briefly acknowledging service restoration. 5-7 clips total. | +| NICE — Primary | T1314, T1567, T0431 | +| NICE — Supporting | T0153, K0773, K0645 | +| New value beyond Phase 1 + S9-S11 | Closes the **full maintenance cycle** for the first time. S2 and S7 taught the pieces in isolation; S11→S12 demonstrates the entire lifecycle: handover-out → planned downtime → maintenance → validation → handover-back. Discipline of finishing what you started. | +| Prerequisites | nats-scenario11 | +| Arc connection | End of S11-S12 mini-arc | -**Primary:** K0740, K0741, T0153 -**Supporting:** K0645, K0773, K1032, S0421, T0431 +--- + +### S13 — Thermal Anomaly + +| Field | Value | +|-------|-------| +| Subtitle | Reading the Trend | +| Status | Built | +| Premise | During an otherwise normal shift, BUC temperature trends upward over 15 minutes. No immediate alarm, but the slope matters. Operator must read the trend, decide to de-rate (lower BUC gain), schedule a swap, mute and switch to backup, or hold and monitor. The right answer is judgment, not a checklist. | +| Duration | 25-30 min | +| Objectives | 18-22 | +| Mechanics reused | S2/S7 BUC configuration, S2 HPA backoff, S5 spectrum monitoring | +| Satellites | TIDEMARK-1 (primary carrier on VT-01) | +| Ground stations | VT-01 | +| Characters & density | Dana (1 check-in at decision point). 3-4 clips total. | +| NICE — Primary | K0064, S0672, T1314 | +| NICE — Supporting | K0740, S0675, T0153 | +| New value beyond Phase 1 + S9-S12 | First scenario where the player must read a **trend** rather than respond to a discrete fault. S5 was a binary spike; S13 is "is this worth acting on yet, or am I overreacting?" Operational judgment under uncertainty. | +| Prerequisites | nats-scenario12 | +| Arc connection | Standalone | + +--- -| Code | Count | Application | -|------|-------|-------------| -| S0421 | 6 | UI navigation to equipment panels | -| K0740 | 5 | Interpreting performance metrics (noise temp, C/N, backoff, constellation) | -| K0773 | 5 | Telecom principles (LNB, spectrum analyzer, modulation, polarization) | -| T0153 | 4 | Monitoring beacon, receiver, tracking, alarms | -| K1032 | 4 | Satellite concepts (beacon, tracking modes, polarization) | -| K0741 | 2 | Availability status (GPSDO lock, alarm dashboard) | -| T0431 | 3 | Hardware checks (GPSDO, LNB, HPA) | -| K0645 | 1 | SOP review (mission brief) | +### S14 — Rain Fade + +| Field | Value | +|-------|-------| +| Subtitle | Adapt Without Handover | +| Status | Built | +| Premise | Light to moderate rain approaches VT-01. The customer (James Okafor) prefers no handover — SLA terms allow degradation but penalize handover events. Operator must enable feed heater, watch AGC headroom, optimize HPA backoff, monitor link margin. The scenario can end either way: adapt successfully, or recognize a threshold-justified handover. | +| Duration | 25-35 min | +| Objectives | 20-25 | +| Mechanics reused | S3 feed heater + AGC, S1 C/N margin tracking, S3 traffic handover (only if thresholds exceeded) | +| Satellites | TIDEMARK-1 | +| Ground stations | VT-01 primary, ME-02 standby | +| Characters & density | Dana (decision check-in). James Okafor (1 clip: SLA constraint context). 4-5 clips total. | +| NICE — Primary | K0689, S0675, K0721 | +| NICE — Supporting | T0153, K0773, S0593 | +| New value beyond Phase 1 + S9-S13 | S3 taught "weather hits → hand over." S14 teaches that **the right call is not always to escape**. Adds customer-constraint reasoning. First explicit `K0721` (risk management) introduction. | +| Prerequisites | nats-scenario13 | +| Arc connection | Standalone | --- -## Phase 2: Independent Operations (Scenarios 9-16) +### S15 — Frequency Coordination + +| Field | Value | +|-------|-------| +| Subtitle | Inter-Operator Spectrum Etiquette | +| Status | Built | +| Premise | Notification from a partner teleport: their next uplink will sit 2 MHz from a current TIDEMARK-3 carrier. Operator verifies guard bands, checks that the VT-01 carrier is not producing spurs that would land on the partner's slot, may need to retune slightly or adjust IF filtering. Coordination, not combat. | +| Duration | 25-30 min | +| Objectives | 18-22 | +| Mechanics reused | S5 spectrum analysis, S5 IF filter configuration, S4/S7 TX modem configuration, S7 uplink validation | +| Satellites | TIDEMARK-3 | +| Ground stations | VT-01 | +| Characters & density | Dana (intro). Partner-teleport coordinator (1-2 clips — render via SYSTEM as email/notification, not a named character, to avoid expanding the roster). 4 clips total. | +| NICE — Primary | K0737, T1143, S0648 | +| NICE — Supporting | K0792, K0773, S0593 | +| New value beyond Phase 1 + S9-S14 | S5 was "someone else interfered with us." S15 reverses the perspective: **make sure we are not interfering with them**. Introduces external-operator coordination — absent from S1-S14. | +| Prerequisites | nats-scenario14 | +| Arc connection | Standalone | -*Theme: Solo shifts, time pressure, customer interactions* +--- -| Lvl | Title | Mechanics Combined | Challenge Type | Learning Outcomes | Primary Codes | Proficiency | -|-----|-------|-------------------|----------------|-------------------|---------------|-------------| -| 9 | Night Shift | Power sequence + alarm response | Equipment fault during off-hours | Resolve system incidents independently without supervision | T1538, S0593 | Apply | -| 10 | Customer Pass | Step-track + link budget | LEO pass with live customer data | Provide technical support while monitoring client systems | S0478, T1580 | Apply | -| 11 | Interference Hunt | Spectrum analysis + frequency calc | Unknown carrier identification | Detect signal anomalies and identify interference sources | K0926, S0648 | Apply | -| 12 | Thermal Runaway | HPA management + backoff adjustment | Equipment overheating scenario | Tune system performance and troubleshoot failing components | K0064, S0672 | Analyze | -| 13 | Handover Chain | Multi-carrier + traffic control | Sequential satellite handovers | Integrate communication systems during operational transitions | K0718, T0129 | Analyze | -| 14 | Rain Fade | AGC + link budget recalc | Adapt power during storm | Optimize link performance under degraded propagation conditions | K0689, S0675 | Analyze | -| 15 | Frequency Conflict | Uplink validation + spectrum mgmt | Resolve overlapping assignments | Assess risk and implement backup coordination procedures | K0675, T1143 | Analyze | -| 16 | Cascade Failure | Fault isolation + power sequence | Multiple sequential failures | Troubleshoot interdependent systems and recover operations | T0531, S0677 | Analyze | +### S16 — Cascade Failure + +| Field | Value | +|-------|-------| +| Subtitle | Multi-System Recovery Under Customer Pressure | +| Status | Built | +| Premise | Two minutes into a routine shift, multiple unrelated alarms fire at once: BUC over-temperature triggering automatic mute, LNB reference unlock, HPA backoff drift. Customers begin calling. Operator must prioritize (RF safety → customer impact → equipment health), work through each fault systematically without supervisor escalation. | +| Duration | 35-45 min | +| Objectives | 25-32 | +| Mechanics reused | S7 BUC fault diagnosis, S8 LNB power cycle, S2 HPA management, S3 alarm prioritization framework, S5 documentation discipline | +| Satellites | TIDEMARK-1, TIDEMARK-2 (background traffic) | +| Ground stations | VT-01 primary | +| Characters & density | Dana (escalation check). James Okafor (customer pressure, 1-2 clips). SYSTEM for all triage quizzes. 5-7 clips total. | +| NICE — Primary | T0531, S0677, S0807 | +| NICE — Supporting | T1538, S0593, T0081, T1606 | +| New value beyond Phase 1 + S9-S15 | S8 was multi-fault but all faults touched AURORA-7 (related). S16 is **unrelated faults at once**, requiring the operator to hold multiple mental tracks. The capstone of Phase 2. | +| Prerequisites | nats-scenario15 | +| Arc connection | Standalone (Phase 2 capstone) | --- ## Phase 3: Crisis Operations (Scenarios 17-24) -*Theme: High-stakes, time-critical decisions, mentoring role* +*Theme: High-stakes, time-critical, multi-system. Player begins taking on mentoring and reporting responsibilities. Crisis cadence — each scenario is a distinct kind of stress.* -| Lvl | Title | Mechanics Combined | Challenge Type | Learning Outcomes | Primary Codes | Proficiency | -|-----|-------|-------------------|----------------|-------------------|---------------|-------------| -| 17 | Solar Event | All RX chain + link margin | Increased noise floor from sun | Assess system threats and determine operational impacts | K0751, T1020 | Analyze | -| 18 | Satellite Anomaly | Step-track + fault isolation | Satellite station-keeping drift | Maintain infrastructure during spacecraft anomaly conditions | K1032, T1314 | Evaluate | -| 19 | Train the New Hire | Teaching mode - explain actions | Mentor new operator through power-up | Deliver technical training and produce instructional guidance | T1411, T1334 | Evaluate | -| 20 | Dual Outage | Traffic control + backup procedures | Two sites down, prioritize recovery | Implement contingency plans and coordinate multi-site recovery | T1144, S0671 | Evaluate | -| 21 | Hostile RF | Interference + uplink validation | Suspected intentional jamming | Protect network communications and identify jamming techniques | K0926, S0615 | Evaluate | -| 22 | End-of-Life Planning | Link budget + degradation trends | Satellite capacity planning | Apply risk management principles and conduct trend analysis | K0721, T1429 | Evaluate | -| 23 | Emergency Bypass | Power sequence + manual override | Automation failure, full manual ops | Execute command-line operations and diagnose hardware faults | S0424, T1588 | Evaluate | -| 24 | Constellation Crisis | All systems | Multi-sat, multi-fault, customer escalation | Solve complex problems and prepare impact assessment reports | S0807, T1606 | Create | +**Phase 3 satellite roster:** Same as Phase 2. +**Phase 3 stations:** Same as Phase 2. +**Phase 3 arc:** No fixed mini-arc by design. Each scenario is a unique crisis archetype. S22-S24 has loose narrative continuity (AURORA-7 sunset → final ops → constellation reorganization) but no hard dependencies. + +--- + +### S17 — Solar Event + +| Field | Value | +|-------|-------| +| Subtitle | Sun Transit Outage | +| Status | Built | +| Premise | The semiannual sun transit window arrives — the Sun passes directly behind TIDEMARK-1 from VT-01's perspective for ~8 minutes, raising the noise floor catastrophically. The event is predictable and mitigatable: pre-coordinate with sister teleport, accept the degradation, ride it out, document customer impact. No equipment failure; just astronomy. | +| Duration | 25-30 min | +| Objectives | 18-22 | +| Mechanics reused | S11-S12 sister-teleport coordination, S10 customer comms, S1/S9 monitoring, S5 documentation | +| Satellites | TIDEMARK-1 | +| Ground stations | VT-01 | +| Characters & density | Dana (briefing + sign-off). Marcus (impact confirm, 1 clip). 4-5 clips total. | +| NICE — Primary | K0751, T1020, K0689 | +| NICE — Supporting | T0153, K1032, S0593 | +| New value beyond Phases 1-2 | First scenario where **the right answer is to accept degradation**, not fight it. Astronomical/environmental factors at predictable scale. Planning around guaranteed degradation, not avoiding it. | +| Prerequisites | nats-scenario16 | +| Arc connection | Standalone | + +--- + +### S18 — Satellite Anomaly + +| Field | Value | +|-------|-------| +| Subtitle | TIDEMARK-2 Station-Keeping Drift | +| Status | Built | +| Premise | Marcus reports from Halifax: TIDEMARK-2's station-keeping thrusters degraded; the bird is drifting more than expected. Ground operator must switch from program-track to step-track to maintain lock, monitor C/N stability, prepare customer impact assessment. The spacecraft team is the prime decision-maker; ground role is to keep the link alive while they diagnose. | +| Duration | 30-35 min | +| Objectives | 22-26 | +| Mechanics reused | S6 step-track, S6 antenna tracking-mode transition, S9 beacon monitoring, S10 Marcus collaboration | +| Satellites | TIDEMARK-2 | +| Ground stations | ME-02 primary, VT-01 standby | +| Characters & density | Marcus (primary, 3-4 clips — spacecraft side reports). Dana (incident-handling support). Catherine (ME-02 hands-on). 6-7 clips total. | +| NICE — Primary | K1032, T1314, K0751 | +| NICE — Supporting | K0721, T0153, S0593 | +| New value beyond Phases 1-2 | First scenario where the **satellite itself is the unknown**, not the ground equipment. Working in concert with a spacecraft team during a vehicle anomaly. When to escalate vs. when to keep operating. | +| Prerequisites | nats-scenario17 | +| Arc connection | Standalone | + +--- + +### S19 — Train the New Hire + +| Field | Value | +|-------|-------| +| Subtitle | Producing the Quick-Reference Card | +| Status | Built | +| Premise | Dana asks the player to draft a quick-reference card for an incoming new hire by **performing the procedure correctly while selecting which callouts and common-mistake warnings to highlight**. Implementation: SYSTEM-voiced quizzes throughout each phase prompt the player to choose the right teaching emphasis ("Which warning belongs on this step?", "What is the most common new-hire mistake here?"). The output is an in-game training document accumulated across the scenario. | +| Duration | 30-35 min | +| Objectives | 22-28 | +| Mechanics reused | S2 power-up sequence (the procedure being taught), S6 step-track (alt teaching path), S7 BUC loopback. Quiz pattern from S6 ("training day"). | +| Satellites | AURORA-7 (step-track is the most teachable mechanic) | +| Ground stations | VT-01 | +| Characters & density | Dana (intro + acknowledgment of the finished doc). SYSTEM throughout for the "which callout?" quizzes. **No new character is introduced for the new hire** — they exist off-screen. 3-4 clips total. | +| NICE — Primary | T1411, T1334, K0645 | +| NICE — Supporting | T1567, S0421, K0773 | +| New value beyond Phases 1-2 | First scenario where the player **must articulate the why**, not just execute the how. Reinforces mastery by requiring the operator to identify which gotchas and reasons matter most. Aligns with mentoring NICE work-role expectations. | +| Prerequisites | nats-scenario18 | +| Arc connection | Standalone | + +--- + +### S20 — Dual Outage + +| Field | Value | +|-------|-------| +| Subtitle | Concurrent Site Loss — Prioritized Recovery | +| Status | Built | +| Premise | A storm front knocks VT-01 receive offline (heavy rain attenuation beyond AGC range). Simultaneously, ME-02 hits an HPA fault. Multiple customers are affected. Operator must prioritize which site to restore first based on SLA exposure, customer mix, and recovery time estimates. | +| Duration | 35-45 min | +| Objectives | 26-32 | +| Mechanics reused | S3/S14 weather mechanics, S2/S7 HPA fault response, S3/S9 multi-station ops, S16 alarm prioritization | +| Satellites | TIDEMARK-1, TIDEMARK-2 | +| Ground stations | Both VT-01 and ME-02 (both degraded) | +| Characters & density | Dana (escalation, 2 clips). Catherine (ME-02 status, 2-3 clips). James Okafor (customer pressure, 1-2 clips). 6-8 clips total. | +| NICE — Primary | T1144, S0671, S0807 | +| NICE — Supporting | T1538, S0593, T0531 | +| New value beyond Phases 1-2 | True **multi-site prioritization** under simultaneous degradation. S16 was one site, multiple faults; S20 is multiple sites, mixed root causes. Decision is which fire to fight first, not how to fight any one fire. | +| Prerequisites | nats-scenario19 | +| Arc connection | Standalone | + +--- + +### S21 — Hostile RF + +| Field | Value | +|-------|-------| +| Subtitle | Suspected Intentional Interference | +| Status | Built | +| Premise | Unusual interference appears on TIDEMARK-2: broadband-ish noise that doesn't fit cross-pol patterns or known terrestrial sources, intermittent in a pattern that suggests deliberation. Operator must document signature, distinguish from natural causes, coordinate with regulator (rendered as in-game ticket workflow), apply receive-side countermeasures while the investigation runs. | +| Duration | 30-35 min | +| Objectives | 22-28 | +| Mechanics reused | S5 spectrum analysis at wide span, S5 interference characterization, S5/S15 documentation discipline | +| Satellites | TIDEMARK-2 | +| Ground stations | ME-02 primary | +| Characters & density | Dana (incident-handling, 2 clips). Marcus (spacecraft-side consultation, 1-2 clips). Regulator interaction as SYSTEM-voiced notifications. 5-6 clips total. | +| NICE — Primary | K0926, S0615, S0648 | +| NICE — Supporting | T0081, S0593, T0153 | +| New value beyond Phases 1-2 | First **adversarial-aware** scenario. S5 was unintentional cross-pol leakage; S21 introduces the question "is this intentional?" Differentiating jamming from interference. Regulatory coordination workflow. | +| Prerequisites | nats-scenario20 | +| Arc connection | Standalone | + +--- + +### S22 — End-of-Life Planning + +| Field | Value | +|-------|-------| +| Subtitle | AURORA-7 Sunset Recommendation | +| Status | Built | +| Premise | Trend data shows AURORA-7's beacon power has been declining for months. The board (Francis Martin) wants a recommendation on retirement timing. Operator runs final data collection, generates a trend report, evaluates business risk, prepares an executive-level impact summary. Light on RF, heavy on analysis and communication. | +| Duration | 25-30 min | +| Objectives | 18-22 | +| Mechanics reused | S6 step-track session (final ops run), S10 Marcus consultation, status-check pattern for analysis questions | +| Satellites | AURORA-7 | +| Ground stations | VT-01 | +| Characters & density | Marcus (Halifax discussion, 2 clips). Dana (intro). Francis Martin (board-level question, 1-2 clips — rare appearance). SYSTEM for analysis quizzes. 5-6 clips total. | +| NICE — Primary | K0721, T1429, T1606 | +| NICE — Supporting | K0751, S0807 | +| New value beyond Phases 1-2 | First **report-producing** scenario. Earlier scenarios ended in log entries; S22 ends in a multi-paragraph impact assessment. Trend analysis. Business-aware communication. Introduces Francis Martin as an active speaker. | +| Prerequisites | nats-scenario21 | +| Arc connection | Loose narrative continuity with S23 (AURORA-7 sunset thread). No hard dependency. | + +--- + +### S23 — Emergency Bypass + +| Field | Value | +|-------|-------| +| Subtitle | Manual Operations During Automation Failure | +| Status | Built | +| Premise | The ACU automation controller crashes mid-shift. Antenna positioning, beacon tracking, and program-track all unavailable. Operator must use manual tracking-mode controls to keep the link alive while IT works the root cause. Reveals what the automation was doing under the hood. | +| Duration | 25-30 min | +| Objectives | 20-25 | +| Mechanics reused | S2/S4/S6 antenna control fundamentals, S6 manual tracking-mode awareness, S5 spectrum-based pointing verification | +| Satellites | TIDEMARK-1 | +| Ground stations | VT-01 | +| Characters & density | Dana (intro + IT-coordination updates). IT contact rendered as SYSTEM-voiced ticket updates (no new character). 4-5 clips total. | +| NICE — Primary | S0424, T1588, S0671 | +| NICE — Supporting | T0531, S0421, K1032 | +| New value beyond Phases 1-2 | First scenario where the **automation itself** is the broken thing. Reveals the dependencies between automation and operator skill. Manual ops as a fallback competency, not a daily skill. | +| Prerequisites | nats-scenario22 | +| Arc connection | Standalone (loose AURORA-7 thread continues if S22 was completed). | + +--- + +### S24 — Constellation Crisis + +| Field | Value | +|-------|-------| +| Subtitle | Campaign Capstone | +| Status | Built | +| Premise | Compound emergency: simultaneous degradation on TIDEMARK-1 and TIDEMARK-2, AURORA-7 in step-track with a marginal beacon, an inbound weather event on VT-01, two customers escalating, and a board notification cycle to manage. Operator orchestrates across both stations, all four satellites, all communication channels. The final exam. | +| Duration | 40-50 min | +| Objectives | 30-38 | +| Mechanics reused | Effectively all S1-S23 mechanics. Specifically: multi-station ops (S3/S9/S20), multi-fault triage (S8/S16/S20), customer comms (S10/S14/S20), reporting (S22). | +| Satellites | TIDEMARK-1, TIDEMARK-2, TIDEMARK-3, AURORA-7 | +| Ground stations | Both VT-01 and ME-02 (both stressed) | +| Characters & density | All named characters appear: Dana (primary throughput), Catherine (ME-02), Marcus (TIDEMARK anomaly), James (customer escalation), Francis (board-level pressure). 8-10 clips total — this is the one Phase 2/3 scenario where dialog density is allowed to expand. | +| NICE — Primary | S0807, T1606, T0531 | +| NICE — Supporting | T1538, S0593, S0677, T1429, T1144 | +| New value beyond Phases 1-2 | Highest-stakes problem-solving. First scenario combining **multi-system technical recovery + executive-level reporting + customer escalation management** simultaneously. Demonstrates the operator can hold the entire job in their head at once. | +| Prerequisites | nats-scenario23 | +| Arc connection | Phase 3 capstone and campaign finale. | + +--- + +## Build Coordination Notes + +These conventions exist so that parallel builds don't contradict each other. Follow them strictly. + +### Tone and dialog density + +- All S9-S24 scenarios use the qualified-operator tone — see [nats-campaign-builder/references/dialog-density-guide.md](../../.claude/skills/nats-campaign-builder/references/dialog-density-guide.md). +- Default every `status-check` to `Character.SYSTEM`. Reserve named characters for narrative beats. +- Per-scenario character clip cap: 4-7 clips (S24 may go to 8-10 as the capstone exception). +- Text-message intro format is canonical (see S7, S8, S9). + +### Character roster (immutable) + +`DANA_TORRES`, `CATHERINE_VEGA`, `MARCUS_CHEN`, `JAMES_OKAFOR`, `FRANCIS_MARTIN`, `SYSTEM`. **Do not introduce new characters.** Charlie Brooks may appear only via phone/text from Europe. + +### Satellite roster (immutable) + +`tidemark1Satellite` (61525), `tidemark2Satellite` (61526), `tidemark3Satellite` (61527), `aurora7Satellite` (28899), `ses10Satellite` (42432, visible only). **Do not add new satellites.** If a scenario seems to require one, raise the question before building. + +### Station roster (immutable) + +`vermontGroundStation` (VT-01), `maineGroundStation` (ME-02). Both fully operational from S9 onward. **Do not add new stations.** + +### Dual registration + +Every new scenario must be registered in **two** places. Missing either causes a runtime "Scenario not found" error: +1. [src/campaigns/nats/campaign-data.ts](../../src/campaigns/nats/campaign-data.ts) — push into `natsCampaignData.scenarios`. +2. [src/scenario-manager.ts](../../src/scenario-manager.ts) — push into the top-level `SCENARIOS` array. + +### Per-scenario quality gates + +Before declaring a scenario done: +- `npm run type-check` passes clean. +- Scenario loads in the dev server and routes correctly from the campaign page. +- An `*-full-completion.spec.ts` Playwright test passes (see [scenario9-full-completion.spec.ts](../../e2e/specs/scenario9-full-completion.spec.ts) for the qualified-operator pattern with multi-station and antenna-repoint helpers). +- A mission brief MDX exists at `signal-range-docs/src/content/docs/campaign-1/scenario-N.mdx`. --- ## Variation Pattern -*Challenge types are varied across phases to prevent predictability* +*Challenge types are varied across phases to prevent predictability.* -| Position | Phase 1 | Phase 2 | Phase 3 | -|----------|---------|---------|---------| -| +1 | Power | Fault | Solar | -| +2 | Weather | Customer | Anomaly | -| +3 | Calculate | Interference | Mentor | -| +4 | Track | Thermal | Dual site | -| +5 | Multi-carrier | Handover | Hostile | -| +6 | Transmit | Rain | Planning | -| +7 | Fault | Conflict | Manual | -| +8 | Complex | Cascade | Everything | +| Position | Phase 1 (built) | Phase 2 (qualified ops) | Phase 3 (crisis ops) | +|----------|-----------------|-------------------------|----------------------| +| +1 (9, 17) | First Day | Morning Rounds | Solar Event | +| +2 (10, 18) | Power | Customer Pass | Satellite Anomaly | +| +3 (11, 19) | Weather | Maintenance arc — out | Train the New Hire | +| +4 (12, 20) | Calculate | Maintenance arc — return | Dual Outage | +| +5 (13, 21) | Multi-Carrier | Thermal Anomaly | Hostile RF | +| +6 (14, 22) | Step-Track | Rain Fade | End-of-Life Planning | +| +7 (15, 23) | Transmit | Frequency Coordination | Emergency Bypass | +| +8 (16, 24) | Fault | Cascade Failure | Constellation Crisis | --- ## NICE Code Reference +*Refreshed to reflect actual codes used across S1-S24. Sorted by NICE work role.* + ### Network Operations (IO-WRL-004) -- K0740: Knowledge of system performance indicators -- K0741: Knowledge of system availability measures -- K0770: Knowledge of system administration principles and practices -- K0773: Knowledge of telecommunications principles and practices -- K0737: Knowledge of bandwidth management tools and techniques -- K0792: Knowledge of network configurations -- K0718: Knowledge of network communications principles and practices -- K0689: Knowledge of network infrastructure principles and practices -- K0721: Knowledge of risk management principles and practices -- K0751: Knowledge of system threats -- K0926: Knowledge of signal jamming tools and techniques -- K1032: Knowledge of satellite-based communication systems and software -- S0077: Skill in securing network communications -- S0421: Skill in operating network equipment -- S0675: Skill in optimizing system performance -- S0815: Skill in troubleshooting network equipment -- T0081: Diagnose network connectivity problems -- T0129: Integrate new systems into existing network architecture -- T0153: Monitor network capacity and performance -- T1143: Develop network backup and recovery procedures -- T1313: Test network infrastructure, including software and hardware devices -- T1314: Maintain network infrastructure, including software and hardware devices +| Code | Description | First introduced | Reinforced in | +|------|-------------|------------------|---------------| +| K0689 | Knowledge of network infrastructure principles and practices | S3 | S14, S17 | +| K0718 | Knowledge of network communications principles and practices | S11 | S12 | +| K0721 | Knowledge of risk management principles and practices | S14 | S18, S22 | +| K0737 | Knowledge of bandwidth management tools and techniques | S5 | S15 | +| K0740 | Knowledge of system performance indicators | S1 | S9, S13 | +| K0741 | Knowledge of system availability measures | S1 | S9, S11 | +| K0751 | Knowledge of system threats | S17 | S18, S22 | +| K0770 | Knowledge of system administration principles and practices | S2 | — | +| K0773 | Knowledge of telecommunications principles and practices | S1 | S4, S5, S15 | +| K0792 | Knowledge of network configurations | S2 | S15 | +| K0926 | Knowledge of signal jamming tools and techniques | S21 | — | +| K1032 | Knowledge of satellite-based communication systems and software | S1 | S4, S6, S18 | +| S0077 | Skill in securing network communications | S7 | — | +| S0421 | Skill in operating network equipment | S1 | All phase 1 | +| S0582 | Skill in troubleshooting system performance | S5 | S8 | +| S0648 | Skill in detecting anomalies | S15 | S21 | +| S0675 | Skill in optimizing system performance | S3 | S10, S14 | +| S0815 | Skill in troubleshooting network equipment | S8 | — | +| T0081 | Diagnose network connectivity problems | S8 | S16, S21 | +| T0129 | Integrate new systems into existing network architecture | S11 | S12 | +| T0153 | Monitor network capacity and performance | S1 | S9, S10, S17 | +| T1143 | Develop network backup and recovery procedures | S15 | — | +| T1313 | Test network infrastructure | S7 | — | +| T1314 | Maintain network infrastructure | S12 | S13, S18 | ### System Administration (IO-WRL-005) -- K0064: Knowledge of performance tuning tools and techniques -- S0582: Skill in troubleshooting system performance -- S0593: Skill in handling incidents -- S0672: Skill in troubleshooting failed system components -- S0677: Skill in recovering failed systems -- S0424: Skill in executing command line tools -- S0671: Skill in implementing network infrastructure contingency and recovery plans -- T1567: Configure system hardware, software, and peripheral equipment -- T1538: Resolve customer-reported system incidents and events -- T1588: Diagnose faulty system and server hardware +| Code | Description | First introduced | Reinforced in | +|------|-------------|------------------|---------------| +| K0064 | Knowledge of performance tuning tools and techniques | S13 | — | +| S0424 | Skill in executing command line tools | S23 | — | +| S0593 | Skill in handling incidents | S11 | S14, S16, S20, S21 | +| S0671 | Skill in implementing contingency and recovery plans | S20 | S23 | +| S0672 | Skill in troubleshooting failed system components | S13 | — | +| S0677 | Skill in recovering failed systems | S16 | S20, S24 | +| T1538 | Resolve customer-reported system incidents | S8 | S16, S20, S24 | +| T1567 | Configure system hardware, software, peripheral equipment | S2 | S12, S19 | +| T1588 | Diagnose faulty system and server hardware | S23 | — | ### Technical Support (IO-WRL-007) -- S0478: Skill in providing customer support -- T1580: Monitor client-level computer system performance +| Code | Description | First introduced | Reinforced in | +|------|-------------|------------------|---------------| +| S0478 | Skill in providing customer support | S10 | — | +| T1580 | Monitor client-level computer system performance | S10 | — | ### Systems Testing and Evaluation (DD-WRL-007) -- T0531: Troubleshoot hardware/software interface and interoperability problems -- T1020: Determine the operational and safety impacts of cybersecurity lapses +| Code | Description | First introduced | Reinforced in | +|------|-------------|------------------|---------------| +| T0531 | Troubleshoot hardware/software interoperability problems | S16 | S20, S23 | +| T1020 | Determine operational and safety impacts | S17 | — | ### Data Analysis (IO-WRL-001) -- S0648: Skill in detecting anomalies -- T1429: Prepare trend analysis reports - -### Cross-Cutting - -- S0615: Skill in protecting a network against malware -- S0807: Skill in solving problems -- T1144: Implement network backup and recovery procedures -- T1334: Produce cybersecurity instructional materials -- T1411: Deliver technical training to customers -- T1606: Prepare impact reports +| Code | Description | First introduced | Reinforced in | +|------|-------------|------------------|---------------| +| T1429 | Prepare trend analysis reports | S22 | S24 | + +### Cross-Cutting / Other + +| Code | Description | First introduced | Reinforced in | +|------|-------------|------------------|---------------| +| K0645 | Knowledge of standard operating procedures (SOPs) | S1 | All scenarios | +| S0615 | Skill in protecting a network against malware | S21 | — | +| S0807 | Skill in solving problems | S16 | S20, S24 | +| T0431 | Check system hardware availability and integrity | S1 | S9, S12 | +| T1144 | Implement network backup and recovery procedures | S20 | S24 | +| T1334 | Produce cybersecurity instructional materials | S19 | — | +| T1411 | Deliver technical training to customers | S19 | — | +| T1606 | Prepare impact reports | S22 | S24 | + +### Coverage Summary + +- **Network Operations (IO-WRL-004):** 24 codes covered across the campaign. Heaviest representation — this is the primary NICE work role NATS targets. +- **System Administration (IO-WRL-005):** 9 codes covered. Concentrated in S11-S13, S16-S20, S23-S24. +- **Technical Support (IO-WRL-007):** 2 codes, both introduced in S10. Reinforcement happens implicitly in S14, S16, S20, S24 via customer-facing situations. +- **Systems Testing (DD-WRL-007):** 2 codes. S16 introduces interoperability troubleshooting; S17 introduces operational impact assessment. +- **Data Analysis (IO-WRL-001):** 1 code (T1429) — trend reporting, S22. +- **Cross-cutting / instructional:** T1334 and T1411 enter via S19's mentoring scenario. diff --git a/e2e/specs/navigation.spec.ts b/e2e/specs/navigation.spec.ts index 1fddaa19..5a5f978c 100644 --- a/e2e/specs/navigation.spec.ts +++ b/e2e/specs/navigation.spec.ts @@ -36,8 +36,9 @@ test.describe('Navigation', () => { await expect(scenarioSelectionPage.backButton).toBeVisible(); await scenarioSelectionPage.backButton.click(); - // App may redirect to / or /campaigns/ - await page.waitForURL(/^http:\/\/localhost:3000\/(campaigns\/?)?$/); + // App may redirect to / or /campaigns/ (host-agnostic: baseURL may be + // localhost or 127.0.0.1) + await page.waitForURL(/^http:\/\/(localhost|127\.0\.0\.1):3000\/(campaigns\/?)?$/); }); test('should handle unknown routes gracefully', async ({ page }) => { diff --git a/e2e/specs/scenario10-full-completion.spec.ts b/e2e/specs/scenario10-full-completion.spec.ts new file mode 100644 index 00000000..f3a3479e --- /dev/null +++ b/e2e/specs/scenario10-full-completion.spec.ts @@ -0,0 +1,379 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 10 - "Customer Pass": High-Throughput Window on AURORA-7. + * + * SeaLink booked a 30-minute high-priority pass on AURORA-7. Marcus Chen is on + * the line throughout. The operator switches the antenna from program-track to + * step-track for the inclined orbit, tightens HPA backoff from 10 dB to 6 dB + * for the pass window, holds sustained margin, then restores 10 dB backoff at + * window close. + * + * Objective types: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection (VT-01) + * - 'click-tab': Tab navigation + * - 'auto': Auto-satisfied by simulation state (no user action) - used for + * maintain-duration objectives and pre-configured-state checks + * - 'set-tracking-mode': Antenna tracking mode change (step-track, no satellite reselect) + * - 'configure-hpa-backoff': HPA backoff field + Apply button + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'auto' + | 'set-tracking-mode' + | 'configure-hpa-backoff'; + +interface Scenario10Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + trackingMode?: string; + hpaBackoff?: number; + /** Seconds to wait for an 'auto' objective with maintainDuration */ + autoWaitSeconds?: number; +} + +const SCENARIO_10_OBJECTIVES: Scenario10Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Customer Pass Brief', + type: 'quiz', + correctAnswer: 'Yes - link is up on program-track. Switching to pass configuration.', + }, + + // ============================================================ + // PHASE 1: PRE-PASS TRACKING SETUP + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'dashboard-sweep-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'dashboard-sweep', + title: 'Pre-Pass Alarm Sweep', + type: 'quiz', + correctAnswer: 'No active alarms - clean board, link up on AURORA-7', + }, + { + id: 'why-step-track', + title: 'Confirm Tracking Mode for the Pass', + type: 'quiz', + correctAnswer: + 'Inclined orbit drift will degrade C/N across the pass - step-track holds beacon optimum continuously', + }, + { + id: 'enable-step-track-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'enable-step-track', + title: 'Switch to Step-Track', + type: 'set-tracking-mode', + trackingMode: 'step-track', + }, + { + id: 'acquire-stable-beacon', + title: 'Acquire Stable Beacon Lock', + type: 'auto', + autoWaitSeconds: 20, + }, + + // ============================================================ + // PHASE 2: CUSTOMER LINK VERIFICATION + // ============================================================ + { + id: 'verify-rx-lock-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'verify-rx-lock', + title: 'Verify Customer Downlink', + type: 'auto', + autoWaitSeconds: 5, + }, + { + id: 'payload-integrity-check', + title: 'Payload Data Integrity', + type: 'quiz', + correctAnswer: 'Frame sync locked + CRC valid + FEC engaged with no uncorrectables', + }, + { + id: 'verify-uplink-active-tab', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'verify-uplink-active', + title: 'Verify Uplink Return Path', + type: 'auto', + autoWaitSeconds: 5, + }, + + // ============================================================ + // PHASE 3: HPA BACKOFF OPTIMIZATION + // ============================================================ + { + id: 'assess-current-backoff', + title: 'Assess Current HPA Backoff', + type: 'quiz', + correctAnswer: + 'Safe but conservative - tighter backoff gives the customer more EIRP without breaking the amp', + }, + { + id: 'optimize-hpa-backoff', + title: 'Tighten HPA Backoff for the Pass', + type: 'configure-hpa-backoff', + hpaBackoff: 6, + }, + { + id: 'verify-no-overdrive', + title: 'Confirm HPA Stays Linear', + type: 'auto', + autoWaitSeconds: 15, + }, + { + id: 'imd-tradeoff-check', + title: 'Acknowledge the IMD Tradeoff', + type: 'quiz', + correctAnswer: + 'IMD products rise as the amp moves closer to saturation - monitor for overdrive across the window', + }, + + // ============================================================ + // PHASE 4: SUSTAINED MARGIN MONITORING + // ============================================================ + { + id: 'sustain-rx-margin', + title: 'Sustain Customer Downlink Margin', + type: 'auto', + autoWaitSeconds: 35, + }, + { + id: 'sustain-beacon-track', + title: 'Sustain Step-Track Through the Pass', + type: 'auto', + autoWaitSeconds: 25, + }, + + // ============================================================ + // PHASE 5: PASS CLOSE + // ============================================================ + { + id: 'restore-conservative-backoff', + title: 'Restore Conservative Backoff', + type: 'configure-hpa-backoff', + hpaBackoff: 10, + }, + { + id: 'final-pass-snapshot', + title: 'Final Pass Disposition', + type: 'quiz', + correctAnswer: + 'AURORA-7 pass complete - step-track held throughout, C/N margin sustained, no overdrive events, HPA returned to 10 dB', + }, + { + id: 'log-customer-pass', + title: 'Log the Customer Pass', + type: 'quiz', + correctAnswer: + 'SeaLink 30-min high-priority pass on AURORA-7 - step-track engaged, HPA optimized to 6 dB backoff for window then restored to 10 dB. C/N margin sustained, no link events.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Set the antenna tracking mode by clicking the appropriate button. + * ACU control tab must be active before calling this. For step-track on + * AURORA-7 the satellite selection is already correct from the scenario + * preconfig, so no target reselection is required. + */ +async function setTrackingMode( + page: import('@playwright/test').Page, + trackingMode: string +): Promise { + // Step-track is an optimization toggle on top of program-track, not a + // separate mode button (same pattern as the scenario 6 spec). + if (trackingMode === 'step-track') { + const stepTrackToggle = page.locator('input[id$="step-track-toggle"]'); + await expect(stepTrackToggle).toBeVisible({ timeout: 5000 }); + + if (!(await stepTrackToggle.isChecked())) { + await stepTrackToggle.click(); + } + await expect(stepTrackToggle).toBeChecked(); + await page.waitForTimeout(300); + return; + } + + const modeButton = page.locator(`.btn-tracking[data-mode="${trackingMode}"]`); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(500); + + // Some tracking modes require an explicit Apply click. Only click Apply if + // the button surfaces as enabled. + const applyBtn = page.locator('button[id$="apply-changes-btn"]'); + if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + if (await applyBtn.isEnabled({ timeout: 500 }).catch(() => false)) { + await applyBtn.click(); + await page.waitForTimeout(300); + } + } +} + +/** + * Configure HPA backoff to achieve target output power. + * Lower backoff = higher output power. + */ +async function configureHpaBackoff( + page: import('@playwright/test').Page, + backoff: number +): Promise { + const backoffInput = page.locator('#hpa-backoff'); + await expect(backoffInput).toBeVisible({ timeout: 5000 }); + await backoffInput.fill(backoff.toString()); + await backoffInput.press('Tab'); + await page.waitForTimeout(150); + + const applyBtn = page.locator('#hpa-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + await page.waitForTimeout(400); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario10Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'set-tracking-mode': + await setTrackingMode(page, objective.trackingMode!); + break; + + case 'configure-hpa-backoff': + await configureHpaBackoff(page, objective.hpaBackoff!); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 2) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 10 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S9) + await missionControlPage.gotoScenario('nats', 'nats-scenario10'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_10_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Sustained-margin and beacon-track holds need longer timeouts than the default + if (objective.type === 'auto' && (objective.autoWaitSeconds ?? 0) >= 25) { + test.setTimeout(120000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario11-full-completion.spec.ts b/e2e/specs/scenario11-full-completion.spec.ts new file mode 100644 index 00000000..de1982ee --- /dev/null +++ b/e2e/specs/scenario11-full-completion.spec.ts @@ -0,0 +1,500 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 11 - "Planned Maintenance: Hand Off": Coordinated Traffic Transfer. + * + * Qualified-operator planned-handover archetype. ME-02 is pre-staged with + * TIDEMARK-1 RX hot. The operator verifies the receive side, brings up + * ME-02 transmit (BUC unmute + HPA enable + TX modem transmit), executes + * the handover via the satellite dashboard, then safes VT-01 for the + * maintenance crew (HPA disable, BUC mute, antenna to maintenance position + * at 5° elevation). + * + * Objective types: + * - 'quiz': Status-check quiz (all SYSTEM-voiced in this scenario) + * - 'select-station': Asset tree station selection + * - 'click-tab': Tab navigation + * - 'toggle-switch': Switch toggle (BUC mute, HPA enable, TX transmit) + * - 'enable-tx-commit': Composite - unmute BUC + enable HPA + enable TX transmit + * - 'set-tracking-mode': Antenna tracking mode (maintenance position) + * - 'select-satellite': Asset tree satellite selection (open dashboard) + * - 'execute-handover': Traffic handover dropdown + execute button + * - 'auto': Pre-staged state already satisfies the condition + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'toggle-switch' + | 'enable-tx-commit' + | 'set-tracking-mode' + | 'select-satellite' + | 'execute-handover' + | 'auto'; + +interface Scenario11Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + switchId?: string; + switchState?: boolean; + trackingMode?: string; + waitForAntennaPosition?: boolean; + satelliteId?: string; + handoverConfig?: { + targetStation: string; + }; +} + +const SCENARIO_11_OBJECTIVES: Scenario11Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Shift Brief', + type: 'quiz', + correctAnswer: + 'TIDEMARK-1 traffic moves to ME-02 for a 2-hour VT-01 maintenance window, then comes back next shift', + }, + + // ============================================================ + // PHASE 1: VT-01 PRE-HANDOVER VERIFICATION + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'vt-pre-handover-dashboard-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'vt-pre-handover-dashboard', + title: 'VT-01 Pre-Handover Sweep', + type: 'quiz', + correctAnswer: + 'A pre-handover snapshot documents what the link looked like healthy, so any post-handover anomaly can be attributed correctly', + }, + { + id: 'vt-confirm-tm1-locked', + title: 'Confirm VT-01 Owns TIDEMARK-1', + type: 'click-tab', + tabId: 'acu-control', + }, + + // ============================================================ + // PHASE 2: ME-02 RECEIVE-SIDE VERIFICATION + // ============================================================ + { + id: 'switch-to-maine', + title: 'Open ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + { + id: 'me-verify-antenna-on-tm1', + title: 'ME-02 Antenna on TIDEMARK-1', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'me-verify-beacon-and-rx', + title: 'ME-02 Receive Chain Hot', + type: 'click-tab', + tabId: 'rx-analysis', + }, + + // ============================================================ + // PHASE 3: ME-02 TRANSMIT COMMIT + // ============================================================ + { + id: 'me-tx-tab', + title: 'Open TX Chain (ME-02)', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'me-stage-tx-for-handover', + title: 'Stage ME-02 Transmit (cold)', + type: 'enable-tx-commit', + }, + { + id: 'understand-commit-point', + title: 'Understand the Commit Point', + type: 'quiz', + correctAnswer: + 'Antenna locked, RX carrier with C/N margin, TX chain staged - modem transmitting into a muted BUC, HPA disabled until the transfer swaps RF authority', + }, + + // ============================================================ + // PHASE 4: EXECUTE HANDOVER + // ============================================================ + { + id: 'open-tm1-dashboard', + title: 'Open TIDEMARK-1 Dashboard', + type: 'select-satellite', + satelliteId: 'sat-61525', + }, + { + id: 'execute-handover', + title: 'Execute Handover', + type: 'execute-handover', + handoverConfig: { + targetStation: 'ME-02', + }, + }, + { + id: 'verify-me-traffic-owner', + title: 'Verify ME-02 Owns Traffic', + type: 'auto', + }, + { + id: 'catherine-confirm-hot', + title: 'Confirm with Catherine', + type: 'quiz', + correctAnswer: 'Switch back to VT-01 and safe the RF chain for the maintenance crew', + }, + + // ============================================================ + // PHASE 5: VT-01 SAFING FOR MAINTENANCE CREW + // ============================================================ + { + id: 'switch-to-vermont-safing', + title: 'Return to VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'vt-safe-tx-tab', + title: 'Open TX Chain (VT-01)', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'vt-safe-tx-chain-hpa-disable', + title: 'Safe VT-01 TX Chain - Disable HPA', + type: 'toggle-switch', + switchId: 'hpa-enable', + switchState: false, + }, + { + id: 'vt-safe-tx-chain-buc-mute', + title: 'Safe VT-01 TX Chain - Mute BUC', + type: 'toggle-switch', + switchId: 'buc-mute', + switchState: true, + }, + { + id: 'vt-acu-tab', + title: 'Open ACU Control (VT-01)', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'vt-antenna-maintenance-position', + title: 'Park VT-01 Antenna at Maintenance Position', + type: 'set-tracking-mode', + trackingMode: 'maintenance', + waitForAntennaPosition: true, + }, + { + id: 'vt-final-rf-safety-check', + title: 'Final RF Safety Check', + type: 'quiz', + correctAnswer: + 'HPA disabled, BUC muted, antenna at maintenance position - no RF energy on the feed, dish accessible', + }, + + // ============================================================ + // SHIFT WRAP + // ============================================================ + { + id: 'log-handover-entry', + title: 'Log the Handover', + type: 'quiz', + correctAnswer: + '1000 - Planned handover TM-1 VT-01 to ME-02 complete. VT-01 RF chain safed, antenna at maintenance position. Crew on-site for HPA waveguide inspection. ME-02 (Vega) carrying traffic. Return to service next shift.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Toggle a switch to a desired state. + */ +async function toggleSwitch( + page: import('@playwright/test').Page, + switchId: string, + desiredState: boolean +): Promise { + const switchEl = page.locator(`#${switchId}`); + await expect(switchEl).toBeVisible({ timeout: 5000 }); + // Let the adapter's throttled DOM sync land before reading - the static + // template can render a stale checked state right after the tab mounts. + await page.waitForTimeout(1200); + const isChecked = await switchEl.isChecked(); + if (isChecked !== desiredState) { + await switchEl.click(); + if (desiredState) { + await expect(switchEl).toBeChecked(); + } else { + await expect(switchEl).not.toBeChecked(); + } + } + await page.waitForTimeout(200); +} + +/** + * Composite action: bring ME-02 transmit hot for the planned handover. + * Unmutes BUC, enables HPA output, enables TX modem transmit. + * + * Order matters: unmute BUC first so the TX path has a signal source, + * then enable HPA (interlock will see BUC unmuted), then enable transmit. + * (Mirrors the safe enable sequence taught in S2/S7.) + */ +async function enableTxForHandover(page: import('@playwright/test').Page): Promise { + // STAGE ONLY: the BUC stays muted and the HPA stays disabled. The handover + // engine swaps RF authority (source TX down, target TX up) at execute time. + // Radiating from both stations at once trips the dual-transmission failure. + + // TX modem power + transmit switch. Power is already true in the pre-staged + // state, but flip if needed for resilience. + const powerSwitch = page.locator('#tx-power-switch'); + if ((await powerSwitch.count()) > 0) { + const isPowered = await powerSwitch.isChecked(); + if (!isPowered) { + await powerSwitch.click(); + await expect(powerSwitch).toBeChecked(); + await page.waitForTimeout(200); + } + } + + const txSwitch = page.locator('#tx-transmit-switch'); + await expect(txSwitch).toBeVisible({ timeout: 5000 }); + if (!(await txSwitch.isChecked())) { + await txSwitch.click(); + await expect(txSwitch).toBeChecked(); + } + + // Wait for the objective manager to evaluate all three conditions. + await page.waitForTimeout(2000); +} + +/** + * Set the antenna tracking mode by clicking the appropriate button. + * For maintenance and stow modes, also clicks Apply to commit position. + */ +async function setTrackingMode( + page: import('@playwright/test').Page, + trackingMode: string +): Promise { + const modeButton = page.locator(`.btn-tracking[data-mode="${trackingMode}"]`); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); + + if (trackingMode === 'maintenance' || trackingMode === 'stow') { + const applyBtn = page.locator('button[id$="apply-changes-btn"]'); + await expect(applyBtn).toBeEnabled({ timeout: 3000 }); + await applyBtn.click(); + await page.waitForTimeout(200); + } +} + +/** + * Wait for the VT-01 antenna to finish slewing to the maintenance position by + * polling the simulation state directly (DOM polling can exit before the slew + * starts and desync the objective chain). + */ +async function waitForAntennaMovement( + page: import('@playwright/test').Page, + timeout = 240000 +): Promise { + await page.waitForFunction( + () => { + const sim = (window as unknown as { + signalRange?: { simulationManager?: { groundStations?: Array<{ antennas?: Array<{ state?: { slewing?: boolean; elevation?: number } }> }> } }; + }).signalRange?.simulationManager; + const antennaState = sim?.groundStations?.[0]?.antennas?.[0]?.state; + if (!antennaState) return false; + // Maintenance position is 5° elevation (±1° objective tolerance) + return antennaState.slewing === false && Math.abs((antennaState.elevation ?? 90) - 5) < 1.5; + }, + undefined, + { timeout } + ); + // Give the objectives manager a beat to register the position + await page.waitForTimeout(1500); +} + +/** + * Select a satellite in the asset tree to open its dashboard. + */ +async function selectSatellite( + page: import('@playwright/test').Page, + satelliteId: string +): Promise { + const satTreeItem = page.locator(`[data-asset-id="${satelliteId}"]`); + await expect(satTreeItem).toBeVisible({ timeout: 10000 }); + await satTreeItem.click(); + await page.waitForTimeout(1000); + await expect(satTreeItem).toHaveClass(/active/, { timeout: 5000 }); +} + +/** + * Execute traffic handover to target station via the satellite dashboard. + */ +async function executeTrafficHandover( + page: import('@playwright/test').Page, + targetStation: string +): Promise { + const handoverSelect = page.locator('#sat-handover-target'); + await expect(handoverSelect).toBeVisible({ timeout: 5000 }); + await handoverSelect.selectOption({ value: targetStation }); + await page.waitForTimeout(300); + + const executeBtn = page.locator('#sat-execute-handover'); + await expect(executeBtn).toBeVisible({ timeout: 5000 }); + await expect(executeBtn).toBeEnabled({ timeout: 10000 }); + await executeBtn.click(); + + await page.waitForTimeout(2000); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario11Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'toggle-switch': + await toggleSwitch(page, objective.switchId!, objective.switchState!); + break; + + case 'enable-tx-commit': + await enableTxForHandover(page); + break; + + case 'set-tracking-mode': + await setTrackingMode(page, objective.trackingMode!); + if (objective.waitForAntennaPosition) { + await waitForAntennaMovement(page); + } + break; + + case 'select-satellite': + await selectSatellite(page, objective.satelliteId!); + break; + + case 'execute-handover': + await executeTrafficHandover(page, objective.handoverConfig!.targetStation); + break; + + case 'auto': + await page.waitForTimeout(2000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 11 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S10) + await missionControlPage.gotoScenario('nats', 'nats-scenario11'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + // Generate a test per objective in order + for (const objective of SCENARIO_11_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Maintenance-position objective needs a longer timeout for slew + if (objective.type === 'set-tracking-mode' && objective.waitForAntennaPosition) { + test.setTimeout(300000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario12-full-completion.spec.ts b/e2e/specs/scenario12-full-completion.spec.ts new file mode 100644 index 00000000..279478d6 --- /dev/null +++ b/e2e/specs/scenario12-full-completion.spec.ts @@ -0,0 +1,634 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 12 - "Planned Maintenance: Return to Service". + * + * Closes the S11-S12 mini-arc. VT-01 has finished a waveguide gasket + * inspection window: antenna stowed, RF chain cold (LNB/BUC/HPA powered + * off). ME-02 (Catherine) is holding TM-1 traffic. The operator brings + * Vermont back online in a clean restoration sequence, catches a small + * leftover from the maintenance crew (BUC gain bumped to 50 dB during + * bench testing), and then pulls TM-1 traffic back from Maine. + * + * Objective types reused from earlier scenarios: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection + * - 'click-tab': Tab navigation + * - 'auto': Auto-satisfied (signal-detected, receiver-lock) + * - 'set-tracking-mode': Antenna tracking mode + satellite selection + * - 'configure-speca': Spectrum analyzer center frequency tuning + * - 'configure-lnb': Power on LNB + LO + gain + * - 'configure-buc-gain': BUC gain adjustment (S7 pattern) + * - 'toggle-switch': Equipment power / enable switches + * - 'select-satellite': Satellite selection in asset tree (S3 pattern) + * - 'execute-handover': Handover target select + execute (S3 pattern, ME-02 → VT-01) + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'auto' + | 'set-tracking-mode' + | 'configure-speca' + | 'configure-lnb' + | 'configure-buc-gain' + | 'toggle-switch' + | 'select-satellite' + | 'execute-handover'; + +interface Scenario12Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + trackingMode?: string; + satelliteNoradId?: string; + waitForAntennaPosition?: boolean; + specaConfig?: { + centerFrequency: number; // MHz + span?: number; // MHz + }; + lnbConfig?: { + loFrequency: number; // MHz + gain: number; // dB + }; + bucGain?: number; // dB + switchId?: string; + switchState?: boolean; + satelliteAssetId?: string; + handoverTargetStation?: string; +} + +const SCENARIO_12_OBJECTIVES: Scenario12Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Restoration Brief', + type: 'quiz', + correctAnswer: 'Confirmed. Beginning restoration.', + }, + + // ============================================================ + // PHASE 1: PRE-RESTORATION STATE VERIFICATION + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'pre-restoration-dashboard-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'pre-restoration-dashboard', + title: 'Confirm Safed State', + type: 'quiz', + correctAnswer: 'Antenna stowed, LNB/BUC/HPA powered off, GPSDO still running', + }, + + // ============================================================ + // PHASE 2: ANTENNA RESTORATION + // ============================================================ + { + id: 'repoint-antenna-tm1-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'repoint-antenna-tm1', + title: 'Repoint VT-01 to TIDEMARK-1', + type: 'set-tracking-mode', + trackingMode: 'program-track', + satelliteNoradId: '61525', + waitForAntennaPosition: true, + }, + + // ============================================================ + // PHASE 3: RX CHAIN RESTORATION + // ============================================================ + { + id: 'power-up-lnb-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'power-up-lnb', + title: 'Power Up LNB', + type: 'configure-lnb', + lnbConfig: { + loFrequency: 5250, + gain: 65, + }, + }, + { + id: 'acquire-tm1-beacon', + title: 'Acquire TIDEMARK-1 Beacon', + type: 'configure-speca', + specaConfig: { + centerFrequency: 1074.5, + }, + }, + { + id: 'verify-rx-modem-lock', + title: 'Verify RX Modem Lock', + type: 'auto', + }, + + // ============================================================ + // PHASE 4: POST-MAINTENANCE LEFTOVER SWEEP + // ============================================================ + { + id: 'tx-chain-inspection-tab', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'tx-chain-inspection', + title: 'TX Chain Pre-Power Inspection', + type: 'quiz', + correctAnswer: + 'BUC gain is at 50 dB - testing value left over from maintenance, operating value is 23 dB', + }, + // ============================================================ + // PHASE 5: TX CHAIN RESTORATION + // ============================================================ + { + // BUC powers up muted; gain correction follows (the buc-gain-set + // condition only evaluates on a powered BUC). + id: 'power-up-buc', + title: 'Power Up BUC (Muted)', + type: 'toggle-switch', + switchId: 'buc-power', + switchState: true, + }, + { + id: 'correct-buc-gain', + title: 'Restore BUC Operating Gain', + type: 'configure-buc-gain', + bucGain: 23, + }, + { + id: 'power-up-hpa', + title: 'Power Up HPA', + type: 'toggle-switch', + switchId: 'hpa-power', + switchState: true, + }, + { + id: 'start-modem-transmitting', + title: 'Enable TX Modem', + type: 'toggle-switch', + switchId: 'tx-transmit-switch', + switchState: true, + }, + { + // TX stays staged cold (BUC muted, HPA disabled): ME-02 still carries + // TM-1; radiating now would trip the dual-transmission failure. The + // handover return swaps RF authority automatically. + id: 'verify-tx-staged', + title: 'Confirm TX Staged Cold', + type: 'quiz', + correctAnswer: + 'Two stations radiating at the same transponder is dual illumination - the handover swaps RF authority in one coordinated action so only one uplink is ever on the air', + }, + + // ============================================================ + // PHASE 6: HANDOVER RETURN FROM ME-02 + // ============================================================ + { + id: 'select-maine-station', + title: 'Switch to ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + { + id: 'pre-handover-criteria', + title: 'Confirm Handover-Ready State', + type: 'quiz', + correctAnswer: + 'Beacon lock + RX modem lock with C/N margin + TX chain staged cold (modem on, BUC muted, HPA disabled)', + }, + { + id: 'execute-handover-return-select-sat', + title: 'Select TIDEMARK-1', + type: 'select-satellite', + satelliteAssetId: 'sat-61525', + }, + { + id: 'execute-handover-return', + title: 'Pull Traffic Back to VT-01', + type: 'execute-handover', + handoverTargetStation: 'VT-01', + }, + { + id: 'verify-handover-success-select-vt', + title: 'Return to VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'verify-handover-success', + title: 'Confirm Return Complete', + type: 'quiz', + correctAnswer: + 'VT-01 owns TM-1 traffic, ME-02 TX stood down, no packet loss reported, no new alarms', + }, + + // ============================================================ + // PHASE 7: CUSTOMER NOTIFICATION & SHIFT CLOSE + // ============================================================ + { + id: 'notify-customer', + title: 'Notify SeaLink', + type: 'quiz', + correctAnswer: + 'VT-01 restored, TM-1 traffic returned to primary station, no service interruption observed.', + }, + { + id: 'final-dashboard-sweep-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'final-dashboard-sweep', + title: 'Final Dashboard Sweep', + type: 'quiz', + correctAnswer: 'No active alarms - all systems nominal, TM-1 traffic on primary', + }, + { + id: 'log-maintenance-complete', + title: 'Log Maintenance Cycle Complete', + type: 'quiz', + correctAnswer: + 'VT-01 returned to service post-waveguide-gasket inspection. TM-1 traffic returned from ME-02. Maintenance leftover (BUC gain 50 dB) corrected before energizing. No customer impact.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Set the antenna tracking mode by clicking the appropriate button. + * ACU control tab must be active before calling this. + */ +async function setTrackingMode( + page: import('@playwright/test').Page, + trackingMode: string +): Promise { + const modeButton = page.locator(`.btn-tracking[data-mode="${trackingMode}"]`); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); + + if (trackingMode === 'maintenance' || trackingMode === 'stow') { + const applyBtn = page.locator('button[id$="apply-changes-btn"]'); + await expect(applyBtn).toBeEnabled({ timeout: 3000 }); + await applyBtn.click(); + await page.waitForTimeout(200); + } +} + +/** + * Select a target satellite from the ACU dropdown and click Move to Target. + * Used after setTrackingMode('program-track'). + */ +async function selectSatelliteAndMove( + page: import('@playwright/test').Page, + satelliteNoradId: string +): Promise { + const satelliteSelect = page.locator('select[id$="satellite-select"]'); + await expect(satelliteSelect).toBeVisible({ timeout: 5000 }); + await satelliteSelect.selectOption({ value: satelliteNoradId }); + await page.waitForTimeout(200); + + const moveBtn = page.locator('button[id$="move-to-target-btn"]'); + await expect(moveBtn).toBeEnabled({ timeout: 5000 }); + await moveBtn.click(); + await page.waitForTimeout(300); +} + +/** + * Wait for antenna movement to complete by polling the simulation state + * directly. DOM-based polling proved unreliable (display locator misses exit + * the wait early while the dish is still slewing from stow, which desyncs the + * whole objective chain). + */ +async function waitForAntennaMovement( + page: import('@playwright/test').Page, + timeout = 240000 +): Promise { + await page.waitForFunction( + () => { + const sim = (window as unknown as { + signalRange?: { simulationManager?: { groundStations?: Array<{ antennas?: Array<{ state?: { slewing?: boolean; isLocked?: boolean } }> }> } }; + }).signalRange?.simulationManager; + const antennaState = sim?.groundStations?.[0]?.antennas?.[0]?.state; + return antennaState ? antennaState.slewing === false && antennaState.isLocked === true : false; + }, + undefined, + { timeout } + ); + // Give the objectives manager a beat to register the lock + await page.waitForTimeout(1500); +} + +/** + * Configure spectrum analyzer center frequency. Span is optional. + */ +async function configureSpectrumAnalyzer( + page: import('@playwright/test').Page, + config: { centerFrequency: number; span?: number } +): Promise { + const centerFreqInput = page.locator('#sa-center-freq'); + await expect(centerFreqInput).toBeVisible({ timeout: 5000 }); + await centerFreqInput.fill(config.centerFrequency.toString()); + await centerFreqInput.press('Tab'); + await page.waitForTimeout(100); + + if (config.span !== undefined) { + const spanInput = page.locator('#sa-span'); + await expect(spanInput).toBeVisible(); + await spanInput.fill(config.span.toString()); + await spanInput.press('Tab'); + await page.waitForTimeout(100); + } + + await page.waitForTimeout(300); +} + +/** + * Power on the LNB and apply operating LO + gain values. + * Waits for thermal stabilization after Apply. + */ +async function configureLnb( + page: import('@playwright/test').Page, + config: { loFrequency: number; gain: number } +): Promise { + const powerSwitch = page.locator('#lnb-power'); + await expect(powerSwitch).toBeVisible({ timeout: 5000 }); + // The static template renders the power switch `checked`; the adapter syncs + // the real (powered-off) state on a ~1 s throttle. Wait for the first sync + // before reading, or the click gets skipped and the LNB never powers on. + await page.waitForTimeout(1500); + if (!(await powerSwitch.isChecked())) { + await powerSwitch.click(); + } + await expect(powerSwitch).toBeChecked(); + await page.waitForTimeout(500); + + const loInput = page.locator('#lnb-lo-frequency'); + await expect(loInput).toBeVisible(); + await loInput.fill(config.loFrequency.toString()); + await loInput.press('Tab'); + await page.waitForTimeout(100); + + const gainInput = page.locator('#lnb-gain'); + await expect(gainInput).toBeVisible(); + await gainInput.fill(config.gain.toString()); + await gainInput.press('Tab'); + await page.waitForTimeout(100); + + const applyBtn = page.locator('#lnb-apply-btn'); + await expect(applyBtn).toBeVisible(); + await applyBtn.click(); + + // LNB thermal stabilization ~3 s + await page.waitForTimeout(4000); +} + +/** + * Set the BUC gain via the TX Chain panel and click Apply. + * TX Chain tab must be active. + */ +async function configureBucGain( + page: import('@playwright/test').Page, + gain: number +): Promise { + const gainInput = page.locator('#buc-gain'); + await expect(gainInput).toBeVisible({ timeout: 5000 }); + await gainInput.fill(gain.toString()); + await gainInput.press('Tab'); + await page.waitForTimeout(100); + + const applyBtn = page.locator('#buc-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + await page.waitForTimeout(300); +} + +/** + * Toggle a switch to the desired state. + */ +async function toggleSwitch( + page: import('@playwright/test').Page, + switchId: string, + desiredState: boolean +): Promise { + const switchEl = page.locator(`#${switchId}`); + await expect(switchEl).toBeVisible({ timeout: 5000 }); + // Let the adapter's throttled DOM sync land before reading - the static + // template can render a stale checked state right after the tab mounts. + await page.waitForTimeout(1200); + const isChecked = await switchEl.isChecked(); + if (isChecked !== desiredState) { + await switchEl.click(); + if (desiredState) { + await expect(switchEl).toBeChecked(); + } else { + await expect(switchEl).not.toBeChecked(); + } + } + await page.waitForTimeout(300); +} + +/** + * Select a satellite by clicking its asset-tree item. + */ +async function selectSatellite( + page: import('@playwright/test').Page, + satelliteAssetId: string +): Promise { + const satTreeItem = page.locator(`[data-asset-id="${satelliteAssetId}"]`); + await expect(satTreeItem).toBeVisible({ timeout: 10000 }); + await satTreeItem.click(); + await page.waitForTimeout(1000); + await expect(satTreeItem).toHaveClass(/active/, { timeout: 5000 }); +} + +/** + * Execute the satellite-dashboard traffic handover to the target station. + */ +async function executeTrafficHandover( + page: import('@playwright/test').Page, + targetStation: string +): Promise { + const handoverSelect = page.locator('#sat-handover-target'); + await expect(handoverSelect).toBeVisible({ timeout: 5000 }); + await handoverSelect.selectOption({ value: targetStation }); + await page.waitForTimeout(300); + + const executeBtn = page.locator('#sat-execute-handover'); + await expect(executeBtn).toBeVisible({ timeout: 5000 }); + await expect(executeBtn).toBeEnabled({ timeout: 10000 }); + await executeBtn.click(); + + await page.waitForTimeout(2000); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario12Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'set-tracking-mode': + await setTrackingMode(page, objective.trackingMode!); + if (objective.trackingMode === 'program-track' && objective.satelliteNoradId) { + await selectSatelliteAndMove(page, objective.satelliteNoradId); + } + if (objective.waitForAntennaPosition) { + await waitForAntennaMovement(page); + } + break; + + case 'configure-speca': + await configureSpectrumAnalyzer(page, objective.specaConfig!); + break; + + case 'configure-lnb': + await configureLnb(page, objective.lnbConfig!); + break; + + case 'configure-buc-gain': + await configureBucGain(page, objective.bucGain!); + break; + + case 'toggle-switch': + await toggleSwitch(page, objective.switchId!, objective.switchState!); + break; + + case 'select-satellite': + await selectSatellite(page, objective.satelliteAssetId!); + break; + + case 'execute-handover': + await executeTrafficHandover(page, objective.handoverTargetStation!); + break; + + case 'auto': + // Signal/lock acquisition takes a few seconds in sim time. + await page.waitForTimeout(3000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 12 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S11) + await missionControlPage.gotoScenario('nats', 'nats-scenario12'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + // Generate a test per objective in order + for (const objective of SCENARIO_12_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Antenna repoint needs a longer timeout: the slew from stow (el 90) + // to TIDEMARK-1 (az 161.8, el 34.2) takes minutes of wall-clock. + if (objective.type === 'set-tracking-mode' && objective.waitForAntennaPosition) { + test.setTimeout(300000); + } + // Handover transfer + service-continuity evaluation can be slow + if (objective.type === 'execute-handover') { + test.setTimeout(90000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario13-full-completion.spec.ts b/e2e/specs/scenario13-full-completion.spec.ts new file mode 100644 index 00000000..9e21378d --- /dev/null +++ b/e2e/specs/scenario13-full-completion.spec.ts @@ -0,0 +1,323 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 13 - "Thermal Anomaly": Reading the Trend. + * + * Mid-shift trend assessment on VT-01. BUC temperature climbing on the + * TIDEMARK-1 carrier (62°C, +0.3°C/min). Pre-alarm. Operator must read + * the trend, identify the root cause (excess BUC gain), pick a course of + * action (de-rate), execute it (reduce BUC gain to 23 dB), and document. + * + * Objective types: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection (VT-01) + * - 'click-tab': Tab navigation + * - 'configure-buc-gain': Lower BUC gain via the TX Chain adjust input + * - 'auto': Auto-satisfied by simulation state (HPA backs off naturally + * once BUC drive drops; no user action required) + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'configure-buc-gain' + | 'auto'; + +interface Scenario13Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + bucGain?: number; +} + +const SCENARIO_13_OBJECTIVES: Scenario13Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Shift Brief', + type: 'quiz', + correctAnswer: 'Yes - moving to VT-01 to read the equipment.', + }, + + // ============================================================ + // PHASE 1: OBSERVATION + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'confirm-no-active-alarm-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'confirm-no-active-alarm', + title: 'Confirm Pre-Alarm State', + type: 'quiz', + correctAnswer: + 'Pre-alarm - you have time to choose a deliberate action instead of a reflexive one', + }, + { + id: 'open-tx-chain', + title: 'Open the TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'read-buc-temp-trend', + title: 'Read the Temperature Trend', + type: 'quiz', + correctAnswer: 'Roughly 25-30 minutes from now if nothing changes', + }, + { + id: 'check-current-draw', + title: 'Cross-Check Current Draw', + type: 'quiz', + correctAnswer: + 'BUC is dissipating more electrical power - consistent with the thermal rise, not a separate fault', + }, + { + id: 'cross-check-spectrum-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'cross-check-spectrum', + title: 'Rule Out an RX-Side Fault', + type: 'auto', + }, + { + id: 'record-baseline-readings', + title: 'Record Baseline Readings', + type: 'quiz', + correctAnswer: + 'Time, BUC temperature, BUC current, BUC gain, HPA backoff - so the next operator can rebuild the curve', + }, + + // ============================================================ + // PHASE 2: DIAGNOSIS & JUDGMENT + // ============================================================ + { + id: 'identify-root-cause', + title: 'Name the Root Cause', + type: 'quiz', + correctAnswer: + 'BUC gain is set higher than required - the module is dissipating the excess as heat instead of useful RF', + }, + { + id: 'evaluate-options', + title: 'Choose a Course of Action', + type: 'quiz', + correctAnswer: + 'De-rate now: reduce BUC gain ~10 dB to cut dissipation, monitor the trend reverse, schedule a swap during the next planned window', + }, + { + id: 'confirm-action-plan', + title: 'Confirm the Sequence', + type: 'quiz', + correctAnswer: + 'Lower BUC gain ~10 dB, verify HPA still in linear region, verify carrier still nominal, then watch the temperature curve bend', + }, + + // ============================================================ + // PHASE 3: EXECUTION + // ============================================================ + { + id: 'reduce-buc-gain-tab', + title: 'Open the TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'reduce-buc-gain', + title: 'De-Rate the BUC', + type: 'configure-buc-gain', + bucGain: 23, + }, + { + id: 'verify-hpa-headroom', + title: 'Verify HPA Still Linear', + type: 'auto', + }, + { + id: 'verify-trend-stabilizing', + title: 'Confirm the Trend Is Bending', + type: 'quiz', + correctAnswer: + 'Watch 5-10 minutes: temperature slope flattens then trends down, current draw drops toward nominal, carrier still locked downstream', + }, + + // ============================================================ + // PHASE 4: SCHEDULE & DOCUMENT + // ============================================================ + { + id: 'schedule-maintenance-ticket', + title: 'Open the Maintenance Ticket', + type: 'quiz', + correctAnswer: + 'Trend record (15-min curve), de-rate action taken, current BUC gain/backoff settings, recommendation to swap module during next planned window', + }, + { + id: 'final-dashboard-sweep-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'final-dashboard-sweep', + title: 'Final Dashboard Sweep', + type: 'quiz', + correctAnswer: + 'No active alarms, BUC running de-rated, carrier nominal, swap ticket open against next planned window', + }, + { + id: 'log-shift-summary', + title: 'Log the Shift Entry', + type: 'quiz', + correctAnswer: + '1003 - VT-01 BUC thermal trend (57°C->62°C over 15 min) addressed by 10 dB gain de-rate. Trend reversing. Swap ticket opened for next planned window. Carrier nominal throughout.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Configure BUC gain via the TX Chain adjust control. + * TX Chain tab must be active before calling this. + */ +async function configureBucGain( + page: import('@playwright/test').Page, + gain: number +): Promise { + const gainInput = page.locator('#buc-gain'); + await expect(gainInput).toBeVisible({ timeout: 5000 }); + await gainInput.fill(gain.toString()); + await gainInput.press('Tab'); + await page.waitForTimeout(100); + + const applyBtn = page.locator('#buc-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + + // Let the simulator propagate the gain change through to HPA drive + // (reduces overdrive, drops output power, allows HPA backoff to recover) + await page.waitForTimeout(2000); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario13Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'configure-buc-gain': + await configureBucGain(page, objective.bucGain!); + break; + + case 'auto': + // Auto-satisfied by simulation state - allow a beat for the + // condition evaluator to register the change. + await page.waitForTimeout(2000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 13 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S12) + await missionControlPage.gotoScenario('nats', 'nats-scenario13'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + for (const objective of SCENARIO_13_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario14-full-completion.spec.ts b/e2e/specs/scenario14-full-completion.spec.ts new file mode 100644 index 00000000..0873b5f1 --- /dev/null +++ b/e2e/specs/scenario14-full-completion.spec.ts @@ -0,0 +1,371 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 14 - "Rain Fade": Adapt Without Handover. + * + * Qualified-operator weather scenario. A moderate rain front passes over + * VT-01. The customer (James Okafor, SeaLink) has asked the operator to + * hold the link rather than execute a handover. The right call for a + * 3 dB fade with AGC headroom is to hold; the test exercises that path. + * + * Objective types: + * - 'quiz': Status-check quiz (SYSTEM or named character) + * - 'select-station': Asset tree station selection (VT-01) + * - 'click-tab': Tab navigation + * - 'toggle-switch': Equipment switch toggle (feed heater) + * - 'auto': Auto-satisfied by simulation state (signal locks, HPA nominal, + * sustained-monitor objectives whose conditions are already met) + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'toggle-switch' + | 'auto'; + +interface Scenario14Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + switchId?: string; + switchState?: boolean; +} + +const SCENARIO_14_OBJECTIVES: Scenario14Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Shift Brief', + type: 'quiz', + correctAnswer: 'Yes, brief reviewed. Standing by for the front.', + }, + + // ============================================================ + // CUSTOMER CONSTRAINT + // ============================================================ + { + id: 'acknowledge-customer-constraint', + title: 'Customer Constraint', + type: 'quiz', + correctAnswer: + 'Their SLA penalizes handover events more heavily than a few dB of margin loss', + }, + + // ============================================================ + // PROACTIVE PROTECTION - FEED HEATER + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'pre-storm-dashboard-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'pre-storm-dashboard', + title: 'Pre-Storm Dashboard Sweep', + type: 'quiz', + correctAnswer: 'No active alarms, link healthy - clean baseline to fade from', + }, + { + id: 'enable-feed-heater-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'enable-feed-heater', + title: 'Enable Feed Heater', + type: 'toggle-switch', + // ACU control IDs are prefixed per-instance (acu-{uuid}-ant0-heater-switch); + // the [id$=...] fallback in toggleSwitch resolves this suffix correctly. + switchId: 'heater-switch', + switchState: true, + }, + { + id: 'understand-heater-vs-rain', + title: 'Heater Purpose for Rain', + type: 'quiz', + correctAnswer: + 'Keeping water from beading and sheeting on the feed - dry surfaces attenuate less than wet ones', + }, + + // ============================================================ + // BASELINE LINK MARGIN + // ============================================================ + { + id: 'open-rx-analysis-baseline', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'baseline-beacon-and-lock', + title: 'Baseline Beacon and Lock', + type: 'auto', + }, + { + id: 'baseline-margin-quiz', + title: 'Link Margin Baseline', + type: 'quiz', + correctAnswer: + 'It defines how much fade the link can absorb before reaching the demodulation threshold', + }, + + // ============================================================ + // ACTIVE MONITORING - RAIN ARRIVES + // ============================================================ + { + id: 'monitor-during-fade', + title: 'Monitor Through the Fade', + type: 'auto', + }, + { + id: 'agc-behavior-quiz', + title: 'AGC Behavior in the Fade', + type: 'quiz', + correctAnswer: + 'AGC is compensating - the demodulator still sees a usable signal, and we still have headroom in the gain stage', + }, + { + id: 'agc-headroom-quiz', + title: 'AGC Headroom Reading', + type: 'quiz', + correctAnswer: + 'Plenty of headroom remaining - link is comfortable, hold is justified', + }, + + // ============================================================ + // OPTIMIZATION - HPA BACKOFF + // ============================================================ + { + id: 'open-tx-chain', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'hpa-backoff-decision-quiz', + title: 'HPA Backoff Decision', + type: 'quiz', + correctAnswer: + 'No - the link is healthy; trading IMD risk for unused margin is a bad bargain', + }, + { + id: 'verify-hpa-still-nominal', + title: 'Verify HPA Still Nominal', + type: 'auto', + }, + + // ============================================================ + // DECISION POINT - HOLD OR HAND OFF + // ============================================================ + { + id: 'handover-threshold-quiz', + title: 'Handover Threshold', + type: 'quiz', + correctAnswer: 'AGC at max with continued fade, or modem dropping lock', + }, + { + id: 'decision-hold', + title: 'Make the Call', + type: 'quiz', + correctAnswer: + 'Hold VT-01. AGC has headroom, modem locked, customer preference honored. Re-evaluate if state changes.', + }, + + // ============================================================ + // SUSTAINED MONITORING THROUGH PEAK + // ============================================================ + { + id: 'sustained-monitor', + title: 'Hold Through the Peak', + type: 'auto', + }, + + // ============================================================ + // POST-STORM AND DOCUMENTATION + // ============================================================ + { + id: 'post-storm-baseline-quiz', + title: 'Post-Storm Recovery', + type: 'quiz', + correctAnswer: + 'C/N recovers toward baseline; AGC backs its gain down; modem lock unchanged', + }, + { + id: 'document-handover-avoided', + title: 'Log the Hold', + type: 'quiz', + correctAnswer: + 'Moderate rain over VT-01, ~3 dB fade. Held TM-1 service per customer SLA preference; AGC max 3 dB, modem lock maintained throughout, no handover.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Toggle a switch element to the specified state. + * Tries exact ID match first, then suffix and substring fallbacks + * to tolerate prefix variations. + */ +async function toggleSwitch( + page: import('@playwright/test').Page, + switchId: string, + targetState: boolean +): Promise { + let switchEl = page.locator(`#${switchId}`); + + if ((await switchEl.count()) === 0) { + switchEl = page.locator(`[id$="${switchId}"]`); + } + if ((await switchEl.count()) === 0) { + // Fallback: match by suffix without station prefix + const bareId = switchId.replace(/^[a-z0-9-]+-/, ''); + switchEl = page.locator(`[id*="${bareId}"]`); + } + + await expect(switchEl.first()).toBeVisible({ timeout: 5000 }); + + // Let the adapter's throttled DOM sync land before reading - the static + // template can render a stale checked state right after the tab mounts. + await page.waitForTimeout(1200); + const isChecked = await switchEl.first().isChecked(); + if (isChecked !== targetState) { + await switchEl.first().click(); + } + + if (targetState) { + await expect(switchEl.first()).toBeChecked(); + } else { + await expect(switchEl.first()).not.toBeChecked(); + } + + await page.waitForTimeout(200); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario14Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'toggle-switch': + await toggleSwitch(page, objective.switchId!, objective.switchState!); + break; + + case 'auto': + // Conditions are satisfied by ambient simulation state. Give the + // engine a beat to confirm the objective transitioned to complete. + await page.waitForTimeout(2000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 14 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S13) + await missionControlPage.gotoScenario('nats', 'nats-scenario14'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + for (const objective of SCENARIO_14_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Monitor-during-fade and sustained-monitor wait for the simulation + // to advance through the rain event. Give them more runway. + if ( + objective.id === 'monitor-during-fade' || + objective.id === 'sustained-monitor' + ) { + test.setTimeout(180000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario15-full-completion.spec.ts b/e2e/specs/scenario15-full-completion.spec.ts new file mode 100644 index 00000000..af1599c8 --- /dev/null +++ b/e2e/specs/scenario15-full-completion.spec.ts @@ -0,0 +1,393 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 15 - "Frequency Coordination": Inter-Operator Spectrum Etiquette. + * + * Qualified-operator coordination work. A partner teleport (RedSky) is about to + * light up a V-pol carrier 2 MHz off our active SeaLink TIDEMARK-3 H-pol edge. + * The player verifies guard band, raises HPA back-off from 5 dB (the latent + * defect) to 10 dB to suppress IMD into RedSky's slot, confirms the customer + * carrier survives, then sends the coordination confirmation back. + * + * Only one physical action: HPA back-off change. Everything else is SYSTEM + * status-checks plus tab/station navigation and one spectrum-analyzer retune. + * + * Objective types: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection (VT-01) + * - 'click-tab': Tab navigation + * - 'configure-speca': Spectrum analyzer reconfiguration + * - 'configure-hpa-backoff': HPA back-off slider/input + * - 'auto': Auto-satisfied by simulation state (no user action needed) + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'configure-speca' + | 'configure-hpa-backoff' + | 'auto'; + +interface Scenario15Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + hpaBackoff?: number; + specaConfig?: { + centerFrequency?: number; // MHz + span?: number; // MHz + minAmplitude?: number; // dBm + maxAmplitude?: number; // dBm + rbwAuto?: boolean; + }; +} + +const SCENARIO_15_OBJECTIVES: Scenario15Objective[] = [ + // ============================================================ + // PHASE 1: MISSION PREP + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Coordination Notice', + type: 'quiz', + correctAnswer: 'Brief reviewed. Notice acknowledged. Starting coordination check.', + }, + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + + // ============================================================ + // PHASE 2: PROCESS COORDINATION NOTICE + // ============================================================ + { + id: 'identify-partner-band', + title: 'Identify RedSky Occupied Band', + type: 'quiz', + correctAnswer: '5957 - 5965 MHz', + }, + { + id: 'identify-our-band', + title: 'Identify Our Occupied Band', + type: 'quiz', + correctAnswer: '5967 - 6003 MHz', + }, + { + id: 'compute-guard-band', + title: 'Compute Guard Band', + type: 'quiz', + correctAnswer: '2 MHz', + }, + { + id: 'assess-guard-adequacy', + title: 'Assess Guard Band Adequacy', + type: 'quiz', + correctAnswer: + "Verify our TX chain is producing clean spectrum - no spurs or IMD landing in RedSky's band", + }, + + // ============================================================ + // PHASE 3: INSPECT TX CONFIGURATION + // ============================================================ + { + id: 'open-tx-chain', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'check-tx-modem-frequency', + title: 'Verify TX Modem Configuration', + type: 'quiz', + correctAnswer: 'Yes - 1015 MHz IF matches 7000 minus 5985, carrier on the right slot', + }, + { + id: 'observe-current-hpa-backoff', + title: 'Observe HPA Backoff', + type: 'quiz', + correctAnswer: + 'Operating close to saturation - third-order IMD products will be elevated and extend several MHz beyond the carrier edges', + }, + { + id: 'understand-imd-mechanism', + title: 'Understand IMD Mechanism', + type: 'quiz', + correctAnswer: + 'Nonlinearity in the amplifier mixes spectral components, generating intermodulation products that fall just outside the carrier edges', + }, + + // ============================================================ + // PHASE 4: MITIGATION DECISION + // ============================================================ + { + id: 'evaluate-mitigation-options', + title: 'Choose Mitigation', + type: 'quiz', + correctAnswer: + 'Increase HPA backoff to 10 dB - reduces IMD without dropping the carrier and without requiring customer coordination', + }, + + // ============================================================ + // PHASE 5: APPLY MITIGATION + // ============================================================ + { + id: 'increase-hpa-backoff', + title: 'Increase HPA Backoff to 10 dB', + type: 'configure-hpa-backoff', + hpaBackoff: 10, + }, + { + id: 'verify-hpa-still-online', + title: 'Verify HPA Still Online', + type: 'auto', + }, + + // ============================================================ + // PHASE 6: VERIFY ON SPECTRUM + // ============================================================ + { + id: 'open-rx-analysis', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'tune-speca-to-downlink', + title: 'Tune Spectrum to TIDEMARK-3 Downlink', + type: 'configure-speca', + specaConfig: { + centerFrequency: 1490, + span: 75, + minAmplitude: -100, + maxAmplitude: -30, + rbwAuto: true, + }, + }, + { + id: 'confirm-carrier-still-nominal', + title: 'Confirm Carrier Still Nominal', + type: 'quiz', + correctAnswer: + 'Wideband carrier still present at slightly reduced power - customer link healthy, IMD skirts dropped well below the adjacent slot noise floor', + }, + { + id: 'verify-receiver-locked', + title: 'Verify Receiver Locked', + type: 'auto', + }, + + // ============================================================ + // PHASE 7: COORDINATE BACK + // ============================================================ + { + id: 'confirm-spectrum-clean-for-partner', + title: 'Draft Confirmation to RedSky', + type: 'quiz', + correctAnswer: + 'Confirmed clear. TIDEMARK-3 TP-1 carrier holds 5967-6003 MHz H-pol with adjacent-channel emissions well below your planned slot. Proceed with your 5961 MHz V-pol uplink as scheduled.', + }, + + // ============================================================ + // PHASE 8: LOG + // ============================================================ + { + id: 'log-coordination-event', + title: 'Log Coordination Event', + type: 'quiz', + correctAnswer: + '0937 - RedSky coordination notice received and confirmed. TIDEMARK-3 TP-1 HPA backoff raised from 5 to 10 dB to suppress adjacent-channel IMD. Cleared RedSky for 5961 MHz V-pol uplink. SeaLink carrier remains nominal.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Configure spectrum analyzer settings. + * Element IDs: #sa-center-freq, #sa-span, #sa-min-amp, #sa-max-amp, #sa-rbw + */ +async function configureSpectrumAnalyzer( + page: import('@playwright/test').Page, + config: NonNullable +): Promise { + if (config.centerFrequency !== undefined) { + const centerInput = page.locator('#sa-center-freq'); + await expect(centerInput).toBeVisible({ timeout: 5000 }); + await centerInput.fill(config.centerFrequency.toString()); + await centerInput.press('Tab'); + await page.waitForTimeout(100); + } + + if (config.span !== undefined) { + const spanInput = page.locator('#sa-span'); + await expect(spanInput).toBeVisible({ timeout: 5000 }); + await spanInput.fill(config.span.toString()); + await spanInput.press('Tab'); + await page.waitForTimeout(100); + } + + if (config.minAmplitude !== undefined) { + const minInput = page.locator('#sa-min-amp'); + if ((await minInput.count()) > 0 && (await minInput.isVisible())) { + await minInput.fill(config.minAmplitude.toString()); + await minInput.press('Tab'); + await page.waitForTimeout(100); + } + } + + if (config.maxAmplitude !== undefined) { + const maxInput = page.locator('#sa-max-amp'); + if ((await maxInput.count()) > 0 && (await maxInput.isVisible())) { + await maxInput.fill(config.maxAmplitude.toString()); + await maxInput.press('Tab'); + await page.waitForTimeout(100); + } + } + + if (config.rbwAuto) { + const rbwSelect = page.locator('#sa-rbw'); + await expect(rbwSelect).toBeVisible({ timeout: 5000 }); + await rbwSelect.selectOption({ label: 'Auto' }); + await page.waitForTimeout(100); + } + + await page.waitForTimeout(300); +} + +/** + * Configure HPA back-off via input + Apply button. + */ +async function configureHpaBackoff( + page: import('@playwright/test').Page, + backoff: number +): Promise { + const backoffInput = page.locator('#hpa-backoff'); + await expect(backoffInput).toBeVisible({ timeout: 5000 }); + await backoffInput.fill(backoff.toString()); + await backoffInput.press('Tab'); + await page.waitForTimeout(100); + + const applyBtn = page.locator('#hpa-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + await page.waitForTimeout(300); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario15Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'configure-speca': + await configureSpectrumAnalyzer(page, objective.specaConfig!); + break; + + case 'configure-hpa-backoff': + await configureHpaBackoff(page, objective.hpaBackoff!); + break; + + case 'auto': + // verify-hpa-still-online and verify-receiver-locked are both pure + // state checks - HPA stayed enabled while we changed back-off, modem + // never stopped transmitting, receiver was locked at scenario start. + // Give the engine a beat to tick the conditions. + await page.waitForTimeout(2000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 15 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S14) + await missionControlPage.gotoScenario('nats', 'nats-scenario15'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + // Generate a test per objective in order + for (const objective of SCENARIO_15_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario16-full-completion.spec.ts b/e2e/specs/scenario16-full-completion.spec.ts new file mode 100644 index 00000000..3df10824 --- /dev/null +++ b/e2e/specs/scenario16-full-completion.spec.ts @@ -0,0 +1,473 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 16 - "Cascade Failure": Multi-System Recovery Under Customer Pressure. + * + * Phase 2 capstone. Three concurrent unrelated faults on VT-01: + * - BUC over-temperature (>70°C) with high current draw + * - LNB sticky reference-lock fault (clears on power cycle) + * - HPA back-off drifted to 2 dB (overdriven) + * + * Recovery order tested: triage → disable HPA output → mute BUC → restore HPA + * back-off → wait for BUC thermal recovery → power-cycle LNB → restore receiver → + * unmute BUC → re-enable HPA → verify clean TX → final sweep → customer + * notification → log entry. + * + * RF safety rule (rf-front-end-core): an enabled HPA with the BUC muted/off + * fails the mission instantly. HPA must come down before the BUC mute and go + * back up only after the BUC is unmuted. + * + * Objective types: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection + * - 'click-tab': Tab navigation + * - 'toggle-switch': Boolean equipment switch (mute, power, enable) + * - 'configure-hpa-backoff': Set HPA back-off via input + Apply + * - 'lnb-power-cycle': OFF, wait, ON sequence + * - 'auto': Auto-satisfied by simulation state (waiting for thermal recovery) + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'toggle-switch' + | 'configure-hpa-backoff' + | 'lnb-power-cycle' + | 'auto'; + +interface Scenario16Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + switchId?: string; + switchState?: boolean; + hpaBackoff?: number; + /** For auto/cooling waits: max seconds to wait for the condition (default 90) */ + waitForSeconds?: number; +} + +const SCENARIO_16_OBJECTIVES: Scenario16Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Incident Brief', + type: 'quiz', + correctAnswer: + 'Triage all alarms first, then act in priority order: RF safety, customer impact, equipment health', + }, + + // ============================================================ + // PHASE 1: TRIAGE AND PRIORITIZATION + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'triage-dashboard-alarms-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'triage-dashboard-alarms', + title: 'Triage the Alarm Board', + type: 'quiz', + correctAnswer: + 'BUC over-temperature and high current, LNB reference unlocked, HPA overdriven', + }, + { + id: 'prioritize-recovery-order', + title: 'Set the Recovery Order', + type: 'quiz', + correctAnswer: + 'RF safety first (HPA overdrive + BUC) → customer impact next (LNB / RX) → final verification', + }, + + // ============================================================ + // PHASE 2: RF SAFETY - DISABLE HPA, MUTE BUC, RESTORE BACK-OFF + // ============================================================ + { + id: 'navigate-tx-chain', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'disable-hpa-for-safety-toggle', + title: 'Disable the HPA Output', + type: 'toggle-switch', + switchId: 'hpa-enable', + switchState: false, + }, + { + id: 'disable-hpa-for-safety', + title: 'Disable the HPA - Confirm Rationale', + type: 'quiz', + correctAnswer: + 'It takes the dirty uplink off the air immediately, and an enabled HPA must never be left without BUC drive - it would amplify raw noise into the feed', + }, + { + id: 'mute-buc-for-cooldown', + title: 'Mute the BUC for Cooldown', + type: 'toggle-switch', + switchId: 'buc-mute', + switchState: true, + }, + { + id: 'diagnose-hpa-overdrive', + title: 'Diagnose HPA Overdrive', + type: 'quiz', + correctAnswer: + 'Output is too close to saturation - IMD products are rising and the amplifier is at risk', + }, + { + id: 'correct-hpa-backoff', + title: 'Restore HPA Back-off to 10 dB', + type: 'configure-hpa-backoff', + hpaBackoff: 10, + }, + + // ============================================================ + // PHASE 3: BUC THERMAL RECOVERY (wait for cooldown) + // ============================================================ + { + id: 'wait-for-buc-cooling', + title: 'Allow BUC to Cool', + type: 'auto', + // Muted BUC cools toward ambient at ~0.14°C/s; 72°C -> <70°C in ~20 s. + waitForSeconds: 45, + }, + + // ============================================================ + // PHASE 4: LNB RECOVERY + // ============================================================ + { + id: 'navigate-rx-analysis', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'diagnose-lnb-fault', + title: 'Diagnose LNB Reference Unlock', + type: 'quiz', + correctAnswer: 'Power cycle the LNB to clear the sticky reference lock fault', + }, + { + id: 'power-cycle-lnb', + title: 'Power Cycle the LNB', + type: 'lnb-power-cycle', + }, + + // verify-rx-locked is auto-satisfied once LNB stabilizes after power cycle + { + id: 'verify-rx-locked', + title: 'Verify Receiver Locked', + type: 'auto', + waitForSeconds: 15, + }, + + // ============================================================ + // PHASE 5: TX RESTORATION + // ============================================================ + { + id: 'return-to-tx-chain', + title: 'Return to TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'unmute-buc-restore-tx', + title: 'Unmute the BUC', + type: 'toggle-switch', + switchId: 'buc-mute', + switchState: false, + }, + { + id: 'restore-hpa-output-toggle', + title: 'Re-enable HPA Output', + type: 'toggle-switch', + switchId: 'hpa-enable', + switchState: true, + }, + { + id: 'verify-tx-output-clean', + title: 'Restore HPA Output - Confirm Spectrum Posture', + type: 'quiz', + correctAnswer: + 'Clean - IMD products are back below coordination limits and we are no longer interfering', + }, + + // ============================================================ + // PHASE 6: VERIFICATION AND CLOSE-OUT + // ============================================================ + { + id: 'final-alarm-sweep-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'final-alarm-sweep', + title: 'Final Dashboard Sweep', + type: 'quiz', + correctAnswer: + 'All three faults cleared - BUC thermal normal, LNB locked, HPA within back-off - link operational', + }, + { + id: 'customer-notification', + title: 'Notify the Customer', + type: 'quiz', + correctAnswer: + 'Three concurrent faults identified and cleared in priority order. Link is operational. Will follow up with a written impact report within the hour.', + }, + { + id: 'log-cascade-event', + title: 'Log the Cascade Event', + type: 'quiz', + correctAnswer: + 'Concurrent unrelated faults VT-01: BUC over-temp (72°C, high I), LNB ref unlock (sticky), HPA back-off drift (2 dB). Recovery in order: HPA output disabled, BUC muted for cooldown, back-off restored to 10 dB, LNB power cycled, BUC unmuted, HPA restored. Customer (SeaLink) notified.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +async function toggleSwitch( + page: import('@playwright/test').Page, + switchId: string, + desiredState: boolean +): Promise { + const switchEl = page.locator(`#${switchId}`); + await expect(switchEl).toBeVisible({ timeout: 5000 }); + // Adapters sync DOM to sim state on a ~1 s throttle; the static template may + // render a stale checked state right after the tab mounts. Let the first + // sync land before reading. + await page.waitForTimeout(1200); + const isChecked = await switchEl.isChecked(); + if (isChecked !== desiredState) { + await switchEl.click(); + if (desiredState) { + await expect(switchEl).toBeChecked(); + } else { + await expect(switchEl).not.toBeChecked(); + } + } + await page.waitForTimeout(300); +} + +async function configureHpaBackoff( + page: import('@playwright/test').Page, + backoff: number +): Promise { + const backoffInput = page.locator('#hpa-backoff'); + await expect(backoffInput).toBeVisible({ timeout: 5000 }); + await backoffInput.fill(backoff.toString()); + await backoffInput.press('Tab'); + await page.waitForTimeout(100); + + const applyBtn = page.locator('#hpa-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + await page.waitForTimeout(500); +} + +async function powerCycleLnb(page: import('@playwright/test').Page): Promise { + const powerSwitch = page.locator('#lnb-power'); + await expect(powerSwitch).toBeVisible({ timeout: 5000 }); + + // Power OFF + if (await powerSwitch.isChecked()) { + await powerSwitch.click(); + await expect(powerSwitch).not.toBeChecked(); + } + await page.waitForTimeout(1500); + + // Power ON + await powerSwitch.click(); + await expect(powerSwitch).toBeChecked(); + + // Wait for thermal stabilization + reference re-acquire + await page.waitForTimeout(5000); +} + +async function closeQuizModalIfPresent(page: import('@playwright/test').Page): Promise { + const quizModal = page.locator('#quiz-modal, .quiz-box'); + try { + if (await quizModal.isVisible({ timeout: 500 })) { + const closeBtn = quizModal + .locator('.draggable-box__close-btn, [class*="close"], button:has-text("×")') + .first(); + if (await closeBtn.isVisible({ timeout: 500 }).catch(() => false)) { + await closeBtn.click({ force: true }); + await page.waitForTimeout(400); + } + } + } catch { + // No quiz modal, continue + } +} + +async function openPendingQuiz(page: import('@playwright/test').Page): Promise { + const pendingIndicatorBtn = page.locator('.pending-quiz-indicator__open-btn'); + const quizModalDirect = page.locator('#quiz-modal, .quiz-box'); + + await page.waitForTimeout(1500); + + let isQuizOpen = await quizModalDirect.isVisible().catch(() => false); + let attempts = 0; + while (!isQuizOpen && attempts < 15) { + attempts++; + try { + await expect(pendingIndicatorBtn).toBeVisible({ timeout: 8000 }); + await pendingIndicatorBtn.evaluate((btn) => (btn as HTMLButtonElement).click()); + await page.waitForTimeout(500); + isQuizOpen = await quizModalDirect.isVisible().catch(() => false); + + if (!isQuizOpen && attempts > 3) { + await page.evaluate(() => { + const qm = ( + window as unknown as { __quizManager__?: { reopenPendingQuiz: () => void } } + ).__quizManager__; + if (qm) { + qm.reopenPendingQuiz(); + } + }); + await page.waitForTimeout(500); + isQuizOpen = await quizModalDirect.isVisible().catch(() => false); + } + } catch { + await page.waitForTimeout(1000); + } + } + + await expect(quizModalDirect).toBeVisible({ timeout: 10000 }); +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario16Objective +): Promise { + if (objective.type !== 'quiz') { + await closeQuizModalIfPresent(page); + } + + switch (objective.type) { + case 'quiz': + await openPendingQuiz(page); + await answerQuizByText(page, objective.correctAnswer!); + await page.waitForTimeout(1000); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'toggle-switch': + await toggleSwitch(page, objective.switchId!, objective.switchState!); + break; + + case 'configure-hpa-backoff': + await configureHpaBackoff(page, objective.hpaBackoff!); + break; + + case 'lnb-power-cycle': + await powerCycleLnb(page); + break; + + case 'auto': + // Wait for simulation-driven condition (e.g. BUC thermal recovery) + await page.waitForTimeout((objective.waitForSeconds ?? 90) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 16 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + (window as unknown as { DEVELOPER_MODE: boolean }).DEVELOPER_MODE = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Direct navigation - bypasses prerequisite check on S15 (not yet built) + await missionControlPage.gotoScenario('nats', 'nats-scenario16'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + for (const objective of SCENARIO_16_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Thermal cooldown can take several minutes of sim time + if (objective.type === 'auto' && (objective.waitForSeconds ?? 0) > 60) { + test.setTimeout((objective.waitForSeconds! + 60) * 1000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario17-full-completion.spec.ts b/e2e/specs/scenario17-full-completion.spec.ts new file mode 100644 index 00000000..e443d491 --- /dev/null +++ b/e2e/specs/scenario17-full-completion.spec.ts @@ -0,0 +1,327 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 17 - "Solar Event": Sun Transit Outage. + * + * Phase 3 opener. A predicted sun transit (weather event type 'sun-transit') + * raises VT-01's sky noise from T+300s to T+600s with a 12 dB sin^2 peak. + * The operator baselines the link, notifies the customer BEFORE the window, + * holds configuration through the peak (demod loses lock and self-recovers), + * verifies recovery, and documents predicted-vs-actual. + * + * Spec notes: + * - 'wait-sky-noise' steps poll the simulation state directly + * (skyNoiseDegradation_dB on VT-01's antenna) instead of sleeping. + * - Total wall-clock is dominated by the transit timeline (~11 min). + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'auto' + | 'wait-sky-noise'; + +interface Scenario17Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + /** For wait-sky-noise: condition to await */ + skyNoise?: { above?: number; below?: number }; + /** For wait-sky-noise: max seconds to wait */ + waitTimeoutSeconds?: number; +} + +const SCENARIO_17_OBJECTIVES: Scenario17Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Transit Prediction', + type: 'quiz', + correctAnswer: 'Acknowledged - prediction sheet reviewed, pre-event checklist starting now.', + }, + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + + // ============================================================ + // PHASE 1: PRE-WINDOW BASELINE AND UNDERSTANDING + // ============================================================ + { + id: 'baseline-dashboard-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'baseline-dashboard', + title: 'Pre-Transit Baseline', + type: 'quiz', + correctAnswer: + 'Anything abnormal AFTER the window starts will be attributed to the Sun - a fault hiding under the transit would survive unnoticed unless the board was provably clean before', + }, + { + id: 'transit-geometry-quiz', + title: 'Transit Geometry', + type: 'quiz', + correctAnswer: + 'The Sun (a ~20,000 K noise source at C-band) passes through the antenna main beam behind the satellite - system noise temperature soars and C/N collapses, with the signal itself unchanged', + }, + { + id: 'transit-predictability-quiz', + title: 'Why It Is Predictable', + type: 'quiz', + correctAnswer: + 'Twice a year near the equinoxes - a few minutes a day for several consecutive days, at a time computable years in advance from the station/satellite geometry', + }, + { + id: 'baseline-rx-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'baseline-rx-check', + title: 'Baseline RX Snapshot', + type: 'auto', + }, + { + id: 'why-not-handover-quiz', + title: 'The Handover Question', + type: 'quiz', + correctAnswer: + 'The outage is brief, predicted, and SLA-excluded with notice - a handover trades that for two transfer events, and ME-02 inherits its own transit on its own schedule anyway', + }, + { + id: 'notify-customer-quiz', + title: 'Pre-Event Notification', + type: 'quiz', + correctAnswer: + 'Predicted solar transit on TIDEMARK-1 from Vermont, window and peak times attached; expect degraded margin and a possible 1-3 minute carrier interruption near peak; service recovers without intervention; this message constitutes SLA advance notice.', + }, + + // ============================================================ + // PHASE 2: THE WINDOW + // ============================================================ + { + id: 'observe-onset-quiz', + title: 'Confirm Predicted Onset (quiz)', + type: 'quiz', + correctAnswer: + 'Confidence this is the predicted transit and not a coincidental fault - the alarm tracking the prediction sheet IS the diagnosis', + }, + { + id: 'observe-onset-wait', + title: 'Wait for Sky Noise Onset', + type: 'wait-sky-noise', + skyNoise: { above: 2 }, + waitTimeoutSeconds: 420, + }, + { + // ride-through-peak completes on its own at >6 dB as long as the spec + // touches nothing - which is the whole point of the objective. + id: 'ride-through-peak', + title: 'Hold Through the Peak', + type: 'wait-sky-noise', + skyNoise: { above: 6 }, + waitTimeoutSeconds: 300, + }, + { + id: 'peak-behavior-quiz', + title: 'What the Peak Looks Like', + type: 'quiz', + correctAnswer: + 'The downlink is buried in solar noise at OUR antenna only - the satellite still hears our uplink perfectly, and the demod will relock on its own as the Sun moves off boresight', + }, + + // ============================================================ + // PHASE 3: RECOVERY AND DOCUMENTATION + // ============================================================ + { + id: 'verify-recovery', + title: 'Verify Self-Recovery', + type: 'wait-sky-noise', + skyNoise: { below: 1 }, + waitTimeoutSeconds: 420, + }, + { + id: 'post-event-sweep-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'post-event-sweep', + title: 'Post-Event Alarm Sweep', + type: 'quiz', + correctAnswer: + 'Any alarm that survived the window - the transit excuses exactly five minutes of sky noise and nothing else', + }, + { + id: 'marcus-confirm', + title: 'Spacecraft-Side Confirmation', + type: 'quiz', + correctAnswer: + 'Nothing abnormal on the spacecraft - our uplink steady throughout, vehicle telemetry nominal; the event existed only at our antenna', + }, + { + id: 'document-impact', + title: 'Impact Documentation', + type: 'quiz', + correctAnswer: + 'Predicted vs actual window times, peak degradation observed, carrier lock-loss duration, notification timestamp (pre-window), and customer impact statement', + }, + { + id: 'log-shift-summary', + title: 'Log the Event', + type: 'quiz', + correctAnswer: + 'Predicted solar transit TM-1/VT-01 executed per SOP-SX-001. Customer notified pre-window. Peak ~12 dB sky noise, brief demod loss near peak, self-recovered to baseline. No operator intervention, no residual alarms. Day 2 of 4 in this transit series - next window tomorrow, ~4 minutes earlier.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** Poll VT-01's antenna sky-noise state until the threshold is met. */ +async function waitForSkyNoise( + page: import('@playwright/test').Page, + condition: { above?: number; below?: number }, + timeoutSeconds: number +): Promise { + await page.waitForFunction( + (cond) => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { skyNoiseDegradation_dB?: number } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + const sky = gs?.antennas?.[0]?.state?.skyNoiseDegradation_dB ?? 0; + if (cond.above !== undefined && sky <= cond.above) return false; + if (cond.below !== undefined && sky >= cond.below) return false; + return true; + }, + condition, + { timeout: timeoutSeconds * 1000 } + ); + // Give the objectives manager a beat to evaluate the custom condition + await page.waitForTimeout(2500); +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario17Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'wait-sky-noise': + await waitForSkyNoise(page, objective.skyNoise!, objective.waitTimeoutSeconds ?? 420); + break; + + case 'auto': + await page.waitForTimeout(3000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 17 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario17'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + for (const objective of SCENARIO_17_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Transit-timeline waits run on astronomy's clock, not ours + if (objective.type === 'wait-sky-noise') { + test.setTimeout(((objective.waitTimeoutSeconds ?? 420) + 60) * 1000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario18-full-completion.spec.ts b/e2e/specs/scenario18-full-completion.spec.ts new file mode 100644 index 00000000..d79f36e7 --- /dev/null +++ b/e2e/specs/scenario18-full-completion.spec.ts @@ -0,0 +1,291 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 18 - "Satellite Anomaly": TIDEMARK-2 Station-Keeping Drift. + * + * A scenario-local drifting TM-2 variant (inclined figure-8, stale ephemeris) + * degrades ME-02's program-track link. The operator reads the signature, + * transitions to step-track, verifies beacon + carrier recovery, holds the + * link, and closes out with role/escalation discipline quizzes. + * + * Objective types: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection (ME-02) + * - 'click-tab': Tab navigation + * - 'set-tracking-mode': Step-track toggle (rides program-track) + * - 'auto': Simulation-satisfied condition (maintain-duration holds) + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'set-tracking-mode' + | 'auto'; + +interface Scenario18Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + trackingMode?: string; + autoWaitSeconds?: number; +} + +const SCENARIO_18_OBJECTIVES: Scenario18Objective[] = [ + { + id: 'review-mission-brief', + title: 'Review Anomaly Brief', + type: 'quiz', + correctAnswer: + 'Keep the link alive and feed Halifax ground observations - the vehicle is theirs, the lock is mine', + }, + { + id: 'select-maine-station', + title: 'Open ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + + // PHASE 1: READ THE SIGNATURE + { + id: 'dashboard-baseline-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'dashboard-baseline', + title: 'Station Health vs Link Health', + type: 'quiz', + correctAnswer: + 'The ground segment is healthy - the degradation is on the space side or in the geometry, which matches Halifax\'s report exactly', + }, + { + id: 'station-keeping-quiz', + title: 'What Died on the Spacecraft', + type: 'quiz', + correctAnswer: + 'Residual inclination accumulates - the bird traces a daily figure-8 in az/el that grows over weeks, and the published ephemeris becomes progressively more wrong', + }, + { + id: 'read-program-track-decay-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'read-program-track-decay', + title: 'Read the Program-Track Decay', + type: 'quiz', + correctAnswer: + 'Program-track IS on target - the ephemeris target. The satellite is somewhere else, and the gap between prediction and reality is being paid in pattern loss', + }, + { + id: 'beamwidth-risk-quiz', + title: 'How Long Until It Falls Off', + type: 'quiz', + correctAnswer: + 'Already lost - the excursion is several beamwidths and only gets worse; any fix based on following the ephemeris fails until Halifax publishes a corrected one', + }, + + // PHASE 2: TRANSITION TO STEP-TRACK + { + id: 'enable-step-track', + title: 'Engage Step-Track', + type: 'set-tracking-mode', + trackingMode: 'step-track', + }, + { + id: 'acquire-stable-beacon', + title: 'Beacon Recovery', + type: 'auto', + autoWaitSeconds: 25, + }, + { + id: 'verify-carrier-recovery-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'verify-carrier-recovery', + title: 'Carrier Recovery', + type: 'auto', + autoWaitSeconds: 25, + }, + { + id: 'no-manual-chase-quiz', + title: 'Why Not Fly It By Hand', + type: 'quiz', + correctAnswer: + 'A human chases where the bird WAS; the loop tracks where it IS - manual nudges add pointing error between corrections, fatigue guarantees a missed one, and a bad nudge can drop the beacon entirely', + }, + + // PHASE 3: HOLD AND REPORT + { + id: 'sustained-hold', + title: 'Hold Through the Drift', + type: 'auto', + autoWaitSeconds: 40, + }, + { + id: 'ground-observations-quiz', + title: 'Feed the Vehicle Team', + type: 'quiz', + correctAnswer: + 'Timestamped step-track pointing history - the dish is physically following the satellite, so its az/el trace IS an independent measurement of the actual orbit', + }, + { + id: 'impact-assessment-quiz', + title: 'Customer Impact Posture', + type: 'quiz', + correctAnswer: + 'No current impact; service nominal under contingency tracking. Elevated risk posture while the vehicle anomaly is open - next decision points are loss of step-track margin or a vehicle-status change from Halifax', + }, + { + id: 'escalation-boundary-quiz', + title: 'Escalation Tripwires', + type: 'quiz', + correctAnswer: + 'Step-track losing the beacon, C/N trending below demod threshold despite good tracking, or Halifax declaring the vehicle unsafe - anything where keeping the link stops being possible or stops being wise', + }, + { + id: 'log-shift-summary', + title: 'Log the Anomaly Response', + type: 'quiz', + correctAnswer: + 'TM-2 vehicle anomaly (Halifax NOC ref): N-S station-keeping suspended, ephemeris stale. ME-02 transitioned to step-track 0935, beacon and carrier recovered, no customer impact. Pointing history streaming to Halifax. Tripwires: step-track margin, vehicle status change. Anomaly OPEN.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** Step-track is a toggle riding program-track (same pattern as S6/S10). */ +async function setTrackingMode( + page: import('@playwright/test').Page, + trackingMode: string +): Promise { + if (trackingMode === 'step-track') { + const stepTrackToggle = page.locator('input[id$="step-track-toggle"]'); + await expect(stepTrackToggle).toBeVisible({ timeout: 5000 }); + + if (!(await stepTrackToggle.isChecked())) { + await stepTrackToggle.click(); + } + await expect(stepTrackToggle).toBeChecked(); + await page.waitForTimeout(300); + return; + } + + const modeButton = page.locator(`.btn-tracking[data-mode="${trackingMode}"]`); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario18Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'ME-02'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'set-tracking-mode': + await setTrackingMode(page, objective.trackingMode!); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 18 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario18'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_18_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + if (objective.type === 'auto' && (objective.autoWaitSeconds ?? 0) >= 30) { + test.setTimeout(120000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario19-full-completion.spec.ts b/e2e/specs/scenario19-full-completion.spec.ts new file mode 100644 index 00000000..681e1b54 --- /dev/null +++ b/e2e/specs/scenario19-full-completion.spec.ts @@ -0,0 +1,406 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 19 - "Train the New Hire": Producing the Quick-Reference Card. + * + * First scenario using the Working Document panel: teaching quizzes carry + * documentLine params and the quick-reference card accumulates as they are + * passed. The spec performs the live AURORA-7 procedure (repoint, + * beacon tune, step-track, verify) interleaved with the card quizzes, then + * verifies the Working Document panel actually contains the card lines. + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'repoint-program-track' + | 'configure-speca' + | 'set-step-track' + | 'verify-working-doc' + | 'auto'; + +interface Scenario19Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + satelliteNoradId?: string; + centerFrequencyMhz?: number; + autoWaitSeconds?: number; +} + +const SCENARIO_19_OBJECTIVES: Scenario19Objective[] = [ + { + id: 'review-mission-brief', + title: 'Review the Assignment', + type: 'quiz', + correctAnswer: 'Ready - procedure live, card building in the Working Document panel.', + }, + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'card-scope-quiz', + title: 'Set the Card\'s Scope', + type: 'quiz', + correctAnswer: + 'The numbers someone needs under pressure plus the mistakes with the highest local base rate - one page, taped to the console; the procedure itself stays in the SOP', + }, + + // PHASE 1: ACQUIRE + { + id: 'repoint-to-aurora-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'repoint-to-aurora', + title: 'Fly It: Acquire AURORA-7', + type: 'repoint-program-track', + satelliteNoradId: '28899', + }, + { + id: 'acquire-callout-quiz', + title: 'Card Line: Acquisition', + type: 'quiz', + correctAnswer: + 'Program-track FIRST - it puts you inside beacon capture range. Nominal: Az 190, El 32, but the bird rides a ±3° figure-8', + }, + { + id: 'tune-beacon-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'tune-beacon', + title: 'Fly It: Find the Beacon', + type: 'configure-speca', + centerFrequencyMhz: 1085, + }, + { + id: 'beacon-formula-quiz', + title: 'Card Line: the Beacon Number', + type: 'quiz', + correctAnswer: + 'Beacon IF = LO − RF = 5250 − 4165 = 1085 MHz. Weak CW (aging bird) - use a narrow span, ~2 kHz', + }, + { + id: 'mistake-lo-quiz', + title: 'Card Line: First Watch-Out', + type: 'quiz', + correctAnswer: + 'Beacon "missing" at 1085? Check the LNB LO = 5250 - an operator fresh from Maine duty once hunted a healthy beacon for an hour with the LO still at Maine\'s 6080 default', + }, + + // PHASE 2: TRACK + { + id: 'enable-step-track-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'enable-step-track', + title: 'Fly It: Engage Step-Track', + type: 'set-step-track', + }, + { + id: 'step-track-rule-quiz', + title: 'Card Line: the Engagement Rule', + type: 'quiz', + correctAnswer: + 'Step-track RIDES program-track - engage it as an optimization on an acquired beacon, never from MANUAL (the loop needs a beacon to optimize)', + }, + { + id: 'hold-beacon', + title: 'Fly It: Hold the Figure-8', + type: 'auto', + autoWaitSeconds: 30, + }, + { + id: 'healthy-track-quiz', + title: 'Card Line: What Healthy Looks Like', + type: 'quiz', + correctAnswer: + 'Healthy = beacon C/N steady at its peak while Az/El visibly wander the figure-8. Moving dish + flat C/N is the loop WORKING, not a fault', + }, + { + id: 'mistake-chase-quiz', + title: 'Card Line: Second Watch-Out', + type: 'quiz', + correctAnswer: + 'C/N sagging mid-track? Verify step-track is still ON before touching the axes - hand-chasing the figure-8 is a losing game an operator here once played for twenty minutes', + }, + + // PHASE 3: VERIFY + { + id: 'verify-receiver-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'verify-receiver', + title: 'Fly It: Prove the Link', + type: 'auto', + autoWaitSeconds: 10, + }, + { + id: 'verify-chain-quiz', + title: 'Card Line: the Proof Chain', + type: 'quiz', + correctAnswer: + 'Proof chain, in order: beacon at 1085 (pointing + LO) → RX locked at 1422 MHz / 24 MHz (carrier) → C/N ≥ 8 (margin). Each link proves something the others don\'t', + }, + { + id: 'mistake-span-quiz', + title: 'Card Line: Third Watch-Out', + type: 'quiz', + correctAnswer: + 'Carrier "gone" but beacon fine? Widen the span - a 24 MHz carrier is invisible at the 2 kHz span you used for the beacon. (Operator here once declared an outage over this)', + }, + { + id: 'tx-numbers-quiz', + title: 'Card Line: the TX Number', + type: 'quiz', + correctAnswer: + 'TX IF = BUC LO − uplink RF = 7500 − 6053 = 1447 MHz. AURORA\'s chain uses BUC LO 7500 - NOT the TIDEMARK 7000', + }, + + // Verify the card BEFORE the final quizzes (the Mission Complete modal + // overlays the sidebar once the last objective lands) + { + id: 'verify-working-doc', + title: 'Working Document: card accumulated all lines', + type: 'verify-working-doc', + }, + + // PHASE 4: REVIEW AND HANDOFF + { + id: 'card-review-quiz', + title: 'Editorial Review', + type: 'quiz', + correctAnswer: + 'Card space is the reader\'s attention under pressure - every line they scan past to find the one they need is time on a degraded link; the card earns trust by containing only what earns its place', + }, + { + id: 'log-handoff', + title: 'Hand Off the Card', + type: 'quiz', + correctAnswer: + 'AURORA-7 quick-reference card complete - built against a live procedure run (acquire, step-track, verify, all green). Sections: Acquire / Track / Verify / Numbers / 3x Watch-Out from station history. Delivered to Dana for the new-hire packet.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** Program-track + select satellite + Move to Target, then wait on sim state. */ +async function repointProgramTrack( + page: import('@playwright/test').Page, + satelliteNoradId: string +): Promise { + const modeButton = page.locator('.btn-tracking[data-mode="program-track"]'); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); + + const satelliteSelect = page.locator('select[id$="satellite-select"]'); + await expect(satelliteSelect).toBeVisible({ timeout: 5000 }); + await satelliteSelect.selectOption({ value: satelliteNoradId }); + await page.waitForTimeout(200); + + const moveBtn = page.locator('button[id$="move-to-target-btn"]'); + await expect(moveBtn).toBeEnabled({ timeout: 5000 }); + await moveBtn.click(); + + // Wait for the slew to finish and lock to register (sim-state, not DOM) + await page.waitForFunction( + () => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { slewing?: boolean; isLocked?: boolean } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + const antennaState = gs?.antennas?.[0]?.state; + return antennaState ? antennaState.slewing === false && antennaState.isLocked === true : false; + }, + undefined, + { timeout: 180000 } + ); + await page.waitForTimeout(1500); +} + +async function configureSpeca( + page: import('@playwright/test').Page, + centerFrequencyMhz: number +): Promise { + const centerFreqInput = page.locator('#sa-center-freq'); + await expect(centerFreqInput).toBeVisible({ timeout: 5000 }); + await centerFreqInput.fill(centerFrequencyMhz.toString()); + await centerFreqInput.press('Tab'); + await page.waitForTimeout(500); +} + +async function setStepTrack(page: import('@playwright/test').Page): Promise { + const stepTrackToggle = page.locator('input[id$="step-track-toggle"]'); + await expect(stepTrackToggle).toBeVisible({ timeout: 5000 }); + if (!(await stepTrackToggle.isChecked())) { + await stepTrackToggle.click(); + } + await expect(stepTrackToggle).toBeChecked(); + await page.waitForTimeout(300); +} + +/** Open the Working Document via the sidebar icon, assert the card content, close it. */ +async function verifyWorkingDocument(page: import('@playwright/test').Page): Promise { + const docIcon = page.locator('.working-doc-icon'); + await expect(docIcon).toBeVisible({ timeout: 5000 }); + await docIcon.click(); + + const docBox = page.locator('#draggable-html-box-working-document'); + await expect(docBox).toBeVisible({ timeout: 5000 }); + + // All 10 documentLine quizzes passed by this point + await expect(docBox).toContainText('10 entries'); + await expect(docBox).toContainText('Beacon IF = LO − RF = 5250 − 4165 = 1085 MHz'); + await expect(docBox).toContainText('Watch Out'); + await expect(docBox).toContainText('NOT the TIDEMARK 7000'); + + // Close it so it cannot block the remaining quiz clicks + const closeBtn = docBox.locator('.draggable-box__close-btn, [id$="-close"]').first(); + if (await closeBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await closeBtn.click(); + await page.waitForTimeout(300); + } +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario19Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'repoint-program-track': + await repointProgramTrack(page, objective.satelliteNoradId!); + break; + + case 'configure-speca': + await configureSpeca(page, objective.centerFrequencyMhz!); + break; + + case 'set-step-track': + await setStepTrack(page); + break; + + case 'verify-working-doc': + await verifyWorkingDocument(page); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 19 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario19'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_19_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // The AURORA repoint slews ~30° of azimuth + if (objective.type === 'repoint-program-track') { + test.setTimeout(240000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario20-full-completion.spec.ts b/e2e/specs/scenario20-full-completion.spec.ts new file mode 100644 index 00000000..1141c164 --- /dev/null +++ b/e2e/specs/scenario20-full-completion.spec.ts @@ -0,0 +1,368 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 20 - "Dual Outage": Concurrent Site Loss, Prioritized Recovery. + * + * VT-01 ices under a severe winter storm (heater inherited OFF) while ME-02's + * HPA sits overdriven at 1 dB back-off with a thermal alarm. The spec flies + * the taught triage order: read VT -> heater ON -> read ME -> disable HPA -> + * restore back-off -> re-enable -> verify VT melt -> customer comms + + * adversarial rule-out + log. + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'toggle-switch' + | 'configure-hpa-backoff' + | 'wait-ice-melt' + | 'auto'; + +interface Scenario20Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + switchId?: string; + switchState?: boolean; + hpaBackoff?: number; + autoWaitSeconds?: number; +} + +const SCENARIO_20_OBJECTIVES: Scenario20Objective[] = [ + { + id: 'review-mission-brief', + title: 'Review the Incident Brief', + type: 'quiz', + correctAnswer: + 'Read both boards before fixing either - triage is a decision about order, and order needs the whole picture', + }, + + // PHASE 1: VERMONT - START THE SLOW RECOVERY + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'vt-read-board-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'vt-read-board', + title: 'Read the Vermont Board', + type: 'quiz', + correctAnswer: + 'The feed heater is OFF - it should have been running before the front arrived; ice is the consequence, the cold heater is the fault', + }, + { + id: 'vt-enable-heater-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'vt-enable-heater', + title: 'Start the Slow Recovery', + type: 'toggle-switch', + switchId: 'heater-switch', + switchState: true, + }, + + // PHASE 2: MAINE - KILL THE DANGEROUS FAULT + { + id: 'select-maine-station', + title: 'Open ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + { + id: 'me-read-board-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'me-read-board', + title: 'Read the Maine Board', + type: 'quiz', + correctAnswer: + 'One fault, two symptoms: back-off at 1 dB drives the amplifier near saturation - IMD rises (overdrive alarm) and the output stage dissipates harder (thermal alarm). Fix the back-off and both clear', + }, + { + id: 'coincidence-quiz', + title: 'Coincidence or Attack?', + type: 'quiz', + correctAnswer: + 'Hold it open and collect the rule-out evidence as you work: independent causes that each fully explain their own site, no unexplained RF on either spectrum. Answer it with evidence after the fixes, not with a shrug before them', + }, + { + id: 'triage-order-quiz', + title: 'Defend the Order', + type: 'quiz', + correctAnswer: + 'VT\'s recovery is slow but starts with one switch - starting it first costs ME nothing. ME\'s fault is actively dangerous (spectrum pollution + amplifier stress) and deterministic to fix, so it gets full attention immediately after', + }, + { + id: 'me-tx-tab', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'me-disable-hpa', + title: 'Take the Dirty Uplink Down', + type: 'toggle-switch', + switchId: 'hpa-enable', + switchState: false, + }, + { + id: 'me-restore-backoff', + title: 'Restore the Back-off', + type: 'configure-hpa-backoff', + hpaBackoff: 10, + }, + { + id: 'me-reenable-hpa', + title: 'Bring Maine Back Clean', + type: 'toggle-switch', + switchId: 'hpa-enable', + switchState: true, + }, + { + id: 'me-verify-quiz', + title: 'Confirm Both Symptoms Cleared', + type: 'quiz', + correctAnswer: + 'It clears on its own - output power dropped ~9 dB, so the output stage dissipates a fraction of the heat; the temperature falls with the dissipation that caused it', + }, + + // PHASE 3: VERIFY VERMONT + { + id: 'return-to-vermont', + title: 'Back to Vermont', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'vt-verify-recovery', + title: 'Verify the Melt', + type: 'wait-ice-melt', + }, + { + id: 'storm-steady-state-quiz', + title: 'Steady State in the Storm', + type: 'quiz', + correctAnswer: + 'Nothing new - the heater holds ice at bay as fast as it forms; the steady state is heater ON plus periodic margin checks until the front clears', + }, + + // PHASE 4: CUSTOMER, EVIDENCE, LOG + { + id: 'james-comms-quiz', + title: 'Call James Back', + type: 'quiz', + correctAnswer: + 'Both trunks restored: Vermont was storm icing (heater now running, holding), Maine was an amplifier config fault (corrected, verified). Causes independent and fully explained - the simultaneity was coincidence, and here is why we are confident.', + }, + { + id: 'adversarial-ruleout-quiz', + title: 'Close the Question Honestly', + type: 'quiz', + correctAnswer: + 'VT degradation tracked the storm exactly (radar + precip sensor agree, heater-on fixed it); ME fault was a config value with mundane history; no unexplained signals on either spectrum; both recoveries behaved as their diagnoses predicted', + }, + { + id: 'log-dual-outage', + title: 'Log the Dual Recovery', + type: 'quiz', + correctAnswer: + 'Concurrent site degradation 0712: VT-01 feed icing (heater off ahead of front - corrected 0716, melt verified) | ME-02 HPA back-off drift to 1 dB (output disabled, 10 dB restored, re-enabled clean by 0734). Causes independent - rule-out documented. Customers notified. Heater discipline flagged for shift-change checklist.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +async function toggleSwitch( + page: import('@playwright/test').Page, + switchId: string, + desiredState: boolean +): Promise { + let switchEl = page.locator(`#${switchId}`); + if ((await switchEl.count()) === 0) { + switchEl = page.locator(`[id$="${switchId}"]`); + } + await expect(switchEl.first()).toBeVisible({ timeout: 5000 }); + // Let the adapter's throttled DOM sync land before reading + await page.waitForTimeout(1200); + const isChecked = await switchEl.first().isChecked(); + if (isChecked !== desiredState) { + await switchEl.first().click(); + } + if (desiredState) { + await expect(switchEl.first()).toBeChecked(); + } else { + await expect(switchEl.first()).not.toBeChecked(); + } + await page.waitForTimeout(300); +} + +async function configureHpaBackoff( + page: import('@playwright/test').Page, + backoff: number +): Promise { + const backoffInput = page.locator('#hpa-backoff'); + await expect(backoffInput).toBeVisible({ timeout: 5000 }); + await backoffInput.fill(backoff.toString()); + await backoffInput.press('Tab'); + await page.waitForTimeout(150); + + const applyBtn = page.locator('#hpa-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + await page.waitForTimeout(400); +} + +/** Wait for VT-01's feed ice to melt below the objective threshold. */ +async function waitForIceMelt(page: import('@playwright/test').Page): Promise { + await page.waitForFunction( + () => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { iceAccumulation_dB?: number } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + const ice = gs?.antennas?.[0]?.state?.iceAccumulation_dB ?? 99; + return ice < 2; + }, + undefined, + { timeout: 420000 } + ); + // Give the objectives manager a beat to evaluate the custom condition + await page.waitForTimeout(2500); +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario20Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'toggle-switch': + await toggleSwitch(page, objective.switchId!, objective.switchState!); + break; + + case 'configure-hpa-backoff': + await configureHpaBackoff(page, objective.hpaBackoff!); + break; + + case 'wait-ice-melt': + await waitForIceMelt(page); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 20 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario20'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_20_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // The melt wait runs on the weather clock + if (objective.type === 'wait-ice-melt') { + test.setTimeout(480000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario21-full-completion.spec.ts b/e2e/specs/scenario21-full-completion.spec.ts new file mode 100644 index 00000000..a0a43c25 --- /dev/null +++ b/e2e/specs/scenario21-full-completion.spec.ts @@ -0,0 +1,335 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 21 - "Hostile RF": Suspected Intentional Interference. + * + * A duty-cycled jammer (InterferenceManager) is injected at the TM-2 + * transponder and relayed to ME-02. The operator characterizes the signature, + * discriminates jamming from accident, verifies the data layer is intact + * (RF denial, not intrusion), applies a notch filter, confirms the carrier + * survives, and builds the regulator package. + * + * The intermittent jammer provides the authentic signature for the player; + * objective completion hinges on the discrimination quizzes, the data-layer + * crypto checks (ACTIVE/Valid by default), the notch filter, and a + * post-notch receiver-lock check (the carrier is ~15 dB above the jammer). + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'configure-speca' + | 'configure-notch' + | 'auto'; + +interface Scenario21Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + specaConfig?: { centerFrequencyMhz: number; spanMhz: number }; + notchConfig?: { centerFrequency: number; bandwidth: number; depth: number; notchIndex: number }; + autoWaitSeconds?: number; +} + +const SCENARIO_21_OBJECTIVES: Scenario21Objective[] = [ + { + id: 'review-mission-brief', + title: 'Review the Interference Flag', + type: 'quiz', + correctAnswer: + 'As an interference incident: characterize, discriminate, mitigate, document - and verify the data layer separately. The same evidence resolves an accident or builds the case for deliberate interference', + }, + { + id: 'select-maine-station', + title: 'Open ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + + // PHASE 1: CHARACTERIZE + { + id: 'open-spectrum-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'open-spectrum', + title: 'Observe the Interference', + type: 'configure-speca', + specaConfig: { centerFrequencyMhz: 1458, spanMhz: 40 }, + }, + { + id: 'duty-cycle-quiz', + title: 'Characterize the Duty Cycle', + type: 'quiz', + correctAnswer: + 'Against accident, toward deliberation: natural and accidental sources are continuous or random; a clean repeating on/off cadence on the scale of a minute implies a hand on a switch or an automated transmitter, not weather or a stuck oscillator', + }, + { + id: 'transponder-vs-local-quiz', + title: 'Transponder or Local?', + type: 'quiz', + correctAnswer: + 'It is in the UPLINK - the satellite is relaying it to every receiving station, so the source is transmitting at the bird, not a local terrestrial source at Maine. That rules out radar/5G/our own equipment and points at a misaligned or deliberate uplink', + }, + { + id: 'signature-shape-quiz', + title: 'Signature vs the Database', + type: 'quiz', + correctAnswer: + 'It matches nothing in the coordination database: cross-pol leakage would mirror a known neighbor\'s carrier, an errant uplink would look like a modulated carrier at a coordinated slot - broadband noise shaped to our carrier inside our passband fits neither', + }, + + // PHASE 2: DENIAL VS INTRUSION + { + id: 'denial-vs-intrusion-quiz', + title: 'Denial or Intrusion?', + type: 'quiz', + correctAnswer: + 'It is a DENIAL attack on the RF layer - it degrades or blocks the signal but does not touch the data, crypto, or keys. The distinction matters because an intrusion (data-layer compromise) has a different escalation path and you must verify which you are facing', + }, + { + id: 'verify-data-layer', + title: 'Verify the Data Layer', + type: 'auto', + autoWaitSeconds: 4, + }, + { + id: 'data-layer-meaning-quiz', + title: 'What Intact Crypto Proves', + type: 'quiz', + correctAnswer: + 'Confirms RF-layer denial only: the link is being degraded, not breached. If crypto state changed or keys went invalid/mismatched mid-incident, it would become a security incident with a different escalation (Dana then security officer; nothing holding keys gets power-cycled)', + }, + + // PHASE 3: MITIGATION + { + id: 'mitigation-choice-quiz', + title: 'Choose Mitigation', + type: 'quiz', + correctAnswer: + 'Notch filter on the interferer band - excise its energy at the cost of a little SNR in that slice; reversible, customer-preserving, and it does nothing to neighbors', + }, + { + id: 'configure-notch', + title: 'Apply the Notch Filter', + type: 'configure-notch', + notchConfig: { centerFrequency: 1470, bandwidth: 8, depth: 30, notchIndex: 0 }, + }, + { + id: 'verify-carrier-survives', + title: 'Confirm the Customer Survives', + type: 'auto', + autoWaitSeconds: 5, + }, + { + id: 'no-burn-through-quiz', + title: 'Why Not Fight Back', + type: 'quiz', + correctAnswer: + 'You cannot out-shout an uplink jammer from the ground without violating coordination and degrading every neighbor on the transponder - a power war escalates the incident, harms bystanders, and still loses. Receive-side mitigation plus regulatory action is the path', + }, + + // PHASE 4: COORDINATE AND REPORT + { + id: 'regulator-package-quiz', + title: 'Build the Regulator Package', + type: 'quiz', + correctAnswer: + 'Victim (satellite/transponder/pol/center freq/BW), interferer (center freq, BW, measured duty cycle, first/last seen), spectrum captures at documented settings, cross-station confirmation (transponder vs local), service impact, and mitigation applied - all timestamped', + }, + { + id: 'attribution-restraint-quiz', + title: 'Conclude Carefully', + type: 'quiz', + correctAnswer: + '"Signature consistent with deliberate uplink interference" - a confidence-bounded characterization backed by the evidence; attribution and intent are for the regulator to determine, not the ground operator to assert', + }, + { + id: 'log-incident', + title: 'Log the Incident', + type: 'quiz', + correctAnswer: + 'TM-2 TP-1 interference: broadband noise ~1470 MHz IF, ~90s/60s duty cycle, confirmed uplink (seen VT-01 + Halifax). Data layer intact (crypto ACTIVE, keys Valid) - RF denial, not intrusion. Notch applied, carrier holding. Signature consistent with deliberate interference - regulator package filed. Monitoring; escalate on any data-layer change.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +async function configureSpeca( + page: import('@playwright/test').Page, + config: { centerFrequencyMhz: number; spanMhz: number } +): Promise { + const centerFreqInput = page.locator('#sa-center-freq'); + await expect(centerFreqInput).toBeVisible({ timeout: 5000 }); + await centerFreqInput.fill(config.centerFrequencyMhz.toString()); + await centerFreqInput.press('Tab'); + await page.waitForTimeout(150); + + const spanInput = page.locator('#sa-span'); + if (await spanInput.isVisible({ timeout: 1000 }).catch(() => false)) { + await spanInput.fill(config.spanMhz.toString()); + await spanInput.press('Tab'); + await page.waitForTimeout(150); + } + await page.waitForTimeout(300); +} + +async function configureNotchFilter( + page: import('@playwright/test').Page, + config: { centerFrequency: number; bandwidth: number; depth: number; notchIndex: number } +): Promise { + const prefix = `notch-${config.notchIndex}`; + + const powerSwitch = page.locator('#notch-power'); + await expect(powerSwitch).toBeVisible({ timeout: 5000 }); + if (!(await powerSwitch.isChecked())) { + await powerSwitch.click(); + await expect(powerSwitch).toBeChecked(); + await page.waitForTimeout(300); + } + + const enableSwitch = page.locator(`#${prefix}-enabled`); + await expect(enableSwitch).toBeVisible(); + if (!(await enableSwitch.isChecked())) { + await enableSwitch.click(); + await expect(enableSwitch).toBeChecked(); + await page.waitForTimeout(200); + } + + const freqInput = page.locator(`#${prefix}-freq`); + await expect(freqInput).toBeVisible(); + await freqInput.fill(config.centerFrequency.toString()); + await freqInput.press('Tab'); + await page.waitForTimeout(100); + + const bwInput = page.locator(`#${prefix}-bw`); + await expect(bwInput).toBeVisible(); + await bwInput.fill(config.bandwidth.toString()); + await bwInput.press('Tab'); + await page.waitForTimeout(100); + + const depthInput = page.locator(`#${prefix}-depth`); + await expect(depthInput).toBeVisible(); + await depthInput.fill(config.depth.toString()); + await depthInput.press('Tab'); + await page.waitForTimeout(100); + + // Apply if there's an apply button + const applyBtn = page.locator('#notch-apply-btn'); + if (await applyBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await applyBtn.click(); + await page.waitForTimeout(300); + } + await page.waitForTimeout(500); +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario21Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'ME-02'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'configure-speca': + await configureSpeca(page, objective.specaConfig!); + break; + + case 'configure-notch': + await configureNotchFilter(page, objective.notchConfig!); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 21 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario21'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_21_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario22-full-completion.spec.ts b/e2e/specs/scenario22-full-completion.spec.ts new file mode 100644 index 00000000..1153a6ea --- /dev/null +++ b/e2e/specs/scenario22-full-completion.spec.ts @@ -0,0 +1,356 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 22 - "End-of-Life Planning": AURORA-7 Sunset Recommendation. + * + * Second Working Document scenario - this one produces an executive impact + * report. The operator runs a final AURORA-7 data pass (acquire, step-track, + * measure beacon), then the analysis quizzes build the report. The spec + * verifies the report content in the Working Document before the final quizzes + * (the Mission Complete modal overlays the sidebar afterward). + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'repoint-program-track' + | 'set-step-track' + | 'configure-speca' + | 'verify-working-doc' + | 'auto'; + +interface Scenario22Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + satelliteNoradId?: string; + centerFrequencyMhz?: number; + autoWaitSeconds?: number; +} + +const SCENARIO_22_OBJECTIVES: Scenario22Objective[] = [ + { + id: 'review-mission-brief', + title: 'Review the Tasking', + type: 'quiz', + correctAnswer: + 'A defensible recommendation grounded in measured data and an honest trend, with assumptions labeled - not a single date with false precision', + }, + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + + // PHASE 1: FINAL DATA RUN + { + id: 'acquire-aurora-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'acquire-aurora', + title: 'Acquire AURORA-7', + type: 'repoint-program-track', + satelliteNoradId: '28899', + }, + { + id: 'engage-step-track', + title: 'Step-Track for the Weak Beacon', + type: 'set-step-track', + }, + { + id: 'measure-beacon-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'measure-beacon', + title: 'Measure Today\'s Beacon', + type: 'configure-speca', + centerFrequencyMhz: 1085, + }, + { + id: 'record-data-point-quiz', + title: 'Report: Today\'s Measurement', + type: 'quiz', + correctAnswer: + 'Beacon at -4.0 dB relative to the 24-month reference; step-track held lock at this level; carrier C/N still above demod threshold - measured, not estimated', + }, + + // PHASE 2: TREND ANALYSIS + { + id: 'trend-slope-quiz', + title: 'Report: the Trend', + type: 'quiz', + correctAnswer: + 'Accelerating - the last 6 months lost ~1.6 dB versus ~1.1 dB in the prior 6; a steepening curve, not a straight line, so naive linear extrapolation understates the near-term decline', + }, + { + id: 'binding-constraint-quiz', + title: 'Report: What Fails First', + type: 'quiz', + correctAnswer: + 'The beacon getting too weak to step-track. AURORA-7 is inclined, so without a trackable beacon every pass becomes manual figure-8 chasing and service quality collapses - the cliff is tracking, not transponder death', + }, + { + id: 'marcus-corroboration-quiz', + title: 'Report: Vehicle Corroboration', + type: 'quiz', + correctAnswer: + 'It raises confidence and explains the mechanism: ground-measured beacon decline and spacecraft-reported power loss are the same story from two independent vantage points - the trend is real, not an artifact of our station', + }, + + // PHASE 3: RECOMMENDATION + { + id: 'sunset-window-quiz', + title: 'Report: the Recommendation', + type: 'quiz', + correctAnswer: + 'Begin customer migration now; target sunset in roughly one to two quarters with a hard review at each monthly data point, and a firm decision trigger when the beacon crosses the step-track floor. A window with tripwires, not a date', + }, + { + id: 'assumptions-quiz', + title: 'Report: Label the Assumptions', + type: 'quiz', + correctAnswer: + 'The decline continues or steepens (no recovery), no single-event failure intervenes, and tracking - not the payload - is the binding constraint. If any breaks, the window changes', + }, + { + id: 'false-precision-quiz', + title: 'Confidence Discipline', + type: 'quiz', + correctAnswer: + 'Explicitly: today\'s -4.0 dB and the historical points are measured; the sunset window is a projection from those points under stated assumptions. Label each so the board knows which is which', + }, + + // Verify the report BEFORE the final quizzes (modal overlays the sidebar after) + { + id: 'verify-working-doc', + title: 'Working Document: report assembled', + type: 'verify-working-doc', + }, + + // PHASE 4: REVIEW AND DELIVER + { + id: 'review-report', + title: 'Review the Assessment', + type: 'quiz', + correctAnswer: + 'It has data, a trend, the binding-constraint risk, a windowed recommendation with tripwires, and labeled assumptions - a board member can read it, understand the basis, and defend the decision to others', + }, + { + id: 'log-delivery', + title: 'Deliver and Log', + type: 'quiz', + correctAnswer: + 'AURORA-7 EOL assessment delivered to board (Martin). Final data run complete (beacon -4.0 dB, step-track held). Trend accelerating, binding constraint = beacon trackability. Recommendation: begin migration now, sunset ~1-2 quarters, trigger at step-track floor. Assumptions stated. Measured vs projected labeled.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +async function repointProgramTrack( + page: import('@playwright/test').Page, + satelliteNoradId: string +): Promise { + const modeButton = page.locator('.btn-tracking[data-mode="program-track"]'); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); + + const satelliteSelect = page.locator('select[id$="satellite-select"]'); + await expect(satelliteSelect).toBeVisible({ timeout: 5000 }); + await satelliteSelect.selectOption({ value: satelliteNoradId }); + await page.waitForTimeout(200); + + const moveBtn = page.locator('button[id$="move-to-target-btn"]'); + await expect(moveBtn).toBeEnabled({ timeout: 5000 }); + await moveBtn.click(); + + await page.waitForFunction( + () => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { slewing?: boolean; isLocked?: boolean } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + const antennaState = gs?.antennas?.[0]?.state; + return antennaState ? antennaState.slewing === false && antennaState.isLocked === true : false; + }, + undefined, + { timeout: 180000 } + ); + await page.waitForTimeout(1500); +} + +async function setStepTrack(page: import('@playwright/test').Page): Promise { + const stepTrackToggle = page.locator('input[id$="step-track-toggle"]'); + await expect(stepTrackToggle).toBeVisible({ timeout: 5000 }); + if (!(await stepTrackToggle.isChecked())) { + await stepTrackToggle.click(); + } + await expect(stepTrackToggle).toBeChecked(); + await page.waitForTimeout(300); +} + +async function configureSpeca( + page: import('@playwright/test').Page, + centerFrequencyMhz: number +): Promise { + const centerFreqInput = page.locator('#sa-center-freq'); + await expect(centerFreqInput).toBeVisible({ timeout: 5000 }); + await centerFreqInput.fill(centerFrequencyMhz.toString()); + await centerFreqInput.press('Tab'); + await page.waitForTimeout(500); +} + +async function verifyWorkingDocument(page: import('@playwright/test').Page): Promise { + const docIcon = page.locator('.working-doc-icon'); + await expect(docIcon).toBeVisible({ timeout: 5000 }); + await docIcon.click(); + + const docBox = page.locator('#draggable-html-box-working-document'); + await expect(docBox).toBeVisible({ timeout: 5000 }); + + // 7 documentLine quizzes passed by this point + await expect(docBox).toContainText('6 entries'); + await expect(docBox).toContainText('Recommendation'); + await expect(docBox).toContainText('Binding constraint = beacon trackability'); + await expect(docBox).toContainText('Assumptions'); + + const closeBtn = docBox.locator('.draggable-box__close-btn, [id$="-close"]').first(); + if (await closeBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await closeBtn.click(); + await page.waitForTimeout(300); + } +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario22Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'repoint-program-track': + await repointProgramTrack(page, objective.satelliteNoradId!); + break; + + case 'set-step-track': + await setStepTrack(page); + break; + + case 'configure-speca': + await configureSpeca(page, objective.centerFrequencyMhz!); + break; + + case 'verify-working-doc': + await verifyWorkingDocument(page); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 22 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario22'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_22_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + if (objective.type === 'repoint-program-track') { + test.setTimeout(240000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario23-full-completion.spec.ts b/e2e/specs/scenario23-full-completion.spec.ts new file mode 100644 index 00000000..8c841468 --- /dev/null +++ b/e2e/specs/scenario23-full-completion.spec.ts @@ -0,0 +1,304 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 23 - "Emergency Bypass": Manual Operations During Automation Failure. + * + * The ACU automation controller is faulted (isAcuAutomationFaulted) - program-track, + * step-track, and move-to-target are disabled in the UI. The dish is parked on + * TIDEMARK-1. The operator commits to MANUAL, proves pointing on the beacon + * (the only trustworthy source), holds the link, and coordinates the repair. + * + * Spec also asserts the ACU-fault UI gating: program-track button disabled, + * MANUAL button still usable. + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'set-manual-mode' + | 'configure-speca' + | 'verify-acu-gating' + | 'auto'; + +interface Scenario23Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + centerFrequencyMhz?: number; + autoWaitSeconds?: number; +} + +const SCENARIO_23_OBJECTIVES: Scenario23Objective[] = [ + { + id: 'review-mission-brief', + title: 'Review the Fault', + type: 'quiz', + correctAnswer: + 'Broken: program-track, step-track, move-to-target, lock logic (the automation brain). Working: servos, manual mode, and the RF chain - the dish can still move, you just have to fly it', + }, + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + + // PHASE 1: ASSESS + { + id: 'confirm-fault-dashboard-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'confirm-fault-dashboard', + title: 'Confirm the Automation Fault', + type: 'quiz', + correctAnswer: + 'Current position (Az 161.8 / El 34.2 on TM-1) and that beacon + carrier are still locked - the dish is ON the bird; whatever you do next must not lose that', + }, + { + id: 'what-automation-did-quiz', + title: 'What the Automation Was Doing', + type: 'quiz', + correctAnswer: + 'Compute the pointing solution (from the prediction sheet), drive the axes manually, and judge lock from the spectrum - the ACU lock indicator is part of the dead automation and cannot be trusted', + }, + { + id: 'no-reboot-quiz', + title: 'Why Not Reboot It', + type: 'quiz', + correctAnswer: + 'A reboot can return with stale/defaulted axis calibration and destroys the crash state IT needs for root cause - bypass keeps the link up AND preserves the evidence; IT cycles it on their schedule', + }, + + // PHASE 2: COMMIT TO MANUAL (+ UI gating assertion) + { + id: 'switch-to-manual-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'verify-acu-gating', + title: 'ACU gating: program-track disabled, manual usable', + type: 'verify-acu-gating', + }, + { + id: 'switch-to-manual', + title: 'Switch to Manual Control', + type: 'set-manual-mode', + }, + { + id: 'manual-deliberate-quiz', + title: 'Why Deliberate Manual', + type: 'quiz', + correctAnswer: + 'In program-track the panel implies an automation is flying the dish when none is - manual makes the truth explicit: YOU are the controller, the displays mean what they say, and there is no phantom loop to fight', + }, + + // PHASE 3: PROVE THE LINK + { + id: 'prove-beacon-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'prove-beacon', + title: 'Prove Pointing on the Beacon', + type: 'configure-speca', + centerFrequencyMhz: 1074.5, + }, + { + id: 'prove-carrier', + title: 'Prove the Carrier', + type: 'auto', + autoWaitSeconds: 5, + }, + { + id: 'instruments-not-feel-quiz', + title: 'Fly Instruments, Not Feel', + type: 'quiz', + correctAnswer: + 'The beacon at 1074.5 MHz on the spectrum and receiver lock - the RF truth, independent of the dead ACU automation. Never the ACU lock indicator', + }, + + // PHASE 4: HOLD AND COORDINATE + { + id: 'sustained-manual-hold', + title: 'Hold Under Manual Control', + type: 'auto', + autoWaitSeconds: 40, + }, + { + id: 'geo-feasibility-quiz', + title: 'Why Manual Holds (This Time)', + type: 'quiz', + correctAnswer: + 'TIDEMARK-1 is a well-behaved GEO bird - it barely moves over hours, so a fixed manual point holds. It would NOT work for an inclined bird like AURORA-7, whose figure-8 needs the step-track this fault disabled', + }, + { + id: 'it-coordination-quiz', + title: 'Coordinate the Repair', + type: 'quiz', + correctAnswer: + 'Confirm the link is stable on manual first, agree a window, expect to re-establish program-track and re-verify pointing AFTER recovery - and be ready to fall back to manual if the restart misbehaves', + }, + { + id: 'log-bypass', + title: 'Log the Bypass', + type: 'quiz', + correctAnswer: + 'ACU automation fault 1358 (IT NOC-2026-2231). Bypassed to manual 1404 - TM-1 held on prediction-sheet pointing, beacon + carrier verified on spectrum (ACU lock indicator NOT trusted, driven by failed processor). Link nominal under manual. Controller NOT rebooted - crash state preserved for IT. Recovery to be re-verified on RF post-restart.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** Select MANUAL via the tracking-mode button. */ +async function setManualMode(page: import('@playwright/test').Page): Promise { + const manualBtn = page.locator('.btn-tracking[data-mode="manual"]'); + await expect(manualBtn).toBeVisible({ timeout: 5000 }); + await manualBtn.click(); + await page.waitForTimeout(500); +} + +/** Assert the ACU-fault UI gating. */ +async function verifyAcuGating(page: import('@playwright/test').Page): Promise { + const programBtn = page.locator('.btn-tracking[data-mode="program-track"]'); + await expect(programBtn).toBeVisible({ timeout: 5000 }); + // Let the throttled sync apply the disabled state + await page.waitForTimeout(1500); + await expect(programBtn).toBeDisabled(); + + const manualBtn = page.locator('.btn-tracking[data-mode="manual"]'); + await expect(manualBtn).toBeEnabled(); +} + +async function configureSpeca( + page: import('@playwright/test').Page, + centerFrequencyMhz: number +): Promise { + const centerFreqInput = page.locator('#sa-center-freq'); + await expect(centerFreqInput).toBeVisible({ timeout: 5000 }); + await centerFreqInput.fill(centerFrequencyMhz.toString()); + await centerFreqInput.press('Tab'); + await page.waitForTimeout(500); +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario23Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'set-manual-mode': + await setManualMode(page); + break; + + case 'verify-acu-gating': + await verifyAcuGating(page); + break; + + case 'configure-speca': + await configureSpeca(page, objective.centerFrequencyMhz!); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 23 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario23'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_23_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + if (objective.type === 'auto' && (objective.autoWaitSeconds ?? 0) >= 30) { + test.setTimeout(120000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario24-full-completion.spec.ts b/e2e/specs/scenario24-full-completion.spec.ts new file mode 100644 index 00000000..8cdc0c8b --- /dev/null +++ b/e2e/specs/scenario24-full-completion.spec.ts @@ -0,0 +1,416 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 24 - "Constellation Crisis": Campaign Capstone. + * + * Five concurrent tracks: VT-01 storm (proactive heater), ME-02 BUC thermal + * trend (de-rate), AURORA-7 customer pass (acquire + step-track), customer + + * board comms. The Working Document is the incident-command log. No new + * mechanics - this validates the orchestration of everything prior. + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'toggle-switch' + | 'configure-buc-gain' + | 'repoint-program-track' + | 'set-step-track' + | 'verify-working-doc' + | 'auto'; + +interface Scenario24Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + switchId?: string; + switchState?: boolean; + bucGain?: number; + satelliteNoradId?: string; + autoWaitSeconds?: number; +} + +const SCENARIO_24_OBJECTIVES: Scenario24Objective[] = [ + { + id: 'review-mission-brief', + title: 'Assume Incident Command', + type: 'quiz', + correctAnswer: + 'Order the board before touching anything: separate the clocks you do not control (storm ETA, pass window, board deadline) from the trends you can bend (BUC heat, customer confidence), and sequence your attention accordingly', + }, + { + id: 'sequencing-quiz', + title: 'Set the Sequence', + type: 'quiz', + correctAnswer: + 'Protect against the fixed clock first: enable VT-01 heater NOW (one switch, beats the front), then de-rate the ME-02 BUC trend, then set up the AURORA pass before its window - acting on the soonest fixed deadline first, cheap protections before expensive ones', + }, + + // TRACK 1: VT-01 STORM + { + id: 'select-vermont', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'enable-heater-proactive-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'enable-heater-proactive', + title: 'Heater Before the Front', + type: 'toggle-switch', + switchId: 'heater-switch', + switchState: true, + }, + + // TRACK 2: ME-02 BUC + { + id: 'select-maine', + title: 'Open ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + { + id: 'recognize-buc-signature-tab', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'recognize-buc-signature', + title: 'Recognize the Signature', + type: 'quiz', + correctAnswer: + 'The same over-gain thermal trend from before - the swap that never cleared procurement. Fix is the de-rate: gain back to the 23 dB operating value, which cuts the dissipation at the source', + }, + { + id: 'derate-buc', + title: 'De-Rate the BUC', + type: 'configure-buc-gain', + bucGain: 23, + }, + { + id: 'verify-buc-trending-down', + title: 'Confirm the Trend Bends', + type: 'auto', + autoWaitSeconds: 5, + }, + + // TRACK 3: AURORA-7 PASS + { + id: 'return-vermont-for-pass', + title: 'Back to VT-01 for the Pass', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'acquire-aurora-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'acquire-aurora', + title: 'Acquire AURORA-7', + type: 'repoint-program-track', + satelliteNoradId: '28899', + }, + { + id: 'step-track-weak-beacon', + title: 'Step-Track the Weak Beacon', + type: 'set-step-track', + }, + { + id: 'pass-go-nogo-quiz', + title: 'Pass Go/No-Go', + type: 'quiz', + correctAnswer: + 'GO with a caveat: step-track holding, margin thin on an end-of-life beacon; proceed with the pass but flag that AURORA is on its sunset trajectory - and deliver that BEFORE 0800, not mid-pass', + }, + + // CUSTOMER + BOARD + { + id: 'customer-escalation-quiz', + title: 'Manage the Customer Channel', + type: 'quiz', + correctAnswer: + 'Per-trunk cause/action/next-update, in thirty seconds each: TM-1 protected ahead of the storm (heater on, holding), TM-2 stabilized (BUC de-rated, no impact) - with a committed next-update time so he stops calling and you keep working', + }, + { + id: 'storm-hold-check', + title: 'Storm Holding', + type: 'quiz', + correctAnswer: + 'The feed stays clear as fast as ice tries to form - no accumulation, link holding; the proactive heater turned a potential outage into a non-event you only have to monitor', + }, + { + id: 'board-note-quiz', + title: 'The Board Note', + type: 'quiz', + correctAnswer: + 'Posture, exposure, action: both stations stable through a concurrent storm + thermal-trend morning, no customer outage; AURORA pass delivered on a sunsetting beacon (reinforces the migration recommendation); residual risk = the pending ME-02 BUC swap. One paragraph, board-level, no jargon', + }, + + // Verify command log BEFORE the final quizzes (modal overlays sidebar after) + { + id: 'verify-working-doc', + title: 'Incident-command log assembled', + type: 'verify-working-doc', + }, + + // CLOSE + { + id: 'review-command-log', + title: 'Review the Command Log', + type: 'quiz', + correctAnswer: + 'It shows the ordered board, the sequence and why, each track\'s action and outcome, the customer and board comms, and the residual risk - someone could pick up your shift cold and know exactly what happened and what is still open', + }, + { + id: 'log-crisis-closed', + title: 'Close the Incident', + type: 'quiz', + correctAnswer: + 'Constellation crisis worked under single-operator incident command: VT-01 storm protected (proactive heater, no ice), ME-02 BUC thermal trend de-rated (no outage), AURORA SeaLink pass delivered on EOL beacon, customers briefed per-trunk, board note filed. Zero customer outage across five concurrent tracks. Residual: ME-02 BUC swap pending. IC closed.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +async function toggleSwitch( + page: import('@playwright/test').Page, + switchId: string, + desiredState: boolean +): Promise { + let switchEl = page.locator(`#${switchId}`); + if ((await switchEl.count()) === 0) { + switchEl = page.locator(`[id$="${switchId}"]`); + } + await expect(switchEl.first()).toBeVisible({ timeout: 5000 }); + await page.waitForTimeout(1200); + const isChecked = await switchEl.first().isChecked(); + if (isChecked !== desiredState) { + await switchEl.first().click(); + } + if (desiredState) { + await expect(switchEl.first()).toBeChecked(); + } else { + await expect(switchEl.first()).not.toBeChecked(); + } + await page.waitForTimeout(300); +} + +async function configureBucGain( + page: import('@playwright/test').Page, + gain: number +): Promise { + const gainInput = page.locator('#buc-gain'); + await expect(gainInput).toBeVisible({ timeout: 5000 }); + await gainInput.fill(gain.toString()); + await gainInput.press('Tab'); + await page.waitForTimeout(100); + + const applyBtn = page.locator('#buc-apply-btn'); + await expect(applyBtn).toBeVisible({ timeout: 5000 }); + await applyBtn.click(); + await page.waitForTimeout(400); +} + +async function repointProgramTrack( + page: import('@playwright/test').Page, + satelliteNoradId: string +): Promise { + const modeButton = page.locator('.btn-tracking[data-mode="program-track"]'); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); + + const satelliteSelect = page.locator('select[id$="satellite-select"]'); + await expect(satelliteSelect).toBeVisible({ timeout: 5000 }); + await satelliteSelect.selectOption({ value: satelliteNoradId }); + await page.waitForTimeout(200); + + const moveBtn = page.locator('button[id$="move-to-target-btn"]'); + await expect(moveBtn).toBeEnabled({ timeout: 5000 }); + await moveBtn.click(); + + await page.waitForFunction( + () => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { slewing?: boolean; isLocked?: boolean } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + const antennaState = gs?.antennas?.[0]?.state; + return antennaState ? antennaState.slewing === false && antennaState.isLocked === true : false; + }, + undefined, + { timeout: 180000 } + ); + await page.waitForTimeout(1500); +} + +async function setStepTrack(page: import('@playwright/test').Page): Promise { + const stepTrackToggle = page.locator('input[id$="step-track-toggle"]'); + await expect(stepTrackToggle).toBeVisible({ timeout: 5000 }); + if (!(await stepTrackToggle.isChecked())) { + await stepTrackToggle.click(); + } + await expect(stepTrackToggle).toBeChecked(); + await page.waitForTimeout(300); +} + +async function verifyWorkingDocument(page: import('@playwright/test').Page): Promise { + const docIcon = page.locator('.working-doc-icon'); + await expect(docIcon).toBeVisible({ timeout: 5000 }); + await docIcon.click(); + + const docBox = page.locator('#draggable-html-box-working-document'); + await expect(docBox).toBeVisible({ timeout: 5000 }); + + // 8 documentLine quizzes passed by this point + await expect(docBox).toContainText('7 entries'); + await expect(docBox).toContainText('Command'); + await expect(docBox).toContainText('Board Note'); + await expect(docBox).toContainText('Residual risk'); + + const closeBtn = docBox.locator('.draggable-box__close-btn, [id$="-close"]').first(); + if (await closeBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await closeBtn.click(); + await page.waitForTimeout(300); + } +} + +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario24Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'toggle-switch': + await toggleSwitch(page, objective.switchId!, objective.switchState!); + break; + + case 'configure-buc-gain': + await configureBucGain(page, objective.bucGain!); + break; + + case 'repoint-program-track': + await repointProgramTrack(page, objective.satelliteNoradId!); + break; + + case 'set-step-track': + await setStepTrack(page); + break; + + case 'verify-working-doc': + await verifyWorkingDocument(page); + break; + + case 'auto': + await page.waitForTimeout((objective.autoWaitSeconds ?? 5) * 1000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 24 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + await missionControlPage.gotoScenario('nats', 'nats-scenario24'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(90000); + }); + + for (const objective of SCENARIO_24_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + if (objective.type === 'repoint-program-track') { + test.setTimeout(240000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/e2e/specs/scenario9-full-completion.spec.ts b/e2e/specs/scenario9-full-completion.spec.ts new file mode 100644 index 00000000..018f6538 --- /dev/null +++ b/e2e/specs/scenario9-full-completion.spec.ts @@ -0,0 +1,481 @@ +import { expect, test } from '@playwright/test'; +import { MissionControlPage } from '../pages/mission-control.page'; +import { + answerQuizByText, + dismissDialogIfPresent, + waitForQuizToAppear, + waitForSimulationReady, +} from '../utils/simulation-helpers'; + +/** + * Scenario 9 - "Morning Rounds": Multi-Satellite Health Check. + * + * First qualified-operator shift. Health check rotation across three TIDEMARK + * birds: VT-01/TIDEMARK-1, ME-02/TIDEMARK-2, then a spot-check of newly + * commissioned TIDEMARK-3 via VT-01. + * + * Objective types: + * - 'quiz': Status-check quiz (Character.SYSTEM) + * - 'select-station': Asset tree station selection (VT-01 or ME-02) + * - 'click-tab': Tab navigation + * - 'auto': Auto-satisfied by simulation state (no user action) + * - 'set-tracking-mode': Antenna tracking mode + satellite selection + * - 'configure-speca': Spectrum analyzer center frequency tuning + */ +type ObjectiveType = + | 'quiz' + | 'select-station' + | 'click-tab' + | 'auto' + | 'set-tracking-mode' + | 'configure-speca'; + +interface Scenario9Objective { + id: string; + title: string; + type: ObjectiveType; + correctAnswer?: string; + tabId?: string; + stationId?: string; + trackingMode?: string; + satelliteNoradId?: string; // For set-tracking-mode in program-track + waitForAntennaPosition?: boolean; + specaConfig?: { + centerFrequency: number; // MHz + span?: number; // kHz (optional - skipped if undefined) + }; +} + +const SCENARIO_9_OBJECTIVES: Scenario9Objective[] = [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + title: 'Review Shift Brief', + type: 'quiz', + correctAnswer: 'Yes, brief reviewed. Starting rounds.', + }, + + // ============================================================ + // PHASE 1: VT-01 / TIDEMARK-1 + // ============================================================ + { + id: 'select-vermont-station', + title: 'Open VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'vt-dashboard-check-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'vt-dashboard-check', + title: 'VT-01 Dashboard Sweep', + type: 'quiz', + correctAnswer: 'No active alarms - all systems nominal', + }, + { + id: 'vt-gpsdo-tab', + title: 'Open GPS Timing', + type: 'click-tab', + tabId: 'gps-timing', + }, + { + id: 'vt-gpsdo-spot-check', + title: 'VT-01 GPSDO Spot Check', + type: 'quiz', + correctAnswer: 'Stable 10 MHz reference available to all downstream RF equipment', + }, + { + id: 'vt-rx-beacon-check', + title: 'VT-01 RX Chain Spot Check', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'vt-tx-hpa-check-tab', + title: 'Open TX Chain', + type: 'click-tab', + tabId: 'tx-chain', + }, + { + id: 'vt-tx-hpa-check', + title: 'VT-01 TX Chain Spot Check', + type: 'quiz', + correctAnswer: 'Standard operating margin - reduces stress on the amplifier', + }, + + // ============================================================ + // PHASE 2: ME-02 / TIDEMARK-2 + // ============================================================ + { + id: 'select-maine-station', + title: 'Open ME-02', + type: 'select-station', + stationId: 'ME-02', + }, + { + id: 'me-dashboard-check-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'me-dashboard-check', + title: 'ME-02 Dashboard Sweep', + type: 'quiz', + correctAnswer: 'No active alarms - station nominal', + }, + { + id: 'me-rx-beacon-check', + title: 'ME-02 TIDEMARK-2 Beacon Check', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'me-verify-tracking-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'me-verify-tracking', + title: 'ME-02 Antenna Tracking Check', + type: 'quiz', + correctAnswer: + 'GEO satellite holding station - ephemeris is accurate enough; no need to hunt the beacon', + }, + { + id: 'me-tx-payload-spot', + title: 'ME-02 Customer Traffic Indicator', + type: 'quiz', + correctAnswer: + 'Frame sync locked + CRC valid + FEC engaged (Reed-Solomon active, no uncorrectables)', + }, + + // ============================================================ + // PHASE 3: TIDEMARK-3 VERIFICATION + // ============================================================ + { + id: 'switch-to-vermont', + title: 'Return to VT-01', + type: 'select-station', + stationId: 'VT-01', + }, + { + id: 'repoint-acu-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'repoint-to-tidemark3', + title: 'Repoint VT-01 to TIDEMARK-3', + type: 'set-tracking-mode', + trackingMode: 'program-track', + satelliteNoradId: '61527', + waitForAntennaPosition: true, + }, + { + id: 'tidemark3-rx-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'tidemark3-beacon-acquire', + title: 'TIDEMARK-3 Beacon Verification', + type: 'configure-speca', + specaConfig: { + centerFrequency: 1078, // MHz + }, + }, + { + id: 'tidemark3-beacon-quality', + title: 'Interpret TIDEMARK-3 Beacon', + type: 'quiz', + correctAnswer: + 'Antenna pointing is correct AND LNB LO is set correctly (5250 - 4172 = 1078)', + }, + { + id: 'return-acu-tab', + title: 'Open ACU Control', + type: 'click-tab', + tabId: 'acu-control', + }, + { + id: 'return-to-tidemark1-antenna', + title: 'Repoint Back to TIDEMARK-1', + type: 'set-tracking-mode', + trackingMode: 'program-track', + satelliteNoradId: '61525', + waitForAntennaPosition: true, + }, + { + id: 'return-rx-tab', + title: 'Open RX Analysis', + type: 'click-tab', + tabId: 'rx-analysis', + }, + { + id: 'return-to-tidemark1', + title: 'Retune Spectrum Analyzer to TM-1', + type: 'configure-speca', + specaConfig: { + centerFrequency: 1074.5, // MHz + }, + }, + + // ============================================================ + // SHIFT WRAP + // ============================================================ + { + id: 'final-alarm-sweep-tab', + title: 'Open Dashboard', + type: 'click-tab', + tabId: 'dashboard', + }, + { + id: 'final-alarm-sweep', + title: 'Final Alarm Sweep', + type: 'quiz', + correctAnswer: + 'TIDEMARK-1 healthy, TIDEMARK-2 healthy, TIDEMARK-3 beacon verified - all three nominal', + }, + { + id: 'log-shift-summary', + title: 'Log Shift Summary', + type: 'quiz', + correctAnswer: + '0700 - Morning rounds complete. VT-01/TM-1, ME-02/TM-2, TM-3 beacon verified via VT-01 spot-check. No anomalies.', + }, +]; + +// ============================================================ +// Helper Functions +// ============================================================ + +/** + * Set the antenna tracking mode by clicking the appropriate button. + * ACU control tab must be active before calling this. + */ +async function setTrackingMode( + page: import('@playwright/test').Page, + trackingMode: string +): Promise { + const modeButton = page.locator(`.btn-tracking[data-mode="${trackingMode}"]`); + await expect(modeButton).toBeVisible({ timeout: 5000 }); + await modeButton.click(); + await page.waitForTimeout(300); + + if (trackingMode === 'maintenance' || trackingMode === 'stow') { + const applyBtn = page.locator('button[id$="apply-changes-btn"]'); + await expect(applyBtn).toBeEnabled({ timeout: 3000 }); + await applyBtn.click(); + await page.waitForTimeout(200); + } +} + +/** + * Select a target satellite from the dropdown and click Move to Target. + * Used after setTrackingMode('program-track'). + */ +async function selectSatelliteAndMove( + page: import('@playwright/test').Page, + satelliteNoradId: string +): Promise { + const satelliteSelect = page.locator('select[id$="satellite-select"]'); + await expect(satelliteSelect).toBeVisible({ timeout: 5000 }); + await satelliteSelect.selectOption({ value: satelliteNoradId }); + await page.waitForTimeout(200); + + const moveBtn = page.locator('button[id$="move-to-target-btn"]'); + await expect(moveBtn).toBeEnabled({ timeout: 5000 }); + await moveBtn.click(); + await page.waitForTimeout(300); +} + +/** + * Wait for antenna movement to complete by watching elevation stability. + */ +async function waitForAntennaMovement( + page: import('@playwright/test').Page, + timeout = 90000 +): Promise { + const startTime = Date.now(); + let lastPosition = ''; + let stableCount = 0; + + await page.waitForTimeout(500); + + while (Date.now() - startTime < timeout) { + await page.waitForTimeout(1000); + + let elDisplay = page + .locator('.fine-adjust-control', { hasText: 'Elevation' }) + .locator('.fine-adjust-value-active'); + + if ((await elDisplay.count()) === 0) { + elDisplay = page.locator('[id*="el-fine"][id$="-value"]'); + } + + try { + const currentPosition = await elDisplay.first().textContent({ timeout: 2000 }); + + if (currentPosition === lastPosition && currentPosition !== '') { + stableCount++; + if (stableCount >= 3) { + return; + } + } else { + stableCount = 0; + lastPosition = currentPosition || ''; + } + } catch { + await page.waitForTimeout(500); + } + } + + console.warn('Antenna movement may not have completed within timeout'); +} + +/** + * Configure spectrum analyzer center frequency. Span is optional. + */ +async function configureSpectrumAnalyzer( + page: import('@playwright/test').Page, + config: { centerFrequency: number; span?: number } +): Promise { + const centerFreqInput = page.locator('#sa-center-freq'); + await expect(centerFreqInput).toBeVisible({ timeout: 5000 }); + await centerFreqInput.fill(config.centerFrequency.toString()); + await centerFreqInput.press('Tab'); + await page.waitForTimeout(100); + + if (config.span !== undefined) { + const spanInput = page.locator('#sa-span'); + await expect(spanInput).toBeVisible(); + const spanInMHz = config.span / 1000; // kHz → MHz + await spanInput.fill(spanInMHz.toString()); + await spanInput.press('Tab'); + await page.waitForTimeout(100); + } + + await page.waitForTimeout(300); +} + +/** + * Execute an objective based on its type. + */ +async function executeObjective( + page: import('@playwright/test').Page, + missionControlPage: MissionControlPage, + objective: Scenario9Objective +): Promise { + switch (objective.type) { + case 'quiz': + await waitForQuizToAppear(page); + await answerQuizByText(page, objective.correctAnswer!); + break; + + case 'select-station': + await missionControlPage.selectGroundStation(objective.stationId || 'VT-01'); + break; + + case 'click-tab': + await missionControlPage.selectTab(objective.tabId!); + break; + + case 'set-tracking-mode': + await setTrackingMode(page, objective.trackingMode!); + if (objective.trackingMode === 'program-track' && objective.satelliteNoradId) { + await selectSatelliteAndMove(page, objective.satelliteNoradId); + } + if (objective.waitForAntennaPosition) { + await waitForAntennaMovement(page); + } + break; + + case 'configure-speca': + await configureSpectrumAnalyzer(page, objective.specaConfig!); + break; + + case 'auto': + await page.waitForTimeout(2000); + break; + } + + await dismissDialogIfPresent(page); +} + +// ============================================================ +// Test Suite +// ============================================================ + +test.describe('Scenario 9 Full Completion', () => { + test.describe.configure({ mode: 'serial' }); + + let page: import('@playwright/test').Page; + let missionControlPage: MissionControlPage; + let context: import('@playwright/test').BrowserContext; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + await page.addInitScript(() => { + (window as unknown as { AUTO_CLOSE_DIALOGS: boolean }).AUTO_CLOSE_DIALOGS = true; + localStorage.clear(); + sessionStorage.clear(); + }); + + missionControlPage = new MissionControlPage(page); + + // Navigate directly (bypasses prerequisite check on S8) + await missionControlPage.gotoScenario('nats', 'nats-scenario9'); + await waitForSimulationReady(page); + + await missionControlPage.dismissDialogIfPresent(); + + await missionControlPage.openMissionBrief(); + await missionControlPage.closeMissionBrief(); + }); + + test.afterAll(async () => { + await context.close(); + }); + + test.beforeEach(async () => { + test.setTimeout(60000); + }); + + // Generate a test per objective in order + for (const objective of SCENARIO_9_OBJECTIVES) { + test(`[${objective.id}] ${objective.title}`, async () => { + // Antenna repoint objectives need a longer timeout for slew + if (objective.type === 'set-tracking-mode' && objective.waitForAntennaPosition) { + test.setTimeout(120000); + } + await executeObjective(page, missionControlPage, objective); + }); + } + + test('Mission Complete: Verify Level Complete Modal', async () => { + const levelCompleteModal = page.locator('#level-complete-modal'); + await expect(levelCompleteModal).toBeVisible({ timeout: 30000 }); + + const modalTitle = levelCompleteModal.locator('.complete-modal__title'); + await expect(modalTitle).toContainText('Mission Complete'); + + const totalScore = levelCompleteModal.locator('.total-value'); + await expect(totalScore).toBeVisible(); + + const scoreText = await totalScore.textContent(); + const score = parseInt(scoreText || '0', 10); + expect(score).toBeGreaterThan(0); + }); +}); diff --git a/package-lock.json b/package-lock.json index ac5391a2..004d25f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "draggabilly": "^3.0.0", "gl-matrix": "^3.4.4", "ootk": "^5.1.1", - "socket.io-client": "^4.5.4", "uuid": "^13.0.0" }, "devDependencies": { @@ -42,8 +41,8 @@ "vitest-canvas-mock": "^1.1.3", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.2.2", - "wrangler": "^4.54.0" + "webpack-dev-server": "^5.2.5", + "wrangler": "^4.100.0" } }, "node_modules/@acemir/cssom": { @@ -109,9 +108,9 @@ "license": "MIT" }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -119,9 +118,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -129,13 +128,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -145,38 +144,38 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", - "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", "dev": true, "license": "MIT OR Apache-2.0", "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.0.tgz", - "integrity": "sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { "unenv": "2.0.0-rc.24", - "workerd": "^1.20260115.0" + "workerd": ">1.20260305.0 <2.0.0-0" }, "peerDependenciesMeta": { "workerd": { @@ -185,9 +184,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260131.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260131.0.tgz", - "integrity": "sha512-+1X4qErc715NUhJZNhtlpuCxajhD5YNre7Cz50WPMmj+BMUrh9h7fntKEadtrUo5SM2YONY7CDzK7wdWbJJBVA==", + "version": "1.20260611.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260611.1.tgz", + "integrity": "sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA==", "cpu": [ "x64" ], @@ -202,9 +201,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260131.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260131.0.tgz", - "integrity": "sha512-M84mXR8WEMEBuX4/dL2IQ4wHV/ALwYjx9if5ePZR8rdbD7if/fkEEoMBq0bGS/1gMLRqqCZLstabxHV+g92NNg==", + "version": "1.20260611.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260611.1.tgz", + "integrity": "sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ==", "cpu": [ "arm64" ], @@ -219,9 +218,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260131.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260131.0.tgz", - "integrity": "sha512-SWzr48bCL9y5wjkj23tXS6t/6us99EAH9T5TAscMV0hfJFZQt97RY/gaHKyRRjFv6jfJZvk7d4g+OmGeYBnwcg==", + "version": "1.20260611.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260611.1.tgz", + "integrity": "sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg==", "cpu": [ "x64" ], @@ -236,9 +235,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260131.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260131.0.tgz", - "integrity": "sha512-mL0kLPGIBJRPeHS3+erJ2t5dJT3ODhsKvR9aA4BcsY7M30/QhlgJIF6wsgwNisTJ23q8PbobZNHBUKIe8l/E9A==", + "version": "1.20260611.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260611.1.tgz", + "integrity": "sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ==", "cpu": [ "arm64" ], @@ -253,9 +252,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260131.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260131.0.tgz", - "integrity": "sha512-hoQqTFBpP1zntP2OQSpt5dEWbd9vSBliK+G7LmDXjKitPkmkRFo2PB4P9aBRE1edPAIO/fpdoJv928k2HaAn4A==", + "version": "1.20260611.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260611.1.tgz", + "integrity": "sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw==", "cpu": [ "x64" ], @@ -381,7 +380,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -422,7 +420,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -437,10 +434,33 @@ "node": ">=10.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -456,9 +476,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -473,9 +493,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -490,9 +510,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -507,9 +527,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -524,9 +544,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -541,9 +561,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -558,9 +578,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -575,9 +595,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -592,9 +612,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -609,9 +629,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -626,9 +646,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -643,9 +663,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -660,9 +680,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -677,9 +697,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -694,9 +714,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -711,9 +731,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -728,9 +748,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -745,9 +765,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -762,9 +782,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -779,9 +799,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -796,9 +816,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -813,9 +833,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -830,9 +850,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -847,9 +867,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -864,9 +884,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -881,9 +901,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1099,9 +1119,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "dev": true, "license": "MIT", "engines": { @@ -1196,6 +1216,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1213,6 +1236,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1230,6 +1256,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1247,6 +1276,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1264,6 +1296,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1281,6 +1316,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1298,6 +1336,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1315,6 +1356,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1332,6 +1376,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1355,6 +1402,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1378,6 +1428,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1401,6 +1454,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1424,6 +1480,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1447,6 +1506,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1470,6 +1532,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1493,6 +1558,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1588,19 +1656,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1778,6 +1833,204 @@ "dev": true, "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", + "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", + "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", + "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", + "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-rsa": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", + "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", + "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pfx": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", + "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", + "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", + "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@playwright/test": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", @@ -1799,7 +2052,6 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1847,24 +2099,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1873,12 +2111,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1887,12 +2128,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1901,26 +2145,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1929,26 +2162,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1957,138 +2179,188 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ - "loong64" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ - "loong64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ - "ppc64" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ - "ppc64" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "riscv64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -2096,27 +2368,83 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@supabase/auth-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz", + "integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "node_modules/@supabase/cli-darwin-arm64": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.106.0.tgz", + "integrity": "sha512-xi/IAjjbebh6xA9Nkkb7ZwC5ACKJ38OqMoYk1y20ScW5Wt+pxTCcOcaWDWu+2u2Pwc3vTYnXhbknVcQt9UcChQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ] }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "node_modules/@supabase/cli-darwin-x64": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.106.0.tgz", + "integrity": "sha512-AT2s/8/3Ffb7IkcUXK6sA/iPCzJhh4Fh/XqzAGNGsVd1xCA/ZGniqU8BL03TqPkN1sR0iDaShjMLLIlHEflyPQ==", "cpu": [ "x64" ], @@ -2124,71 +2452,83 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "darwin" ] }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "node_modules/@supabase/cli-linux-arm64": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.106.0.tgz", + "integrity": "sha512-L/pX7fkV31x7zPrUOcD6KMtZ03j16o41XxgkACFoOAE9lFz8KaJOBSHvn2QLeWEa2kLz7jwpisvpshPSMjuxOw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "node_modules/@supabase/cli-linux-arm64-musl": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.106.0.tgz", + "integrity": "sha512-2LExuOHuU6odXorNTvq4M/OvueW+wOs3KDsaCpk+scQMnB6LbmqNidEjXAsKpZu3zyE8MmmtdbJBlRNN/KLdcw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "node_modules/@supabase/cli-linux-x64": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.106.0.tgz", + "integrity": "sha512-+ihtqFNURc8ssULgQoHQBR4AtUmxez9dtqjclqOV0y5r65SX7Ginmiby4XDW9LPlqHT+MB7OyD1vk0T6pQUtPA==", "cpu": [ - "ia32" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "node_modules/@supabase/cli-linux-x64-musl": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.106.0.tgz", + "integrity": "sha512-wz7heB3wwzmRdLRgqngd2VjPPNIdv6/Y8Q0dX/ZvjcL9/Nk8BYSZ7yjypapLpra+swRnbEJ4jAU6eAShiaB1Hg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "node_modules/@supabase/cli-windows-arm64": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.106.0.tgz", + "integrity": "sha512-QkEfRDiuuQwcfASo2DIJZ59AFV6xpqhPNo4r0kSVt38StYk3iL7ZcSOkZvGQWLlJ8kXKPBWEMhLnfHQF9r+Xfg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -2197,57 +2537,19 @@ "win32" ] }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", - "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/@supabase/cli-windows-x64": { + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.106.0.tgz", + "integrity": "sha512-1tQFbrH1KJhWJqHrY087XPz1WRi20eKG1pebG+6PjSD6XN+j0+x1PTOtBZrylXgY1fOg8BqgU8kl3KX8hI5l9w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@supabase/auth-js": { - "version": "2.81.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz", - "integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==", "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@supabase/functions-js": { "version": "2.81.1", @@ -2288,27 +2590,6 @@ "node": ">=20.0.0" } }, - "node_modules/@supabase/realtime-js/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@supabase/storage-js": { "version": "2.81.1", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.1.tgz", @@ -2354,6 +2635,17 @@ "url": "https://github.com/sponsors/codecalm" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2421,28 +2713,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2539,21 +2809,10 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/phoenix": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", @@ -2644,29 +2903,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", + "magicast": "^0.5.2", "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2685,31 +2944,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2718,7 +2977,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2730,26 +2989,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.9", "pathe": "^2.0.3" }, "funding": { @@ -2757,13 +3016,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2772,9 +3032,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", "funding": { @@ -2782,14 +3042,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -3042,12 +3303,11 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3089,12 +3349,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3125,9 +3384,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -3347,6 +3606,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3358,9 +3632,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -3369,13 +3643,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3410,13 +3677,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/batch": { @@ -3446,50 +3716,6 @@ "node": "*" } }, - "node_modules/bin-links": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz", - "integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==", - "dev": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "proc-log": "^6.0.0", - "read-cmd-shim": "^6.0.0", - "write-file-atomic": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/bin-links/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/bin-links/node_modules/write-file-atomic": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.0.tgz", - "integrity": "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3511,9 +3737,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, "license": "MIT", "dependencies": { @@ -3525,7 +3751,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -3621,9 +3847,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3645,9 +3871,9 @@ } }, "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -3664,13 +3890,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3712,6 +3937,16 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3784,9 +4019,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -3866,16 +4101,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -3914,16 +4139,6 @@ "node": ">=6" } }, - "node_modules/cmd-shim": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz", - "integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4050,6 +4265,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -4092,12 +4314,11 @@ } }, "node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4397,9 +4618,10 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4682,9 +4904,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.244", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", - "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "version": "1.5.373", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.373.tgz", + "integrity": "sha512-G2Hym8JIf/QreuseqkDibgH8Ci8KfJzqGDKdakbhSx9UltwRBH2cBLAWU/lBX0sCdv0TlhyxQyDCnSfxgMWsjA==", "dev": true, "license": "ISC" }, @@ -4708,37 +4930,15 @@ "node": ">= 0.8" } }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -4867,9 +5067,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -4934,9 +5134,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4947,49 +5147,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -5015,7 +5198,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5496,15 +5678,15 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -5523,7 +5705,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -5581,9 +5763,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -5620,30 +5802,6 @@ "node": ">=0.8.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5766,16 +5924,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -5809,19 +5967,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5971,19 +6116,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/gl-matrix": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", @@ -7169,11 +7301,28 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7248,16 +7397,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/jsdom/node_modules/undici": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.20.0.tgz", - "integrity": "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7265,13 +7404,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7330,14 +7462,14 @@ } }, "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/levn": { @@ -7354,10 +7486,283 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true, "license": "MIT", "engines": { @@ -7397,9 +7802,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -7431,14 +7836,14 @@ } }, "node_modules/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, @@ -7582,46 +7987,34 @@ } }, "node_modules/miniflare": { - "version": "4.20260131.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260131.0.tgz", - "integrity": "sha512-CtObRzlAzOUpCFH+MgImykxmDNKthrgIYtC+oLC3UGpve6bGLomKUW4u4EorTvzlQFHe66/9m/+AYbBbpzG0mQ==", + "version": "4.20260611.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260611.0.tgz", + "integrity": "sha512-i+JwEo8vN96naz1WL3ntFgFyRluBDYL408zwhHKvR2jefJ464KsZ/gCmJAQ5k+oaWeb5Ug+s7yne5AyiAEswjg==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", - "undici": "7.18.2", - "workerd": "1.20260131.0", - "ws": "8.18.0", + "sharp": "0.34.5", + "undici": "7.24.8", + "workerd": "1.20260611.1", + "ws": "8.20.1", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, - "node_modules/miniflare/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "node_modules/miniflare/node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">=20.18.1" } }, "node_modules/minimalistic-assert": { @@ -7632,9 +8025,9 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7654,29 +8047,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -7691,6 +8061,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/multicast-dns": { @@ -7708,9 +8079,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -7761,73 +8132,16 @@ "tslib": "^2.0.3" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-fetch/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", "dev": true, "license": "MIT", "engines": { - "node": ">= 12" - } - }, - "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" + "node": ">=18" } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7838,16 +8152,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", - "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -8190,9 +8494,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true, "license": "MIT" }, @@ -8211,9 +8515,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -8236,6 +8540,37 @@ "node": ">=8" } }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/playwright": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", @@ -8294,9 +8629,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -8313,9 +8648,8 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -8428,16 +8762,6 @@ "renderkid": "^3.0.0" } }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8479,10 +8803,30 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8495,16 +8839,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8562,16 +8896,6 @@ "node": ">= 0.8" } }, - "node_modules/read-cmd-shim": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", - "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8613,6 +8937,13 @@ "node": ">= 10.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8742,16 +9073,6 @@ "node": ">=8" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -8762,49 +9083,38 @@ "node": ">= 4" } }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/run-applescript": { @@ -8957,17 +9267,17 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "dev": true, "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/semver": { @@ -9036,13 +9346,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-index": { @@ -9285,9 +9595,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "dev": true, "license": "MIT", "engines": { @@ -9380,34 +9690,6 @@ "dev": true, "license": "ISC" }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -9421,13 +9703,17 @@ } }, "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/source-map": { @@ -9511,9 +9797,9 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -9644,23 +9930,23 @@ } }, "node_modules/supabase": { - "version": "2.75.1", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.75.1.tgz", - "integrity": "sha512-7cIOjjUSxNNaVFAmtrVc9hQAzzFYj+FHzRWe7dOfw5SPTcyjsKDRsQM3FTHW/8O33BQITxVKCL3S/oXsfMV2bA==", + "version": "2.106.0", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.106.0.tgz", + "integrity": "sha512-jV0PJi5LqXUMZ4kriRLvST3DX1YDhrxOgnFJLOn6oUw3KQEFJPXhA60yLQ9aWG1E1uZCp4vUJwSuSGq3HPfaSQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "dependencies": { - "bin-links": "^6.0.0", - "https-proxy-agent": "^7.0.2", - "node-fetch": "^3.3.2", - "tar": "7.5.7" - }, "bin": { - "supabase": "bin/supabase" + "supabase": "dist/supabase.js" }, - "engines": { - "npm": ">=8" + "optionalDependencies": { + "@supabase/cli-darwin-arm64": "2.106.0", + "@supabase/cli-darwin-x64": "2.106.0", + "@supabase/cli-linux-arm64": "2.106.0", + "@supabase/cli-linux-arm64-musl": "2.106.0", + "@supabase/cli-linux-x64": "2.106.0", + "@supabase/cli-linux-x64-musl": "2.106.0", + "@supabase/cli-windows-arm64": "2.106.0", + "@supabase/cli-windows-x64": "2.106.0" } }, "node_modules/supports-color": { @@ -9697,9 +9983,9 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -9710,33 +9996,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/terser": { "version": "5.44.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", @@ -9757,16 +10016,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -9780,24 +10038,50 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } } }, "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9898,14 +10182,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -9933,12 +10217,11 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9947,9 +10230,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -10113,18 +10396,16 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -10136,498 +10417,51 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "tslib": "^1.9.3" + }, "engines": { - "node": ">=18" + "node": ">= 6.0.0" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } + "license": "0BSD" }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" + "node": ">= 0.6" } }, "node_modules/typed-array-buffer": { @@ -10714,7 +10548,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10743,9 +10576,9 @@ } }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { @@ -10764,7 +10597,6 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -10789,9 +10621,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -10854,9 +10686,9 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -10877,19 +10709,17 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -10905,9 +10735,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -10920,13 +10751,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -10952,31 +10786,12 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10985,32 +10800,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -11026,12 +10840,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -11052,6 +10869,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -11060,6 +10883,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -11078,9 +10904,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -11104,13 +10930,12 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "dev": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { @@ -11127,16 +10952,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -11148,38 +10963,35 @@ } }, "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -11203,7 +11015,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -11285,12 +11096,11 @@ } }, "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11370,15 +11180,15 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz", + "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==", "dev": true, "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", @@ -11388,9 +11198,9 @@ "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", - "compression": "^1.7.4", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", + "express": "^4.22.1", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", @@ -11398,7 +11208,7 @@ "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", + "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", @@ -11441,12 +11251,11 @@ } }, "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11498,28 +11307,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -11536,9 +11323,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "dev": true, "license": "MIT", "engines": { @@ -11546,12 +11333,11 @@ } }, "node_modules/webpack/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11583,6 +11369,16 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -11800,13 +11596,12 @@ } }, "node_modules/workerd": { - "version": "1.20260131.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260131.0.tgz", - "integrity": "sha512-4zZxOdWeActbRfydQQlj7vZ2ay01AjjNC4K3stjmWC3xZHeXeN3EAROwsWE83SZHhtw4rn18srrhtXoQvQMw3Q==", + "version": "1.20260611.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260611.1.tgz", + "integrity": "sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -11814,41 +11609,42 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260131.0", - "@cloudflare/workerd-darwin-arm64": "1.20260131.0", - "@cloudflare/workerd-linux-64": "1.20260131.0", - "@cloudflare/workerd-linux-arm64": "1.20260131.0", - "@cloudflare/workerd-windows-64": "1.20260131.0" + "@cloudflare/workerd-darwin-64": "1.20260611.1", + "@cloudflare/workerd-darwin-arm64": "1.20260611.1", + "@cloudflare/workerd-linux-64": "1.20260611.1", + "@cloudflare/workerd-linux-arm64": "1.20260611.1", + "@cloudflare/workerd-windows-64": "1.20260611.1" } }, "node_modules/wrangler": { - "version": "4.62.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.62.0.tgz", - "integrity": "sha512-DogP9jifqw85g33BqwF6m21YBW5J7+Ep9IJLgr6oqHU0RkA79JMN5baeWXdmnIWZl+VZh6bmtNtR+5/Djd32tg==", + "version": "4.100.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.100.0.tgz", + "integrity": "sha512-dSQO7DO+mD6XDzkVWIWBoGLO3yw+lacWSc/KhFvd7pgfpth+kX98qb5SGRHZN8ACCDhhfwzDLXwB6qHsIHhfBg==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.12.0", + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", - "esbuild": "0.27.0", - "miniflare": "4.20260131.0", + "esbuild": "0.27.3", + "miniflare": "4.20260611.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260131.0" + "workerd": "1.20260611.1" }, "bin": { + "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260131.0" + "@cloudflare/workers-types": "^4.20260611.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -11864,9 +11660,9 @@ "license": "MIT" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -11917,14 +11713,6 @@ "dev": true, "license": "MIT" }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8b182671..fbb858f7 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,8 @@ "vitest-canvas-mock": "^1.1.3", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.2.2", - "wrangler": "^4.54.0" + "webpack-dev-server": "^5.2.5", + "wrangler": "^4.100.0" }, "dependencies": { "@supabase/supabase-js": "^2.81.1", @@ -68,7 +68,14 @@ "draggabilly": "^3.0.0", "gl-matrix": "^3.4.4", "ootk": "^5.1.1", - "socket.io-client": "^4.5.4", "uuid": "^13.0.0" + }, + "overrides": { + "esbuild": "^0.28.1", + "ws": "^8.21.0", + "serialize-javascript": "^7.0.5", + "sockjs": { + "uuid": "^11.1.1" + } } } diff --git a/playwright.config.ts b/playwright.config.ts index 5a9b8519..4a41abdd 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,7 +16,9 @@ export default defineConfig({ timeout: 10000, }, use: { - baseURL: 'http://localhost:3000', + // 127.0.0.1 (not localhost) so another dev server bound to IPv6 ::1:3000 + // cannot shadow the webpack server, which binds the IPv4/dual stack. + baseURL: 'http://127.0.0.1:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', @@ -29,7 +31,7 @@ export default defineConfig({ ], webServer: { command: 'npm run dev', - url: 'http://localhost:3000', + url: 'http://127.0.0.1:3000', reuseExistingServer: !process.env.CI, timeout: 120000, }, diff --git a/src/app.ts b/src/app.ts index e8b37f33..035e7f2d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,16 +1,16 @@ -import { BaseElement } from './components/base-element'; -import { getEl } from './engine/utils/get-el'; -import { Body } from './pages/layout/body/body'; -import { Footer } from './pages/layout/footer/footer'; -import { Header } from './pages/layout/header/header'; -import { SandboxPage } from './pages/sandbox-page'; -import { ScenarioSelectionPage } from './pages/scenario-selection'; +import { BaseElement } from '@app/components/base-element'; +import { getEl } from '@app/engine/utils/get-el'; +import { Body } from '@app/pages/layout/body/body'; +import { Footer } from '@app/pages/layout/footer/footer'; +import { Header } from '@app/pages/layout/header/header'; +import { SandboxPage } from '@app/pages/sandbox-page'; +import { ScenarioSelectionPage } from '@app/pages/scenario-selection'; import { Router } from './router'; -import { SimulationManager } from './simulation/simulation-manager'; -import { Auth } from './user-account/auth'; -import { initUserDataService } from './user-account/user-data-service'; -import { EventBus } from './events/event-bus'; -import { Events } from './events/events'; +import { SimulationManager } from '@app/simulation/simulation-manager'; +import { Auth } from '@app/user-account/auth'; +import { initUserDataService } from '@app/user-account/user-data-service'; +import { EventBus } from '@app/events/event-bus'; +import { Events } from '@app/events/events'; /** * Main Application Class diff --git a/src/campaigns/nats/campaign-data.ts b/src/campaigns/nats/campaign-data.ts index 3980be44..db7927e4 100644 --- a/src/campaigns/nats/campaign-data.ts +++ b/src/campaigns/nats/campaign-data.ts @@ -1,4 +1,4 @@ -import type { CampaignData } from '../campaign-types'; +import type { CampaignData } from '@app/campaigns/campaign-types'; import { sandboxData } from './sandbox'; import { scenario1Data } from './scenario1'; import { scenario2Data } from './scenario2'; @@ -8,6 +8,22 @@ import { scenario5Data } from './scenario5'; import { scenario6Data } from './scenario6'; import { scenario7Data } from './scenario7'; import { scenario8Data } from './scenario8'; +import { scenario9Data } from './scenario9'; +import { scenario10Data } from './scenario10'; +import { scenario11Data } from './scenario11'; +import { scenario12Data } from './scenario12'; +import { scenario13Data } from './scenario13'; +import { scenario14Data } from './scenario14'; +import { scenario15Data } from './scenario15'; +import { scenario16Data } from './scenario16'; +import { scenario17Data } from './scenario17'; +import { scenario18Data } from './scenario18'; +import { scenario19Data } from './scenario19'; +import { scenario20Data } from './scenario20'; +import { scenario21Data } from './scenario21'; +import { scenario22Data } from './scenario22'; +import { scenario23Data } from './scenario23'; +import { scenario24Data } from './scenario24'; /** * NATS Campaign: North Atlantic Teleport Services @@ -36,6 +52,22 @@ export const natsCampaignData: CampaignData = { scenario6Data, scenario7Data, scenario8Data, + scenario9Data, + scenario10Data, + scenario11Data, + scenario12Data, + scenario13Data, + scenario14Data, + scenario15Data, + scenario16Data, + scenario17Data, + scenario18Data, + scenario19Data, + scenario20Data, + scenario21Data, + scenario22Data, + scenario23Data, + scenario24Data, ], isLocked: false, isDisabled: false, diff --git a/src/campaigns/nats/satellites.ts b/src/campaigns/nats/satellites.ts index 82eaf896..100fd9f6 100644 --- a/src/campaigns/nats/satellites.ts +++ b/src/campaigns/nats/satellites.ts @@ -137,6 +137,83 @@ export const tidemark2Satellite = new Satellite( } ); +/** + * TIDEMARK-3: Newest TIDEMARK constellation bird + * + * Commissioned by the overnight commissioning crew post-S8. Sits at 47°W, + * between TIDEMARK-1 (53°W) and SES-10 (67°W). Standard 36 MHz C-band + * transponder with H-pol uplink, V-pol beacon. + * + * Frequency Plan: + * - Uplink RF: 5985 MHz (TP-1 center) + * - Downlink RF: 3760 MHz (5985 - 2225 offset) + * - Beacon RF: 4172 MHz (CW) + * + * With VT-01 LNB LO at 5250 MHz: + * - Beacon IF: 1078 MHz (5250 - 4172) + * + * With VT-01 BUC LO at 7000 MHz: + * - TX IF: 1015 MHz (7000 - 5985) + */ +export const tidemark3Satellite = new Satellite( + 'TIDEMARK-3', + 61527, + [ + // Uplink signals - routed to transponders based on frequency and polarization + { + signalId: 'TIDEMARK-3-TDMA-Composite', + serverId: 1, + noradId: 61527, + frequency: 5985e6 as RfFrequency, + polarization: 'H', + power: 20 as dBm, + bandwidth: 36e6 as Hertz, + modulation: 'QPSK' as ModulationType, + fec: '3/4' as FECType, + feed: '', + isDegraded: false, + origin: SignalOrigin.SATELLITE_RX, + noiseFloor: null, + gainInPath: 0 as dBi, + } + ], + [], // Beacons defined in transponderConfigs + { + az: 140.5 as Degrees, + el: 37.8 as Degrees, + rotation: 8 as Degrees, + frequencyOffset: 2.225e9 as Hertz, + // Ephemeris error: smallest of the constellation (newest bird, freshest TLE) + ephemerisErrorAz: 0.10 as Degrees, + ephemerisErrorEl: 0.07 as Degrees, + transponderConfigs: [ + { + id: 'TP-1', + uplinkCenterFrequency: 5985e6 as RfFrequency, + bandwidth: 36e6 as Hertz, + frequencyOffset: 2.225e9 as Hertz, // Downlink center: 3760 MHz + polarization: 'H', + beacon: { + frequency: 4172e6 as RfFrequency, + signalId: 'TIDEMARK-3-Beacon', + serverId: 1, + noradId: 61527, + power: 30 as dBm, + bandwidth: 1e3 as Hertz, + modulation: 'CW' as ModulationType, + fec: 'null' as FECType, + polarization: 'V', + feed: '', + isDegraded: false, + origin: SignalOrigin.TRANSMITTER, + noiseFloor: null, + gainInPath: 0 as dBi, + }, + } as TransponderConfig, + ], + } +); + export const ses10Satellite = new Satellite( 'SES-10', 42432, @@ -220,7 +297,7 @@ export const ses10Satellite = new Satellite( * * With VT-01 LNB LO at 5250 MHz: * - Beacon IF: 1085 MHz (5250 - 4165) - * - Downlink IF: 1645 MHz (5250 - 3605) + * - Downlink IF: 1422 MHz (5250 - 3828) * * With BUC LO at 7500 MHz: * - TX IF: 1447 MHz (7500 - 6053) @@ -268,7 +345,7 @@ export const aurora7Satellite = new Satellite( id: 'TP-1', uplinkCenterFrequency: 6053e6 as RfFrequency, bandwidth: 24e6 as Hertz, // Narrower than TIDEMARK - frequencyOffset: 2.225e9 as Hertz, // Downlink at 3605 MHz + frequencyOffset: 2.225e9 as Hertz, // Downlink at 3828 MHz polarization: 'H', beacon: { frequency: 4165e6 as RfFrequency, diff --git a/src/campaigns/nats/scenario1.ts b/src/campaigns/nats/scenario1.ts index 29bb1ca4..886b7ca8 100644 --- a/src/campaigns/nats/scenario1.ts +++ b/src/campaigns/nats/scenario1.ts @@ -269,12 +269,13 @@ export const scenario1Data: ScenarioData = { { type: 'equipment-powered', description: 'LNB Powered', - params: { equipment: 'lnb' }, + params: { equipment: 'lnb', requiresObservation: true, observationTab: 'rx-analysis' }, mustMaintain: true, }, { type: 'lnb-thermally-stable', description: 'LNB Thermally Stabilized', + params: { requiresObservation: true, observationTab: 'rx-analysis' }, mustMaintain: true, }, { @@ -367,6 +368,8 @@ export const scenario1Data: ScenarioData = { params: { signalId: 'TIDEMARK-1-Beacon', minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', }, mustMaintain: true, }, diff --git a/src/campaigns/nats/scenario10.ts b/src/campaigns/nats/scenario10.ts new file mode 100644 index 00000000..34dd78a3 --- /dev/null +++ b/src/campaigns/nats/scenario10.ts @@ -0,0 +1,803 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import { SignalOrigin } from '@app/signal-origin'; +import type { dB, dBm, FECType, Hertz, IfFrequency, MHz, ModulationType } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { aurora7Satellite, tidemark1Satellite } from './satellites'; + +/** + * NATS Level 10: "Customer Pass" + * + * Phase: Qualified Operations (Phase 2, Scenario 2 of 8) + * Time Pressure: Light (per-objective timers only; the "pass window" is narrative) + * Calculation Required: NO - frequencies pre-configured by previous shift + * New UI Elements: None - reuses S6 step-track and S2/S4 HPA backoff mechanics + * + * NICE Framework Alignment: + * Primary Codes: + * - S0478: Skill in providing customer support + * - T1580: Monitor client-level computer system performance + * - T0153: Monitor network capacity and performance + * + * Supporting Codes: + * - K1032: Knowledge of satellite-based communication systems + * - K0740: Knowledge of system performance indicators + * - S0675: Skill in optimizing system performance + * + * Premise: SeaLink (Marcus Chen, Halifax) has booked a 30-minute high-priority + * data window on AURORA-7. The link is already up from the previous shift on + * program-track with conservative HPA backoff. For the pass window the operator + * needs to: switch to step-track for stable beacon hold on the inclined orbit, + * verify the customer downlink is clean, optimize HPA backoff for more EIRP + * without overdriving, monitor sustained margin while Marcus watches the link + * live, then restore conservative settings when the pass closes. + * + * Tone: Qualified-operator. Dana introduces by text. Marcus Chen speaks at + * pre-pass, mid-pass (mid-window throughput acknowledgment), and post-pass. + * Dana signs off. All quizzes use Character.SYSTEM. + * + * Story Continuity: + * - First scenario with an active customer voice during operations (S4 was + * bracketing only). + * - AURORA-7 still aging; same inclined-orbit behavior as S6/S8. + * - BUC LO 7500 MHz + TX IF 1447 MHz matches the S6 convention so the + * player isn't asked to re-derive frequencies mid-customer-pass. + * + * Technical Reference (AURORA-7): + * - LNB LO: 5250 MHz → Beacon IF 1085 MHz, Downlink IF 1422 MHz + * - BUC LO: 7500 MHz → TX IF 1447 MHz (matches S6 convention) + * - Bandwidth: 24 MHz + */ + +export const scenario10Data: ScenarioData = { + id: 'nats-scenario10', + prerequisiteScenarioIds: ['nats-scenario9'], + url: 'nats/scenarios/nats-scenario10', + imageUrl: 'nats/10/card.png', + number: 10, + title: 'Customer Pass', + subtitle: 'High-Throughput Window on AURORA-7', + duration: '25-30 min', + difficulty: 'intermediate', + missionType: 'Customer Operations', + description: `SeaLink has booked a 30-minute high-priority data window on AURORA-7 - a routine maritime synchronization burst Marcus Chen in Halifax is watching live from the spacecraft side. The link is up from the overnight shift on program-track with a conservative 10 dB HPA backoff.

For a sustained high-throughput window on an inclined-orbit bird, that's not the right configuration. You need step-track to hold beacon stable through the figure-8 drift, and a tighter HPA backoff to give the customer the EIRP margin they're paying for - without overdriving the amp.

Customer is on the line. Marcus will be watching payload telemetry on his end throughout the pass. Standard work - just don't break the link with a customer watching.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + ], + timeLimitSeconds: 30 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + antennasState: [ + { + // Antenna on AURORA-7 in program-track. Beacon lock marginal due to + // inclined-orbit drift - step-track is required for sustained pass. + isPowered: true, + azimuth: 190 as Degrees, + elevation: 32 as Degrees, + polarization: 0 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 28899, // AURORA-7 + targetAzimuth: 190 as Degrees, + targetElevation: 32 as Degrees, + targetPolarization: 0 as Degrees, + slewing: false, + beaconCN: 6.5 as dB, // Marginal under program-track drift + beaconFrequencyHz: 1085e6 as Hertz, // AURORA-7 beacon IF (5250 - 4165) + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + lnb: { + isPowered: true, + loFrequency: 5250 as MHz, + gain: 65 as dB, + isExtRefLocked: true, + }, + buc: { + isPowered: true, + isMuted: false, // Uplink live - SeaLink return path + isLoopback: false, + loFrequency: 7500 as MHz, // Matches S6 AURORA-7 convention + isExtRefLocked: true, + gain: 23 as dB, + }, + hpa: { + isPowered: true, + isHpaEnabled: true, + isHpaSwitchEnabled: true, + backOff: 10, // Conservative - player optimizes to ~6 dB for pass + outputPower: 50 as dBm, + isOverdriven: false, + gain: 44 as dB, + }, + gpsdo: { + isPowered: true, + isLocked: true, + gnssSignalPresent: true, + isGnssSwitchUp: true, + }, + }), + ], + spectrumAnalyzers: [ + { + ...vermontGroundStation.spectrumAnalyzers[0], + // Pre-tuned on AURORA-7 beacon at 1085 MHz IF + centerFrequency: 1085e6 as Hertz, + span: 100e3 as Hertz, + rbw: 1e3 as Hertz, + referenceLevel: -90 as dBm, + minAmplitude: -100 as dBm, + maxAmplitude: -70 as dBm, + }, + ], + receivers: [ + { + ...vermontGroundStation.receivers[0], + modems: [ + { + ...vermontGroundStation.receivers[0].modems[0], + frequency: 1422 as MHz, // AURORA-7 downlink IF (5250 - 3828) + bandwidth: 24 as MHz, + modulation: 'QPSK', + fec: '3/4', + }, + ], + }, + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + activeModem: 1, + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + modem_number: 1, + isTransmitting: true, + isTransmittingSwitchUp: true, + ifSignal: { + ...vermontGroundStation.transmitters[0].modems[0].ifSignal, + signalId: 'AURORA-7-Uplink', + noradId: 28899, + frequency: 1447e6 as IfFrequency, // 7500 - 6053 + bandwidth: 24e6 as Hertz, + modulation: 'QPSK' as ModulationType, + fec: '3/4' as FECType, + origin: SignalOrigin.TRANSMITTER, + power: -7 as dBm, + }, + }, + ], + }, + ], + }, + ], + satellites: [aurora7Satellite, tidemark1Satellite], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-10?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Customer Pass Brief', + description: 'Open the pass brief and acknowledge the SeaLink window.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Acknowledge Pass Window', + params: { + character: Character.SYSTEM, + question: 'Ready to take the SeaLink high-throughput pass on AURORA-7?', + options: ['Yes - link is up on program-track. Switching to pass configuration.'], + correctIndex: 0, + explanation: 'Pass clock running. Customer is on the line.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: PRE-PASS TRACKING SETUP + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'dashboard-sweep', + nice: ['T0153', 'K0741'], + title: 'Pre-Pass Alarm Sweep', + description: 'Check the Dashboard for active alarms before the customer window opens.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Confirm Alarm State', + params: { + character: Character.SYSTEM, + question: 'What is the alarm state on VT-01 going into the pass?', + options: [ + 'No active alarms - clean board, link up on AURORA-7', + 'LNB reference unlocked', + 'HPA output fault', + 'GPSDO holdover', + ], + correctIndex: 0, + explanation: 'Clean board. Safe to push the link harder for the customer window.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'why-step-track', + nice: ['K1032', 'K0773'], + title: 'Confirm Tracking Mode for the Pass', + description: 'Choose the right tracking strategy for a 30-minute sustained pass on AURORA-7.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['dashboard-sweep'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Pick the Tracking Mode', + params: { + character: Character.SYSTEM, + question: 'AURORA-7 is on program-track right now. Why is that wrong for a 30-minute high-throughput window?', + options: [ + 'Inclined orbit drift will degrade C/N across the pass - step-track holds beacon optimum continuously', + 'Program-track consumes more antenna motor cycles than step-track over a long pass', + 'Program-track is only certified for TIDEMARK birds, not AURORA', + 'Step-track is required by the customer contract regardless of orbit type', + ], + correctIndex: 0, + explanation: 'Program-track follows ephemeris and lets the figure-8 drift bleed off C/N. For a sustained pass on an inclined bird, step-track keeps the beacon at peak.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'enable-step-track', + nice: ['S0421', 'K1032'], + title: 'Switch to Step-Track', + description: 'Command the antenna into step-track mode.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['why-step-track'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Active', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'acquire-stable-beacon', + nice: ['T0153', 'K1032'], + title: 'Acquire Stable Beacon Lock', + description: 'Hold beacon lock under step-track long enough to confirm stable peak.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['enable-step-track'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-beacon-locked', + description: 'Beacon Lock Sustained', + mustMaintain: true, + maintainDuration: 15, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: CUSTOMER LINK VERIFICATION + // ============================================================ + { + id: 'verify-rx-lock', + nice: ['T0153', 'T1580', 'K0740'], + title: 'Verify Customer Downlink', + description: 'Confirm the AURORA-7 downlink modem is locked with C/N margin for the pass.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['acquire-stable-beacon'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'RX Modem Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Above 8 dB', + params: { modemNumber: 1, minCNRatio: 8, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'payload-integrity-check', + nice: ['T1580', 'K0740'], + title: 'Payload Data Integrity', + description: 'Confirm the indicators that prove the customer payload path is clean.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-rx-lock'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Payload Health Indicators', + params: { + character: Character.SYSTEM, + question: 'Marcus wants confirmation his payload telemetry is clean. Which indicator set proves the data path is healthy end-to-end?', + options: [ + 'Frame sync locked + CRC valid + FEC engaged with no uncorrectables', + 'Receive power above -70 dBm alone', + 'Antenna beacon lock alone', + 'Modem temperature inside spec', + ], + correctIndex: 0, + explanation: 'Beacon lock proves RF. Frame sync + CRC + FEC prove the data path. Different layers, different evidence - the customer cares about the data path.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'verify-uplink-active', + nice: ['T0431', 'K0740'], + title: 'Verify Uplink Return Path', + description: 'Confirm the uplink (BUC unmuted, HPA enabled, no overdrive) is carrying the SeaLink return traffic.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['payload-integrity-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-unmuted', + description: 'BUC Unmuted', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Enabled', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Not Overdriven', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: HPA BACKOFF OPTIMIZATION FOR THE PASS + // ============================================================ + { + id: 'assess-current-backoff', + nice: ['S0675', 'K0740'], + title: 'Assess Current HPA Backoff', + description: 'Evaluate the HPA backoff that the link inherited from the overnight shift.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-uplink-active'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Backoff Assessment', + params: { + character: Character.SYSTEM, + question: 'HPA is at 10 dB backoff. For a 30-minute customer window with sustained margin requirement, what is the operational reality?', + options: [ + 'Safe but conservative - tighter backoff gives the customer more EIRP without breaking the amp', + 'Already optimal - never lower backoff below 10 dB for any customer pass', + 'Too aggressive - raise backoff to 15 dB before the pass', + 'Backoff is irrelevant; only HPA output power matters', + ], + correctIndex: 0, + explanation: '10 dB is a quiet-night default. For a paying customer expecting sustained margin, 6-7 dB buys real EIRP - so long as the amp stays linear.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'optimize-hpa-backoff', + nice: ['S0675', 'S0421'], + title: 'Tighten HPA Backoff for the Pass', + description: 'Reduce HPA backoff to 6 dB to give the customer more EIRP during the pass window.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['assess-current-backoff'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-back-off-set', + description: 'Backoff: 6 dB', + params: { backOff: 6, backOffTolerance: 1 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-no-overdrive', + nice: ['T0153', 'S0675'], + title: 'Confirm HPA Stays Linear', + description: 'Verify the HPA is not overdriven after the backoff change.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['optimize-hpa-backoff'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'hpa-not-overdriven', + description: 'HPA Not Overdriven', + mustMaintain: true, + maintainDuration: 10, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'imd-tradeoff-check', + nice: ['K0740', 'S0675'], + title: 'Acknowledge the IMD Tradeoff', + description: 'Confirm the operational tradeoff between EIRP and amplifier linearity.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-no-overdrive'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'EIRP vs Linearity', + params: { + character: Character.SYSTEM, + question: 'Lowering backoff from 10 dB to 6 dB increases EIRP. What is the tradeoff to watch during a sustained pass?', + options: [ + 'IMD products rise as the amp moves closer to saturation - monitor for overdrive across the window', + 'Backoff has no effect on intermodulation distortion', + 'Lower backoff cools the amp because gain is lower', + 'EIRP and IMD are independent and can be optimized separately', + ], + correctIndex: 0, + explanation: 'Closer to saturation means more IMD. The decision is to spend a small amount of linearity for meaningful EIRP - then watch for overdrive across the window.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 4: SUSTAINED MARGIN MONITORING + // ============================================================ + { + id: 'sustain-rx-margin', + nice: ['T1580', 'T0153'], + title: 'Sustain Customer Downlink Margin', + description: 'Hold receive C/N above the customer threshold for the pass window.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['imd-tradeoff-check'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-snr-threshold', + description: 'C/N Held Above 9 dB', + params: { modemNumber: 1, minCNRatio: 9 }, + mustMaintain: true, + maintainDuration: 30, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'sustain-beacon-track', + nice: ['K1032', 'T0153'], + title: 'Sustain Step-Track Through the Pass', + description: 'Beacon stays locked under step-track as the satellite drifts.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['sustain-rx-margin'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-beacon-locked', + description: 'Beacon Stays Locked', + mustMaintain: true, + maintainDuration: 20, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Held', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 5: PASS CLOSE + // ============================================================ + { + id: 'restore-conservative-backoff', + nice: ['S0421', 'S0675'], + title: 'Restore Conservative Backoff', + description: 'Pass window has closed. Return HPA backoff to the 10 dB resting value to reduce stress between passes.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['sustain-beacon-track'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'hpa-back-off-set', + description: 'Backoff: 10 dB', + params: { backOff: 10, backOffTolerance: 1 }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Healthy', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'final-pass-snapshot', + nice: ['T1580', 'S0478'], + title: 'Final Pass Disposition', + description: 'Summarize what the customer received during the window.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['restore-conservative-backoff'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Pass Summary', + params: { + character: Character.SYSTEM, + question: 'Which line is the right disposition to give Marcus for the pass?', + options: [ + 'AURORA-7 pass complete - step-track held throughout, C/N margin sustained, no overdrive events, HPA returned to 10 dB', + 'Pass aborted - step-track failed to acquire', + 'Pass complete but link was on program-track the whole time', + 'Pass complete - HPA was overdriven briefly mid-window', + ], + correctIndex: 0, + explanation: 'Clean delivery. That is what the customer is paying for - and what gets logged.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'log-customer-pass', + nice: ['K0645', 'S0478'], + title: 'Log the Customer Pass', + description: 'Select the correct shift-log entry for the SeaLink window.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['final-pass-snapshot'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry correctly records this pass in the operations log?', + options: [ + 'SeaLink 30-min high-priority pass on AURORA-7 - step-track engaged, HPA optimized to 6 dB backoff for window then restored to 10 dB. C/N margin sustained, no link events.', + 'AURORA-7 routine monitoring - no customer impact.', + 'SeaLink pass deferred - link not ready.', + 'AURORA-7 HPA fault during customer window.', + ], + correctIndex: 0, + explanation: 'Capture what changed (backoff), what was sustained (margin), and what was restored (resting state). Next operator picks up with full context.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:48] +

+

+ "SeaLink booked a 30-minute high-priority window on AURORA-7 starting at 10:00. Marcus from Halifax is on the line - he'll be watching payload telemetry live. Link's up on program-track from the overnight shift, but you'll want step-track for the pass and a tighter HPA backoff. Standard work. Don't break it with a customer watching." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/10/intro.mp3'), + }, + objectives: { + 'why-step-track': { + text: ` +

+ Marcus here from Halifax. We're spun up on our end - payload's nominal, watching the spacecraft side telemetry now. Customer's expecting clean throughput from 10:00 to 10:30 sharp. +

+

+ Whenever you've got the ground side in pass configuration, give me a wave and I'll start watching the inbound. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/10/obj-why-step-track.mp3'), + }, + 'sustain-rx-margin': { + text: ` +

+ Beauty. Payload telemetry's locked on our end - frame sync solid, no FEC uncorrectables, link's coming through clean. The C/N's holding right where we want it. +

+

+ Keep doing what you're doing. I'll let you know when the customer's twenty-eight minutes are up. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/10/obj-sustain-rx-margin.mp3'), + }, + 'sustain-beacon-track': { + text: ` +

+ That's the window. Pass closed at 10:30 on our side - customer's confirmed the data, telemetry shows a clean delivery, no dropouts. Nicely done. +

+

+ Bring the amp back to resting and I'll send the disposition through to SeaLink ops. Thanks, eh. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/10/obj-sustain-beacon-track.mp3'), + }, + 'log-customer-pass': { + text: ` +

+ Marcus says clean. That's a paid pass logged with margin to spare. Get it in the book and take a breath. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/10/obj-log-customer-pass.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario11.ts b/src/campaigns/nats/scenario11.ts new file mode 100644 index 00000000..329bd240 --- /dev/null +++ b/src/campaigns/nats/scenario11.ts @@ -0,0 +1,845 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import { SignalOrigin } from '@app/signal-origin'; +import type { dB, dBi, dBm, FECType, Hertz, IfFrequency, MHz, ModulationType } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite, tidemark3Satellite } from './satellites'; + +/** + * NATS Level 11: "Planned Maintenance: Hand Off" + * + * Phase: Qualified Operations (Phase 2, Scenario 3 of 8) + * Arc: Start of S11-S12 mini-arc (planned maintenance cycle) + * Time Pressure: None (planned window, procedural cleanliness is the grade) + * Calculation Required: NO + * New UI Elements: None + * + * NICE Framework Alignment: + * Primary Codes: + * - K0718: Knowledge of network communications principles and practices + * - T0129: Integrate new systems into existing network architecture + * - S0593: Skill in handling incidents + * + * Supporting Codes: + * - K0645: Knowledge of standard operating procedures (SOPs) + * - K0741: Knowledge of system availability measures + * - S0421: Skill in operating network equipment + * - K0770: Knowledge of system administration principles and practices + * + * Premise: Scheduled 2-hour VT-01 maintenance window for HPA waveguide + * gasket inspection. ME-02 has been pre-coordinated with Catherine to take + * TIDEMARK-1 traffic for the window. The operator executes the planned + * handover, then safes the Vermont RF chain and parks the antenna at + * maintenance position for the inbound crew. + * + * Unlike S3's emergency weather handover, there is no time pressure. The + * grade is procedural cleanliness: pre-handover verification, formal + * commit, post-handover safing. Same mechanic, planning discipline. + * + * Tone: Qualified operator. Dialog limited to a Dana text-message intro, + * one Catherine coordination beat at the receive side, a Catherine + * confirmation after the handover commits, a Dana phase transition into + * safing, and a Dana sign-off. All quizzes are SYSTEM. + * + * Story Continuity: + * - ME-02 has been pre-coordinated and is already tracking TM-1 in + * parallel with VT-01 (RX-only). Catherine has the receive side hot + * and is waiting on the operator's commit. + * - This is the first of a 2-scenario mini-arc. S12 brings VT-01 back + * to service and takes TM-1 traffic back from ME-02. + */ + +export const scenario11Data: ScenarioData = { + id: 'nats-scenario11', + prerequisiteScenarioIds: ['nats-scenario10'], + url: 'nats/scenarios/nats-scenario11', + imageUrl: 'nats/11/card.png', + number: 11, + title: 'Planned Maintenance: Hand Off', + subtitle: 'Coordinated Traffic Transfer to Sister Teleport', + duration: '25-30 min', + difficulty: 'intermediate', + missionType: 'Planned Operations', + description: `Scheduled maintenance window opens at 10:00 for HPA waveguide gasket inspection on VT-01. Two hours of downtime, pre-coordinated with Maine.

Catherine has ME-02 standing by - already tracking TIDEMARK-1 in parallel, RX hot, waiting on your commit. Your job: verify her receive side, stage ME-02's transmit chain (cold - the transfer swaps RF authority), execute the handover, then safe VT-01 for the maintenance crew.

No fire, no weather, no surprise. This is procedural work and the grade is cleanliness. Maintenance crew arrives in thirty minutes.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'ME-02: Operational - RX Standing By', + ], + timeLimitSeconds: 30 * 60, // 30 minutes + settings: { + isSync: true, + groundStations: [ + // VT-01: healthy, owns TIDEMARK-1 traffic, about to hand off + { + ...vermontGroundStation, + }, + // ME-02: operational, RX-only on TIDEMARK-1, TX pre-staged but cold (BUC muted, HPA switch off) + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 161.8 as Degrees, + elevation: 34.2 as Degrees, + polarization: 14 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61525, // TIDEMARK-1 + targetAzimuth: 161.8 as Degrees, + targetElevation: 34.2 as Degrees, + targetPolarization: 14 as Degrees, + slewing: false, + beaconCN: 10.4 as dB, + beaconFrequencyHz: 1074.5e6 as Hertz, // 5250 LNB LO - 4175.5 beacon RF + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // Receive side hot, TX pre-staged but cold until operator commits + buc: { isMuted: true }, + hpa: { isHpaEnabled: false, isHpaSwitchEnabled: false }, + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1074.5e6 as Hertz, // TM-1 beacon IF + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [{ + activeModem: 1, + modems: [{ + isPowered: true, + antenna_id: 1, + modem_number: 1, + isFaulted: false, + isTransmitting: false, + isTransmittingSwitchUp: false, + isFaultSwitchUp: false, + id: 1, + isLoopback: false, + ifSignal: { + signalId: 'TIDEMARK-1-Teleport', + serverId: 1, + noradId: 61525, + polarization: 'V', + feed: '', + isDegraded: false, + origin: SignalOrigin.TRANSMITTER, + noiseFloor: null, + gainInPath: 0 as dBi, + frequency: 1094e6 as IfFrequency, + power: -7 as dBm, + bandwidth: 36e6 as Hertz, + modulation: 'QPSK' as ModulationType, + fec: '3/4' as FECType, + }, + }], + }], + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1532 as MHz, // TM-1 downlink IF (5250 - 3718), matches VT-01 + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [ + tidemark1Satellite, + tidemark2Satellite, + tidemark3Satellite, + ses10Satellite, + ], + trafficOwnership: [ + { + satelliteNoradId: 61525, // TIDEMARK-1 + initialOwnerId: 'VT-01', + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-11?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Shift Brief', + description: 'Open the shift brief and acknowledge you understand the maintenance window plan.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Shift Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Acknowledge Handover Plan', + params: { + character: Character.SYSTEM, + question: 'What is the planned outcome of this shift?', + options: [ + 'TIDEMARK-1 traffic moves to ME-02 for a 2-hour VT-01 maintenance window, then comes back next shift', + 'TIDEMARK-1 traffic moves to ME-02 permanently and VT-01 is decommissioned', + 'TIDEMARK-1 is taken out of service for the duration of the maintenance window', + 'ME-02 takes over all VT-01 satellites for the maintenance window', + ], + correctIndex: 0, + explanation: 'Planned handover for a maintenance window. ME-02 holds TM-1 while the crew works VT-01. Return to service is the next shift.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: VT-01 PRE-HANDOVER VERIFICATION + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-pre-handover-dashboard', + nice: ['T0129', 'K0741'], + title: 'VT-01 Pre-Handover Sweep', + description: 'Confirm the VT-01 link is clean before initiating the handover.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Pre-Handover Baseline', + params: { + character: Character.SYSTEM, + question: 'Why does a planned handover start with a baseline check of the source station?', + options: [ + 'A pre-handover snapshot documents what the link looked like healthy, so any post-handover anomaly can be attributed correctly', + 'It is required by the satellite operator before they allow a transfer', + 'It gives the maintenance crew advance notice that work is starting', + 'It is needed to recover billing data', + ], + correctIndex: 0, + explanation: 'Baseline first. If something looks off on ME-02 after the transfer, you need to know whether it started before or after the handover.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-confirm-tm1-locked', + nice: ['T0129', 'S0421'], + title: 'Confirm VT-01 Owns TIDEMARK-1', + description: 'Verify VT-01 antenna is still tracking TIDEMARK-1 and is the current traffic owner.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-pre-handover-dashboard'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on TIDEMARK-1', + params: { noradId: 61525, requiresObservation: true, observationTab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'traffic-owner', + description: 'VT-01 Owns TIDEMARK-1 Traffic', + // Evaluator compares the objective's groundStation against the + // current owner of params.satelliteId. + params: { satelliteId: 61525 }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 2: ME-02 RECEIVE-SIDE VERIFICATION (Catherine) + // ============================================================ + { + id: 'switch-to-maine', + nice: ['S0421', 'K0718'], + title: 'Open ME-02', + description: 'Switch to the Maine Ground Station to verify the receive side before committing.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['vt-confirm-tm1-locked'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'me-verify-antenna-on-tm1', + nice: ['T0129', 'K0718'], + title: 'ME-02 Antenna on TIDEMARK-1', + description: 'Confirm Catherine has ME-02 locked on TIDEMARK-1 in program-track.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['switch-to-maine'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'ME-02 Locked on TIDEMARK-1', + params: { noradId: 61525, requiresObservation: true, observationTab: 'acu-control' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'me-verify-beacon-and-rx', + nice: ['T0129', 'K0741'], + title: 'ME-02 Receive Chain Hot', + description: 'Verify the TIDEMARK-1 beacon is visible at ME-02 and the receiver is locked with healthy C/N margin.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-verify-antenna-on-tm1'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon at ME-02', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'ME-02 Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N >= 10 dB', + params: { + minCNRatio: 10, + modemNumber: 1, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: ME-02 TRANSMIT COMMIT + // ============================================================ + { + id: 'me-stage-tx-for-handover', + nice: ['T0129', 'S0421', 'K0770'], + title: 'Stage ME-02 Transmit', + description: 'Enable transmit on Modem 1 so the TX chain is staged - but leave the BUC muted and the HPA disabled. The handover transfer, not the operator, swaps RF authority between stations.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-verify-beacon-and-rx'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'tx-modem-transmitting', + description: 'Modem 1 Transmitting (into muted BUC)', + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-muted', + description: 'BUC Stays Muted Until the Swap', + maintainUntilObjectiveComplete: true, + }, + { + type: 'hpa-disabled', + description: 'HPA Stays Disabled Until the Swap', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'understand-commit-point', + nice: ['K0718', 'S0593'], + title: 'Understand the Commit Point', + description: 'Confirm what "ready to commit" means in a planned handover.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-stage-tx-for-handover'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Commit Criteria', + params: { + character: Character.SYSTEM, + question: 'What makes ME-02 ready to commit to the TIDEMARK-1 handover?', + options: [ + 'Antenna locked, RX carrier with C/N margin, TX chain staged - modem transmitting into a muted BUC, HPA disabled until the transfer swaps RF authority', + 'BUC unmuted and HPA enabled now - the station must be radiating before the transfer', + 'Antenna locked is sufficient - the rest happens during the transfer', + 'A successful loopback test before the transfer', + ], + correctIndex: 0, + explanation: 'Receive side proves the link works in; transmit side is staged but cold. If both stations radiated at the same transponder, the satellite would see two carriers - dual illumination. The handover stands the source down and brings the target up in one coordinated swap.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 4: EXECUTE HANDOVER + // ============================================================ + { + id: 'open-tm1-dashboard', + nice: ['S0421'], + title: 'Open TIDEMARK-1 Dashboard', + description: 'Select TIDEMARK-1 in the asset tree to access the traffic transfer controls.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['understand-commit-point'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'satellite-selected', + description: 'TIDEMARK-1 Selected', + params: { assetSatelliteId: 'sat-61525' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'execute-handover', + nice: ['T0129', 'S0593', 'K0741'], + title: 'Execute Handover', + description: 'Transfer TIDEMARK-1 traffic from VT-01 to ME-02.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['open-tm1-dashboard'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'traffic-transferred', + description: 'Traffic Transferred to ME-02', + params: { + sourceStation: 'VT-01', + targetStation: 'ME-02', + satelliteId: 61525, + }, + mustMaintain: false, + }, + { + type: 'service-continuity', + description: 'Service Continuity Maintained', + params: { maxPacketLoss: 0.1 }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-me-traffic-owner', + nice: ['T0129', 'K0741'], + title: 'Verify ME-02 Owns Traffic', + description: 'Confirm ME-02 is now the traffic owner for TIDEMARK-1.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['execute-handover'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'traffic-owner', + description: 'ME-02 Owns TIDEMARK-1', + // Evaluator compares the objective's groundStation against the + // current owner of params.satelliteId. + params: { satelliteId: 61525 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'catherine-confirm-hot', + nice: ['K0718', 'K0741'], + title: 'Confirm with Catherine', + description: 'Acknowledge Catherine\'s confirmation that ME-02 has the link clean.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['verify-me-traffic-owner'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Cross-Station Confirmation', + params: { + character: Character.SYSTEM, + question: 'Catherine reports ME-02 is carrying TIDEMARK-1 clean. What is the next correct action?', + options: [ + 'Switch back to VT-01 and safe the RF chain for the maintenance crew', + 'Stay on ME-02 and continue monitoring until the maintenance window closes', + 'Power down ME-02 to conserve equipment hours', + 'Re-enable the VT-01 transmitter as a hot standby', + ], + correctIndex: 0, + explanation: 'Handover is committed. The unfinished work is on the source station - safe the RF chain and park the antenna for the crew.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 5: VT-01 SAFING FOR MAINTENANCE CREW + // ============================================================ + { + id: 'switch-to-vermont-safing', + nice: ['S0421'], + title: 'Return to VT-01', + description: 'Switch back to Vermont to safe the station for the maintenance crew.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['catherine-confirm-hot'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-safe-tx-chain', + nice: ['S0421', 'K0770', 'S0593'], + title: 'Verify VT-01 TX Chain Safed', + description: 'The handover stood VT-01\'s transmit down automatically - HPA disabled, BUC muted. Verify it on the panel; never trust an automatic safing you have not looked at.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['switch-to-vermont-safing'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-disabled', + description: 'HPA Disabled', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-muted', + description: 'BUC Muted', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'vt-antenna-maintenance-position', + nice: ['S0421', 'K0770'], + title: 'Park VT-01 Antenna at Maintenance Position', + description: 'Set the antenna tracking mode to MAINTENANCE so the dish parks at 5° elevation for the crew.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-safe-tx-chain'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Tracking Mode Set to Maintenance', + params: { trackingMode: 'maintenance' }, + mustMaintain: true, + }, + { + type: 'antenna-position', + description: 'Antenna at Maintenance Position', + params: { + elevation: 5 as Degrees, + tolerance: 1, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'vt-final-rf-safety-check', + nice: ['K0770', 'S0593'], + title: 'Final RF Safety Check', + description: 'Confirm VT-01 is safe to hand to the maintenance crew.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-antenna-maintenance-position'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Crew-Ready Criteria', + params: { + character: Character.SYSTEM, + question: 'Which combination of conditions confirms VT-01 is safe for the maintenance crew to begin work?', + options: [ + 'HPA disabled, BUC muted, antenna at maintenance position - no RF energy on the feed, dish accessible', + 'HPA disabled alone is sufficient - the BUC cannot transmit without HPA', + 'Antenna stowed at 90° elevation is the correct maintenance position', + 'Powering off the entire RF rack is required before the crew approaches', + ], + correctIndex: 0, + explanation: 'RF de-energized AND dish in a position the crew can reach. Stow at 90° protects against weather; maintenance at 5° lets a person work the feed.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // SHIFT WRAP + // ============================================================ + { + id: 'log-handover-entry', + nice: ['K0645', 'T0129'], + title: 'Log the Handover', + description: 'Select the correct entry for the operations log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-final-rf-safety-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Handover Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which line correctly records this handover in the operations log?', + options: [ + '1000 - Planned handover TM-1 VT-01 to ME-02 complete. VT-01 RF chain safed, antenna at maintenance position. Crew on-site for HPA waveguide inspection. ME-02 (Vega) carrying traffic. Return to service next shift.', + '1000 - Emergency handover TM-1 VT-01 to ME-02. Cause unknown. Investigation pending.', + '1000 - Maintenance crew arrived. Traffic handover deferred to next shift.', + '1000 - TM-1 traffic resumed on VT-01 after maintenance complete.', + ], + correctIndex: 0, + explanation: 'Who, what, why, where it stands, who has the link. The next operator should be able to pick up the shift without asking a question.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:42] +

+

+ "Maintenance window opens at 10:00 - HPA waveguide gasket on VT-01. Catherine's pre-staged ME-02 on TM-1, RX hot, waiting on your commit. Verify her side, stage ME-02 transmit, do the transfer, safe VT-01 for the crew. They're at the gate in thirty." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/11/intro.mp3'), + }, + objectives: { + 'switch-to-maine': { + text: ` +

+ Catherine here. I've got TIDEMARK-1 in program-track from this side, beacon's clean, C/N's holding above ten. Receive side is yours to verify. Stage my TX cold - the transfer brings it up when you commit. +

+ `, + character: Character.CATHERINE_VEGA, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/11/obj-switch-to-maine.mp3'), + }, + 'catherine-confirm-hot': { + text: ` +

+ Maine has TM-1. Carrier's clean over here, no packet loss across the transfer. I've got it for the window - go take care of your station. +

+ `, + character: Character.CATHERINE_VEGA, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/11/obj-catherine-confirm-hot.mp3'), + }, + 'switch-to-vermont-safing': { + text: ` +

+ Maine's carrying. The transfer stood your TX down - verify HPA and BUC on the panel, then antenna to maintenance. Crew lead is on his way up. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/11/obj-switch-to-vermont-safing.mp3'), + }, + 'log-handover-entry': { + text: ` +

+ Clean handover. Log it and meet me in the conference room - I want to walk through the gasket replacement procedure with you before they start. +

+

+ Next shift you bring it back. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/11/obj-log-handover-entry.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario12.ts b/src/campaigns/nats/scenario12.ts new file mode 100644 index 00000000..c7f58bcf --- /dev/null +++ b/src/campaigns/nats/scenario12.ts @@ -0,0 +1,1020 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, Hertz, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 12: "Planned Maintenance: Return to Service" + * + * Phase: Qualified Operations (Phase 2, Scenario 4 of 8) + * Time Pressure: None (planned procedure - procedural cleanliness is the grade) + * Calculation Required: NO - operational values already known + * New UI Elements: None - reuses S2 power-up, S7 leftover-detection, S3 handover. + * + * NICE Framework Alignment: + * Primary Codes: + * - T1314: Maintain network infrastructure + * - T1567: Configure system hardware, software, peripheral equipment + * - T0431: Check system hardware availability, functionality, integrity + * + * Supporting Codes: + * - T0153: Monitor network capacity and performance + * - K0773: Knowledge of telecommunications principles and practices + * - K0645: Knowledge of standard operating procedures (SOPs) + * - K0741: Knowledge of system availability measures + * - S0421: Skill in operating network equipment + * - S0593: Skill in handling incidents + * + * Premise: Closes the S11-S12 planned-maintenance mini-arc. The maintenance crew + * has finished waveguide gasket inspection on VT-01. TM-1 traffic is currently + * being held at ME-02 (Catherine took it during S11). Player brings VT-01 back + * online in a clean restoration sequence, catches a small leftover from the + * maintenance crew (BUC gain bumped during testing), then coordinates the + * traffic return from Catherine. + * + * Tone: Qualified operator. Dana intros via text and signs off; Catherine handles + * the cross-station release dialogue; Marcus closes the customer loop. All quizzes + * are SYSTEM-voiced. No tab coaching. + * + * Story Continuity: + * - S11 left VT-01 stowed and powered down; ME-02 holds TM-1 traffic. + * - The maintenance leftover (BUC gain at 50 dB instead of 23 dB operating value) + * is the same class of mistake as S7 — establishes that the maintenance crew's + * habits haven't changed. + * - Catherine is a peer, not a supervisor. She talks like Catherine, not Dana. + * + * Technical Reference (TIDEMARK-1 from VT-01): + * - Az 161.8°, El 34.2°, Pol 14° + * - LNB LO: 5250 MHz; Beacon RF: 4175.5 MHz; Beacon IF: 1074.5 MHz + * - BUC LO: 7000 MHz; Teleport carrier RF: 5906 MHz (TP-2); TX IF: 1094 MHz + * - Operating BUC gain: 23 dB (post-maintenance leftover: 50 dB - same as S7 testing value) + */ + +export const scenario12Data: ScenarioData = { + id: 'nats-scenario12', + prerequisiteScenarioIds: ['nats-scenario11'], + url: 'nats/scenarios/nats-scenario12', + imageUrl: 'nats/12/card.png', + number: 12, + title: 'Planned Maintenance: Return to Service', + subtitle: 'Post-Maintenance Restoration', + duration: '25-35 min', + difficulty: 'intermediate', + missionType: 'Maintenance Recovery', + description: `Waveguide gasket inspection is done. Maintenance crew is clear of the antenna and signed out. VT-01 is stowed, RF chain cold, and Catherine is holding TM-1 traffic on ME-02.

Bring Vermont back the clean way: antenna on target, RX chain validated against the beacon, transmit side swept for any leftovers the maintenance crew left behind, then a coordinated handover return from Maine. Marcus will want to hear from us once SeaLink's link is back where it started.

No clock pressure. Just do it right.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'ME-02: Holding TM-1 traffic', + ], + timeLimitSeconds: 35 * 60, // 35 minutes + settings: { + isSync: true, + groundStations: [ + // VT-01: stowed for maintenance window, RF chain cold, BUC has a + // gain leftover from maintenance crew's bench testing (50 dB instead + // of operating 23 dB) + { + ...vermontGroundStation, + antennasState: [ + { + isPowered: true, + azimuth: 0 as Degrees, + elevation: 90 as Degrees, + polarization: 0 as Degrees, + trackingMode: 'stow', + isBeaconLocked: false, + targetSatelliteId: null, + slewing: false, + beaconCN: 0 as dB, + isLocked: false, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // RF chain safed for maintenance window. LNB was operational before + // the window so it restabilizes quickly on power-up; we don't override + // the thermalStabilizationTime fields (inherit instant-stable from + // vermontGroundStation defaults). + lnb: { + isPowered: false, + }, + buc: { + isPowered: false, + isMuted: true, + isLoopback: false, + // ----- Maintenance leftover ----- + // Crew bumped BUC gain to 50 dB during bench testing and forgot + // to dial it back. Won't matter until the chain is energized, but + // if HPA is brought up at this gain the output stage gets driven + // hard. Operator should catch this before powering up. + gain: 50 as dB, + temperature: 25, + currentDraw: 0, + outputPower: -10 as dBm, + }, + hpa: { + isPowered: false, + isHpaEnabled: false, + isHpaSwitchEnabled: false, + outputPower: 0 as dBm, + backOff: 10, + }, + }), + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + isTransmitting: false, + isTransmittingSwitchUp: false, + }, + ], + }, + ], + }, + // ME-02: holding TIDEMARK-1 traffic. Catherine has the bird while VT-01 + // is in the maintenance window. LO overridden to 5250 to match VT-01 IFs. + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 161.8 as Degrees, + elevation: 34.2 as Degrees, + polarization: 14 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61525, // TIDEMARK-1 + targetAzimuth: 161.8 as Degrees, + targetElevation: 34.2 as Degrees, + targetPolarization: 14 as Degrees, + slewing: false, + beaconCN: 10.4 as dB, + beaconFrequencyHz: 1074.5e6 as Hertz, + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // ME-02 is holding TM-1: LNB LO override 5250 (matches VT-01 IFs), + // TX chain hot and carrying customer traffic until handover return. + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1074.5e6 as Hertz, + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + }, + ], + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1532 as MHz, // TM-1 downlink IF (5250 - 3718), matches VT-01 + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [ + tidemark1Satellite, + tidemark2Satellite, + ses10Satellite, + ], + trafficOwnership: [ + { + satelliteNoradId: 61525, // TIDEMARK-1 + initialOwnerId: 'ME-02', // Catherine is holding it at scenario start + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-12?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Restoration Brief', + description: 'Open the restoration brief and acknowledge you are ready to bring VT-01 back online.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Ready for Restoration', + params: { + character: Character.SYSTEM, + question: 'Maintenance complete, antenna stowed, ME-02 holding TM-1. Ready to bring VT-01 back?', + options: ['Confirmed. Beginning restoration.'], + correctIndex: 0, + explanation: 'Restoration timer started. Procedural cleanliness is the grade today.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: PRE-RESTORATION STATE VERIFICATION + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'pre-restoration-dashboard', + nice: ['T0431', 'K0741'], + title: 'Confirm Safed State', + description: 'Verify VT-01 is in the expected post-maintenance state - cold RF chain, antenna stowed.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Verify Post-Maintenance State', + params: { + character: Character.SYSTEM, + question: 'What state should VT-01 be in before starting restoration?', + options: [ + 'Antenna stowed, LNB/BUC/HPA powered off, GPSDO still running', + 'Antenna on TIDEMARK-1, full RF chain hot, modem transmitting', + 'Antenna stowed, RF chain hot, modem standby', + 'All equipment powered down including GPSDO', + ], + correctIndex: 0, + explanation: 'You parked the dish at maintenance position (5°) for the crew; on sign-out they stowed it at 90° per post-work SOP. Stowed antenna + cold RF chain + GPSDO still up for timing continuity is the expected handback state. Anything else means somebody touched something they should not have.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 2: ANTENNA RESTORATION + // ============================================================ + { + id: 'repoint-antenna-tm1', + nice: ['T1567', 'S0421', 'K1032'], + title: 'Repoint VT-01 to TIDEMARK-1', + description: 'Set tracking mode to program-track and acquire TIDEMARK-1.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['pre-restoration-dashboard'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Program-Track Mode', + params: { trackingMode: 'program-track' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on TIDEMARK-1', + params: { noradId: 61525 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: RX CHAIN RESTORATION + // ============================================================ + { + id: 'power-up-lnb', + nice: ['T1567', 'S0421'], + title: 'Power Up LNB', + description: 'Power on the LNB and wait for thermal stabilization. LO is preserved at 5,250 MHz.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['repoint-antenna-tm1'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'equipment-powered', + description: 'LNB Powered', + params: { equipment: 'lnb' }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'lnb-thermally-stable', + description: 'LNB Thermally Stable', + maintainUntilObjectiveComplete: true, + }, + { + type: 'lnb-reference-locked', + description: 'LNB Reference Locked', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'acquire-tm1-beacon', + nice: ['T0153', 'K0773'], + title: 'Acquire TIDEMARK-1 Beacon', + description: 'Tune the spectrum analyzer to the TM-1 beacon IF (1,074.5 MHz) and verify the beacon.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['power-up-lnb'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum Analyzer at 1,074.5 MHz IF', + params: { + centerFrequency: 1074.5e6 as Hertz, + centerFrequencyTolerance: 0.5e6, + }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon Detected', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -100 as dBm, + }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-rx-modem-lock', + nice: ['T0153', 'K0740'], + title: 'Verify RX Modem Lock', + description: 'Confirm the receiver modem regains lock on the carrier with healthy C/N margin.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['acquire-tm1-beacon'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N ≥ 10 dB', + params: { minCNRatio: 10, requiresObservation: true, observationTab: 'rx-analysis' }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: POST-MAINTENANCE LEFTOVER SWEEP + // ============================================================ + { + id: 'tx-chain-inspection', + nice: ['T0431', 'S0593'], + title: 'TX Chain Pre-Power Inspection', + description: 'Inspect the TX chain panel before energizing. Look for anything the maintenance crew left out of operating spec.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-rx-modem-lock'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Identify the Leftover', + params: { + character: Character.SYSTEM, + question: 'BUC, HPA, and TX modem are all powered off as expected. What is out of operating spec?', + options: [ + 'BUC gain is at 50 dB - testing value left over from maintenance, operating value is 23 dB', + 'BUC LO is wrong - operating value should be 6,500 MHz, not 7,000 MHz', + 'HPA backoff is at 10 dB - should be 0 dB for full operation', + 'Nothing is out of spec - state matches the operating baseline', + ], + correctIndex: 0, + explanation: 'The bench-test gain (50 dB) was never dialed back to 23 dB. With BUC powered off this is harmless, but bringing the chain up at this gain would over-drive the HPA the moment loopback or transmit was engaged. Catch it before you energize.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + // ============================================================ + // PHASE 5: TX CHAIN RESTORATION + // ============================================================ + { + id: 'power-up-buc', + nice: ['T1567', 'S0421', 'K0770'], + title: 'Power Up BUC (Muted)', + description: 'Power on the BUC - it stays muted from the safed state, so no RF flows. Reference lock should come up against the locked GPSDO.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['tx-chain-inspection'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'equipment-powered', + description: 'BUC Powered', + params: { equipment: 'buc' }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-muted', + description: 'BUC Still Muted', + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-reference-locked', + description: 'BUC Reference Locked', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'correct-buc-gain', + nice: ['T1567', 'S0421'], + title: 'Restore BUC Operating Gain', + description: 'With the BUC powered but still muted, dial the gain back to its 23 dB operating value before the rest of the chain comes up.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['power-up-buc'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-gain-set', + description: 'BUC Gain Set to 23 dB', + params: { + gain: 23 as dB, + gainTolerance: 2 as dB, + }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'power-up-hpa', + nice: ['T1567', 'S0421'], + title: 'Power Up HPA', + description: 'Power on the HPA. Output stays disabled until enable is toggled.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['correct-buc-gain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'equipment-powered', + description: 'HPA Powered', + params: { equipment: 'hpa' }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'start-modem-transmitting', + nice: ['T1567', 'S0421'], + title: 'Enable TX Modem', + description: 'Enable the transmit modem. Operational config (1,094 MHz IF, QPSK 3/4, 36 MHz) is preserved.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['power-up-hpa'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'tx-modem-transmitting', + description: 'TX Modem Transmitting', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'verify-tx-staged', + nice: ['T1567', 'K0645', 'S0421'], + title: 'Confirm TX Staged Cold', + description: 'Confirm the transmit chain is staged for the handover return: modem transmitting into a muted BUC, HPA output disabled. The transfer brings VT-01 RF up as Maine stands down.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['start-modem-transmitting'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'tx-modem-transmitting', + description: 'TX Modem Transmitting (into muted BUC)', + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-muted', + description: 'BUC Stays Muted Until the Swap', + maintainUntilObjectiveComplete: true, + }, + { + type: 'hpa-disabled', + description: 'HPA Stays Disabled Until the Swap', + maintainUntilObjectiveComplete: true, + }, + { + type: 'status-check', + description: 'Why TX Stays Cold', + params: { + character: Character.SYSTEM, + question: 'ME-02 is still carrying TIDEMARK-1. Why does VT-01\'s transmit chain stay cold (BUC muted, HPA disabled) until the handover executes?', + options: [ + 'Two stations radiating at the same transponder is dual illumination - the handover swaps RF authority in one coordinated action so only one uplink is ever on the air', + 'The BUC cannot be unmuted while the antenna is in program-track', + 'It saves power until the customer confirms the return', + 'The HPA needs the maintenance crew to re-certify it before it can radiate', + ], + correctIndex: 0, + explanation: 'Same rule that protected the S11 hand-off: the satellite must never see two carriers fighting on one transponder. Stage the chain, then let the transfer stand Maine down and bring Vermont up atomically.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 6: HANDOVER RETURN FROM ME-02 + // ============================================================ + { + id: 'select-maine-station', + nice: ['S0421'], + title: 'Switch to ME-02', + description: 'Move to the Maine console to coordinate the handover return with Catherine.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['verify-tx-staged'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'pre-handover-criteria', + nice: ['T0153', 'K0741', 'S0593'], + title: 'Confirm Handover-Ready State', + description: 'Confirm what has to be true on VT-01 before pulling traffic back from Maine.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['select-maine-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Handover Criteria', + params: { + character: Character.SYSTEM, + question: 'Which combination is required on VT-01 before pulling TM-1 traffic back?', + options: [ + 'Beacon lock + RX modem lock with C/N margin + TX chain staged cold (modem on, BUC muted, HPA disabled)', + 'BUC unmuted and HPA radiating - the link must be hot before the transfer', + 'Antenna pointed at TM-1 only - the rest comes up when traffic transfers', + 'No specific criteria - planned handovers cannot fail', + ], + correctIndex: 0, + explanation: 'Symmetric to S11: the receiving station proves its receive side and stages its transmit side cold. The transfer stands Maine down and brings Vermont up in one swap - radiating early would put two carriers on the transponder.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'execute-handover-return', + nice: ['T0129', 'S0421', 'S0593'], + title: 'Pull Traffic Back to VT-01', + description: 'Execute the planned handover return: TIDEMARK-1 traffic from ME-02 back to VT-01.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['pre-handover-criteria'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'satellite-selected', + description: 'TIDEMARK-1 Selected', + params: { assetSatelliteId: 'sat-61525' }, + mustMaintain: false, + }, + { + type: 'traffic-transferred', + description: 'Traffic Returned to VT-01', + params: { + sourceStation: 'ME-02', + targetStation: 'VT-01', + satelliteId: 61525, + }, + mustMaintain: false, + }, + { + type: 'service-continuity', + description: 'Service Continuity Maintained', + params: { maxPacketLoss: 0.1 }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-handover-success', + nice: ['T0153', 'K0741'], + title: 'Confirm Return Complete', + description: 'Verify the handover return landed clean.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['execute-handover-return'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + { + type: 'buc-unmuted', + description: 'VT-01 BUC Unmuted by the Transfer', + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'VT-01 HPA Enabled by the Transfer', + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Handover Result', + params: { + character: Character.SYSTEM, + question: 'Traffic ownership has returned to VT-01. What confirms the return was clean?', + options: [ + 'VT-01 owns TM-1 traffic, ME-02 TX stood down, no packet loss reported, no new alarms', + 'ME-02 antenna automatically stowed', + 'VT-01 C/N ratio increased after the transfer', + 'Customer dialed in to manually re-register', + ], + correctIndex: 0, + explanation: 'Clean return: target station owns the traffic, source station released, no service interruption, no alarms. From the customer side, nothing happened.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 7: CUSTOMER NOTIFICATION & SHIFT CLOSE + // ============================================================ + { + id: 'notify-customer', + nice: ['S0478', 'K0645'], + title: 'Notify SeaLink', + description: 'Confirm the right customer notification for a clean return-to-service.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-handover-success'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Customer Comms', + params: { + character: Character.SYSTEM, + question: 'Marcus is waiting for confirmation. What is the right message?', + options: [ + 'VT-01 restored, TM-1 traffic returned to primary station, no service interruption observed.', + 'Service restored, please rerun all customer health checks on your end.', + 'Brief outage during handover - apologies, please flag any data loss.', + 'No notification needed - the link was never down from their perspective.', + ], + correctIndex: 0, + explanation: 'Notify the spacecraft side of the change in source station and confirm no impact. They do not need to act, but they do need to know where the uplink is coming from now.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'final-dashboard-sweep', + nice: ['T0153', 'K0741'], + title: 'Final Dashboard Sweep', + description: 'Final alarm sweep on VT-01 before logging the maintenance cycle complete.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['notify-customer'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Post-Restoration Health', + params: { + character: Character.SYSTEM, + question: 'Final VT-01 health check after restoration.', + options: [ + 'No active alarms - all systems nominal, TM-1 traffic on primary', + 'BUC over-temperature', + 'LNB reference unlocked', + 'HPA overdriven', + ], + correctIndex: 0, + explanation: 'Clean restoration. The BUC gain catch earlier is the reason the HPA is not overdriven right now.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'log-maintenance-complete', + nice: ['K0645', 'T1314'], + title: 'Log Maintenance Cycle Complete', + description: 'Select the correct entry to close out the maintenance cycle in the ops log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['final-dashboard-sweep'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry correctly closes out the S11-S12 maintenance cycle?', + options: [ + 'VT-01 returned to service post-waveguide-gasket inspection. TM-1 traffic returned from ME-02. Maintenance leftover (BUC gain 50 dB) corrected before energizing. No customer impact.', + 'VT-01 restored. Customer reported brief outage during handover.', + 'Maintenance window incomplete - HPA work deferred.', + 'Traffic transferred to ME-02 for weather event.', + ], + correctIndex: 0, + explanation: 'Logging the leftover is the part that matters. Next shift sees what the maintenance crew did and what we caught - that is how the pattern gets fixed.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:14] +

+

+ "Crew's clear. Gasket replaced, signed off. Catherine has TM-1 on Maine - she's good for another hour but no reason to make her wait. Bring Vermont back, pull traffic home, ping Marcus when done. Standard restoration." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/12/intro.mp3'), + }, + objectives: { + 'verify-rx-modem-lock': { + text: ` +

+ RX side's back. Before you energize anything on transmit - actually look at the TX panel. Maintenance crews leave bench-test values behind. Same crew left the BUC unmuted in loopback a few months back. Catch it on the panel, not after the HPA's been on for ten seconds. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/12/obj-verify-rx-modem-lock.mp3'), + }, + 'select-maine-station': { + text: ` +

+ Catherine here. I've been watching your beacon come up on the network status - looks like you're ready to take TM-1 back. C/N's been steady our side, no events. Whenever you're set, give me the call. +

+ `, + character: Character.CATHERINE_VEGA, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/12/obj-select-maine-station.mp3'), + }, + 'execute-handover-return': { + text: ` +

+ Clean release on my end. Maine TX is down, antenna's still on TM-1 if you want a warm standby. Nice work bringing Vermont back without a hole in the link. +

+ `, + character: Character.CATHERINE_VEGA, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/12/obj-execute-handover-return.mp3'), + }, + 'notify-customer': { + text: ` +

+ Marcus from Halifax. Spacecraft side just saw your uplink come back on TM-1 - clean signal, right in the passband, no anomalies. Customers never noticed the swap. Beauty, eh? +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/12/obj-notify-customer.mp3'), + }, + 'log-maintenance-complete': { + text: ` +

+ Maintenance cycle closed. You caught the BUC gain leftover - that's the kind of save that doesn't show up on a dashboard. Log it the way you wrote it; I want the next crew that gets these guys to see the pattern. +

+

+ Good shift. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/12/obj-log-maintenance-complete.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario13.ts b/src/campaigns/nats/scenario13.ts new file mode 100644 index 00000000..cec5c68b --- /dev/null +++ b/src/campaigns/nats/scenario13.ts @@ -0,0 +1,699 @@ +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { maineGroundStation, vermontGroundStation } from './ground-stations'; +import { tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 13: "Thermal Anomaly" + * + * Phase: Qualified Operations (Phase 2, Scenario 5 of 8) + * + * Premise: Mid-shift. VT-01 is up on TIDEMARK-1 carrying normal customer + * traffic. The BUC temperature has been creeping upward for the last + * ~15 minutes: 57°C -> 62°C with a steady ~+0.3°C/min slope. No alarm + * (the over-temperature trip is at 70°C). BUC current draw is also + * elevated (4.1A vs nominal ~3.0A). Everything else on the station is + * nominal: GPSDO locked, RX beacon clean, antenna locked, HPA backoff + * 8 dB and not overdriven. + * + * The trend signature points at one root cause: BUC gain is sitting at + * 33 dB - 10 dB above the 23 dB operating value (same class of leftover + * the S12 maintenance crew taught us to look for). The BUC is dissipating + * the excess as heat into its own chassis. The right call is to de-rate + * (reduce BUC gain back to 23 dB) which preserves carrier within SLA + * margin, lets the BUC cool, and buys time for a planned swap. + * + * Alternative responses (swap now, mute and switch to backup, hold and + * monitor) are not wrong - they're just costlier than necessary for a + * pre-alarm trend. The lesson is judgment under uncertainty, not a + * checklist. + * + * Tone: Qualified operator. Dana texts the trend flag at the start, + * checks in once at the decision point, acknowledges the action, and + * signs off. All knowledge checks are SYSTEM. + */ +export const scenario13Data: ScenarioData = { + id: 'nats-scenario13', + prerequisiteScenarioIds: ['nats-scenario12'], + url: 'nats/scenarios/nats-scenario13', + imageUrl: 'nats/13/card.png', + number: 13, + title: 'Thermal Anomaly', + subtitle: 'Reading the Trend', + duration: '25-30 min', + difficulty: 'intermediate', + missionType: 'Trend Assessment', + description: `Mid-shift on VT-01. TIDEMARK-1 carrying normal customer traffic. The trend display flagged something fifteen minutes ago: BUC temperature has been climbing roughly a third of a degree per minute - 57°C up to 62°C, no alarm yet, but the slope is unambiguous.

Nothing else has moved. GPSDO locked, RX beacon clean, HPA in backoff. The question is whether to act now, schedule a swap and keep going, switch to backup, or hold and monitor.

The right answer is judgment, not a checklist. Read the trend, pick a course of action, and execute it without putting the customer in the dark.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'ME-02: Operational', + ], + timeLimitSeconds: 30 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // Elevated thermal state - pre-alarm but trending. The root cause + // is BUC gain set higher than the HPA drive target requires. + buc: { + isMuted: false, + isLoopback: false, + loFrequency: 7000 as MHz, + isExtRefLocked: true, + gain: 33 as dB, // Elevated 10 dB above the 23 dB operating value + // Higher-rated BUC unit on this chain: keeps the over-gain state + // pre-alarm (no saturation warning) so the *trend* is the only + // signal - that's the point of the scenario. + saturationPower: 28 as dBm, + temperature: 62, // °C - climbing ~0.3°C/min for the last 15 min + currentDraw: 4.1, // A - elevated from nominal ~3.0A + }, + hpa: { + isHpaEnabled: true, + isHpaSwitchEnabled: true, + backOff: 8 as dB, // Slightly tight but not overdriven + }, + lnb: { + isPowered: true, + loFrequency: 5250 as MHz, + gain: 60, + }, + }), + ], + }, + { ...maineGroundStation, isOperational: true }, + ], + satellites: [tidemark1Satellite, tidemark2Satellite], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-13?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Shift Brief', + description: 'Open the shift brief to see the trend flag and the customer context.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Shift Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Ready to Assess', + params: { + character: Character.SYSTEM, + question: 'Brief reviewed. Ready to assess the BUC trend?', + options: ['Yes - moving to VT-01 to read the equipment.'], + correctIndex: 0, + explanation: 'Shift clock running. Pre-alarm trend - no time pressure, but no reason to dawdle either.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: OBSERVATION + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'confirm-no-active-alarm', + nice: ['T0153', 'K0741'], + title: 'Confirm Pre-Alarm State', + description: 'Check the Dashboard. Confirm the BUC has not tripped its over-temperature alarm yet.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Alarm Threshold Awareness', + params: { + character: Character.SYSTEM, + question: 'BUC over-temperature trip is at 70°C. Current reading is 62°C. What does that mean for your decision timeline?', + options: [ + 'Pre-alarm - you have time to choose a deliberate action instead of a reflexive one', + 'Already in alarm - mute the BUC immediately', + 'No relevance - the trip point is fixed and the trend is unrelated', + 'Threshold has been raised by the maintenance crew - ignore the reading', + ], + correctIndex: 0, + explanation: 'Pre-alarm trends are the ideal time to act. Acting at alarm means you are already late.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'open-tx-chain', + nice: ['T0153', 'K0740'], + title: 'Open the TX Chain', + description: 'Switch to the TX Chain view to read live BUC telemetry.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['confirm-no-active-alarm'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'read-buc-temp-trend', + nice: ['T0153', 'K0064'], + title: 'Read the Temperature Trend', + description: 'Read the BUC temperature against the 15-minute history shown in the brief and call the trend.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['open-tx-chain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Trend Projection', + params: { + character: Character.SYSTEM, + question: 'BUC temperature is 62°C, rising about +0.3°C/min for the last 15 minutes. If the slope holds, when does it cross the 70°C trip?', + options: [ + 'Roughly 25-30 minutes from now if nothing changes', + 'Already past it - the dashboard alarm is suppressed', + 'Never - thermal trends always self-stabilize before trip', + 'In a few seconds - the slope accelerates exponentially', + ], + correctIndex: 0, + explanation: 'Linear extrapolation: 8°C of headroom at +0.3°C/min is roughly 25 minutes. Enough time for a deliberate response.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'check-current-draw', + nice: ['T0153', 'S0672'], + title: 'Cross-Check Current Draw', + description: 'Read BUC current draw and interpret what it tells you about the cause of the heat.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['read-buc-temp-trend'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Current Draw Interpretation', + params: { + character: Character.SYSTEM, + question: 'BUC current draw is 4.1A (nominal ~3.0A). What does the elevated current tell you?', + options: [ + 'BUC is dissipating more electrical power - consistent with the thermal rise, not a separate fault', + 'Power supply is failing and pushing extra current into the module', + 'Current is unrelated to thermal state - look at LNB telemetry instead', + 'BUC is in over-current shutdown', + ], + correctIndex: 0, + explanation: 'Heat in an amplifier comes from electrical power that does not leave as RF. Higher current + higher temperature is one story, not two.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'cross-check-spectrum', + nice: ['T0153', 'K0773'], + title: 'Rule Out an RX-Side Fault', + description: 'Verify the TIDEMARK-1 beacon is still clean on the RX side. If the RX is healthy, the trend is isolated to the TX chain.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['check-current-draw'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon Still Present', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'record-baseline-readings', + nice: ['K0645', 'K0740'], + title: 'Record Baseline Readings', + description: 'Note the current state before you act. Anyone reading the log later needs the trend curve, not just the outcome.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['cross-check-spectrum'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'What to Capture', + params: { + character: Character.SYSTEM, + question: 'Before you act, which set of readings belongs in the log to make this trend reconstructable later?', + options: [ + 'Time, BUC temperature, BUC current, BUC gain, HPA backoff - so the next operator can rebuild the curve', + 'BUC temperature alone - the rest is derivable from it', + 'Customer SLA metrics only - the equipment state is irrelevant', + 'A screenshot of the dashboard', + ], + correctIndex: 0, + explanation: 'Trend records are only useful if someone else can replay them. Capture the inputs, not just the outputs.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 2: DIAGNOSIS & JUDGMENT + // ============================================================ + { + id: 'identify-root-cause', + nice: ['S0672', 'K0064'], + title: 'Name the Root Cause', + description: 'All inputs are nominal except the BUC. Identify the most likely root cause.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['record-baseline-readings'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Root Cause Hypothesis', + params: { + character: Character.SYSTEM, + question: 'BUC gain is 33 dB. The operating value for this chain is 23 dB. What is the most likely root cause of the thermal trend?', + options: [ + 'BUC gain is set higher than required - the module is dissipating the excess as heat instead of useful RF', + 'BUC is failing internally and should be swapped immediately - no other action is safe', + 'HPA is overdriven and bleeding heat backward into the BUC', + 'Ambient temperature in the equipment room is rising', + ], + correctIndex: 0, + explanation: 'When the gain stage is set above what the drive chain needs, the excess turns into heat. Classic over-gain dissipation.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'evaluate-options', + nice: ['K0721', 'S0672'], + title: 'Choose a Course of Action', + description: 'Four options on the table. Pick the one with the right cost-to-effect ratio for a pre-alarm trend.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['identify-root-cause'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Operational Response', + params: { + character: Character.SYSTEM, + question: 'You have time. The trend is real but pre-alarm. Which response addresses the root cause at the lowest customer cost?', + options: [ + 'De-rate now: reduce BUC gain ~10 dB to cut dissipation, monitor the trend reverse, schedule a swap during the next planned window', + 'Swap the BUC now - take the carrier down hard and let maintenance replace the module', + 'Mute and switch traffic to the backup modem chain immediately', + 'Hold and monitor - thermal trends usually flatten on their own', + ], + correctIndex: 0, + preserveOptionOrder: true, + explanation: 'De-rating addresses the cause (excess dissipation) without taking the customer down. Swap-now is over-spend; mute-and-switch is over-reaction; hold-and-monitor ignores a trend that has not flattened in 15 minutes.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'confirm-action-plan', + nice: ['T1314', 'K0721'], + title: 'Confirm the Sequence', + description: 'Spell out the de-rate sequence before you touch the gain knob.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['evaluate-options'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'De-Rate Sequence', + params: { + character: Character.SYSTEM, + question: 'What is the correct sequence for the de-rate?', + options: [ + 'Lower BUC gain ~10 dB, verify HPA still in linear region, verify carrier still nominal, then watch the temperature curve bend', + 'Mute the BUC, change the gain, then unmute - to be safe', + 'Lower BUC gain in a single step to 0 dB, then ramp back up while watching telemetry', + 'Change BUC gain only after physically swapping the module', + ], + correctIndex: 0, + preserveOptionOrder: true, + explanation: 'Live adjustment is safe when the HPA has backoff headroom. Muting would interrupt the customer; ramping from 0 is unnecessary theater.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 3: EXECUTION + // ============================================================ + { + id: 'reduce-buc-gain', + nice: ['T1314', 'S0421'], + title: 'De-Rate the BUC', + description: 'Lower BUC gain back to the 23 dB operating value (down from 33 dB).', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['confirm-action-plan'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-gain-set', + description: 'BUC Gain Reduced to ~23 dB', + params: { gain: 23, gainTolerance: 2 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-hpa-headroom', + nice: ['T0431', 'K0740'], + title: 'Verify HPA Still Linear', + description: 'Confirm the HPA is not overdriven after the change. With less drive from the BUC, the HPA should be sitting in a comfortable backoff.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['reduce-buc-gain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'hpa-not-overdriven', + description: 'HPA Within Linear Region', + mustMaintain: true, + }, + { + type: 'buc-not-saturated', + description: 'BUC Output Not Saturated', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-trend-stabilizing', + nice: ['T0153', 'T0431'], + title: 'Confirm the Trend Is Bending', + description: 'Decide what evidence will confirm the de-rate actually took effect on the thermal trend.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-hpa-headroom'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Evidence of Success', + params: { + character: Character.SYSTEM, + question: 'How do you know the de-rate worked before you log the shift?', + options: [ + 'Watch 5-10 minutes: temperature slope flattens then trends down, current draw drops toward nominal, carrier still locked downstream', + 'Temperature drops instantly the moment the gain knob moves', + 'Trust the gain change is correct and move on - thermal verification is not needed', + 'Mute the carrier and re-measure cold-start temperature', + ], + correctIndex: 0, + preserveOptionOrder: true, + explanation: 'Thermal mass is slow. The slope changes before the absolute value does. Watch the curve, not a single reading.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: SCHEDULE & DOCUMENT + // ============================================================ + { + id: 'schedule-maintenance-ticket', + nice: ['K0645', 'T1314'], + title: 'Open the Maintenance Ticket', + description: 'Choose the right contents for the swap ticket so the maintenance crew has what they need.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-trend-stabilizing'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Ticket Contents', + params: { + character: Character.SYSTEM, + question: 'What belongs in the BUC swap ticket for the next planned maintenance window?', + options: [ + 'Trend record (15-min curve), de-rate action taken, current BUC gain/backoff settings, recommendation to swap module during next planned window', + 'Just "BUC running hot" - the maintenance crew will figure out the rest', + 'A request to replace the entire RF front end', + 'A note that the trend was a false alarm and no action is needed', + ], + correctIndex: 0, + preserveOptionOrder: true, + explanation: 'Tickets are handoffs. The next person needs context, not a verdict.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'final-dashboard-sweep', + nice: ['T0153', 'K0741'], + title: 'Final Dashboard Sweep', + description: 'Final look at the Dashboard before closing the shift entry.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['schedule-maintenance-ticket'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Final State Summary', + params: { + character: Character.SYSTEM, + question: 'Final state of VT-01 after the action?', + options: [ + 'No active alarms, BUC running de-rated, carrier nominal, swap ticket open against next planned window', + 'BUC in alarm, customer down, ticket open', + 'BUC swapped on-shift, customer briefly down, ticket closed', + 'No change - hold and monitor still in effect', + ], + correctIndex: 0, + preserveOptionOrder: true, + explanation: 'Clean summary. Customer never noticed; trend addressed at the source; next crew has a clear handoff.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'log-shift-summary', + nice: ['K0645', 'T0153'], + title: 'Log the Shift Entry', + description: 'Select the correct entry for the operations log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['final-dashboard-sweep'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry correctly records this action in the operations log?', + options: [ + '1003 - VT-01 BUC thermal trend (57°C->62°C over 15 min) addressed by 10 dB gain de-rate. Trend reversing. Swap ticket opened for next planned window. Carrier nominal throughout.', + '1003 - BUC failure on VT-01, customer outage logged.', + '1003 - Hold and monitor - no action taken.', + '1003 - BUC swap completed on-shift.', + ], + correctIndex: 0, + preserveOptionOrder: true, + explanation: 'Trend, action, evidence of effect, and the open ticket - all in one line.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 10:03] +

+

+ "Trend display flagged BUC temp on VT-01. Up from 57 to 62 over the last fifteen, no alarm yet. Carrier is fine. Take a look at it and decide what you want to do - I trust your read." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/13/intro.mp3'), + }, + objectives: { + 'identify-root-cause': { + text: ` +

+ Take your time on this one. I'd rather you pick the right action than the fast one. Walk me through what you want to do. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/13/obj-identify-root-cause.mp3'), + }, + 'verify-trend-stabilizing': { + text: ` +

+ Good call on the de-rate. Get the ticket open before end of shift so maintenance can pick the swap up on the next planned window. Don't let it slip. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/13/obj-verify-trend-stabilizing.mp3'), + }, + 'log-shift-summary': { + text: ` +

+ Trend's bending. Customer never knew anything happened. That's the job - catch it on the curve, not at the alarm. +

+

+ See you tomorrow. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/13/obj-log-shift-summary.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario14.ts b/src/campaigns/nats/scenario14.ts new file mode 100644 index 00000000..78106b61 --- /dev/null +++ b/src/campaigns/nats/scenario14.ts @@ -0,0 +1,889 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Receiver } from '@app/equipment/receiver/receiver'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, Hertz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 14: "Rain Fade" + * + * Phase: Qualified Operations (Phase 2, Scenario 6 of 8) + * Time Pressure: Moderate (rain front arriving early in the scenario) + * Calculation Required: NO + * New UI Elements: None - reuses S3 (feed heater + AGC) and S1 (C/N margin) + * + * NICE Framework Alignment: + * Primary Codes: + * - K0689: Knowledge of network infrastructure principles and practices + * - S0675: Skill in optimizing system performance + * - K0721: Knowledge of risk management principles and practices + * + * Supporting Codes: + * - T0153: Monitor network capacity and performance + * - K0773: Knowledge of telecommunications principles and practices + * - S0593: Skill in handling incidents + * + * Premise: Light to moderate rain is moving over VT-01. The customer + * (James Okafor, SeaLink fleet captain) has called ahead - their SLA + * terms allow degradation but penalize handover events. He wants the + * operator to hold VT-01 through the weather if at all possible. + * + * The operator's job: enable feed heater, watch AGC headroom, monitor + * link margin, and make the call - hold or hand off. The right answer + * for a moderate rain front is to hold; the wrong answer is to escape + * the moment the link wavers. + * + * S3 taught "weather hits → hand over." S14 teaches that escape is not + * always correct. Customer-constraint reasoning is explicit, and this + * is the first scenario in the campaign that introduces K0721 (risk + * management) as a primary code. + * + * Tone: Qualified operator. Dialog capped at 4 clips. SYSTEM for all + * quizzes by default. Named characters only for the customer call, + * the decision check-in, and the close. + * + * Story Continuity: + * - Dana is on-site, primary supervisor. + * - ME-02 has been running TIDEMARK-2 since S3 - available as fallback + * but currently busy with its own customers. + * - James Okafor speaks the customer-impact side; he doesn't talk RF. + */ + +export const scenario14Data: ScenarioData = { + id: 'nats-scenario14', + prerequisiteScenarioIds: ['nats-scenario13'], + url: 'nats/scenarios/nats-scenario14', + imageUrl: 'nats/14/card.png', + number: 14, + title: 'Rain Fade', + subtitle: 'Adapt Without Handover', + duration: '25-35 min', + difficulty: 'intermediate', + missionType: 'Weather Contingency', + description: `Rain front moving over Vermont. Light to moderate, maybe twenty minutes through. The link will fade but it shouldn't black out.

The customer - James Okafor at SeaLink - has called ahead. Their SLA terms penalize handover events more than they penalize a few dB of margin loss, so he's asked us to hold VT-01 through the weather if we can. ME-02 is busy on TIDEMARK-2 and would have to drop its own customers to take TIDEMARK-1.

Your job: enable the feed heater, watch AGC headroom, track the beacon C/N, and make the call. Hold or hand off - the right answer is the one the link supports.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End (Feed Heater, AGC)', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'ME-02: Operational (TIDEMARK-2)', + ], + timeLimitSeconds: 35 * 60, // 35 minutes + settings: { + isSync: true, + groundStations: [ + // VT-01: tracking TIDEMARK-1, healthy at baseline. Rain begins at +5min. + { + ...vermontGroundStation, + }, + // ME-02: operational, currently serving TIDEMARK-2. Present as a + // theoretical fallback but engaged with its own traffic. + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 219.7 as Degrees, + elevation: 26.3 as Degrees, + polarization: -25 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61526, // TIDEMARK-2 + targetAzimuth: 219.7 as Degrees, + targetElevation: 26.3 as Degrees, + targetPolarization: -25 as Degrees, + slewing: false, + beaconCN: 10.2 as dB, + beaconFrequencyHz: 1070e6 as Hertz, + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // ME-02 mirrors VT-01's LNB LO so TM-2 beacon lands at 1070 MHz IF. + // ME-02 TX is active on its own TIDEMARK-2 traffic; player should + // not touch it during this scenario. + buc: { isMuted: true }, + hpa: { isHpaEnabled: false, isHpaSwitchEnabled: false }, + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1070e6 as Hertz, + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [], + receivers: [Receiver.getDefaultState()], + }, + ], + satellites: [ + tidemark1Satellite, + tidemark2Satellite, + ses10Satellite, + ], + weatherEvents: [ + { + id: 'vermont-rain-front', + groundStationId: 'VT-01', + type: 'rain', + severity: 'moderate', + startTime: 300, // 5 minutes into the scenario + duration: 1200, // 20 minutes of rain + // 3 dB of degradation - within AGC compensation range; link should + // hold with margin. Compare to S3's 8 dB severe blizzard. + linkMarginDegradation: 3, + }, + ], + trafficOwnership: [ + { + satelliteNoradId: 61525, // TIDEMARK-1 + initialOwnerId: 'VT-01', + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-14?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Shift Brief', + description: 'Open the shift brief, including the customer note from SeaLink, and acknowledge you are ready to begin.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Shift Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Ready to Begin', + params: { + character: Character.SYSTEM, + question: 'Have you reviewed the shift brief and SeaLink customer note?', + options: ['Yes, brief reviewed. Standing by for the front.'], + correctIndex: 0, + explanation: 'Front arrives in about five minutes. Get protective measures in place before it does.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // CUSTOMER CONSTRAINT + // ============================================================ + { + id: 'acknowledge-customer-constraint', + nice: ['K0721', 'S0593'], + title: 'Customer Constraint', + description: 'James from SeaLink has called ahead about the weather. Acknowledge what he is asking for.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Customer SLA Posture', + params: { + character: Character.SYSTEM, + question: 'James from SeaLink is asking us to hold VT-01 through the rain rather than hand off. What is the operational reason?', + options: [ + 'Their SLA penalizes handover events more heavily than a few dB of margin loss', + 'ME-02 has not been certified to carry SeaLink traffic', + 'The handover process always causes a customer outage', + 'Maine is currently weathered in as well', + ], + correctIndex: 0, + explanation: 'SeaLink runs vessel telemetry that survives short C/N dips but logs a hard event on every uplink change. A handover event costs them more contractually than 3 dB of fade does. The customer is telling us what they value - listen.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PROACTIVE PROTECTION - FEED HEATER + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['acknowledge-customer-constraint'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'pre-storm-dashboard', + nice: ['T0153', 'K0741'], + title: 'Pre-Storm Dashboard Sweep', + description: 'Confirm VT-01 is healthy at baseline before the rain arrives.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Pre-Storm Baseline', + params: { + character: Character.SYSTEM, + question: 'What is VT-01 reporting before the front arrives?', + options: [ + 'No active alarms, link healthy - clean baseline to fade from', + 'BUC over-temperature, do not transmit', + 'Antenna tracking error', + 'GPSDO in holdover', + ], + correctIndex: 0, + explanation: 'Clean baseline. Note the C/N now so you know what "nominal" looks like when the rain starts pulling it down.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'enable-feed-heater', + nice: ['S0421', 'K0689'], + title: 'Enable Feed Heater', + description: 'Open the ACU Control panel and enable the feed heater before the front arrives.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['pre-storm-dashboard'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'feed-heater-enabled', + description: 'Feed Heater Enabled', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'understand-heater-vs-rain', + nice: ['K0689', 'K0773'], + title: 'Heater Purpose for Rain', + description: 'Confirm what the feed heater is doing for a rain event (versus snow).', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['enable-feed-heater'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Heater Role During Rain', + params: { + character: Character.SYSTEM, + question: 'The heater is doing what for a rain event that is well above freezing?', + options: [ + 'Keeping water from beading and sheeting on the feed - dry surfaces attenuate less than wet ones', + 'Heating the LNB to compensate for cold rain', + 'Preventing ice (rain at 12°C cannot freeze)', + 'Boosting RF gain through the feed', + ], + correctIndex: 0, + explanation: 'Standing water on the feed is itself an attenuator. The heater keeps surfaces above dew point so droplets evaporate instead of pooling. Same hardware as the anti-icing case in S3, different mechanism.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // BASELINE LINK MARGIN + // ============================================================ + { + id: 'open-rx-analysis-baseline', + nice: ['S0421'], + title: 'Open RX Analysis', + description: 'Switch to the RX Analysis tab to monitor the link as the front moves in.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['understand-heater-vs-rain'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'baseline-beacon-and-lock', + nice: ['T0153', 'K0773', 'K1032'], + title: 'Baseline Beacon and Lock', + description: 'Confirm the TIDEMARK-1 beacon is healthy and the receiver is locked with margin before the rain hits.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['open-rx-analysis-baseline'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon Detected', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -95 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'Baseline C/N ≥ 10 dB', + params: { minCNRatio: 10, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'baseline-margin-quiz', + nice: ['K0740', 'K0721'], + title: 'Link Margin Baseline', + description: 'Pin down what "link margin" means for the call you may have to make.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['baseline-beacon-and-lock'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Margin Definition', + params: { + character: Character.SYSTEM, + question: 'Why does noting the baseline C/N matter for this scenario specifically?', + options: [ + 'It defines how much fade the link can absorb before reaching the demodulation threshold', + 'It tells the customer how much power we are using', + 'It calibrates the spectrum analyzer for the storm', + 'It is required by the modem firmware', + ], + correctIndex: 0, + explanation: 'Margin = baseline C/N minus demod threshold. With 10+ dB of headroom and 3 dB of expected fade, we are comfortable. Without baseline data, every dip looks scary.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // ACTIVE MONITORING - RAIN ARRIVES + // ============================================================ + { + id: 'monitor-during-fade', + nice: ['T0153', 'S0675'], + title: 'Monitor Through the Fade', + description: 'Hold position on the RX Analysis tab as the front passes. Confirm the link stays above threshold.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['baseline-margin-quiz'], + timeLimitSeconds: 6 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Lock Maintained', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Above Storm Threshold (≥ 7 dB)', + params: { minCNRatio: 7, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + maintainDuration: 20, + }, + { + type: 'antenna-locked', + description: 'Antenna Locked on TIDEMARK-1', + params: { noradId: 61525, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'agc-behavior-quiz', + nice: ['K0773', 'T0153'], + title: 'AGC Behavior in the Fade', + description: 'Confirm what the AGC is doing as the rain attenuates the input signal.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['monitor-during-fade'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'AGC Compensation', + params: { + character: Character.SYSTEM, + question: 'Input level dropped a few dB as the rain hit, but the post-AGC output level is steady. What does that tell you?', + options: [ + 'AGC is compensating - the demodulator still sees a usable signal, and we still have headroom in the gain stage', + 'The fade is over and the rain has stopped', + 'The LNB has automatically raised its gain', + 'The receiver has switched to a backup carrier', + ], + correctIndex: 0, + explanation: 'AGC absorbs the first several dB of fade transparently. The thing to watch is not the output level (that is what AGC stabilizes) but how much gain the AGC is using. When it approaches its max, you are out of cushion.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'agc-headroom-quiz', + nice: ['K0689', 'K0721'], + title: 'AGC Headroom Reading', + description: 'Establish how AGC headroom maps to operational risk.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['agc-behavior-quiz'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'AGC Headroom Interpretation', + params: { + character: Character.SYSTEM, + question: 'AGC is currently using ~3 dB of its compensation range. What does that say about the decision to hold?', + options: [ + 'Plenty of headroom remaining - link is comfortable, hold is justified', + 'AGC at any non-zero value means we should hand over immediately', + 'AGC compensation has no relationship to handover decisions', + 'We must reduce HPA backoff to relieve the AGC', + ], + correctIndex: 0, + explanation: 'AGC near floor = wide cushion. The handover trigger is not AGC active - it is AGC near max with continued fade, or modem unlock. We are nowhere near either.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // OPTIMIZATION - HPA BACKOFF + // ============================================================ + { + id: 'open-tx-chain', + nice: ['S0421'], + title: 'Open TX Chain', + description: 'Switch to the TX Chain panel. The HPA is a knob you could turn - decide whether you should.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['agc-headroom-quiz'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'hpa-backoff-decision-quiz', + nice: ['S0675', 'K0721'], + title: 'HPA Backoff Decision', + description: 'Reducing HPA backoff would add a couple of dB of uplink power. Decide whether that is the right move right now.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['open-tx-chain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Backoff Tradeoff', + params: { + character: Character.SYSTEM, + question: 'The downlink C/N is holding above threshold with AGC headroom to spare. Should you reduce HPA backoff to push more uplink power?', + options: [ + 'No - the link is healthy; trading IMD risk for unused margin is a bad bargain', + 'Yes - always run the HPA hot during weather', + 'Yes - the customer requires maximum uplink power during a fade', + 'No - because the HPA cannot be adjusted while transmitting', + ], + correctIndex: 0, + explanation: 'Optimization is not "turn every dial to max." If the link has margin you do not need, the responsible move is to not spend it. Reduced backoff means higher IMD on neighbors and more amplifier stress for no operational benefit here. Keep the configuration nominal.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-hpa-still-nominal', + nice: ['T0431', 'S0675'], + title: 'Verify HPA Still Nominal', + description: 'Confirm the HPA remains within operating limits with backoff unchanged.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['hpa-backoff-decision-quiz'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Output Enabled', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Within Operating Limits', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // DECISION POINT - HOLD OR HAND OFF + // ============================================================ + { + id: 'handover-threshold-quiz', + nice: ['K0721', 'S0593'], + title: 'Handover Threshold', + description: 'Pin down the conditions under which the call flips from "hold" to "hand off."', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-hpa-still-nominal'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'When Handover IS Justified', + params: { + character: Character.SYSTEM, + question: 'The customer prefers no handover, but the customer does not get the last word on RF reality. Which condition flips the call to "hand off now"?', + options: [ + 'AGC at max with continued fade, or modem dropping lock', + 'Any drop in C/N from baseline', + 'The first raindrop hitting the dish', + 'When ME-02 reports it would be available', + ], + correctIndex: 0, + explanation: 'The customer can ask us to favor "hold." The customer cannot ask us to keep serving on a dead link. The trigger is operational, not contractual: AGC at max and still fading, or actual lock loss.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'decision-hold', + nice: ['K0721', 'S0593'], + title: 'Make the Call', + description: 'Given current link state, log your operational call for the next ten minutes.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['handover-threshold-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Hold or Hand Off', + params: { + character: Character.DANA_TORRES, + question: 'Your call - what are we doing for the rest of this front?', + options: [ + 'Hold VT-01. AGC has headroom, modem locked, customer preference honored. Re-evaluate if state changes.', + 'Begin immediate handover to ME-02.', + 'Stow the antenna and accept the outage.', + 'Reduce HPA backoff to push more uplink power.', + ], + correctIndex: 0, + explanation: 'Right call. We are holding. State the trigger conditions to yourself so you know when to flip - that is the discipline of holding, not just inertia.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // SUSTAINED MONITORING THROUGH PEAK + // ============================================================ + { + id: 'sustained-monitor', + nice: ['T0153', 'S0675'], + title: 'Hold Through the Peak', + description: 'Maintain the link through the heaviest part of the front. Lock and C/N must stay above threshold.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['decision-hold'], + timeLimitSeconds: 8 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-signal-locked', + description: 'Receiver Lock Maintained', + params: { modemNumber: 1 }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Holds ≥ 7 dB', + params: { minCNRatio: 7 }, + mustMaintain: true, + maintainDuration: 30, + }, + { + type: 'antenna-locked', + description: 'Antenna Locked on TIDEMARK-1', + params: { noradId: 61525 }, + mustMaintain: true, + }, + { + type: 'feed-heater-enabled', + description: 'Feed Heater Still Enabled', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // POST-STORM AND DOCUMENTATION + // ============================================================ + { + id: 'post-storm-baseline-quiz', + nice: ['T0153', 'K0741'], + title: 'Post-Storm Recovery', + description: 'Confirm what you expect as the front clears.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['sustained-monitor'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Post-Storm Expectation', + params: { + character: Character.SYSTEM, + question: 'The rain is clearing. What do you expect to see in the next few minutes?', + options: [ + 'C/N recovers toward baseline; AGC backs its gain down; modem lock unchanged', + 'C/N stays depressed - the link has been permanently degraded', + 'AGC stays at max because it cannot reset', + 'Modem must be manually re-locked', + ], + correctIndex: 0, + explanation: 'Rain fade is transient. The AGC tracks the recovery downward without intervention, and the demod has had lock the whole time. Nothing to do but watch it normalize.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'document-handover-avoided', + nice: ['K0645', 'T0153', 'S0593'], + title: 'Log the Hold', + description: 'Select the correct entry for the shift log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['post-storm-baseline-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry correctly logs this event for the next shift?', + options: [ + 'Moderate rain over VT-01, ~3 dB fade. Held TM-1 service per customer SLA preference; AGC max 3 dB, modem lock maintained throughout, no handover.', + 'Emergency handover to ME-02 due to rain.', + 'Lost lock on TM-1 during storm; service restored after storm cleared.', + 'No weather event; routine shift.', + ], + correctIndex: 0, + explanation: 'Log what happened, what you chose, why, and the evidence the choice was correct. Next operator picks up with the trigger thresholds you were watching.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 13:18] +

+

+ "Rain front in about five minutes. Light to moderate, twenty minutes through. James from SeaLink called - their SLA likes margin loss better than handover events, so he wants us to hold if we can. ME-02 is busy on TM-2. Brief's on your terminal." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/14/intro.mp3'), + }, + objectives: { + 'acknowledge-customer-constraint': { + text: ` +

+ Hey - it's James over at SeaLink. Saw your weather notice on the portal. Look, every handover event lights up the contract dashboard for our board, and we'd rather take a little ride through a few dB than have that show up. +

+

+ Hold us on Vermont if you can. If you can't, you can't - we'll wear it. But your call, not a marketing call. +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/14/obj-acknowledge-customer-constraint.mp3'), + }, + 'handover-threshold-quiz': { + text: ` +

+ AGC's behaving, modem's locked, customer wants the hold. I see no reason to escape. Make the call and tell me what you'd flip on. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/14/obj-handover-threshold-quiz.mp3'), + }, + 'document-handover-avoided': { + text: ` +

+ Clean ride through. Log it with the trigger thresholds you were watching - if it'd gone the other way, the next operator should know what we were looking at. Good shift. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/14/obj-document-handover-avoided.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario15.ts b/src/campaigns/nats/scenario15.ts new file mode 100644 index 00000000..3550e769 --- /dev/null +++ b/src/campaigns/nats/scenario15.ts @@ -0,0 +1,889 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dBm, FECType, Hertz, IfFrequency, MHz, ModulationType } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite, tidemark3Satellite } from './satellites'; + +/** + * NATS Level 15: "Frequency Coordination" + * + * Phase: Qualified Operations (Phase 2, Scenario 7 of 8) + * Time Pressure: Moderate (RedSky's uplink window opens in 25 minutes) + * Calculation Required: YES - guard-band arithmetic + * New Mechanic Introduced: External-operator coordination (not present in S1-S14) + * + * NICE Framework Alignment: + * Primary Codes: + * - K0737: Knowledge of bandwidth management tools and techniques + * - T1143: Develop network backup and recovery procedures + * - S0648: Skill in detecting anomalies + * + * Supporting Codes: + * - K0792: Knowledge of network configurations + * - K0773: Knowledge of telecommunications principles and practices + * - S0593: Skill in handling incidents + * - K0645: Knowledge of standard operating procedures (SOPs) + * - S0421: Skill in operating network equipment + * - K0721: Knowledge of risk management principles and practices + * + * Premise: A coordination notice has arrived from RedSky Teleport. Their next + * scheduled uplink will sit on TIDEMARK-3's V-pol cross-pol slot at 5961 MHz + * center, 8 MHz BW. Edge-to-edge guard band against our active SeaLink carrier + * (5985 MHz center, 36 MHz BW H-pol on TP-1) is only 2 MHz. + * + * VT-01's HPA is currently running at 5 dB backoff - close enough to saturation + * that third-order IMD products extend well into RedSky's planned slot. Polarization + * isolation alone won't save us at this distance. The player must verify the guard + * band, recognize the IMD risk, raise HPA backoff to 10 dB, confirm the customer + * carrier is still nominal, and send a clean coordination confirmation back to RedSky. + * + * S5 was "someone else interfered with us." S15 reverses the perspective: make sure + * we are not interfering with them. The mitigation is the HPA backoff lever - the + * same control surface used in S2 and S9, but exercised for a coordination reason + * rather than amplifier protection. + * + * Tone: Qualified-operator. Dana opens with a text-message notice, checks in at the + * mitigation point, and signs off when the confirmation goes back to RedSky. Every + * status-check uses Character.SYSTEM. RedSky's coordinator is rendered through + * SYSTEM quizzes and objective descriptions, not as a new character. + * + * Story Continuity: + * - Charlie is in Europe; not involved. + * - Dana is the only named voice. + * - RedSky is a partner teleport, not part of the NATS roster. + * - TIDEMARK-3 is the customer-traffic bird here (carrying SeaLink). + */ + +export const scenario15Data: ScenarioData = { + id: 'nats-scenario15', + prerequisiteScenarioIds: ['nats-scenario14'], + url: 'nats/scenarios/nats-scenario15', + imageUrl: 'nats/15/card.png', + number: 15, + title: 'Frequency Coordination', + subtitle: 'Inter-Operator Spectrum Etiquette', + duration: '25-30 min', + difficulty: 'intermediate', + missionType: 'Spectrum Coordination', + description: `A coordination notice from RedSky Teleport hit your inbox at 09:08. Their next scheduled uplink sits just 2 MHz from the edge of the SeaLink carrier currently riding TIDEMARK-3 TP-1. They want acknowledgment that our spectrum won't bleed into their slot when they light up in 25 minutes.

Standard inter-operator etiquette: verify the guard band, check that our transmit chain isn't producing IMD products that would land in the neighbor's passband, document, and confirm back. If our skirts are dirty, clean them up before they go on the air.

This isn't combat. It's good neighbor work.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End (TX active)', + 'Spectrum Analyzer', + 'TX Modem (carrying SeaLink traffic on TIDEMARK-3)', + 'HPA (online)', + ], + timeLimitSeconds: 30 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + antennasState: [ + { + ...vermontGroundStation.antennasState[0], + azimuth: 140.5 as Degrees, + elevation: 37.8 as Degrees, + polarization: 8 as Degrees, + targetSatelliteId: 61527, // TIDEMARK-3 + targetAzimuth: 140.5 as Degrees, + targetElevation: 37.8 as Degrees, + targetPolarization: 8 as Degrees, + beaconFrequencyHz: 1078e6 as Hertz, // TM-3 beacon IF (5250 - 4172) + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // SeaLink customer traffic running on TIDEMARK-3 TP-1. + // HPA at 5 dB backoff - close enough to compression that third-order + // IMD products extend several MHz beyond the 36 MHz carrier edges. + // This is the latent defect: clean carrier in isolation, dirty + // adjacent-channel emissions that threaten RedSky's planned slot. + hpa: { + backOff: 5, + isHpaEnabled: true, + isHpaSwitchEnabled: true, + }, + buc: { + isMuted: false, + loFrequency: 7000 as MHz, + }, + }), + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + isTransmitting: true, + isTransmittingSwitchUp: true, + ifSignal: { + ...vermontGroundStation.transmitters[0].modems[0].ifSignal, + frequency: 1015e6 as IfFrequency, // TM-3 TX IF: 7000 - 5985 + bandwidth: 36e6 as Hertz, + power: -7 as dBm, + modulation: 'QPSK' as ModulationType, + fec: '3/4' as FECType, + signalId: 'TIDEMARK-3-Teleport', + noradId: 61527, + polarization: 'H', + }, + }, + ], + }, + ], + receivers: [ + { + ...vermontGroundStation.receivers[0], + modems: [ + { + ...vermontGroundStation.receivers[0].modems[0], + frequency: 1490 as MHz, // TIDEMARK-3 downlink IF (5250 - 3760) + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + }, + ], + }, + ], + spectrumAnalyzers: [ + { + ...vermontGroundStation.spectrumAnalyzers[0], + // Default state: tuned to TM-3 beacon for routine watch + centerFrequency: 1078e6 as Hertz, + span: 2e3 as Hertz, + }, + ], + }, + ], + satellites: [ + tidemark3Satellite, + tidemark1Satellite, + tidemark2Satellite, + ses10Satellite, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-15?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // PHASE 1: MISSION PREP + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Coordination Notice', + description: 'Open the shift brief and the inbound RedSky coordination request.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Ready to Begin', + params: { + character: Character.SYSTEM, + question: 'Have you reviewed the brief and the RedSky notice?', + options: ['Brief reviewed. Notice acknowledged. Starting coordination check.'], + correctIndex: 0, + explanation: 'Coordination window opens in 25 minutes. Plenty of time for a clean check.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 2: PROCESS COORDINATION NOTICE + // ============================================================ + { + id: 'identify-partner-band', + nice: ['K0737', 'S0648'], + title: 'Identify RedSky Occupied Band', + description: 'Confirm the RF range RedSky will occupy when their carrier lights up.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Partner Band', + params: { + character: Character.SYSTEM, + question: 'RedSky\'s notice: center 5961 MHz, bandwidth 8 MHz, V-pol on the TIDEMARK-3 cross-pol slot. What RF range will they occupy?', + options: [ + '5957 - 5965 MHz', + '5961 - 5969 MHz', + '5953 - 5969 MHz', + '5957 - 5961 MHz', + ], + correctIndex: 0, + explanation: 'Center plus or minus half-bandwidth: 5961 plus or minus 4 MHz equals 5957 to 5965 MHz. Their upper edge is 5965.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'identify-our-band', + nice: ['K0737', 'K0773'], + title: 'Identify Our Occupied Band', + description: 'Confirm the RF range our active TIDEMARK-3 TP-1 carrier currently occupies.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['identify-partner-band'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Our Band', + params: { + character: Character.SYSTEM, + question: 'Our SeaLink carrier on TIDEMARK-3 TP-1 is center 5985 MHz, 36 MHz BW, H-pol. What RF range are we occupying?', + options: [ + '5967 - 6003 MHz', + '5949 - 6021 MHz', + '5985 - 6021 MHz', + '5967 - 5985 MHz', + ], + correctIndex: 0, + explanation: '5985 plus or minus 18 MHz equals 5967 to 6003 MHz. Our lower edge is 5967.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'compute-guard-band', + nice: ['K0737', 'S0648'], + title: 'Compute Guard Band', + description: 'Determine the edge-to-edge guard band between RedSky\'s upper edge and our lower edge.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['identify-our-band'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Guard Band', + params: { + character: Character.SYSTEM, + question: 'RedSky upper edge: 5965 MHz. Our lower edge: 5967 MHz. What is the edge-to-edge guard band?', + options: [ + '2 MHz', + '4 MHz', + '24 MHz', + '0 MHz - we overlap', + ], + correctIndex: 0, + explanation: '5967 minus 5965 equals 2 MHz. Cross-pol isolation buys us additional margin, but the frequency gap is tight.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'assess-guard-adequacy', + nice: ['K0737', 'K0721'], + title: 'Assess Guard Band Adequacy', + description: 'Decide whether 2 MHz edge-to-edge with our current TX configuration is good enough.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['compute-guard-band'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Guard Adequacy', + params: { + character: Character.SYSTEM, + question: 'A 2 MHz edge-to-edge guard with about 28 dB cross-pol isolation. What is the right next step?', + options: [ + 'Verify our TX chain is producing clean spectrum - no spurs or IMD landing in RedSky\'s band', + 'Demand RedSky retune their carrier', + 'Increase our TX power so RedSky\'s carrier is irrelevant to us', + 'Take no action - 2 MHz guard plus cross-pol isolation is always sufficient', + ], + correctIndex: 0, + explanation: 'Guard-band math is necessary but not sufficient. Polarization isolation only buys margin if our own spectrum is clean. Inspect the TX chain.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: INSPECT TX CONFIGURATION + // ============================================================ + { + id: 'open-tx-chain', + nice: ['S0421'], + title: 'Open TX Chain', + description: 'Navigate to the TX Chain tab to inspect the current transmit configuration.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['assess-guard-adequacy'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'check-tx-modem-frequency', + nice: ['K0792', 'K0773'], + title: 'Verify TX Modem Configuration', + description: 'Confirm the TX modem is on the expected TIDEMARK-3 uplink IF.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['open-tx-chain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'tx-modem-frequency-set', + description: 'TX Modem at 1015 MHz IF', + params: { + frequency: 1015e6, + frequencyTolerance: 1e6, + }, + mustMaintain: true, + }, + { + type: 'tx-modem-bandwidth-set', + description: 'TX Modem at 36 MHz BW', + params: { + bandwidth: 36e6, + bandwidthTolerance: 1e6, + }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'TX IF Sanity Check', + params: { + character: Character.SYSTEM, + question: 'BUC LO 7000 MHz, RF target 5985 MHz. Does the displayed TX IF match the expected value?', + options: [ + 'Yes - 1015 MHz IF matches 7000 minus 5985, carrier on the right slot', + 'No - TX IF should be at 5985 MHz', + 'No - TX IF should be at 1057 MHz', + 'Cannot determine without checking the receiver', + ], + correctIndex: 0, + explanation: 'TX IF equals BUC LO minus uplink RF: 7000 minus 5985 equals 1015 MHz. The TX modem is on the right slot.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'observe-current-hpa-backoff', + nice: ['K0773', 'S0648'], + title: 'Observe HPA Backoff', + description: 'Note the current HPA backoff and reason about its impact on spectral cleanliness.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['check-tx-modem-frequency'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Read Current Backoff', + params: { + character: Character.SYSTEM, + question: 'The HPA is currently at 5 dB backoff. What does that imply for the transmit spectrum?', + options: [ + 'Operating close to saturation - third-order IMD products will be elevated and extend several MHz beyond the carrier edges', + 'Operating with comfortable margin - IMD will be well below the noise floor', + 'Operating in pure linear region - no IMD generated', + 'Backoff has no bearing on adjacent-channel emissions', + ], + correctIndex: 0, + explanation: '5 dB is a marginal backoff for a modulated carrier. Third-order IMD scales aggressively as the HPA approaches compression - spectral skirts widen and adjacent-channel power rises.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'understand-imd-mechanism', + nice: ['K0773', 'K0737'], + title: 'Understand IMD Mechanism', + description: 'Confirm your understanding of how HPA compression generates adjacent-channel energy.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['observe-current-hpa-backoff'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'IMD Mechanism', + params: { + character: Character.SYSTEM, + question: 'Why does a wideband modulated carrier driven near HPA saturation produce energy outside the nominal channel?', + options: [ + 'Nonlinearity in the amplifier mixes spectral components, generating intermodulation products that fall just outside the carrier edges', + 'The wideband carrier physically expands due to thermal effects in the waveguide', + 'The BUC LO drifts when the HPA heats up under heavy drive', + 'The antenna feed re-radiates energy into adjacent passbands', + ], + correctIndex: 0, + explanation: 'Near saturation, the HPA stops behaving linearly. Spectral components mix and produce IMD - the dominant third-order products fall just outside the carrier edges and drop with frequency offset.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: MITIGATION DECISION + // ============================================================ + { + id: 'evaluate-mitigation-options', + nice: ['S0593', 'K0721'], + title: 'Choose Mitigation', + description: 'Pick the right mitigation. Customer traffic must stay up; RedSky goes live in 25 minutes.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['understand-imd-mechanism'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Mitigation Choice', + params: { + character: Character.SYSTEM, + question: 'SeaLink traffic must continue. Which mitigation is the right tool?', + options: [ + 'Increase HPA backoff to 10 dB - reduces IMD without dropping the carrier and without requiring customer coordination', + 'Cut TX modem power to zero - simplest fix', + 'Retune our TIDEMARK-3 carrier to avoid RedSky entirely - requires SeaLink SLA work', + 'Engage a notch filter on the RX side at 5961 MHz - protects our receiver', + ], + correctIndex: 0, + explanation: 'Backoff is the correct lever: it linearizes the amplifier, suppresses IMD, and is reversible. Cutting power kills the customer. Retuning needs SLA renegotiation. An RX notch does nothing for the neighbor.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // PHASE 5: APPLY MITIGATION + // ============================================================ + { + id: 'increase-hpa-backoff', + nice: ['S0593', 'K0792'], + title: 'Increase HPA Backoff to 10 dB', + description: 'Raise the HPA backoff to 10 dB to clean up the spectral skirts.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['evaluate-mitigation-options'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-back-off-set', + description: 'HPA Backoff at 10 dB', + params: { + backOff: 10, + backOffTolerance: 1, + }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 20, + }, + { + id: 'verify-hpa-still-online', + nice: ['T0431', 'K0740'], + title: 'Verify HPA Still Online', + description: 'Confirm the HPA is enabled, transmitting, and not overdriven after the backoff change.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['increase-hpa-backoff'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Enabled', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Operating Linearly', + mustMaintain: true, + }, + { + type: 'tx-modem-transmitting', + description: 'Customer Traffic Still Flowing', + params: { isTransmitting: true }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 6: VERIFY ON SPECTRUM + // ============================================================ + { + id: 'open-rx-analysis', + nice: ['S0421'], + title: 'Open RX Analysis', + description: 'Switch to the RX Analysis tab to use the spectrum analyzer.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-hpa-still-online'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'tune-speca-to-downlink', + nice: ['K0737', 'S0421'], + title: 'Tune Spectrum to TIDEMARK-3 Downlink', + description: 'Reconfigure the spectrum analyzer to view the TIDEMARK-3 downlink IF region (1490 MHz with VT-01 LNB LO at 5250 and downlink RF 3760). Use a wide span so RedSky\'s adjacent slot is visible too.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['open-rx-analysis'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Center 1490 MHz IF', + params: { + centerFrequency: 1490e6 as Hertz, + centerFrequencyTolerance: 5e6, + }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'speca-span-set', + description: 'Span Wide Enough for Adjacent Slot', + params: { + span: 75e6, + frequencyTolerance: 25e6, + }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'speca-rbw-set', + description: 'RBW Auto', + params: { rbw: null as unknown as number }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'speca-min-amplitude', + description: 'Min Amplitude near -100 dBm', + params: { + minAmplitude: -100 as dBm, + minAmplitudeTolerance: 15, + }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'speca-max-amplitude', + description: 'Max Amplitude near -30 dBm', + params: { + maxAmplitude: -30 as dBm, + maxAmplitudeTolerance: 15, + }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'confirm-carrier-still-nominal', + nice: ['T0153', 'K0740'], + title: 'Confirm Carrier Still Nominal', + description: 'Verify the SeaLink TIDEMARK-3 downlink is still healthy after the backoff change.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['tune-speca-to-downlink'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Carrier Still Up', + params: { + character: Character.SYSTEM, + question: 'With HPA backoff now at 10 dB, what should be true about the TIDEMARK-3 downlink at 1490 MHz IF?', + options: [ + 'Wideband carrier still present at slightly reduced power - customer link healthy, IMD skirts dropped well below the adjacent slot noise floor', + 'Carrier gone - the backoff change effectively muted the BUC', + 'Carrier doubled in width - backoff caused bandwidth expansion', + 'Beacon disappeared - HPA backoff affects beacon visibility', + ], + correctIndex: 0, + explanation: 'Backing off the HPA reduces output power a few dB and dramatically suppresses IMD. Customer link remains operational with cleaner spectral skirts.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-receiver-locked', + nice: ['T0153', 'K0741'], + title: 'Verify Receiver Locked', + description: 'Confirm the customer-traffic receiver is still demodulating cleanly.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['confirm-carrier-still-nominal'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 7: COORDINATE BACK + // ============================================================ + { + id: 'confirm-spectrum-clean-for-partner', + nice: ['S0648', 'K0792'], + title: 'Draft Confirmation to RedSky', + description: 'Decide what to send back to RedSky\'s coordinator.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-receiver-locked'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Ready to Confirm', + params: { + character: Character.SYSTEM, + question: 'After raising HPA backoff to 10 dB, what is the correct status to send back to RedSky?', + options: [ + 'Confirmed clear. TIDEMARK-3 TP-1 carrier holds 5967-6003 MHz H-pol with adjacent-channel emissions well below your planned slot. Proceed with your 5961 MHz V-pol uplink as scheduled.', + 'Confirmed clear. We have muted our carrier to give you the spectrum.', + 'Cannot confirm. Recommend RedSky delay their uplink until further notice.', + 'Confirmed clear, but we will be retuning to 5950 MHz to give wider margin.', + ], + correctIndex: 0, + explanation: 'Factual coordination response: state your occupied band, confirm adjacent-channel emissions are below the neighbor\'s threshold, and clear them to proceed.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 8: LOG + // ============================================================ + { + id: 'log-coordination-event', + nice: ['K0645', 'T1143'], + title: 'Log Coordination Event', + description: 'Select the correct shift log entry for this coordination.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['confirm-spectrum-clean-for-partner'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry correctly records this event for the next shift?', + options: [ + '0937 - RedSky coordination notice received and confirmed. TIDEMARK-3 TP-1 HPA backoff raised from 5 to 10 dB to suppress adjacent-channel IMD. Cleared RedSky for 5961 MHz V-pol uplink. SeaLink carrier remains nominal.', + '0937 - RedSky coordination request denied. Recommended they relocate to a different orbital slot.', + '0937 - No action taken. RedSky\'s uplink does not affect us.', + '0937 - TIDEMARK-3 SeaLink customer traffic dropped to accommodate RedSky.', + ], + correctIndex: 0, + explanation: 'The log captures what we did, why, and what we cleared. Next shift picks up with full context if RedSky\'s uplink behaves unexpectedly.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:12] +

+

+ "Coordination notice in your inbox - RedSky Teleport. They're lighting up a V-pol carrier on TM-3 in 25 minutes, 2 MHz off our edge. Verify our spectrum is clean and send the confirmation back. Routine. Don't drop the SeaLink carrier." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/15/intro.mp3'), + }, + objectives: { + 'compute-guard-band': { + text: ` +

+ 2 MHz edge-to-edge plus cross-pol. Tight but workable - if our skirts are clean. Check the TX chain. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/15/obj-compute-guard-band.mp3'), + }, + 'increase-hpa-backoff': { + text: ` +

+ 10 dB backoff is standard for shared-spectrum work. RedSky's coordinator will see the difference on their end before they ever key up. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/15/obj-increase-hpa-backoff.mp3'), + }, + 'log-coordination-event': { + text: ` +

+ Confirmation sent to RedSky. Good neighbor work. Next coordination notice goes through the same process - they'll know to expect a clean response from this station. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/15/obj-log-coordination-event.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario16.ts b/src/campaigns/nats/scenario16.ts new file mode 100644 index 00000000..134371b3 --- /dev/null +++ b/src/campaigns/nats/scenario16.ts @@ -0,0 +1,900 @@ +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { maineGroundStation, vermontGroundStation } from './ground-stations'; +import { tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 16: "Cascade Failure" + * + * Phase: Qualified Operations (Phase 2 capstone, Scenario 8 of 8) + * Time Pressure: Moderate-high (35-45 min, customer pressure) + * Calculation Required: NO - this is multi-fault recovery + * New UI Elements: None - reuses all Phase 1 mechanics + * + * NICE Framework Alignment: + * Primary Codes: + * - T0531: Troubleshoot hardware/software interoperability problems + * - S0677: Skill in recovering failed systems + * - S0807: Skill in solving problems + * + * Supporting Codes: + * - T1538: Resolve customer-reported system incidents + * - S0593: Skill in handling incidents + * - T0081: Diagnose network connectivity problems + * - T1606: Prepare impact reports + * - K0645: Knowledge of standard operating procedures (SOPs) + * - K0741: Knowledge of system availability measures + * + * Premise: Two minutes into what should have been a routine shift, three + * unrelated faults converge on VT-01 simultaneously: + * 1. BUC over-temperature (>70°C, high current draw - needs a mute to + * cool down) + * 2. LNB reference unlock (sticky fault, clears on power cycle) + * 3. HPA backoff drift (back-off dropped to 2 dB, HPA is overdriven) + * + * SeaLink's James Okafor calls about service degradation. Dana is in the + * admin office and tells the player to take the lead. The player must + * prioritize correctly: RF safety first (disable the overdriven HPA output, + * THEN mute the BUC - never leave an enabled HPA without drive, per the + * S2 sequencing rule), customer impact next (stabilize the receive side), + * equipment health last (methodical recovery of each subsystem). + * + * Story Continuity (Phase 2 capstone): + * - Charlie remains in Europe. + * - Dana is the on-site supervisor but stays hands-off this shift. + * - James Okafor is the customer voice carrying SLA pressure. + * - ME-02 (Catherine) carries TIDEMARK-2 in the background, unaffected. + * + * Mechanical reuse: + * - S7 BUC fault management and loopback-state thinking + * - S8 LNB power-cycle recovery and intermittent-fault triage discipline + * - S2 HPA configuration and RF safety sequencing + * - S3 alarm-prioritization framework, scaled to multi-fault + * - S5 documentation discipline + */ + +export const scenario16Data: ScenarioData = { + id: 'nats-scenario16', + prerequisiteScenarioIds: ['nats-scenario15'], + url: 'nats/scenarios/nats-scenario16', + imageUrl: 'nats/16/card.png', + number: 16, + title: 'Cascade Failure', + subtitle: 'Multi-System Recovery Under Customer Pressure', + duration: '35-45 min', + difficulty: 'advanced', + missionType: 'Incident Response', + description: `Two minutes into your shift and the alarm board is lit up. BUC over-temperature, LNB reference unlocked, HPA overdriven - all on VT-01, all at once. The TIDEMARK-1 customer link is degraded and James Okafor from SeaLink is on the line.

None of these faults are related. You have to triage them in the right order: protect the equipment and the spectrum first, restore customer impact next, then methodically clear each fault.

Dana is in the admin office. She trusts you with this one.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'ME-02: Operational (TIDEMARK-2)', + ], + timeLimitSeconds: 45 * 60, // 45 minutes + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // BUC: over-temperature, drawing high current, not yet muted + buc: { + isPowered: true, + isMuted: false, + isLoopback: false, + loFrequency: 7000 as MHz, + isExtRefLocked: true, + gain: 23 as dB, + temperature: 72, // Over the 70°C alarm threshold + currentDraw: 4.8, // Above 4.5 A alarm threshold + }, + // HPA: backoff has drifted down, output is overdriven + hpa: { + isPowered: true, + isHpaEnabled: true, + isHpaSwitchEnabled: true, + backOff: 2, // Below 3 dB threshold -> isOverdriven true + outputPower: 53 as dBm, + isOverdriven: true, + imdLevel: -26, // Poor IMD due to overdrive + temperature: 60, + }, + // LNB: sticky reference unlock fault, clears on power cycle + lnb: { + isPowered: true, + loFrequency: 5250 as MHz, + gain: 65 as dB, + isExtRefLocked: false, + hasRefLockFault: true, + noiseTemperature: 55, + temperature: 30, + }, + }), + ], + }, + // ME-02 carrying TIDEMARK-2 in the background - unaffected by VT-01 faults + { + ...maineGroundStation, + isOperational: true, + }, + ], + satellites: [tidemark1Satellite, tidemark2Satellite], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-16?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Incident Brief', + description: 'Open the incident brief and acknowledge you are taking the lead on the response.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Incident Brief Reviewed', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Take the Lead', + params: { + character: Character.SYSTEM, + question: 'Three concurrent alarms on VT-01 and SeaLink is on the line. How will you proceed?', + options: [ + 'Triage all alarms first, then act in priority order: RF safety, customer impact, equipment health', + 'Fix whichever fault is easiest to clear first to reduce the alarm count', + 'Wake Dana and wait for direction before touching anything', + 'Power cycle the entire RF chain to reset everything at once', + ], + correctIndex: 0, + explanation: 'Triage before action. Knowing all three faults lets you pick the order that protects equipment and the spectrum first.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 1: TRIAGE AND PRIORITIZATION + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'triage-dashboard-alarms', + nice: ['T0153', 'K0741', 'S0593'], + title: 'Triage the Alarm Board', + description: 'Open the Dashboard and confirm what is actually broken.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Identify Concurrent Faults', + params: { + character: Character.SYSTEM, + question: 'Which alarms are active on VT-01 right now?', + options: [ + 'BUC over-temperature and high current, LNB reference unlocked, HPA overdriven', + 'BUC reference unlock and LNB high current draw only', + 'HPA over-temperature and antenna tracking lost', + 'GPSDO holdover and modem fault on TX Modem 1', + ], + correctIndex: 0, + explanation: 'Three concurrent, unrelated alarms. The BUC and HPA conditions threaten the equipment and the uplink spectrum; the LNB condition is degrading the customer receive.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'prioritize-recovery-order', + nice: ['S0807', 'K0721', 'S0593'], + title: 'Set the Recovery Order', + description: 'Decide the order of operations before touching any equipment.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['triage-dashboard-alarms'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Recovery Priority', + params: { + character: Character.SYSTEM, + question: 'In what order should the three faults be addressed?', + options: [ + 'RF safety first (HPA overdrive + BUC) → customer impact next (LNB / RX) → final verification', + 'LNB first because the customer is calling', + 'BUC first because the over-temperature could be permanent damage', + 'Whichever one is easiest to clear to reduce the alarm count', + ], + correctIndex: 0, + explanation: 'An overdriven HPA is putting a dirty signal on the spectrum and risking the amplifier. Disable the HPA output, mute the BUC for cooldown, fix the backoff, then turn to the LNB to restore the customer downlink.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: RF SAFETY - STOP THE OVERDRIVEN UPLINK + // ============================================================ + { + id: 'navigate-tx-chain', + nice: ['S0421'], + title: 'Open TX Chain', + description: 'Navigate to the TX Chain panel.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['prioritize-recovery-order'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'disable-hpa-for-safety', + nice: ['S0593', 'S0677'], + title: 'Disable the HPA Output', + description: 'Disable the HPA output to take the overdriven signal off the antenna. Amplifier comes down before its drive does - same sequencing rule as a planned power-down.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['navigate-tx-chain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-disabled', + description: 'HPA Output Disabled', + maintainUntilObjectiveComplete: true, + }, + { + type: 'status-check', + description: 'Why HPA First', + params: { + character: Character.SYSTEM, + question: 'Why is disabling the HPA output the correct first action - before muting the BUC?', + options: [ + 'It takes the dirty uplink off the air immediately, and an enabled HPA must never be left without BUC drive - it would amplify raw noise into the feed', + 'The HPA controls the BUC - disabling it automatically mutes the BUC too', + 'Disabling the HPA clears the LNB reference fault as a side effect', + 'Order does not matter as long as both end up off', + ], + correctIndex: 0, + explanation: 'Same rule as every planned power-down: amplifier off before drive off. Killing the HPA stops the overdriven signal AND keeps the chain safe for the BUC mute that comes next. Mute the BUC first and the enabled HPA amplifies noise into the feed.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'mute-buc-for-cooldown', + nice: ['S0677', 'T1314'], + title: 'Mute the BUC', + description: 'With the HPA output disabled, mute the BUC so the over-temperature module can begin cooling.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['disable-hpa-for-safety'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-muted', + description: 'BUC Muted', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'diagnose-hpa-overdrive', + nice: ['T0081', 'T0531'], + title: 'Diagnose HPA Overdrive', + description: 'Identify why the HPA was overdriven before you touch any more controls.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['mute-buc-for-cooldown'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'HPA Overdrive Cause', + params: { + character: Character.SYSTEM, + question: 'The HPA back-off has drifted to 2 dB. What does that mean for the output?', + options: [ + 'Output is too close to saturation - IMD products are rising and the amplifier is at risk', + 'Output is too low - the amplifier is starving and will trip offline', + 'Output is correct - 2 dB is standard back-off for full-power uplinks', + 'Output is muted automatically by the HPA controller', + ], + correctIndex: 0, + explanation: 'Below 3 dB back-off, the HPA is overdriven. IMD products spill into adjacent transponders and the amplifier wears prematurely. Restore back-off to the standard 10 dB margin.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'correct-hpa-backoff', + nice: ['S0677', 'T1567'], + title: 'Restore HPA Back-off', + description: 'Set the HPA back-off to the standard 10 dB operating margin.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['diagnose-hpa-overdrive'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-back-off-set', + description: 'HPA Back-off at 10 dB', + params: { + backOff: 10, + backOffTolerance: 1, + }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA No Longer Overdriven', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // PHASE 3: BUC THERMAL RECOVERY + // ============================================================ + { + id: 'wait-for-buc-cooling', + nice: ['T1314', 'K0740'], + title: 'Allow BUC to Cool', + description: 'With the BUC muted, the temperature will trend down. Wait for it to return below the 70°C threshold.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['correct-hpa-backoff'], + timeLimitSeconds: 5 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-muted', + description: 'BUC Stays Muted While Cooling', + mustMaintain: true, + }, + { + type: 'buc-temperature-normal', + description: 'BUC Temperature Below 70°C', + params: { maxTemperature: 70 }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-current-normal', + description: 'BUC Current Draw Normal', + params: { maxCurrentDraw: 4.5 }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: LNB REFERENCE LOCK RECOVERY + // ============================================================ + { + id: 'navigate-rx-analysis', + nice: ['S0421'], + title: 'Open RX Analysis', + description: 'Switch to the RX Analysis panel to work the LNB fault.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['wait-for-buc-cooling'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'diagnose-lnb-fault', + nice: ['T0081', 'S0582'], + title: 'Diagnose LNB Reference Unlock', + description: 'Confirm what kind of LNB fault you are looking at before acting.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['navigate-rx-analysis'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'LNB Fault Type', + params: { + character: Character.SYSTEM, + question: 'The GPSDO is locked and the LNB is powered, but the LNB shows reference unlocked. What is the right corrective action?', + options: [ + 'Power cycle the LNB to clear the sticky reference lock fault', + 'Replace the LNB - the reference input has failed', + 'Increase LNB gain to compensate for the unlock', + 'Switch the LNB to internal reference mode', + ], + correctIndex: 0, + explanation: 'A sticky reference lock fault clears on power cycle. The 10 MHz reference is available upstream (GPSDO locked); the LNB just needs to re-acquire.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'power-cycle-lnb', + nice: ['S0677', 'T1567'], + title: 'Power Cycle the LNB', + description: 'Power off the LNB, then power it back on. The sticky fault will clear and the LNB will re-acquire the reference.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['diagnose-lnb-fault'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'equipment-powered', + description: 'LNB Powered', + params: { equipment: 'lnb' }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'lnb-reference-locked', + description: 'LNB Reference Locked', + maintainUntilObjectiveComplete: true, + }, + { + type: 'lnb-thermally-stable', + description: 'LNB Thermally Stable', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-rx-locked', + nice: ['T0153', 'K0740'], + title: 'Verify Receiver Locked', + description: 'Confirm the receiver has reacquired TIDEMARK-1 with a healthy C/N margin.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['power-cycle-lnb'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + maintainUntilObjectiveComplete: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Above Operating Threshold', + params: { modemNumber: 1, minCNRatio: 8, requiresObservation: true, observationTab: 'rx-analysis' }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 5: TX RESTORATION + // ============================================================ + { + id: 'return-to-tx-chain', + nice: ['S0421'], + title: 'Return to TX Chain', + description: 'Switch back to the TX Chain panel to bring the uplink back online.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-rx-locked'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'unmute-buc-restore-tx', + nice: ['S0677', 'T1567'], + title: 'Unmute the BUC', + description: 'With back-off corrected and the BUC cooled, unmute the BUC first - drive comes up before the amplifier.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['return-to-tx-chain'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-unmuted', + description: 'BUC Unmuted', + maintainUntilObjectiveComplete: true, + }, + { + type: 'buc-temperature-normal', + description: 'BUC Temperature Still Normal', + params: { maxTemperature: 70 }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-tx-output-clean', + nice: ['T0153', 'K0740'], + title: 'Restore HPA Output', + description: 'Re-enable the HPA output now that the BUC is driving again, and confirm the uplink is clean.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['unmute-buc-restore-tx'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Enabled', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Within Operating Limits', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Spectrum Posture Confirmed', + params: { + character: Character.SYSTEM, + question: 'With back-off at 10 dB and the BUC cool, what does the uplink look like to neighboring transponders now?', + options: [ + 'Clean - IMD products are back below coordination limits and we are no longer interfering', + 'Still overdriven - back-off does not affect IMD on the spectrum', + 'Inverted polarization - the mute/unmute cycle flipped the OMT', + 'Reduced power - the customer link cannot meet SLA at 10 dB back-off', + ], + correctIndex: 0, + explanation: 'Standard 10 dB back-off keeps IMD products well below the coordination floor while still meeting SLA power.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 6: VERIFICATION AND CLOSE-OUT + // ============================================================ + { + id: 'final-alarm-sweep', + nice: ['T0153', 'K0741'], + title: 'Final Dashboard Sweep', + description: 'Confirm the alarm board is clean before notifying the customer.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-tx-output-clean'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'buc-temperature-normal', + description: 'BUC Thermal Normal', + params: { maxTemperature: 70 }, + mustMaintain: true, + }, + { + type: 'buc-current-normal', + description: 'BUC Current Normal', + params: { maxCurrentDraw: 4.5 }, + mustMaintain: true, + }, + { + type: 'lnb-reference-locked', + description: 'LNB Reference Locked', + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Within Limits', + mustMaintain: true, + }, + { + type: 'status-check', + description: 'All Clear', + params: { + character: Character.SYSTEM, + question: 'Summarize the post-recovery state of VT-01.', + options: [ + 'All three faults cleared - BUC thermal normal, LNB locked, HPA within back-off - link operational', + 'BUC cleared, LNB still degraded - acceptable to return to service', + 'HPA cleared, BUC and LNB still degraded - mark as partial recovery', + 'All faults still active - escalate immediately', + ], + correctIndex: 0, + explanation: 'Clean board. All three independent faults addressed in priority order. Ready to brief the customer.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'customer-notification', + nice: ['T1538', 'S0478'], + title: 'Notify the Customer', + description: 'Choose the right message to send back to SeaLink.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['final-alarm-sweep'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Customer Message', + params: { + character: Character.SYSTEM, + question: 'James Okafor is still on the line. Which message is the right one?', + options: [ + 'Three concurrent faults identified and cleared in priority order. Link is operational. Will follow up with a written impact report within the hour.', + 'Everything looks fine on our end. Probably a problem on your side.', + 'We had a major outage and the link is still degraded. Stand by.', + 'Equipment failure - we are escalating to vendor support and have no ETA.', + ], + correctIndex: 0, + explanation: 'Acknowledge the impact, state what was done, commit to written follow-up. Customer comms during incidents is half technical, half credibility.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-cascade-event', + nice: ['T1606', 'K0645'], + title: 'Log the Cascade Event', + description: 'Select the correct entry for the incident log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['customer-notification'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Incident Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which log entry correctly captures this incident?', + options: [ + 'Concurrent unrelated faults VT-01: BUC over-temp (72°C, high I), LNB ref unlock (sticky), HPA back-off drift (2 dB). Recovery in order: HPA output disabled, BUC muted for cooldown, back-off restored to 10 dB, LNB power cycled, BUC unmuted, HPA restored. Customer (SeaLink) notified.', + 'VT-01 alarms cleared. Routine maintenance complete.', + 'Single fault: HPA failure. Replaced amplifier.', + 'Customer reported issue - no fault found at ground station.', + ], + correctIndex: 0, + explanation: 'Concurrent unrelated faults must be logged separately with the order and rationale of recovery. The next operator inherits the trend data and the customer follow-up.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:34] +

+

+ "Board over here is lit up - BUC, LNB, HPA all yelling. James Okafor's on the line about TM-1 service. I'm in admin and can't break free for the next twenty minutes." +

+

+ "Take the lead. Triage before you touch anything." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/16/intro.mp3'), + }, + objectives: { + 'triage-dashboard-alarms': { + text: ` +

+ We've been seeing intermittent drops on the North Atlantic route for the last ten minutes. Vessels are losing charter data mid-passage. What's the status? +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.FRUSTRATED, + audioUrl: getAssetUrl('/assets/campaigns/nats/16/obj-triage.mp3'), + }, + 'mute-buc-for-cooldown': { + text: ` +

+ Good sequencing - amp down before the drive. Spectrum's clean and the BUC can cool while you sort the rest. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/16/obj-mute-buc.mp3'), + }, + 'verify-tx-output-clean': { + text: ` +

+ Service is back. Thank you. Let me know if it goes sideways again. +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/16/obj-verify-tx.mp3'), + }, + 'log-cascade-event': { + text: ` +

+ Three unrelated faults in two minutes, prioritized correctly, customer kept informed. That's the job at this level. +

+

+ File the impact report before end of shift. Nice work. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/16/obj-log-cascade.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario17.ts b/src/campaigns/nats/scenario17.ts new file mode 100644 index 00000000..5e11463e --- /dev/null +++ b/src/campaigns/nats/scenario17.ts @@ -0,0 +1,724 @@ +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dBm } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 17: "Solar Event" + * + * Phase: Crisis Operations (Phase 3, Scenario 1 of 8) + * Time Pressure: The transit window is fixed - the clock is astronomy's, not ours + * Calculation Required: NO + * New Mechanic: sun-transit weather event (engine: skyNoiseDegradation on the + * RX path, rise-peak-fall profile, RX-only) + * + * NICE Framework Alignment: + * Primary Codes: + * - K0751: Knowledge of system threats + * - T1020: Determine operational and safety impacts + * - K0689: Knowledge of network infrastructure principles and practices + * + * Supporting Codes: + * - T0153: Monitor network capacity and performance + * - K1032: Knowledge of satellite-based communication systems + * - S0593: Skill in handling incidents + * + * Premise: The semiannual sun transit window on TIDEMARK-1 from VT-01. The Sun + * crosses the antenna boresight for ~5 minutes, raising the system noise + * temperature until the demodulator briefly loses the carrier. The event is + * predicted, contractually excused (with advance notice), and unfixable. + * + * The first Phase 3 lesson inverts every prior scenario: the right answer is to + * accept degradation. Notify before, hold configuration during, verify and + * document after. The only failure modes are panic actions and a late + * notification. + * + * Tone: Crisis-phase operational. Dana briefing + sign-off, Marcus one + * spacecraft-side confirmation. All quizzes SYSTEM. 4 clips. + * + * Sim notes: + * - weatherEvents 'sun-transit': starts T+300s, 300s duration, 12 dB peak. + * Profile sin^2 -> degradation >6 dB roughly T+375..525s, clear by ~T+585s. + * - 12 dB peak takes beacon and carrier C/N below threshold: demod genuinely + * unlocks near peak and self-recovers. Uplink is unaffected by design. + * - Custom-evaluator conditions read skyNoiseDegradation_dB via the + * window.signalRange handle (no imports - avoids manager import cycles). + */ + +/** Current sky-noise degradation (dB) on VT-01's antenna, 0 if unavailable. */ +const vt01SkyNoise = (): number => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { skyNoiseDegradation_dB?: number } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + return gs?.antennas?.[0]?.state?.skyNoiseDegradation_dB ?? 0; +}; + +export const scenario17Data: ScenarioData = { + id: 'nats-scenario17', + prerequisiteScenarioIds: ['nats-scenario16'], + url: 'nats/scenarios/nats-scenario17', + imageUrl: 'nats/17/card.png', + number: 17, + title: 'Solar Event', + subtitle: 'Sun Transit Outage', + duration: '25-30 min', + difficulty: 'intermediate', + missionType: 'Environmental Operations', + description: `The semiannual sun transit window arrives this morning: for about five minutes the Sun passes directly behind TIDEMARK-1 as seen from Vermont, and the antenna stares into a 20,000-kelvin noise source at full gain. The noise floor climbs, the margin collapses, and near the peak the demodulator will lose the carrier.

Nothing is broken. Nothing can be fixed. The event was predicted to the minute by the ephemeris service, the SLA excuses it with advance notice, and the geometry resolves itself.

Your job is the discipline: notify the customer before the window, hold the configuration through it, verify the recovery, and document what actually happened. The only way to fail a sun transit is to fight one.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'Sun Transit Prediction Sheet', + ], + timeLimitSeconds: 30 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + }, + ], + satellites: [tidemark1Satellite, tidemark2Satellite, ses10Satellite], + weatherEvents: [ + { + id: 'tm1-sun-transit', + groundStationId: 'VT-01', + type: 'sun-transit', + severity: 'severe', + startTime: 300, // Window opens 5 minutes into the shift + duration: 300, // ~5-minute transit + linkMarginDegradation: 12, // Peak dB - enough to break demod lock near center + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-17?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Transit Prediction', + description: 'Open the shift brief and the sun transit prediction sheet.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Acknowledge the Window', + params: { + character: Character.SYSTEM, + question: 'Transit window opens five minutes into the shift. Ready?', + options: ['Acknowledged - prediction sheet reviewed, pre-event checklist starting now.'], + correctIndex: 0, + explanation: 'The window does not move for anyone. Everything before it is preparation; everything during it is discipline.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: PRE-WINDOW BASELINE AND UNDERSTANDING + // ============================================================ + { + id: 'baseline-dashboard', + nice: ['T0153', 'K0741'], + title: 'Pre-Transit Baseline', + description: 'Confirm a clean board before the window - any alarm visible now is NOT the transit.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Baseline State', + params: { + character: Character.SYSTEM, + question: 'Why does the pre-transit baseline matter more than usual today?', + options: [ + 'Anything abnormal AFTER the window starts will be attributed to the Sun - a fault hiding under the transit would survive unnoticed unless the board was provably clean before', + 'The baseline calibrates the prediction sheet', + 'The SLA requires a baseline screenshot', + 'It does not - the transit makes the baseline meaningless', + ], + correctIndex: 0, + explanation: 'Predictable events make perfect camouflage. Clean board before the window means everything during it is the Sun - and anything still wrong after it is not.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'transit-geometry-quiz', + nice: ['K1032', 'K0751'], + title: 'Transit Geometry', + description: 'Confirm what physically happens during a sun transit.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['baseline-dashboard'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Physics', + params: { + character: Character.SYSTEM, + question: 'What actually degrades the link during a sun transit?', + options: [ + 'The Sun (a ~20,000 K noise source at C-band) passes through the antenna main beam behind the satellite - system noise temperature soars and C/N collapses, with the signal itself unchanged', + 'Solar radiation pressure pushes the satellite off station', + 'The Sun physically blocks the radio path to the satellite', + 'Solar heating detunes the LNB local oscillator', + ], + correctIndex: 0, + explanation: 'The carrier power never changes - the noise under it rises. That is why nothing on the ground or the spacecraft can fix it: the antenna is pointed at the satellite, and the Sun is standing directly behind it.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'transit-predictability-quiz', + nice: ['K0751', 'K0689'], + title: 'Why It Is Predictable', + description: 'Confirm the seasonal pattern that makes transits plannable.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['transit-geometry-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Season', + params: { + character: Character.SYSTEM, + question: 'When do sun transits occur for a GEO satellite, and for how long?', + options: [ + 'Twice a year near the equinoxes - a few minutes a day for several consecutive days, at a time computable years in advance from the station/satellite geometry', + 'Randomly, whenever solar activity peaks', + 'Once a year at the summer solstice, for several hours', + 'Only during solar eclipses', + ], + correctIndex: 0, + explanation: 'Pure geometry: the Sun crosses the geostationary arc as seen from your latitude around each equinox. Every teleport publishes its transit calendar - which is why a late customer notification is an operator failure, never a surprise.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'baseline-rx-check', + nice: ['T0153', 'K0773'], + title: 'Baseline RX Snapshot', + description: 'Document the healthy link before the window: beacon present, receiver locked, C/N at baseline.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['transit-predictability-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon Present', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'Baseline C/N ≥ 10 dB', + params: { minCNRatio: 10, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'why-not-handover-quiz', + nice: ['T1020', 'K0721'], + title: 'The Handover Question', + description: 'Decide - and be able to defend - why the traffic stays on VT-01 through the window.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['baseline-rx-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Hold or Move', + params: { + character: Character.SYSTEM, + question: 'A handover to ME-02 would dodge the transit. Why is riding through still the right call?', + options: [ + 'The outage is brief, predicted, and SLA-excluded with notice - a handover trades that for two transfer events, and ME-02 inherits its own transit on its own schedule anyway', + 'ME-02 cannot receive TIDEMARK-1', + 'Handover during any weather event is prohibited', + 'It is not - handover is always the safer choice', + ], + correctIndex: 0, + explanation: 'Same lesson as the S14 rain fade, sharpened: escape has a price, and here the thing escaped costs less than the escape. Every station on the arc takes its transits; the constellation-level answer is notification discipline, not musical chairs.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'notify-customer-quiz', + nice: ['T1020', 'S0593'], + title: 'Pre-Event Notification', + description: 'Send the customer notice BEFORE the window opens - the notification is the SLA action.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['why-not-handover-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Notice', + params: { + character: Character.SYSTEM, + question: 'Which notification goes to SeaLink ops right now?', + options: [ + 'Predicted solar transit on TIDEMARK-1 from Vermont, window and peak times attached; expect degraded margin and a possible 1-3 minute carrier interruption near peak; service recovers without intervention; this message constitutes SLA advance notice.', + 'TIDEMARK-1 will have an outage today. Will advise.', + 'No notification - the SLA excludes solar events automatically.', + 'Notification after the event, with the measured impact attached.', + ], + correctIndex: 0, + explanation: 'Specific, timed, actionable, and BEFORE the event. The SLA exclusion is conditional on advance notice - a notification timestamped after window-open is contractually worthless.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: THE WINDOW + // ============================================================ + { + id: 'observe-onset', + nice: ['T0153', 'S0648'], + title: 'Confirm Predicted Onset', + description: 'The window is opening. Confirm the sky-noise rise matches the prediction.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['notify-customer-quiz'], + timeLimitSeconds: 6 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'custom', + description: 'Sky Noise Rising (transit underway)', + params: { + evaluator: () => vt01SkyNoise() > 2, + }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Onset Read', + params: { + character: Character.SYSTEM, + question: 'The elevated sky-noise alarm is climbing on schedule. What does "on schedule" buy you?', + options: [ + 'Confidence this is the predicted transit and not a coincidental fault - the alarm tracking the prediction sheet IS the diagnosis', + 'Nothing - every alarm requires the full fault-isolation procedure', + 'Permission to mute the transmit chain', + 'A reason to repoint and check the geometry', + ], + correctIndex: 0, + explanation: 'The prediction is the baseline. Onset within a minute of the sheet, profile shaped like the sheet - that is a healthy station experiencing astronomy. Deviation from the sheet is what would demand investigation.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'ride-through-peak', + nice: ['S0593', 'T1020', 'K0689'], + title: 'Hold Through the Peak', + description: 'Sky noise is approaching peak and the demod will drop. Hold the configuration: uplink stays exactly as it is, no RX changes, no panic.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['observe-onset'], + timeLimitSeconds: 5 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'custom', + description: 'Transit Peak Reached (>6 dB sky noise)', + params: { + evaluator: () => vt01SkyNoise() > 6, + }, + mustMaintain: false, + }, + { + type: 'tx-modem-transmitting', + description: 'Uplink Untouched - Modem Transmitting', + mustMaintain: true, + }, + { + type: 'buc-unmuted', + description: 'BUC Stays Unmuted', + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Stays Enabled', + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Program-Track Held (no chasing)', + params: { trackingMode: 'program-track' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'peak-behavior-quiz', + nice: ['K0689', 'K1032'], + title: 'What the Peak Looks Like', + description: 'Confirm your read of the link at maximum degradation.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['ride-through-peak'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Peak Read', + params: { + character: Character.SYSTEM, + question: 'Near peak the receiver loses carrier lock. What is true about the link right now?', + options: [ + 'The downlink is buried in solar noise at OUR antenna only - the satellite still hears our uplink perfectly, and the demod will relock on its own as the Sun moves off boresight', + 'The link is down in both directions until the transit ends', + 'The satellite transponder is saturated by solar energy', + 'The carrier is gone and must be re-acquired manually after the window', + ], + correctIndex: 0, + explanation: 'Sun transit is a receive-side event at one station. The uplink never flinched - which is exactly why the transmit chain stays untouched. Anything you "fix" now becomes a real problem you created during a fake one.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: RECOVERY AND DOCUMENTATION + // ============================================================ + { + id: 'verify-recovery', + nice: ['T0153', 'T0431'], + title: 'Verify Self-Recovery', + description: 'The Sun is moving off boresight. Confirm the link recovers to baseline with zero operator action.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['peak-behavior-quiz'], + timeLimitSeconds: 6 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'custom', + description: 'Sky Noise Cleared (<1 dB)', + params: { + evaluator: () => vt01SkyNoise() < 1, + }, + mustMaintain: false, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Re-Locked', + params: { modemNumber: 1 }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Back at Baseline (≥ 10 dB)', + params: { minCNRatio: 10 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'post-event-sweep', + nice: ['T0153', 'K0741'], + title: 'Post-Event Alarm Sweep', + description: 'Confirm the board is clean again - anything still alarming is NOT the Sun.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-recovery'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Clean Exit', + params: { + character: Character.SYSTEM, + question: 'Post-window board check: what are you specifically looking for?', + options: [ + 'Any alarm that survived the window - the transit excuses exactly five minutes of sky noise and nothing else', + 'Confirmation the sky-noise alarm is latched for the report', + 'Nothing - recovery was verified at the receiver already', + 'Elevated BUC temperature from the solar exposure', + ], + correctIndex: 0, + explanation: 'The window is a clean five-minute box. Healthy before, healthy after, astronomy in between. An alarm that outlives the box was never the Sun - and now is when you catch it.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'marcus-confirm', + nice: ['T1020', 'K1032'], + title: 'Spacecraft-Side Confirmation', + description: 'Cross-check with Halifax that the spacecraft view matches the ground view.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['post-event-sweep'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Two Views, One Event', + params: { + character: Character.SYSTEM, + question: 'What should Halifax have seen during our transit window?', + options: [ + 'Nothing abnormal on the spacecraft - our uplink steady throughout, vehicle telemetry nominal; the event existed only at our antenna', + 'A matching outage on the spacecraft bus', + 'Loss of our uplink during the peak minutes', + 'Elevated transponder temperature', + ], + correctIndex: 0, + explanation: 'If Halifax saw anything, it was not (only) a transit. Their nominal telemetry is the final cross-check that closes the event as pure geometry.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'document-impact', + nice: ['T1020', 'S0593'], + title: 'Impact Documentation', + description: 'Record what actually happened versus what was predicted.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['marcus-confirm'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Record', + params: { + character: Character.SYSTEM, + question: 'Which set of facts belongs in the impact record?', + options: [ + 'Predicted vs actual window times, peak degradation observed, carrier lock-loss duration, notification timestamp (pre-window), and customer impact statement', + 'Just "sun transit occurred as predicted"', + 'The full spectrum analyzer trace history for the day', + 'Only the lock-loss duration - the rest was predicted anyway', + ], + correctIndex: 0, + explanation: 'Predicted-vs-actual is what makes the next prediction trustworthy, the notification timestamp is what makes the SLA exclusion stick, and the impact statement is what the account team quotes. Three audiences, one log entry.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-shift-summary', + nice: ['K0645', 'T0153'], + title: 'Log the Event', + description: 'Close out the transit in the operations log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['document-impact'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry correctly records this event?', + options: [ + 'Predicted solar transit TM-1/VT-01 executed per SOP-SX-001. Customer notified pre-window. Peak ~12 dB sky noise, brief demod loss near peak, self-recovered to baseline. No operator intervention, no residual alarms. Day 2 of 4 in this transit series - next window tomorrow, ~4 minutes earlier.', + 'TM-1 outage this morning. Resolved.', + 'Sun transit - no entry needed, event was excused.', + 'Emergency response to solar interference completed successfully.', + ], + correctIndex: 0, + explanation: 'Including tomorrow\'s window in today\'s entry is the mark of someone who understands the series. The next operator walks in pre-briefed.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 11:38] +

+

+ "Sun transit window on TM-1 this morning - prediction sheet's attached. Window opens at 1149, peak around 1151:30, clear by 1154. You know the drill from the read-ahead: this one you don't fight. Notify, ride it through, document. The only way to fail a sun transit is to panic during one." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/17/intro.mp3'), + }, + objectives: { + 'notify-customer-quiz': { + text: ` +

+ Notice is out and timestamped - good. Window opens in a couple of minutes. Eyes on the board, hands off the equipment. I'll be watching the same alarm you are. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/17/obj-notify-customer-quiz.mp3'), + }, + 'verify-recovery': { + text: ` +

+ Marcus in Halifax. Vehicle telemetry was nominal straight through your window - your uplink never wavered on our side, not even at peak. Whatever the Sun did to your noise floor, the spacecraft never knew about it. Clean transit, eh. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/17/obj-verify-recovery.mp3'), + }, + 'log-shift-summary': { + text: ` +

+ Textbook. You notified before the customer could ask, held still while the link looked terrible, and verified instead of assuming. Phase three starts with the hardest skill there is - knowing when the right move is no move. +

+

+ Same window tomorrow, four minutes earlier. Log's already got it. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/17/obj-log-shift-summary.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario18.ts b/src/campaigns/nats/scenario18.ts new file mode 100644 index 00000000..61ab2c27 --- /dev/null +++ b/src/campaigns/nats/scenario18.ts @@ -0,0 +1,803 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Satellite, TransponderConfig } from '@app/equipment/satellite/satellite'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import { SignalOrigin } from '@app/signal-origin'; +import type { dB, dBi, dBm, FECType, Hertz, IfFrequency, MHz, ModulationType, RfFrequency } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite } from './satellites'; + +/** + * NATS Level 18: "Satellite Anomaly" + * + * Phase: Crisis Operations (Phase 3, Scenario 2 of 8) + * Time Pressure: Moderate - the drift grows while you decide + * Calculation Required: NO + * New Mechanic: a scenario-local DRIFTING variant of TIDEMARK-2 (same bird, + * same NORAD ID, same frequency plan - but with station-keeping suspended: + * inclined-orbit figure-8 drift and a stale ephemeris). First scenario where + * the satellite itself, not the ground segment, is the unknown. + * + * NICE Framework Alignment: + * Primary Codes: + * - K1032: Knowledge of satellite-based communication systems + * - T1314: Maintain network infrastructure + * - K0751: Knowledge of system threats + * + * Supporting Codes: + * - K0721: Knowledge of risk management principles and practices + * - T0153: Monitor network capacity and performance + * - S0593: Skill in handling incidents + * + * Premise: Halifax reports TIDEMARK-2's north-south station-keeping thrusters + * are degraded and burns are suspended. Residual inclination is building - the + * bird traces a growing daily figure-8 the stale ephemeris no longer predicts. + * Program-track is bleeding C/N. The operator (covering ME-02) must recognize + * the signature, transition to step-track, hold the link, and feed the + * spacecraft team ground-side observations - while staying inside the ground + * role: Halifax flies the vehicle; we keep the lock. + * + * Tone: Crisis-phase. Marcus is the primary voice (spacecraft side), Dana + * provides incident support. All quizzes SYSTEM. 6 clips. + * + * Sim notes: + * - The drifting TM-2 variant replaces the roster TM-2 for this scenario + * only (same NORAD 61526; the canonical satellite is not in the list). + * - ephemerisError 0.18/0.12 deg: program-track holds a degraded link + * (~5 dB down) - bad enough to read, good enough to acquire from. + */ + +const tidemark2DriftingSatellite = new Satellite( + 'TIDEMARK-2', + 61526, + [ + { + signalId: 'TIDEMARK-2-TDMA-Composite', + serverId: 1, + noradId: 61526, + frequency: 6017e6 as RfFrequency, + polarization: 'H', + power: 20 as dBm, + bandwidth: 36e6 as Hertz, + modulation: 'QPSK' as ModulationType, + fec: '3/4' as FECType, + feed: '', + isDegraded: false, + origin: SignalOrigin.SATELLITE_RX, + noiseFloor: null, + gainInPath: 0 as dBi, + }, + ], + [], + { + az: 219.7 as Degrees, + el: 26.3 as Degrees, + rotation: -25 as Degrees, + frequencyOffset: 2.225e9 as Hertz, + // Stale ephemeris: station-keeping suspended, prediction no longer matches + ephemerisErrorAz: 0.18 as Degrees, + ephemerisErrorEl: 0.12 as Degrees, + orbitType: 'geosynchronous', + geosyncConfig: { + minAz: 218.4 as Degrees, // Growing figure-8: ±1.3° and widening daily + maxAz: 221.0 as Degrees, + minEl: 25.0 as Degrees, + maxEl: 27.6 as Degrees, + }, + transponderConfigs: [ + { + id: 'TP-1', + uplinkCenterFrequency: 6017e6 as RfFrequency, + bandwidth: 36e6 as Hertz, + frequencyOffset: 2.225e9 as Hertz, // Downlink center: 3792 MHz + polarization: 'H', + beacon: { + frequency: 4180e6 as RfFrequency, + signalId: 'TIDEMARK-2-Beacon', + serverId: 1, + noradId: 61526, + power: 31 as dBm, + bandwidth: 1e3 as Hertz, + modulation: 'CW' as ModulationType, + fec: 'null' as FECType, + polarization: 'V', + feed: '', + isDegraded: false, + origin: SignalOrigin.TRANSMITTER, + noiseFloor: null, + gainInPath: 0 as dBi, + }, + } as TransponderConfig, + { + id: 'TP-2', + uplinkCenterFrequency: 5980e6 as RfFrequency, + bandwidth: 36e6 as Hertz, + frequencyOffset: 2.225e9 as Hertz, + polarization: 'H', + } as TransponderConfig, + ], + } +); + +export const scenario18Data: ScenarioData = { + id: 'nats-scenario18', + prerequisiteScenarioIds: ['nats-scenario17'], + url: 'nats/scenarios/nats-scenario18', + imageUrl: 'nats/18/card.png', + number: 18, + title: 'Satellite Anomaly', + subtitle: 'TIDEMARK-2 Station-Keeping Drift', + duration: '30-35 min', + difficulty: 'advanced', + missionType: 'Anomaly Response', + description: `Halifax called at 0850: TIDEMARK-2's station-keeping thrusters are degraded and north-south burns are suspended while the vehicle team investigates. Residual inclination is building - from Maine's dish the bird now traces a growing daily figure-eight that the published ephemeris no longer predicts.

Program-track follows the ephemeris. The ephemeris is wrong. You can watch the C/N bleed in real time.

You have flown an inclined bird before - AURORA-7 taught you everything this needs. The difference is that this time the satellite is the patient, the spacecraft team is the surgeon, and the ground operator's whole job is to keep the monitors attached: hold the lock, feed Halifax your track data, and know exactly where the ground role ends.`, + equipment: [ + '9-meter C-band Antenna (ME-02)', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'Halifax vehicle status feed', + ], + timeLimitSeconds: 35 * 60, + settings: { + isSync: true, + groundStations: [ + // VT-01: standby on TIDEMARK-1, untouched this shift + { + ...vermontGroundStation, + }, + // ME-02: carrying TIDEMARK-2, program-track degrading on the stale ephemeris + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 219.7 as Degrees, + elevation: 26.3 as Degrees, + polarization: -25 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61526, + targetAzimuth: 219.7 as Degrees, + targetElevation: 26.3 as Degrees, + targetPolarization: -25 as Degrees, + slewing: false, + beaconCN: 7.6 as dB, // Degraded from the usual 10+ - the drift at work + beaconFrequencyHz: 1070e6 as Hertz, + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // ME-02 carries TM-2: full chain hot (BUC unmuted, HPA enabled + // inherit from VT-01 defaults), LNB LO 5250 for matching IFs + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1070e6 as Hertz, // TM-2 beacon IF + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + ifSignal: { + ...vermontGroundStation.transmitters[0].modems[0].ifSignal, + signalId: 'TIDEMARK-2-Teleport', + noradId: 61526, + frequency: 1020e6 as IfFrequency, // TM-2 TP-2: 7000 - 5980 + }, + }, + ], + }, + ], + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1458 as MHz, // TM-2 downlink IF (5250 - 3792) + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [tidemark2DriftingSatellite, tidemark1Satellite, ses10Satellite], + trafficOwnership: [ + { + satelliteNoradId: 61526, + initialOwnerId: 'ME-02', + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-18?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Anomaly Brief', + description: 'Open the brief and Halifax\'s 0850 vehicle status report.', + groundStation: 'ME-02', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Take the Shift', + params: { + character: Character.SYSTEM, + question: 'Vehicle anomaly on TIDEMARK-2, ground link degrading. What is YOUR job this shift?', + options: [ + 'Keep the link alive and feed Halifax ground observations - the vehicle is theirs, the lock is mine', + 'Diagnose the thruster fault from the ground telemetry', + 'Hand TIDEMARK-2 traffic to VT-01 preemptively', + 'Command the satellite back to its slot', + ], + correctIndex: 0, + explanation: 'Role clarity is the first decision of a vehicle anomaly. Halifax flies the spacecraft; the ground station flies the antenna.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-maine-station', + nice: ['S0421'], + title: 'Open ME-02', + description: 'Select the Maine Ground Station.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: READ THE SIGNATURE + // ============================================================ + { + id: 'dashboard-baseline', + nice: ['T0153', 'K0741'], + title: 'Station Health vs Link Health', + description: 'Check the Dashboard - separate what is wrong from what is healthy.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['select-maine-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'The Split', + params: { + character: Character.SYSTEM, + question: 'The board shows degraded link margin but every equipment indicator is green. What does that combination tell you?', + options: [ + 'The ground segment is healthy - the degradation is on the space side or in the geometry, which matches Halifax\'s report exactly', + 'A hidden equipment fault - green indicators cannot be trusted during an anomaly', + 'The dashboard is stale and needs a refresh', + 'Weather - check the precipitation sensor', + ], + correctIndex: 0, + explanation: 'Healthy equipment + sick link = look up, not down. The S16 cascade taught fault isolation on the ground; this is the same discipline pointed at the sky.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'station-keeping-quiz', + nice: ['K1032', 'K0751'], + title: 'What Died on the Spacecraft', + description: 'Confirm what suspended north-south station-keeping means for the ground.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['dashboard-baseline'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Orbit Mechanics', + params: { + character: Character.SYSTEM, + question: 'With N-S station-keeping burns suspended, what happens to TIDEMARK-2 from our point of view?', + options: [ + 'Residual inclination accumulates - the bird traces a daily figure-8 in az/el that grows over weeks, and the published ephemeris becomes progressively more wrong', + 'The satellite falls out of orbit within days', + 'The satellite drifts east along the GEO arc into another operator\'s slot immediately', + 'Nothing observable - station-keeping only matters for collision avoidance', + ], + correctIndex: 0, + explanation: 'Exactly AURORA-7\'s life story, fast-forwarded. Inclination is the axis N-S burns fight; without them the figure-8 starts small and grows roughly 0.8-0.9° per year - except this drift is days old and already past the prediction.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'read-program-track-decay', + nice: ['T0153', 'K1032'], + title: 'Read the Program-Track Decay', + description: 'Open ACU Control and read what program-track is doing with a stale ephemeris.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['station-keeping-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Why the C/N Sags', + params: { + character: Character.SYSTEM, + question: 'Beacon C/N is several dB below its usual value and wandering. Program-track reports it is exactly on target. Reconcile that.', + options: [ + 'Program-track IS on target - the ephemeris target. The satellite is somewhere else, and the gap between prediction and reality is being paid in pattern loss', + 'The beacon transmitter on the satellite is failing', + 'The ACU encoder is slipping and the dish is not where it reports', + 'Cross-pol interference from SES-10', + ], + correctIndex: 0, + explanation: 'A 9-meter C-band dish has a half-degree-class beamwidth. A few tenths of a degree of ephemeris error puts the bird on the shoulder of the beam - present, degraded, and wandering with the figure-8.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'beamwidth-risk-quiz', + nice: ['K0721', 'K1032'], + title: 'How Long Until It Falls Off', + description: 'Project the risk: a growing figure-8 against a fixed beamwidth.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['read-program-track-decay'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Clock', + params: { + character: Character.SYSTEM, + question: 'The figure-8 is ±1.3° and growing; the dish beamwidth is ~0.5°. What does that mean for program-track as a strategy?', + options: [ + 'Already lost - the excursion is several beamwidths and only gets worse; any fix based on following the ephemeris fails until Halifax publishes a corrected one', + 'Fine for another month - the margins are comfortable', + 'Acceptable if we raise LNB gain to compensate', + 'Program-track will self-correct as it learns the drift', + ], + correctIndex: 0, + explanation: 'No gain knob recovers pointing loss. When reality has left the prediction by multiple beamwidths, the only thing worth tracking is the satellite itself - which is what the beacon is for.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: TRANSITION TO STEP-TRACK + // ============================================================ + { + id: 'enable-step-track', + nice: ['S0421', 'K1032'], + title: 'Engage Step-Track', + description: 'Switch tracking strategy: let the beacon, not the ephemeris, steer the dish.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['beamwidth-risk-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Engaged', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'acquire-stable-beacon', + nice: ['T0153', 'K1032'], + title: 'Beacon Recovery', + description: 'Hold beacon lock under step-track - watch the C/N climb back as the loop finds the real satellite.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['enable-step-track'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-beacon-locked', + description: 'Beacon Lock Sustained', + mustMaintain: true, + maintainDuration: 15, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-carrier-recovery', + nice: ['T0153', 'T1314'], + title: 'Carrier Recovery', + description: 'Confirm the customer carrier recovered with the pointing.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['acquire-stable-beacon'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Recovered (≥ 9 dB)', + params: { minCNRatio: 9, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + maintainDuration: 15, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'no-manual-chase-quiz', + nice: ['K0721', 'S0421'], + title: 'Why Not Fly It By Hand', + description: 'Confirm why manual pointing is the wrong fallback for a drifting bird.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['verify-carrier-recovery'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Hands Off the Axes', + params: { + character: Character.SYSTEM, + question: 'A colleague suggests nudging the dish manually every few minutes instead of trusting step-track. Why is that worse?', + options: [ + 'A human chases where the bird WAS; the loop tracks where it IS - manual nudges add pointing error between corrections, fatigue guarantees a missed one, and a bad nudge can drop the beacon entirely', + 'Manual mode disables the LNB', + 'It is not worse - manual tracking is more precise than step-track', + 'Manual pointing voids the antenna warranty', + ], + correctIndex: 0, + explanation: 'Step-track corrects continuously against the measured beacon. The day automation actually dies you will fly manual because you must (that day comes in this campaign) - but choosing manual while the loop works is choosing worse performance at higher risk.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: HOLD AND REPORT + // ============================================================ + { + id: 'sustained-hold', + nice: ['T0153', 'T1314'], + title: 'Hold Through the Drift', + description: 'Sustain the recovered link while the figure-8 wanders - this is the new normal until Halifax fixes the vehicle.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['no-manual-chase-quiz'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-beacon-locked', + description: 'Beacon Held', + mustMaintain: true, + maintainDuration: 30, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Maintained', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Carrier Held', + params: { modemNumber: 1 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'ground-observations-quiz', + nice: ['T1314', 'K1032'], + title: 'Feed the Vehicle Team', + description: 'Choose what ground data actually helps Halifax\'s investigation.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['sustained-hold'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Useful Telemetry', + params: { + character: Character.SYSTEM, + question: 'Which ground-side dataset is most valuable to the spacecraft team right now?', + options: [ + 'Timestamped step-track pointing history - the dish is physically following the satellite, so its az/el trace IS an independent measurement of the actual orbit', + 'The receiver\'s frame error counters', + 'BUC and HPA temperatures', + 'The spectrum analyzer screenshot archive', + ], + correctIndex: 0, + explanation: 'When step-track follows the beacon, the antenna becomes a tracking instrument. Halifax can fit your pointing trace against their dynamics model - ground stations have refined orbits this way for decades.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'impact-assessment-quiz', + nice: ['K0721', 'T1020'], + title: 'Customer Impact Posture', + description: 'Set the customer impact statement for an anomaly with no current service effect.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['ground-observations-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Statement', + params: { + character: Character.SYSTEM, + question: 'Service is fully restored under step-track. What does the customer impact assessment say?', + options: [ + 'No current impact; service nominal under contingency tracking. Elevated risk posture while the vehicle anomaly is open - next decision points are loss of step-track margin or a vehicle-status change from Halifax', + 'No impact, case closed', + 'Severe impact - recommend customers migrate immediately', + 'Impact unknown - decline to assess until Halifax finishes', + ], + correctIndex: 0, + explanation: 'Honest posture: green today, amber risk, named tripwires. The S22 board work later in this phase grades exactly this skill - say what you know, label what you are watching.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'escalation-boundary-quiz', + nice: ['S0593', 'K0721'], + title: 'Escalation Tripwires', + description: 'Define when this stops being a ride-along and starts being Dana\'s call.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['impact-assessment-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Tripwires', + params: { + character: Character.SYSTEM, + question: 'Which events escalate beyond the ground-operator role?', + options: [ + 'Step-track losing the beacon, C/N trending below demod threshold despite good tracking, or Halifax declaring the vehicle unsafe - anything where keeping the link stops being possible or stops being wise', + 'Any change in the figure-8 size', + 'Each hourly mark, automatically', + 'Nothing - the operator owns the incident end to end', + ], + correctIndex: 0, + explanation: 'You own the link while the link is holdable. The moment the question becomes "should this traffic move?" or "should this bird carry traffic at all?" - that is supervision and spacecraft authority, with your data underneath it.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-shift-summary', + nice: ['K0645', 'T0153'], + title: 'Log the Anomaly Response', + description: 'Record the shift for the next operator - the anomaly outlives your shift.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['escalation-boundary-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry hands this off correctly?', + options: [ + 'TM-2 vehicle anomaly (Halifax NOC ref): N-S station-keeping suspended, ephemeris stale. ME-02 transitioned to step-track 0935, beacon and carrier recovered, no customer impact. Pointing history streaming to Halifax. Tripwires: step-track margin, vehicle status change. Anomaly OPEN.', + 'TM-2 had tracking problems, fixed with step-track.', + 'Vehicle anomaly resolved - returned to program-track.', + 'See Halifax for details.', + ], + correctIndex: 0, + explanation: 'OPEN in capital letters is the load-bearing word. The next operator inherits a healthy link inside an unresolved anomaly - the log must make both halves of that true.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:07] +

+

+ "Marcus called the ops line at 0850 - TM-2's north-south burns are coming up short, thruster degradation, vehicle team on it. The bird's drifting past the ephemeris and Maine's link is paying for it. You're covering ME-02 today: keep the link, feed Halifax what you see, and know where your job ends. The vehicle is theirs. The lock is yours." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/18/intro.mp3'), + }, + objectives: { + 'beamwidth-risk-quiz': { + text: ` +

+ Marcus here. Confirming what you're seeing: residual inclination is building faster than we'd like and the published ephemeris is days stale. Don't wait on a corrected set from us - it'll be a while. You've got a beacon; I'd trust it over my numbers right now. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/18/obj-beamwidth-risk-quiz.mp3'), + }, + 'verify-carrier-recovery': { + text: ` +

+ And there it is - I can see your C/N from the payload side, right back where it belongs. The dish is flying the bird instead of the prediction. Nicely done, eh. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/18/obj-verify-carrier-recovery.mp3'), + }, + 'ground-observations-quiz': { + text: ` +

+ One more thing - keep that pointing history flowing to us. Your dish is the best orbit-determination instrument on this anomaly right now. Every hour of clean step-track trace tightens our solution for the recovery burn. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/18/obj-ground-observations-quiz.mp3'), + }, + 'log-shift-summary': { + text: ` +

+ Good shift. You read the signature, made one decisive change, and stayed inside your lane while owning everything in it. That's anomaly response - the vehicle team gets to work their problem because the link stopped being one. +

+

+ Anomaly stays open. Tomorrow's operator starts from your log. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/18/obj-log-shift-summary.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario19.ts b/src/campaigns/nats/scenario19.ts new file mode 100644 index 00000000..ae361ac0 --- /dev/null +++ b/src/campaigns/nats/scenario19.ts @@ -0,0 +1,765 @@ +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dBm, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import { vermontGroundStation } from './ground-stations'; +import { aurora7Satellite, tidemark1Satellite } from './satellites'; + +/** + * NATS Level 19: "Train the New Hire" + * + * Phase: Crisis Operations (Phase 3, Scenario 3 of 8) + * Time Pressure: None - teaching is the work + * Calculation Required: YES - the card's numbers must be derived correctly + * New Mechanic: the Working Document panel. Each teaching quiz carries a + * params.documentLine; correct answers append to the in-game quick-reference + * card the player is producing. The deliverable is visible and reviewable. + * + * NICE Framework Alignment: + * Primary Codes: + * - T1411: Deliver technical training to customers + * - T1334: Produce cybersecurity instructional materials + * - K0645: Knowledge of standard operating procedures (SOPs) + * + * Supporting Codes: + * - T1567: Configure system hardware, software, peripheral equipment + * - S0421: Skill in operating network equipment + * - K0773: Knowledge of telecommunications principles and practices + * + * Premise: Dana wants a one-page AURORA-7 acquisition/track quick-reference + * card for an incoming new hire - written by someone who flies the procedure, + * not copied from a vendor manual. The player performs the live procedure + * end-to-end and, at each step, chooses which callout, formula, or + * common-mistake warning belongs on the card. Correct choices build the card + * in the Working Document panel. + * + * The deeper lesson: executing a procedure and knowing which three of its + * fifty steps a new operator will get wrong are different skills. This is the + * campaign's mentoring beat - the player must articulate the WHY. + * + * Tone: Reflective-operational. Dana intro + acknowledgment only (3 clips). + * All quizzes SYSTEM. The new hire never appears on screen. + */ + +export const scenario19Data: ScenarioData = { + id: 'nats-scenario19', + prerequisiteScenarioIds: ['nats-scenario18'], + url: 'nats/scenarios/nats-scenario19', + imageUrl: 'nats/19/card.png', + number: 19, + title: 'Train the New Hire', + subtitle: 'Producing the Quick-Reference Card', + duration: '30-35 min', + difficulty: 'intermediate', + missionType: 'Knowledge Transfer', + description: `A new hire starts ground school next month, and Dana wants a one-page quick-reference card for the AURORA-7 acquisition and step-track procedure - written by someone who actually flies it.

The method: run the procedure live this afternoon. At each step, choose the one callout, formula, or warning that belongs on the card. The card builds in front of you as you work - and at the end it gets handed to someone for whom it may be the only thing between a clean acquisition and a lost afternoon.

Anyone can execute a procedure. Teaching it means knowing which mistakes are actually waiting for the person who comes after you.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'Working Document panel (the card)', + ], + timeLimitSeconds: 35 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1422 as MHz, // AURORA-7 downlink IF (5250 - 3828) + bandwidth: 24 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [aurora7Satellite, tidemark1Satellite], + workingDocument: { + title: 'AURORA-7 Quick-Reference Card', + description: 'Console copy for the incoming new hire - built live during the procedure run.', + }, + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-19?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review the Assignment', + description: 'Open the brief - the deliverable today is a document, built by flying the procedure.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Accept the Assignment', + params: { + character: Character.SYSTEM, + question: 'Ready to fly the AURORA-7 procedure and build the card as you go?', + options: ['Ready - procedure live, card building in the Working Document panel.'], + correctIndex: 0, + explanation: 'Every teaching choice you make appends to the card. Choose like the reader\'s first solo shift depends on it.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'card-scope-quiz', + nice: ['T1334', 'T1411'], + title: 'Set the Card\'s Scope', + description: 'Decide what kind of document this is before writing a line of it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Scope Decision', + params: { + character: Character.SYSTEM, + question: 'What is a quick-reference card, as opposed to the SOP it accompanies?', + options: [ + 'The numbers someone needs under pressure plus the mistakes with the highest local base rate - one page, taped to the console; the procedure itself stays in the SOP', + 'A condensed copy of every SOP step in smaller type', + 'A vendor datasheet excerpt', + 'A training syllabus for the first month', + ], + correctIndex: 0, + explanation: 'Selection is the work. Everything on the card competes for attention during a real acquisition - a line that does not earn its place costs the reader time when they can least afford it.', + pointPenalty: 5, + documentSection: 'Header', + documentLine: 'AURORA-7 ACQUIRE & TRACK - console copy | Audience: first-solo operator | The SOP is the procedure; this card is the pressure kit', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 1: ACQUIRE (and teach the acquisition) + // ============================================================ + { + id: 'repoint-to-aurora', + nice: ['S0421', 'K1032'], + title: 'Fly It: Acquire AURORA-7', + description: 'Program-track to AURORA-7 (NORAD 28899) - the live half of the lesson.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['card-scope-quiz'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Program-Track Mode', + params: { trackingMode: 'program-track' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on AURORA-7', + params: { noradId: 28899 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'acquire-callout-quiz', + nice: ['T1411', 'K1032'], + title: 'Card Line: Acquisition', + description: 'You just acquired the bird. Which callout belongs on the card for this step?', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['repoint-to-aurora'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Acquisition Callout', + params: { + character: Character.SYSTEM, + question: 'Which line teaches the acquisition step best?', + options: [ + 'Program-track FIRST - it puts you inside beacon capture range. Nominal: Az 190, El 32, but the bird rides a ±3° figure-8', + 'Acquire AURORA-7 using the standard acquisition procedure', + 'The antenna must be powered before acquisition can begin', + 'AURORA-7 was launched over nineteen years ago and is approaching end of life', + ], + correctIndex: 0, + explanation: 'Numbers plus the why. "Use the standard procedure" teaches nothing; trivia about launch dates costs card space the reader pays for at 2 AM.', + pointPenalty: 5, + documentSection: 'Acquire', + documentLine: 'Program-track FIRST - puts you inside beacon capture range. Nominal Az 190 / El 32, bird rides a ±3° figure-8', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'tune-beacon', + nice: ['S0421', 'K0773'], + title: 'Fly It: Find the Beacon', + description: 'Tune the spectrum analyzer to the AURORA-7 beacon IF and confirm it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['acquire-callout-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum at 1085 MHz IF', + params: { + centerFrequency: 1085e6, + centerFrequencyTolerance: 1e6, + }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'AURORA-7 Beacon Detected', + params: { + signalId: 'AURORA-7-Beacon', + minPower: -100 as dBm, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'beacon-formula-quiz', + nice: ['T1411', 'K0773'], + title: 'Card Line: the Beacon Number', + description: 'The beacon is on your screen. Put the number on the card the way the reader needs it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['tune-beacon'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Beacon Callout', + params: { + character: Character.SYSTEM, + question: 'Which beacon line belongs on the card?', + options: [ + 'Beacon IF = LO − RF = 5250 − 4165 = 1085 MHz. Weak CW (aging bird) - use a narrow span, ~2 kHz', + 'The beacon is at 1085', + 'Beacon frequencies are listed in the satellite operator\'s handbook, appendix C', + 'IF = |LO − RF| (see SOP for values)', + ], + correctIndex: 0, + explanation: 'The formula WITH the worked numbers, plus the trap (weak CW needs a narrow span). A bare "1085" breaks the day the LO changes; a bare formula breaks at 2 AM.', + pointPenalty: 5, + documentSection: 'Acquire', + documentLine: 'Beacon IF = LO − RF = 5250 − 4165 = 1085 MHz. Weak CW - use ~2 kHz span', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'mistake-lo-quiz', + nice: ['T1334', 'K0645'], + title: 'Card Line: First Watch-Out', + description: 'Pick the highest-value warning for the acquisition phase from the station\'s actual history.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['beacon-formula-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Watch-Out Selection', + params: { + character: Character.SYSTEM, + question: 'Station history lists five past mistakes. Which acquisition-phase warning earns card space?', + options: [ + 'Beacon "missing" at 1085? Check the LNB LO = 5250 - an operator fresh from Maine duty once hunted a healthy beacon for an hour with the LO still at Maine\'s 6080 default', + 'Do not look directly into the feed horn', + 'Remember that AURORA-7 is older than the other satellites', + 'Double-check that the spectrum analyzer is powered on', + ], + correctIndex: 0, + explanation: 'It actually happened here, it costs an hour, and the check takes five seconds. That ratio - cost of mistake over cost of check - is what earns a warning its card space.', + pointPenalty: 5, + documentSection: 'Watch Out', + documentLine: 'Beacon "missing"? Verify LNB LO = 5250 - the Maine default (6080) bites cross-site operators', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: TRACK (and teach the tracking) + // ============================================================ + { + id: 'enable-step-track', + nice: ['S0421', 'K1032'], + title: 'Fly It: Engage Step-Track', + description: 'Engage step-track on the acquired beacon - the half of the procedure the card is really for.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['mistake-lo-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Engaged', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'step-track-rule-quiz', + nice: ['T1411', 'K1032'], + title: 'Card Line: the Engagement Rule', + description: 'Capture the step-track rule that prevents the most common engagement error.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['enable-step-track'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Engagement Rule', + params: { + character: Character.SYSTEM, + question: 'Which step-track line goes on the card?', + options: [ + 'Step-track RIDES program-track - engage it as an optimization on an acquired beacon, never from MANUAL (the loop needs a beacon to optimize)', + 'Enable step-track when tracking AURORA-7', + 'Step-track was invented to track inclined-orbit satellites', + 'Step-track may be engaged in any antenna mode', + ], + correctIndex: 0, + explanation: 'A new hire here once engaged step-track from manual with no beacon in the capture cone and watched the dish wander. The rule plus the reason makes the mistake impossible to repeat.', + pointPenalty: 5, + documentSection: 'Track', + documentLine: 'Step-track RIDES program-track - engage on an acquired beacon, never from MANUAL', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'hold-beacon', + nice: ['T0153', 'K1032'], + title: 'Fly It: Hold the Figure-8', + description: 'Hold beacon lock while the bird wanders - watch what "healthy" looks like so you can describe it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['step-track-rule-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-beacon-locked', + description: 'Beacon Lock Held', + mustMaintain: true, + maintainDuration: 20, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'healthy-track-quiz', + nice: ['T1411', 'T0153'], + title: 'Card Line: What Healthy Looks Like', + description: 'Describe the picture of a working step-track so the reader can recognize it - and its absence.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['hold-beacon'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Healthy Picture', + params: { + character: Character.SYSTEM, + question: 'Which description of a healthy step-track belongs on the card?', + options: [ + 'Healthy = beacon C/N steady at its peak while Az/El visibly wander the figure-8. Moving dish + flat C/N is the loop WORKING, not a fault', + 'Healthy step-track means the antenna position never changes', + 'Check the step-track LED is illuminated', + 'C/N values are documented in the link budget on file', + ], + correctIndex: 0, + explanation: 'The counterintuitive part IS the lesson: motion is health. A new operator who expects a still dish will "fix" a working loop - this line inoculates them.', + pointPenalty: 5, + documentSection: 'Track', + documentLine: 'Healthy = C/N steady at peak while Az/El wander the figure-8. Moving dish + flat C/N = loop WORKING', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'mistake-chase-quiz', + nice: ['T1334', 'K0645'], + title: 'Card Line: Second Watch-Out', + description: 'Pick the tracking-phase warning with the highest base rate.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['healthy-track-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Watch-Out Selection', + params: { + character: Character.SYSTEM, + question: 'Which tracking warning earns the card space?', + options: [ + 'C/N sagging mid-track? Verify step-track is still ON before touching the axes - hand-chasing the figure-8 is a losing game an operator here once played for twenty minutes', + 'Do not unplug the ACU during tracking', + 'AURORA-7\'s transponder bandwidth is narrower than TIDEMARK\'s', + 'Step-track parameters are configured by engineering and should not be modified', + ], + correctIndex: 0, + explanation: 'Again the local base rate: the chase mistake has actually happened, has a cheap check (is the loop on?), and an expensive failure mode (losing the beacon entirely).', + pointPenalty: 5, + documentSection: 'Watch Out', + documentLine: 'C/N sagging? Check step-track is ON before touching axes - never hand-chase the figure-8', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: VERIFY (and teach the proof chain) + // ============================================================ + { + id: 'verify-receiver', + nice: ['T0153', 'S0421'], + title: 'Fly It: Prove the Link', + description: 'Verify the receiver end-to-end - the proof chain the card must teach.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['mistake-chase-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N ≥ 8 dB', + params: { minCNRatio: 8, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-chain-quiz', + nice: ['T1411', 'K0773'], + title: 'Card Line: the Proof Chain', + description: 'Reduce link verification to the minimum chain that actually proves it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-receiver'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Proof Chain', + params: { + character: Character.SYSTEM, + question: 'Which verification line belongs on the card?', + options: [ + 'Proof chain, in order: beacon at 1085 (pointing + LO) → RX locked at 1422 MHz / 24 MHz (carrier) → C/N ≥ 8 (margin). Each link proves something the others don\'t', + 'Verify the link is working before reporting completion', + 'Run the full link budget calculation and compare against the design values', + 'If the dashboard shows green, the link is verified', + ], + correctIndex: 0, + explanation: 'Ordered, numeric, and explains what each check proves - the S9 lesson (beacon proves RF, lock proves data, margin proves durability) compressed to one card line.', + pointPenalty: 5, + documentSection: 'Verify', + documentLine: 'Proof chain: beacon 1085 (pointing+LO) → RX locked 1422 / 24 MHz (carrier) → C/N ≥ 8 (margin)', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'mistake-span-quiz', + nice: ['T1334', 'K0773'], + title: 'Card Line: Third Watch-Out', + description: 'One more warning slot. Spend it on the verification-phase trap.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-chain-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Watch-Out Selection', + params: { + character: Character.SYSTEM, + question: 'Last warning slot on the card - which one?', + options: [ + 'Carrier "gone" but beacon fine? Widen the span - a 24 MHz carrier is invisible at the 2 kHz span you used for the beacon. (Operator here once declared an outage over this)', + 'The spectrum analyzer reference level should be checked weekly', + 'Always log out of the console at end of shift', + 'AURORA-7 may be retired in the coming years - check the operations bulletin', + ], + correctIndex: 0, + explanation: 'The span trap is the natural sequel to the card\'s own beacon advice ("use 2 kHz span") - a good card warns about the mistakes its own instructions set up.', + pointPenalty: 5, + documentSection: 'Watch Out', + documentLine: 'Carrier "gone" but beacon fine? WIDEN THE SPAN - 24 MHz won\'t show at the beacon\'s 2 kHz', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'tx-numbers-quiz', + nice: ['T1411', 'K0773'], + title: 'Card Line: the TX Number', + description: 'The card needs the transmit-side number even though today\'s run was receive-only.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['mistake-span-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'TX Card Line', + params: { + character: Character.SYSTEM, + question: 'Which transmit-side line belongs on the card?', + options: [ + 'TX IF = BUC LO − uplink RF = 7500 − 6053 = 1447 MHz. AURORA\'s chain uses BUC LO 7500 - NOT the TIDEMARK 7000', + 'Transmit procedures are covered in a separate SOP', + 'TX IF = 1447 MHz', + 'The BUC local oscillator converts IF to RF', + ], + correctIndex: 0, + explanation: 'The 7500-vs-7000 trap has burned a real operator here (carrier 500 MHz off). The worked formula carries both the number and the trap in one line.', + pointPenalty: 5, + documentSection: 'Numbers', + documentLine: 'TX IF = 7500 − 6053 = 1447 MHz. AURORA BUC LO is 7500 - NOT the TIDEMARK 7000', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: EDITORIAL REVIEW AND HANDOFF + // ============================================================ + { + id: 'card-review-quiz', + nice: ['T1334', 'T1411'], + title: 'Editorial Review', + description: 'Review the finished card in the Working Document panel - and defend what you left off.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['tx-numbers-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Cut List', + params: { + character: Character.SYSTEM, + question: 'The card intentionally omits launch history, link-budget math, and the full SOP steps. Why is leaving things OFF the card a teaching decision?', + options: [ + 'Card space is the reader\'s attention under pressure - every line they scan past to find the one they need is time on a degraded link; the card earns trust by containing only what earns its place', + 'Shorter documents are cheaper to print', + 'The omitted material is classified', + 'It is not a decision - the card was simply finished', + ], + correctIndex: 0, + explanation: 'Dana\'s test for the card is whether SHE would tape it to a console. Editing is the difference between a reference and a re-printed manual.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-handoff', + nice: ['K0645', 'T1411'], + title: 'Hand Off the Card', + description: 'Log the deliverable for Dana.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['card-review-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Handoff Entry', + params: { + character: Character.SYSTEM, + question: 'Which log entry closes the assignment?', + options: [ + 'AURORA-7 quick-reference card complete - built against a live procedure run (acquire, step-track, verify, all green). Sections: Acquire / Track / Verify / Numbers / 3x Watch-Out from station history. Delivered to Dana for the new-hire packet.', + 'Training card finished.', + 'Procedure run complete, no anomalies.', + 'Card drafted - pending engineering review of all values.', + ], + correctIndex: 0, + explanation: '"Built against a live run" is the card\'s provenance - it is what separates this document from the vendor manual it replaces.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 12:55] +

+

+ "New hire starts ground school next month. I want a one-page quick-reference card for AURORA-7 acquisition and step-track - written by someone who flies it, not copied from the vendor book. Run the procedure this afternoon; at each step pick the one callout that belongs on the card. It builds in the Working Document panel as you go. When you're done I want to be able to hand it to someone on day one." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/19/intro.mp3'), + }, + objectives: { + 'mistake-chase-quiz': { + text: ` +

+ I'm reading the card as it builds - the Watch-Out section is exactly right so far. Notice every line in it is something that actually happened here? That's the difference between a warning and a superstition. Keep going. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/19/obj-mistake-chase-quiz.mp3'), + }, + 'log-handoff': { + text: ` +

+ This is the card I'd have wanted my first week. Procedure flown clean, numbers worked not copied, and three warnings that each cost somebody here a real afternoon. It goes in the new-hire packet tonight. +

+

+ You know the material differently now than you did this morning. That's not an accident - teaching it is the last step of learning it. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/19/obj-log-handoff.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario2.ts b/src/campaigns/nats/scenario2.ts index 977833ed..e69c57b4 100644 --- a/src/campaigns/nats/scenario2.ts +++ b/src/campaigns/nats/scenario2.ts @@ -4,7 +4,7 @@ import type { ScenarioData } from '@app/ScenarioData'; import type { dBm, Hertz, MHz } from '@app/types'; import { getAssetUrl } from '@app/utils/asset-url'; import type { Degrees } from 'ootk'; -import { createRfFrontEnd } from '../rf-front-end-factory'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; import { maineGroundStation, vermontGroundStation } from './ground-stations'; import { ses10Satellite, tidemark1Satellite } from './satellites'; @@ -920,6 +920,8 @@ export const scenario2Data: ScenarioData = { params: { signalId: 'TIDEMARK-1-Beacon', minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', }, mustMaintain: true, }, diff --git a/src/campaigns/nats/scenario20.ts b/src/campaigns/nats/scenario20.ts new file mode 100644 index 00000000..50dd07fd --- /dev/null +++ b/src/campaigns/nats/scenario20.ts @@ -0,0 +1,839 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, Hertz, IfFrequency, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 20: "Dual Outage" + * + * Phase: Crisis Operations (Phase 3, Scenario 4 of 8) + * Time Pressure: High - both sites degrading simultaneously + * Calculation Required: NO - this is triage and parallel recovery + * New Value: true multi-SITE prioritization (S16 was one site, many faults; + * S20 is two sites, unrelated root causes) plus the campaign's first + * explicit adversarial-awareness beat: "two at once - coincidence or + * attack?" answered with evidence, not vibes. + * + * NICE Framework Alignment: + * Primary Codes: + * - T1144: Implement network backup and recovery procedures + * - S0671: Skill in implementing contingency and recovery plans + * - S0807: Skill in solving problems + * + * Supporting Codes: + * - T1538: Resolve customer-reported system incidents + * - S0593: Skill in handling incidents + * - T0531: Troubleshoot hardware/software interoperability problems + * - K0751: Knowledge of system threats + * + * Premise: A winter storm is icing VT-01's feed (the inherited failure: the + * heater was left off ahead of the front) while ME-02 simultaneously throws an + * HPA overdrive with an over-temperature alarm (back-off drifted to 1 dB). + * Dana is 40 minutes out. The operator triages: start VT's slow recovery (one + * switch - heater on), work ME's dangerous-but-deterministic fault end to end, + * then verify both recoveries and document why simultaneity was coincidence. + * + * Triage logic taught: a slow recovery you can START costs nothing to start + * first; a dangerous fast fault gets your full attention immediately after; + * verification happens in the order recoveries complete. + * + * Sim notes: + * - VT-01: 'snow'/severe weather event from T=0; heater OFF (vermont + * default) so ice accumulates (~10 dB max, tau 720 s). Heater ON melts + * at 1 dB/min. Ice recovery checked via custom evaluator. + * - ME-02: HPA backOff 1 -> isOverdriven; temperature recomputed from + * output power each tick, so restoring back-off clears the thermal alarm + * deterministically. BUC stays unmuted throughout (RF-safety rule). + */ + +/** Current ice accumulation (dB) on VT-01's feed, 99 if unavailable. */ +const vt01Ice = (): number => { + const w = window as unknown as { + signalRange?: { + simulationManager?: { + groundStations?: Array<{ + state?: { id?: string }; + antennas?: Array<{ state?: { iceAccumulation_dB?: number } }>; + }>; + }; + }; + }; + const gs = w.signalRange?.simulationManager?.groundStations?.find(g => g.state?.id === 'VT-01'); + return gs?.antennas?.[0]?.state?.iceAccumulation_dB ?? 99; +}; + +export const scenario20Data: ScenarioData = { + id: 'nats-scenario20', + prerequisiteScenarioIds: ['nats-scenario19'], + url: 'nats/scenarios/nats-scenario20', + imageUrl: 'nats/20/card.png', + number: 20, + title: 'Dual Outage', + subtitle: 'Concurrent Site Loss - Prioritized Recovery', + duration: '35-45 min', + difficulty: 'advanced', + missionType: 'Incident Response', + description: `Two boards lit at once. Vermont is in the front edge of a winter storm and the feed is icing - the heater that should have been running since last night is off, and the RX margin is bleeding toward the demod floor. Maine just threw an HPA overdrive with an over-temperature alarm stacked on top: the back-off walked all the way down to 1 dB.

Unrelated problems. Same shift. One operator. Dana is forty minutes out on bad roads, and James Okafor is already asking whether two stations failing at once is something worse than bad luck.

Triage them: the slow recovery you can start costs nothing to start first; the dangerous fault gets your full attention immediately after; and the question James asked deserves an answer built from evidence.`, + equipment: [ + '9-meter C-band Antennas (both sites)', + 'RF Front Ends (both sites)', + 'Spectrum Analyzers', + 'RX/TX Modems', + 'Weather radar feed', + ], + timeLimitSeconds: 45 * 60, + settings: { + isSync: true, + groundStations: [ + // VT-01: storm overhead, heater OFF (the inherited failure), ice building + { + ...vermontGroundStation, + }, + // ME-02: carrying TIDEMARK-2; HPA back-off drifted to 1 dB - overdriven + // and over-temperature. BUC unmuted (traffic flowing). + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 219.7 as Degrees, + elevation: 26.3 as Degrees, + polarization: -25 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61526, + targetAzimuth: 219.7 as Degrees, + targetElevation: 26.3 as Degrees, + targetPolarization: -25 as Degrees, + slewing: false, + beaconCN: 10.2 as dB, + beaconFrequencyHz: 1070e6 as Hertz, + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // The fault: back-off drifted to 1 dB. Overdriven, IMD elevated, + // and the amplifier is cooking (temperature recomputes from + // output power). Traffic still flowing - BUC unmuted. + hpa: { + backOff: 1, + isHpaEnabled: true, + isHpaSwitchEnabled: true, + }, + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1070e6 as Hertz, + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + ifSignal: { + ...vermontGroundStation.transmitters[0].modems[0].ifSignal, + signalId: 'TIDEMARK-2-Teleport', + noradId: 61526, + frequency: 1020e6 as IfFrequency, // TM-2 TP-2: 7000 - 5980 + }, + }, + ], + }, + ], + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1458 as MHz, // TM-2 downlink IF (5250 - 3792) + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [tidemark1Satellite, tidemark2Satellite, ses10Satellite], + weatherEvents: [ + { + id: 'vermont-winter-storm', + groundStationId: 'VT-01', + type: 'snow', + severity: 'severe', + startTime: 0, // Already overhead at shift start + duration: 3600, // Outlasts the scenario - the heater is the fix, not patience + linkMarginDegradation: 10, + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-20?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review the Incident Brief', + description: 'Open the brief. Two sites, two fault summaries, one triage table.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Take Command', + params: { + character: Character.SYSTEM, + question: 'Two sites degrading at once and Dana is 40 minutes out. First move?', + options: [ + 'Read both boards before fixing either - triage is a decision about order, and order needs the whole picture', + 'Fix whichever site you happen to be looking at', + 'Wait for Dana - dual outages exceed solo authority', + 'Hand all traffic to whichever site is healthier', + ], + correctIndex: 0, + explanation: 'The S16 rule scaled up: triage before action. Thirty seconds of reading both boards buys the order that costs the customers least.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 1: READ VERMONT, START THE SLOW RECOVERY + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Start with Vermont - the storm is the fault with momentum.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-read-board', + nice: ['T0153', 'K0741'], + title: 'Read the Vermont Board', + description: 'Dashboard: identify what the storm is doing and what should have prevented it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Vermont Diagnosis', + params: { + character: Character.SYSTEM, + question: 'Ice is accumulating on the VT-01 feed during an active storm. What is the actual failure here?', + options: [ + 'The feed heater is OFF - it should have been running before the front arrived; ice is the consequence, the cold heater is the fault', + 'The storm itself - no operator action is relevant', + 'The antenna drive is frozen', + 'The LNB has failed in the cold', + ], + correctIndex: 0, + explanation: 'Weather is not a fault; being unprepared for forecast weather is. The S3/S14 discipline - heater before the front - was missed on the previous shift, and now the recovery costs minutes instead of nothing.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'vt-enable-heater', + nice: ['S0671', 'S0421'], + title: 'Start the Slow Recovery', + description: 'Enable the feed heater NOW - one switch starts a recovery that runs while you work Maine.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-read-board'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'feed-heater-enabled', + description: 'Feed Heater ON', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // PHASE 2: READ MAINE, KILL THE DANGEROUS FAULT + // ============================================================ + { + id: 'select-maine-station', + nice: ['S0421'], + title: 'Open ME-02', + description: 'Vermont\'s recovery is running. Now the dangerous one.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['vt-enable-heater'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'me-read-board', + nice: ['T0153', 'T0531'], + title: 'Read the Maine Board', + description: 'Dashboard: confirm the HPA fault signature.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['select-maine-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Maine Diagnosis', + params: { + character: Character.SYSTEM, + question: 'ME-02 shows HPA overdrive and HPA over-temperature together. What is the relationship?', + options: [ + 'One fault, two symptoms: back-off at 1 dB drives the amplifier near saturation - IMD rises (overdrive alarm) and the output stage dissipates harder (thermal alarm). Fix the back-off and both clear', + 'Two independent HPA faults that must be worked separately', + 'The thermal alarm caused the back-off to drift', + 'Sensor error - an amplifier cannot be hot and overdriven at once', + ], + correctIndex: 0, + explanation: 'S13\'s lesson in an HPA jacket: trace symptoms to the single input that explains them all. Back-off is the input; heat and IMD are the outputs.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'coincidence-quiz', + nice: ['K0751', 'S0807'], + title: 'Coincidence or Attack?', + description: 'James asked the question. Set the posture before you finish the fixes.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-read-board'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Adversarial Posture', + params: { + character: Character.SYSTEM, + question: 'Two sites degrading simultaneously. How do you treat the "is this an attack?" question right now?', + options: [ + 'Hold it open and collect the rule-out evidence as you work: independent causes that each fully explain their own site, no unexplained RF on either spectrum. Answer it with evidence after the fixes, not with a shrug before them', + 'Dismiss it - storms and config drift happen all the time', + 'Assume attack and disconnect both stations from the network', + 'It cannot be answered at the operator level', + ], + correctIndex: 0, + explanation: 'Simultaneity is what coordinated interference would look like - and what a Friday in January looks like. The discipline is neither paranoia nor dismissal: keep the question open exactly as long as the evidence takes.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'triage-order-quiz', + nice: ['S0807', 'S0671'], + title: 'Defend the Order', + description: 'You started Vermont\'s heater before coming here. Make the triage logic explicit.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['coincidence-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Triage Logic', + params: { + character: Character.SYSTEM, + question: 'Why was "flip VT\'s heater, then work ME\'s HPA end-to-end" the right order?', + options: [ + 'VT\'s recovery is slow but starts with one switch - starting it first costs ME nothing. ME\'s fault is actively dangerous (spectrum pollution + amplifier stress) and deterministic to fix, so it gets full attention immediately after', + 'Vermont is the primary station and always comes first', + 'Alphabetical order by station identifier', + 'The HPA fault could have waited - the order was arbitrary', + ], + correctIndex: 0, + explanation: 'Triage is about clock management: start what runs unattended, then serialize your attention on what needs it. Ten seconds at VT bought minutes of parallel recovery.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'me-disable-hpa', + nice: ['S0593', 'S0677'], + title: 'Take the Dirty Uplink Down', + description: 'Disable the HPA output - the overdriven signal comes off the air first.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['triage-order-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-disabled', + description: 'HPA Output Disabled', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'me-restore-backoff', + nice: ['S0677', 'T1567'], + title: 'Restore the Back-off', + description: 'Set HPA back-off to the 10 dB operating margin while the output is safely down.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-disable-hpa'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-back-off-set', + description: 'Back-off at 10 dB', + params: { + backOff: 10, + backOffTolerance: 1, + }, + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'me-reenable-hpa', + nice: ['S0677', 'T1567'], + title: 'Bring Maine Back Clean', + description: 'Re-enable the HPA output. The BUC stayed unmuted throughout - drive was never the problem.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-restore-backoff'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Output Enabled', + maintainUntilObjectiveComplete: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Operating Linearly', + maintainUntilObjectiveComplete: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'me-verify-quiz', + nice: ['T0531', 'K0740'], + title: 'Confirm Both Symptoms Cleared', + description: 'Verify the single-input diagnosis held: back-off restored, both alarms gone.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-reenable-hpa'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Maine Verification', + params: { + character: Character.SYSTEM, + question: 'Back-off is at 10 dB and the output is re-enabled. What does the thermal alarm do, and why?', + options: [ + 'It clears on its own - output power dropped ~9 dB, so the output stage dissipates a fraction of the heat; the temperature falls with the dissipation that caused it', + 'It stays latched until maintenance resets it physically', + 'It clears only after the BUC is power-cycled', + 'It stays on as a 24-hour cooldown precaution', + ], + correctIndex: 0, + explanation: 'Confirmation that the diagnosis was right: one input (back-off), two symptoms, both gone. If the thermal alarm had stayed up, the single-fault story would be wrong - and you would start looking for the second fault.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: VERIFY VERMONT'S RECOVERY + // ============================================================ + { + id: 'return-to-vermont', + nice: ['S0421'], + title: 'Back to Vermont', + description: 'Maine is clean. Check on the recovery you started fifteen minutes ago.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['me-verify-quiz'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-verify-recovery', + nice: ['S0671', 'T0153'], + title: 'Verify the Melt', + description: 'Confirm the heater is winning: ice below 2 dB and the receiver healthy again.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['return-to-vermont'], + timeLimitSeconds: 8 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'feed-heater-enabled', + description: 'Heater Still Running', + mustMaintain: true, + }, + { + type: 'custom', + description: 'Ice Melted Below 2 dB', + params: { + evaluator: () => vt01Ice() < 2, + }, + mustMaintain: false, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1 }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N Recovered (≥ 9 dB)', + params: { minCNRatio: 9 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'storm-steady-state-quiz', + nice: ['S0671', 'K0689'], + title: 'Steady State in the Storm', + description: 'The storm has not stopped. Define the posture for the rest of it.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-verify-recovery'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Storm Posture', + params: { + character: Character.SYSTEM, + question: 'The front is still overhead. What keeps Vermont healthy for the rest of it?', + options: [ + 'Nothing new - the heater holds ice at bay as fast as it forms; the steady state is heater ON plus periodic margin checks until the front clears', + 'Repeating the melt cycle every thirty minutes', + 'A precautionary handover of TM-1 to Maine', + 'Raising LNB gain to compensate for the storm', + ], + correctIndex: 0, + explanation: 'The S14 lesson holds: with the right protections running, weather is something you monitor, not something you fight. The failure this morning was a cold heater, not a strong storm.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: CUSTOMER, EVIDENCE, LOG + // ============================================================ + { + id: 'james-comms-quiz', + nice: ['T1538', 'S0478'], + title: 'Call James Back', + description: 'He asked two questions an hour ago: what happened, and should he be worried.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['storm-steady-state-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Customer Callback', + params: { + character: Character.SYSTEM, + question: 'What does James get?', + options: [ + 'Both trunks restored: Vermont was storm icing (heater now running, holding), Maine was an amplifier config fault (corrected, verified). Causes independent and fully explained - the simultaneity was coincidence, and here is why we are confident.', + 'Everything is fixed. Bad luck, like I said.', + 'Technical incident report attached, 14 pages, conclusions on page 11.', + 'Still investigating, will update within 24 hours.', + ], + correctIndex: 0, + explanation: 'Cause, action, status for each site - then the answer to the question he actually asked, with the reasoning shown. "Here is why we are confident" is what makes "coincidence" a finding instead of a hope.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'adversarial-ruleout-quiz', + nice: ['K0751', 'S0807'], + title: 'Close the Question Honestly', + description: 'Write the rule-out into the record - the evidence, not the conclusion.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['james-comms-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Rule-Out', + params: { + character: Character.SYSTEM, + question: 'Which evidence set closes the "coincidence or attack" question?', + options: [ + 'VT degradation tracked the storm exactly (radar + precip sensor agree, heater-on fixed it); ME fault was a config value with mundane history; no unexplained signals on either spectrum; both recoveries behaved as their diagnoses predicted', + 'Both faults were fixed, therefore neither was an attack', + 'No attacker would target a teleport in Vermont', + 'The question cannot be closed without a federal investigation', + ], + correctIndex: 0, + explanation: 'Each cause independently explains its own site, the fixes behaved as predicted, and the spectra are clean. Document it every time it IS coincidence - that record is what makes you credible the day it is not.', + pointPenalty: 10, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'log-dual-outage', + nice: ['K0645', 'T1144'], + title: 'Log the Dual Recovery', + description: 'One entry, two sites, full timeline.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['adversarial-ruleout-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Incident Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry records this incident correctly?', + options: [ + 'Concurrent site degradation 0712: VT-01 feed icing (heater off ahead of front - corrected 0716, melt verified) | ME-02 HPA back-off drift to 1 dB (output disabled, 10 dB restored, re-enabled clean by 0734). Causes independent - rule-out documented. Customers notified. Heater discipline flagged for shift-change checklist.', + 'Both stations had problems this morning. Fixed.', + 'Weather event at VT-01. See separate ME-02 ticket.', + 'Dual outage resolved - details available on request.', + ], + correctIndex: 0, + explanation: 'Timeline, both causes, both fixes, the rule-out, and the process fix (heater on the shift-change checklist) so the inherited failure stops being inheritable.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Call from Dana at 07:09 - road noise] +

+

+ "Two boards lit at once. Vermont's icing - the heater's been off since last night, don't ask - and Maine just threw an HPA overdrive with a temperature alarm on top. I'm forty minutes out on bad roads. Triage them: SLA exposure first, recovery time second. Don't give me hero sequencing - give me the order that costs the customers least." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/20/intro.mp3'), + }, + objectives: { + 'vt-enable-heater': { + text: ` +

+ Seeing margin alerts on both our trunks at once. Two stations at the same time - should I be worried this is something other than bad luck? Call me when you know. Not when it's fixed - when you KNOW. +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/20/obj-vt-enable-heater.mp3'), + }, + 'me-verify-quiz': { + text: ` +

+ Just passed the county line - catching up on the log. Heater running at Vermont, Maine's amp back in its lane, and you kept the BUC out of it. Right order, right reasons. Finish the verification and get James his answer. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/20/obj-me-verify-quiz.mp3'), + }, + 'adversarial-ruleout-quiz': { + text: ` +

+ Appreciate the callback - and the homework behind it. "Here's why we're confident" lands a lot better with my board than "trust us." Both trunks look clean from our side. We're good. +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/20/obj-adversarial-ruleout-quiz.mp3'), + }, + 'log-dual-outage': { + text: ` +

+ Walking in now - and the incident's already closed with the process fix in the log. Two sites, one operator, zero customer drama. That's the qualification I actually care about. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/20/obj-log-dual-outage.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario21.ts b/src/campaigns/nats/scenario21.ts new file mode 100644 index 00000000..4d8f14eb --- /dev/null +++ b/src/campaigns/nats/scenario21.ts @@ -0,0 +1,782 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, Hertz, IfFrequency, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 21: "Hostile RF" + * + * Phase: Crisis Operations (Phase 3, Scenario 5 of 8) + * Time Pressure: Moderate - the interferer is active and intermittent + * Calculation Required: NO (guard-band reasoning, no RF math) + * New Mechanic: time-windowed interference (engine: InterferenceManager + * injects a duty-cycled jammer at the satellite transponder, so it is + * relayed to the receiving station - the uplink-interference case that + * makes discrimination meaningful). + * + * NICE Framework Alignment: + * Primary Codes: + * - K0926: Knowledge of signal jamming tools and techniques + * - S0615: Skill in protecting a network against malware + * - S0648: Skill in detecting anomalies + * + * Supporting Codes: + * - T0081: Diagnose network connectivity problems + * - S0593: Skill in handling incidents + * - T0153: Monitor network capacity and performance + * - K0751: Knowledge of system threats + * + * Premise: An intermittent, broadband-ish interferer rides the TIDEMARK-2 + * downlink - on/off in a regular pattern that fits neither cross-pol leakage + * nor known terrestrial sources. The operator characterizes the signature, + * discriminates deliberate jamming from accidental interference (duty cycle, + * transponder-vs-local, data-layer integrity), applies receive-side + * mitigation, builds the regulator package, and - critically - verifies this + * is RF-layer denial and NOT a data-layer intrusion (crypto/key integrity). + * + * The campaign's first adversarial-aware scenario, deepened: a rigorous + * jamming-vs-interference workflow that maps onto real ITU interference- + * reporting practice and the denial-vs-intrusion distinction. + * + * Tone: Crisis-phase, investigative. Dana (incident handling), Marcus + * (spacecraft-side consultation), regulator rendered as SYSTEM notifications. + * All quizzes SYSTEM. 5-6 clips. + * + * Sim notes: + * - interferenceEvents: duty-cycled jammer on TM-2 (NORAD 61526) at uplink + * 6005 MHz H-pol (inside TP-1 passband 5999-6035) -> relayed to downlink + * 3780 MHz -> 1470 MHz IF at ME-02 (LNB LO 5250). Carrier sits at 1458 + * MHz IF; the jammer is ~12 MHz off, characterizable on a wide span. + * - 90 s on / 60 s off, repeating - the regular duty cycle IS the + * deliberation signature. + * - Crypto defaults ACTIVE/Valid: the data-layer checks pass, proving the + * attack is RF denial, not intrusion. (If they ever changed mid-incident, + * the scenario's whole posture would change - that is the teaching point.) + */ + +export const scenario21Data: ScenarioData = { + id: 'nats-scenario21', + prerequisiteScenarioIds: ['nats-scenario20'], + url: 'nats/scenarios/nats-scenario21', + imageUrl: 'nats/21/card.png', + number: 21, + title: 'Hostile RF', + subtitle: 'Suspected Intentional Interference', + duration: '30-35 min', + difficulty: 'advanced', + missionType: 'Threat Response', + description: `Catherine's day shift flagged it before she left: intermittent broadband noise riding the TIDEMARK-2 downlink, on-off-on, gone before she could characterize it. It's back this morning. It doesn't match the cross-pol neighbors and it doesn't match weather.

Treat it as an interference incident from the first minute. Characterize the signature, discriminate jamming from a benign accident, mitigate what you can on the receive side, and build the regulator package as you go. And verify the half of this that matters most: an RF interferer denies your signal - it does not touch your data. Prove the data layer is intact, or you are no longer working an interference event.

If it turns out to be somebody's misaligned uplink, the same evidence resolves it. If it's deliberate, the evidence IS the response.`, + equipment: [ + '9-meter C-band Antenna (ME-02)', + 'RF Front End (notch filter)', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'Crypto module (data-layer integrity)', + ], + timeLimitSeconds: 35 * 60, + settings: { + isSync: true, + groundStations: [ + // VT-01: standby on TIDEMARK-1 + { + ...vermontGroundStation, + }, + // ME-02: carrying TIDEMARK-2, taking the interference + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 219.7 as Degrees, + elevation: 26.3 as Degrees, + polarization: -25 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61526, + targetAzimuth: 219.7 as Degrees, + targetElevation: 26.3 as Degrees, + targetPolarization: -25 as Degrees, + slewing: false, + beaconCN: 10.2 as dB, + beaconFrequencyHz: 1070e6 as Hertz, + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // ME-02 carries TM-2: chain hot, LNB LO 5250 for matching IFs + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -85 as dBm, + centerFrequency: 1458e6 as Hertz, // TM-2 carrier IF + span: 40e6 as Hertz, // Wide enough to see the carrier and the adjacent jammer + rbw: 100e3 as Hertz, + minAmplitude: -100 as dBm, + maxAmplitude: -40 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: true, isUpdating: true, mode: 'maxhold' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + ifSignal: { + ...vermontGroundStation.transmitters[0].modems[0].ifSignal, + signalId: 'TIDEMARK-2-Teleport', + noradId: 61526, + frequency: 1020e6 as IfFrequency, // TM-2 TP-2: 7000 - 5980 + }, + }, + ], + }, + ], + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1458 as MHz, // TM-2 downlink IF (5250 - 3792) + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [tidemark2Satellite, tidemark1Satellite, ses10Satellite], + interferenceEvents: [ + { + id: 'tm2-hostile', + satelliteNoradId: 61526, + frequency: 6005e6, // Uplink, inside TM-2 TP-1 passband (5999-6035), H-pol + bandwidth: 6e6, + // dBm at the transponder input. Carrier composite is 20 dBm, so C/I + // ~15 dB - visible and degrading, but the carrier keeps demod lock + // (the training point is mitigation, not a hard outage). + power: 5, + polarization: 'H', + startTime: 20, + duration: 3600, // Persists across the scenario + periodSeconds: 150, // 90s on, 60s off - a regular, deliberate cadence + onSeconds: 90, + }, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-21?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review the Interference Flag', + description: 'Open the brief and Catherine\'s end-of-shift note.', + groundStation: 'ME-02', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Frame the Incident', + params: { + character: Character.SYSTEM, + question: 'An intermittent interferer is on TM-2. How do you treat it from minute one?', + options: [ + 'As an interference incident: characterize, discriminate, mitigate, document - and verify the data layer separately. The same evidence resolves an accident or builds the case for deliberate interference', + 'Wait to see if it clears on its own before doing anything', + 'Assume it is jamming and immediately disconnect TM-2', + 'Power-cycle the LNB - intermittent usually means a flaky receiver', + ], + correctIndex: 0, + explanation: 'Evidence first, conclusion last. Whether it ends as "misaligned partner uplink" or "deliberate denial," the work is identical until the evidence decides - and that discipline is what makes the eventual conclusion defensible.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-maine-station', + nice: ['S0421'], + title: 'Open ME-02', + description: 'Select the Maine Ground Station.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: CHARACTERIZE THE SIGNATURE + // ============================================================ + { + id: 'open-spectrum', + nice: ['S0648', 'T0153'], + title: 'Observe the Interference', + description: 'Open RX Analysis. The spectrum is set to a wide span around the TM-2 carrier so the adjacent interferer is visible when it keys up.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['select-maine-station'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum Centered on TM-2 Carrier (1458 MHz IF)', + params: { + centerFrequency: 1458e6, + centerFrequencyTolerance: 2e6, + }, + mustMaintain: true, + }, + { + type: 'speca-span-set', + description: 'Wide Span (≥ 30 MHz) to See the Adjacent Interferer', + params: { + span: 40e6, + frequencyTolerance: 15e6, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'duty-cycle-quiz', + nice: ['K0926', 'S0648'], + title: 'Characterize the Duty Cycle', + description: 'The interferer is on, then off, then on, in a regular pattern. Interpret it.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['open-spectrum'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Duty Cycle Read', + params: { + character: Character.SYSTEM, + question: 'The interferer runs a regular cycle - roughly ninety seconds on, a minute off, repeating. What does that timing argue?', + options: [ + 'Against accident, toward deliberation: natural and accidental sources are continuous or random; a clean repeating on/off cadence on the scale of a minute implies a hand on a switch or an automated transmitter, not weather or a stuck oscillator', + 'Toward radar - rotating antennas produce exactly this minute-scale cadence', + 'Toward a failing power supply on a neighbor\'s uplink', + 'Nothing - duty cycle carries no diagnostic information', + ], + correctIndex: 0, + explanation: 'Radar duty cycles are seconds, not minutes. Accidental interference is continuous or erratic. A deliberate, minute-scale on/off pattern is one of the strongest behavioral indicators of intent - though it is an indicator, not proof.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'transponder-vs-local-quiz', + nice: ['K0926', 'T0081'], + title: 'Transponder or Local?', + description: 'The single most valuable discrimination test: is the interferer on the bird, or local to your site?', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['duty-cycle-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'The Locality Test', + params: { + character: Character.SYSTEM, + question: 'You confirm VT-01 and Halifax see the same interferer on TM-2 TP-1. What does that establish?', + options: [ + 'It is in the UPLINK - the satellite is relaying it to every receiving station, so the source is transmitting at the bird, not a local terrestrial source at Maine. That rules out radar/5G/our own equipment and points at a misaligned or deliberate uplink', + 'It is local terrestrial interference at all three sites simultaneously', + 'It proves the satellite transponder itself is malfunctioning', + 'Nothing - other stations seeing it is expected for any signal', + ], + correctIndex: 0, + explanation: 'This is the highest-information test in the matrix. Local interference appears at one station; uplink interference is relayed to all. Every-station visibility moves the source from "somewhere near Maine" to "transmitting at the satellite."', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'signature-shape-quiz', + nice: ['K0926', 'S0648'], + title: 'Signature vs the Database', + description: 'Compare bandwidth and placement against what the coordination database would explain.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['transponder-vs-local-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shape Read', + params: { + character: Character.SYSTEM, + question: 'The interferer is broadband-ish, sized close to our carrier, sitting inside our transponder near our allocation. How does that compare to benign explanations?', + options: [ + 'It matches nothing in the coordination database: cross-pol leakage would mirror a known neighbor\'s carrier, an errant uplink would look like a modulated carrier at a coordinated slot - broadband noise shaped to our carrier inside our passband fits neither', + 'It is a textbook cross-pol leakage signature', + 'It matches a coordinated adjacent-satellite carrier exactly', + 'Bandwidth and placement carry no diagnostic weight', + ], + correctIndex: 0, + explanation: 'Accidents look like known things in the wrong place. Noise deliberately shaped and placed to deny a specific carrier looks like nothing the coordination database can account for - the absence of a benign match is itself evidence.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: DENIAL VS INTRUSION (data-layer integrity) + // ============================================================ + { + id: 'denial-vs-intrusion-quiz', + nice: ['S0615', 'K0751'], + title: 'Denial or Intrusion?', + description: 'Before mitigating, frame what kind of attack this is - it determines the escalation path.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['signature-shape-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Attack Class', + params: { + character: Character.SYSTEM, + question: 'If this is deliberate, what KIND of attack is RF jamming, and why does the distinction matter?', + options: [ + 'It is a DENIAL attack on the RF layer - it degrades or blocks the signal but does not touch the data, crypto, or keys. The distinction matters because an intrusion (data-layer compromise) has a different escalation path and you must verify which you are facing', + 'It is an intrusion - jamming always implies the attacker has system access', + 'It is malware on the modem', + 'The distinction is academic - all attacks get the same response', + ], + correctIndex: 0, + explanation: 'Jamming denies availability; it does not breach confidentiality or integrity. Conflating denial with intrusion sends you down the wrong escalation path - you must check the data layer to know which incident you actually have.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'verify-data-layer', + nice: ['S0615', 'T0081'], + title: 'Verify the Data Layer', + description: 'Confirm the crypto and key state are intact - this is RF denial, not a breach.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['denial-vs-intrusion-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'rx-crypto-status', + description: 'RX Crypto ACTIVE', + params: { cryptoMode: 'ACTIVE' }, + mustMaintain: true, + }, + { + type: 'rx-key-status', + description: 'RX Key Valid', + params: { keyStatus: 'Valid' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'data-layer-meaning-quiz', + nice: ['S0615', 'K0751'], + title: 'What Intact Crypto Proves', + description: 'Confirm what the healthy data layer establishes - and what would change if it were not.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['verify-data-layer'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Data-Layer Finding', + params: { + character: Character.SYSTEM, + question: 'Crypto is ACTIVE, keys Valid, no frame anomalies outside the jamming windows. What does that confirm - and what would flip the incident?', + options: [ + 'Confirms RF-layer denial only: the link is being degraded, not breached. If crypto state changed or keys went invalid/mismatched mid-incident, it would become a security incident with a different escalation (Dana then security officer; nothing holding keys gets power-cycled)', + 'Confirms nothing - crypto is unrelated to interference', + 'Confirms the interferer has been decrypted and identified', + 'Means the jamming has stopped', + ], + correctIndex: 0, + explanation: 'Two layers, two verifications, never conflated. Intact crypto bounds this to a denial-of-availability event. The hypothetical - a crypto change mid-incident - is exactly the tripwire that would escalate it to an intrusion.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: RECEIVE-SIDE MITIGATION + // ============================================================ + { + id: 'mitigation-choice-quiz', + nice: ['S0593', 'K0737'], + title: 'Choose Mitigation', + description: 'Pick the right receive-side mitigation - and rule out the wrong one.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['data-layer-meaning-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Mitigation Selection', + params: { + character: Character.SYSTEM, + question: 'The interferer is a band of noise ~12 MHz off the carrier center, inside our passband. Best receive-side mitigation?', + options: [ + 'Notch filter on the interferer band - excise its energy at the cost of a little SNR in that slice; reversible, customer-preserving, and it does nothing to neighbors', + 'Increase our uplink power to "burn through" the interferer', + 'Retune the customer carrier to a clean frequency immediately', + 'Mute the receiver until the interferer stops', + ], + correctIndex: 0, + explanation: 'A notch is the surgical tool: it removes the interferer\'s energy from the receive path. Power escalation degrades neighbors, violates coordination, and feeds the incident - there is NO transmit-side mitigation for uplink jamming available to a ground station.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'configure-notch', + nice: ['S0593', 'K0737'], + title: 'Apply the Notch Filter', + description: 'Configure a notch on the interferer band at 1470 MHz IF (~12 MHz above the 1458 carrier center).', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['mitigation-choice-quiz'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'notch-filter-configured', + description: 'Notch at 1470 MHz IF on the Interferer', + params: { + notchCenterFrequency: 1470, + notchCenterFrequencyTolerance: 2, + notchBandwidth: 8, + notchBandwidthTolerance: 3, + notchDepth: 30, + notchDepthTolerance: 10, + notchIndex: 0, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-carrier-survives', + nice: ['T0153', 'S0593'], + title: 'Confirm the Customer Survives', + description: 'Verify the TM-2 carrier is still locked after the notch - mitigation must not cost the customer the link.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['configure-notch'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-signal-locked', + description: 'Receiver Still Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'no-burn-through-quiz', + nice: ['K0926', 'S0593'], + title: 'Why Not Fight Back', + description: 'Confirm why escalating power against the jammer is the wrong move.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['verify-carrier-survives'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'No Power War', + params: { + character: Character.SYSTEM, + question: 'A colleague suggests cranking the uplink to overpower the interferer. Why is that wrong?', + options: [ + 'You cannot out-shout an uplink jammer from the ground without violating coordination and degrading every neighbor on the transponder - a power war escalates the incident, harms bystanders, and still loses. Receive-side mitigation plus regulatory action is the path', + 'It would work but takes too long to ramp the HPA', + 'The customer contract forbids power changes', + 'It is fine - more power always improves the link', + ], + correctIndex: 0, + explanation: 'Burn-through is a denial response that creates more denial. The operator\'s tools are receive-side mitigation and the regulator; offense belongs to the spectrum authority and geolocation services, not the victim\'s HPA.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: COORDINATE AND REPORT + // ============================================================ + { + id: 'regulator-package-quiz', + nice: ['S0648', 'K0926'], + title: 'Build the Regulator Package', + description: 'Select the fields the spectrum authority needs to act.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['no-burn-through-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Report Fields', + params: { + character: Character.SYSTEM, + question: 'Which set of fields makes a regulator interference report actionable?', + options: [ + 'Victim (satellite/transponder/pol/center freq/BW), interferer (center freq, BW, measured duty cycle, first/last seen), spectrum captures at documented settings, cross-station confirmation (transponder vs local), service impact, and mitigation applied - all timestamped', + 'A one-line summary: "being jammed, please help"', + 'Only our internal trouble-ticket number', + 'The interferer\'s identity and location - which we determine ourselves', + ], + correctIndex: 0, + explanation: 'Per ITU-R practice the report is evidence, not conclusion: measured parameters, captures, cross-station confirmation, and impact - everything timestamped. Attribution (who, where) belongs to the regulator and geolocation providers, not the ground operator.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'attribution-restraint-quiz', + nice: ['K0751', 'S0648'], + title: 'Conclude Carefully', + description: 'Set the language for the finding - confidence without overreach.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['regulator-package-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Finding Language', + params: { + character: Character.SYSTEM, + question: 'How should the operator\'s written finding be worded?', + options: [ + '"Signature consistent with deliberate uplink interference" - a confidence-bounded characterization backed by the evidence; attribution and intent are for the regulator to determine, not the ground operator to assert', + '"Confirmed hostile jamming by [named actor]" - state it plainly', + '"Probably just interference, no action needed"', + 'Avoid any characterization - just file the raw data', + ], + correctIndex: 0, + explanation: 'The evidence supports "consistent with deliberate" - it does not support naming an actor or declaring intent as fact. Disciplined language is what makes your evidence usable; overreach is what gets it dismissed.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-incident', + nice: ['K0645', 'S0593'], + title: 'Log the Incident', + description: 'Record the interference incident and its disposition.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['attribution-restraint-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Incident Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry records this correctly?', + options: [ + 'TM-2 TP-1 interference: broadband noise ~1470 MHz IF, ~90s/60s duty cycle, confirmed uplink (seen VT-01 + Halifax). Data layer intact (crypto ACTIVE, keys Valid) - RF denial, not intrusion. Notch applied, carrier holding. Signature consistent with deliberate interference - regulator package filed. Monitoring; escalate on any data-layer change.', + 'TM-2 had some interference, applied a notch, fixed.', + 'Confirmed hostile jamming, link saved by burn-through.', + 'Intermittent RX issue, power-cycled LNB.', + ], + correctIndex: 0, + explanation: 'Everything load-bearing: signature, duty cycle, the uplink confirmation, the intact data layer (denial not intrusion), the mitigation, the disciplined finding, and the escalation tripwire. The next shift inherits a complete, defensible picture.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Call from Dana at 10:22] +

+

+ "Catherine flagged something before she left - intermittent broadband noise on the TM-2 downlink, on-off-on, gone before she could pin it. It's back. Doesn't look like our cross-pol neighbors, doesn't look like weather. Treat it as an interference incident from minute one: characterize, discriminate, mitigate, document. And check the data layer - if this is deliberate it's denial, not a breach, but I want that confirmed, not assumed." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/21/intro.mp3'), + }, + objectives: { + 'transponder-vs-local-quiz': { + text: ` +

+ Marcus in Halifax - confirming from our side: we see the same interferer on TM-2 TP-1 that you do, and the spacecraft telemetry's clean, transponder's nominal. If we're both seeing it through the bird, it's coming up on the uplink, not local to either of us. That's not weather and it's not our hardware. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/21/obj-transponder-vs-local-quiz.mp3'), + }, + 'verify-data-layer': { + text: ` +

+ [Regulator coordination desk - SYSTEM notification, 10:41] +

+

+ "Interference report channel open for TM-2 TP-1. Submit measured parameters, spectrum captures, and cross-station confirmation. Do not attempt source attribution; geolocation tasking will be handled here. Acknowledge receipt of mitigation guidance: receive-side only, no power escalation." +

+ `, + character: Character.SYSTEM, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/21/obj-verify-data-layer.mp3'), + }, + 'configure-notch': { + text: ` +

+ Good - data layer's clean, so we're working a denial event, not a breach. That keeps it on our desk and the regulator's, not the security officer's. Get the notch in and prove the customer's still up, then send the package. Nice methodical work. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/21/obj-configure-notch.mp3'), + }, + 'log-incident': { + text: ` +

+ That's how you work a hostile-RF event: you measured everything, concluded nothing you couldn't back, kept the customer up, and handed the regulator a package they can actually act on. "Consistent with deliberate" - not a name, not a flag, just the evidence. +

+

+ If it changes character - especially anything on the data side - it escalates instantly. Until then, you've got it. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/21/obj-log-incident.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario22.ts b/src/campaigns/nats/scenario22.ts new file mode 100644 index 00000000..d293c118 --- /dev/null +++ b/src/campaigns/nats/scenario22.ts @@ -0,0 +1,634 @@ +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dBm, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import { vermontGroundStation } from './ground-stations'; +import { aurora7Satellite, tidemark1Satellite } from './satellites'; + +/** + * NATS Level 22: "End-of-Life Planning" + * + * Phase: Crisis Operations (Phase 3, Scenario 6 of 8) + * Time Pressure: None - this is analysis and communication + * Calculation Required: Light (trend extrapolation, no RF math) + * Mechanic Reused: the Working Document panel (from S19), now producing an + * executive impact assessment rather than a how-to card. + * + * NICE Framework Alignment: + * Primary Codes: + * - K0721: Knowledge of risk management principles and practices + * - T1429: Prepare trend analysis reports + * - T1606: Prepare impact reports + * + * Supporting Codes: + * - K0751: Knowledge of system threats + * - S0807: Skill in solving problems + * - T1020: Determine operational and safety impacts + * + * Premise: AURORA-7's beacon power has declined for months (power subsystem + * aging) and the decline is steepening. Francis Martin (board) wants an + * operations recommendation on retirement timing. The player runs a final + * data-collection pass, then builds a trend assessment and sunset + * recommendation in the Working Document - the campaign's first + * report-producing scenario. Light on RF, heavy on analysis and the + * discipline of labeling confidence. + * + * Tone: Reflective-analytical. Marcus (Halifax vehicle view), Dana (intro), + * Francis (board-level, rare). All quizzes SYSTEM. 5 clips. + * + * Sim notes: + * - AURORA-7 starts on program-track; player engages step-track for the + * final ops run (the beacon is weak - end-of-life). The data-collection + * phase is real; the rest is analysis quizzes that build the report. + */ + +export const scenario22Data: ScenarioData = { + id: 'nats-scenario22', + prerequisiteScenarioIds: ['nats-scenario21'], + url: 'nats/scenarios/nats-scenario22', + imageUrl: 'nats/22/card.png', + number: 22, + title: 'End-of-Life Planning', + subtitle: 'AURORA-7 Sunset Recommendation', + duration: '25-30 min', + difficulty: 'intermediate', + missionType: 'Analysis & Reporting', + description: `AURORA-7's beacon power has been declining for months - the power subsystem on a nineteen-year-old bird running out of watts - and the slope is steepening. The board wants a recommendation on retirement timing, from operations rather than from a spreadsheet.

Run one more data-collection pass this morning, then build the assessment. The Working Document panel is set up as the report template; it fills in as you make your calls. Francis Martin wants a recommendation he can forward without editing: how long the spacecraft can responsibly carry service, what degrades first, and what the migration window looks like.

Light on RF today. Heavy on reading a trend honestly and saying what you know without false precision.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'Working Document panel (the report)', + ], + timeLimitSeconds: 30 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1422 as MHz, // AURORA-7 downlink IF (5250 - 3828) + bandwidth: 24 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [aurora7Satellite, tidemark1Satellite], + workingDocument: { + title: 'AURORA-7 End-of-Life Assessment', + description: 'Operations recommendation for the SeaLink board - built from today\'s data run and trend analysis.', + }, + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-22?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review the Tasking', + description: 'Open the brief and Francis\'s request.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Frame the Deliverable', + params: { + character: Character.SYSTEM, + question: 'Francis wants a retirement recommendation "from operations." What does that mean you must produce?', + options: [ + 'A defensible recommendation grounded in measured data and an honest trend, with assumptions labeled - not a single date with false precision', + 'A precise retirement date to the day', + 'A decision to retire immediately to avoid risk', + 'A restatement of the vendor\'s end-of-life bulletin', + ], + correctIndex: 0, + explanation: 'The board can handle uncertainty; it cannot handle a confident number that unravels in a quarter. "From operations" means built from what the dish actually measures.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: FINAL DATA COLLECTION RUN + // ============================================================ + { + id: 'acquire-aurora', + nice: ['S0421', 'K1032'], + title: 'Acquire AURORA-7', + description: 'Program-track to AURORA-7 (NORAD 28899) for the final data run.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Program-Track Mode', + params: { trackingMode: 'program-track' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on AURORA-7', + params: { noradId: 28899 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'engage-step-track', + nice: ['S0421', 'K1032'], + title: 'Step-Track for the Weak Beacon', + description: 'Engage step-track - the beacon is 4 dB down from its prime and needs every bit of pointing accuracy.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['acquire-aurora'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Engaged', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + { + type: 'antenna-beacon-locked', + description: 'Beacon Held', + mustMaintain: true, + maintainDuration: 15, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'measure-beacon', + nice: ['T1429', 'K0773'], + title: 'Measure Today\'s Beacon', + description: 'Tune the spectrum analyzer to the beacon IF and capture today\'s level - the newest point on the trend.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['engage-step-track'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum at 1085 MHz IF', + params: { + centerFrequency: 1085e6, + centerFrequencyTolerance: 1e6, + }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'AURORA-7 Beacon Captured', + params: { + signalId: 'AURORA-7-Beacon', + minPower: -110 as dBm, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'record-data-point-quiz', + nice: ['T1429', 'K0740'], + title: 'Report: Today\'s Measurement', + description: 'Record today\'s data point in the assessment.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['measure-beacon'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Data Point', + params: { + character: Character.SYSTEM, + question: 'What goes into the report as today\'s measured data?', + options: [ + 'Beacon at -4.0 dB relative to the 24-month reference; step-track held lock at this level; carrier C/N still above demod threshold - measured, not estimated', + 'Beacon is fine', + 'The satellite will fail soon', + 'Beacon power is declining (no number)', + ], + correctIndex: 0, + explanation: 'Measured values, stated as measured. The number with its reference and the fact that step-track still holds at this level - that is data, and the report is built on data first.', + pointPenalty: 5, + documentSection: 'Data (measured today)', + documentLine: 'Beacon -4.0 dB vs 24-mo reference. Step-track holds lock at this level. Carrier C/N above demod threshold.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: TREND ANALYSIS + // ============================================================ + { + id: 'trend-slope-quiz', + nice: ['T1429', 'S0807'], + title: 'Report: the Trend', + description: 'Characterize the decline from the beacon-power history.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['record-data-point-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Trend Shape', + params: { + character: Character.SYSTEM, + question: 'The history: -1.1 dB at 12 months, -2.4 at 6 months, -4.0 today. How do you characterize the trend?', + options: [ + 'Accelerating - the last 6 months lost ~1.6 dB versus ~1.1 dB in the prior 6; a steepening curve, not a straight line, so naive linear extrapolation understates the near-term decline', + 'Linear and steady at about 0.3 dB/month', + 'Flattening - the decline is slowing', + 'Random - no meaningful trend', + ], + correctIndex: 0, + explanation: 'Reading acceleration vs linearity is the whole analysis. A power subsystem shedding load tends to worsen non-linearly; calling it linear would sell the board a longer runway than the spacecraft has.', + pointPenalty: 5, + documentSection: 'Trend', + documentLine: 'Beacon decline ACCELERATING: -1.1 (12mo) -> -2.4 (6mo) -> -4.0 (now). Non-linear - linear extrapolation understates near-term.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'binding-constraint-quiz', + nice: ['K0751', 'T1020'], + title: 'Report: What Fails First', + description: 'Identify the operational cliff that actually ends usable service.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['trend-slope-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Binding Constraint', + params: { + character: Character.SYSTEM, + question: 'What is the binding constraint - the thing that ends usable service first?', + options: [ + 'The beacon getting too weak to step-track. AURORA-7 is inclined, so without a trackable beacon every pass becomes manual figure-8 chasing and service quality collapses - the cliff is tracking, not transponder death', + 'The transponder ceasing to relay - the payload fails first', + 'The satellite de-orbiting', + 'Fuel exhaustion for station-keeping', + ], + correctIndex: 0, + explanation: 'The S18/S23 lesson pays off: an inclined bird is only usable while step-track can hold its beacon. The recommendation hinges on when the declining beacon crosses the trackability floor, not on the transponder.', + pointPenalty: 5, + documentSection: 'Risk', + documentLine: 'Binding constraint = beacon trackability (inclined bird). When beacon too weak to step-track, every pass goes manual and quality collapses. NOT transponder death.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'marcus-corroboration-quiz', + nice: ['T1020', 'K1032'], + title: 'Report: Vehicle Corroboration', + description: 'Fold in Halifax\'s spacecraft-side view of the power trend.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['binding-constraint-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Corroboration', + params: { + character: Character.SYSTEM, + question: 'Marcus confirms the vehicle bus is shedding RF load as the array ages. How does that affect confidence in the trend?', + options: [ + 'It raises confidence and explains the mechanism: ground-measured beacon decline and spacecraft-reported power loss are the same story from two independent vantage points - the trend is real, not an artifact of our station', + 'It contradicts the ground data and lowers confidence', + 'It is irrelevant - only ground measurements matter', + 'It means the satellite is fine and the dish is faulty', + ], + correctIndex: 0, + explanation: 'Two independent measurements agreeing on cause AND trend is the strongest evidence a report can carry. It also rules out "our antenna is degrading" - the spacecraft sees its own power dropping.', + pointPenalty: 5, + documentSection: 'Trend', + documentLine: 'Corroborated by Halifax: vehicle bus shedding RF load as array ages. Ground + spacecraft agree on cause and trend (independent vantage points).', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: RECOMMENDATION + // ============================================================ + { + id: 'sunset-window-quiz', + nice: ['K0721', 'T1606'], + title: 'Report: the Recommendation', + description: 'Make the call - a window with decision points, not a single date.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['marcus-corroboration-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Sunset Recommendation', + params: { + character: Character.SYSTEM, + question: 'What is the responsible recommendation?', + options: [ + 'Begin customer migration now; target sunset in roughly one to two quarters with a hard review at each monthly data point, and a firm decision trigger when the beacon crosses the step-track floor. A window with tripwires, not a date', + 'Retire immediately - the risk is unacceptable', + 'No action for a year - the link still works today', + 'Set a precise retirement date 8 months out', + ], + correctIndex: 0, + explanation: 'A window plus tripwires gives customers runway while protecting against the accelerating curve. "Begin migration now, decide at the trackability floor" is defensible because it is tied to a measurable event, not a guess.', + pointPenalty: 10, + preserveOptionOrder: true, + documentSection: 'Recommendation', + documentLine: 'Begin customer migration NOW. Target sunset ~1-2 quarters, monthly data review, firm trigger = beacon crosses step-track floor. Window with tripwires, not a fixed date.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'assumptions-quiz', + nice: ['K0721', 'T1606'], + title: 'Report: Label the Assumptions', + description: 'State what the recommendation depends on - the honesty that makes it defensible.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['sunset-window-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Stated Assumptions', + params: { + character: Character.SYSTEM, + question: 'Which assumptions must the report state explicitly?', + options: [ + 'The decline continues or steepens (no recovery), no single-event failure intervenes, and tracking - not the payload - is the binding constraint. If any breaks, the window changes', + 'None - the recommendation is certain', + 'Only that the customer will cooperate with migration', + 'That AURORA-7 was built to a 15-year design life', + ], + correctIndex: 0, + explanation: 'A recommendation that names its assumptions can be defended and updated; one that hides them collapses the first time reality diverges. Stating them is what separates analysis from a guess in a suit.', + pointPenalty: 5, + documentSection: 'Assumptions', + documentLine: 'Assumes: decline continues/steepens (no recovery), no single-event failure, tracking is the binding constraint. Window revises if any breaks.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'false-precision-quiz', + nice: ['T1606', 'S0807'], + title: 'Confidence Discipline', + description: 'Confirm the line between what you measured and what you projected.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['assumptions-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Measured vs Projected', + params: { + character: Character.SYSTEM, + question: 'How should the report distinguish measured facts from projections?', + options: [ + 'Explicitly: today\'s -4.0 dB and the historical points are measured; the sunset window is a projection from those points under stated assumptions. Label each so the board knows which is which', + 'Present everything with equal confidence so the recommendation sounds strong', + 'Only report measurements; omit the projection entirely', + 'Round the projection to a single date to look decisive', + ], + correctIndex: 0, + explanation: 'Measured and projected carry different weight; conflating them is how a report loses credibility in a quarter. The board can act on a clearly-labeled projection - it cannot trust a number it later learns was a guess dressed as a fact.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: REVIEW AND DELIVER + // ============================================================ + { + id: 'review-report', + nice: ['T1606', 'T1429'], + title: 'Review the Assessment', + description: 'Open the Working Document and confirm the report is board-ready.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['false-precision-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Board-Ready Check', + params: { + character: Character.SYSTEM, + question: 'What makes this assessment something Francis can forward without editing?', + options: [ + 'It has data, a trend, the binding-constraint risk, a windowed recommendation with tripwires, and labeled assumptions - a board member can read it, understand the basis, and defend the decision to others', + 'It is short', + 'It concludes with a single confident date', + 'It avoids any mention of uncertainty', + ], + correctIndex: 0, + explanation: 'Forward-without-editing is the bar: complete enough to stand alone, honest enough to survive scrutiny, structured so the reasoning is visible. That is the difference between operations input and an opinion.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-delivery', + nice: ['K0645', 'T1606'], + title: 'Deliver and Log', + description: 'Log the deliverable to the board.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-report'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Delivery Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which log entry closes the tasking?', + options: [ + 'AURORA-7 EOL assessment delivered to board (Martin). Final data run complete (beacon -4.0 dB, step-track held). Trend accelerating, binding constraint = beacon trackability. Recommendation: begin migration now, sunset ~1-2 quarters, trigger at step-track floor. Assumptions stated. Measured vs projected labeled.', + 'EOL report sent to Francis.', + 'AURORA-7 retirement recommended.', + 'Data run complete, analysis pending.', + ], + correctIndex: 0, + explanation: 'The log mirrors the report\'s spine: data, trend, constraint, recommendation, assumptions. Anyone reading it later knows exactly what was delivered and on what basis.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 09:21] +

+

+ "Board's doing budget planning and Francis wants an AURORA-7 retirement recommendation - from operations, not from a spreadsheet. You've flown that bird more than anyone on this rotation. Run one more data pass this morning, then build the assessment. The Working Document panel is your report template; it fills in as you make the calls. He wants something he can forward without editing." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/22/intro.mp3'), + }, + objectives: { + 'measure-beacon': { + text: ` +

+ Marcus from Halifax - I hear you're writing AURORA's obituary. For your trend: our vehicle telemetry shows the array down to about seventy percent of beginning-of-life output, and the bus is auto-shedding RF load to protect the housekeeping buses. Your beacon decline and our power numbers are the same story. She's earned her retirement, eh. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.SAD, + audioUrl: getAssetUrl('/assets/campaigns/nats/22/obj-measure-beacon.mp3'), + }, + 'sunset-window-quiz': { + text: ` +

+ Francis Martin. Appreciate you taking this on directly. What the board needs is exactly what you're building - not a date we'll have to walk back, but a window with triggers we can plan a migration around. Cost-of-being-wrong runs both directions here: retire too early and we waste an asset, too late and we strand customers. Give me the honest middle. +

+ `, + character: Character.FRANCIS_MARTIN, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/22/obj-sunset-window-quiz.mp3'), + }, + 'review-report': { + text: ` +

+ That's a document I can take to the board without rewriting a line - and more to the point, one I can defend when they push on it. The labeled assumptions are what'll save us the quarter this goes sideways. Well done. +

+ `, + character: Character.FRANCIS_MARTIN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/22/obj-review-report.mp3'), + }, + 'log-delivery': { + text: ` +

+ First report you've written that goes straight to a board instead of into a shift log. Notice it's the same discipline as a good log entry - say what you know, label what you don't, make the next reader able to act. Scales all the way up. +

+

+ Nineteen years. Good bird. Good send-off. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/22/obj-log-delivery.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario23.ts b/src/campaigns/nats/scenario23.ts new file mode 100644 index 00000000..0c66d5b0 --- /dev/null +++ b/src/campaigns/nats/scenario23.ts @@ -0,0 +1,624 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, Hertz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite } from './satellites'; + +/** + * NATS Level 23: "Emergency Bypass" + * + * Phase: Crisis Operations (Phase 3, Scenario 7 of 8) + * Time Pressure: Moderate - the link is up but the safety net is gone + * Calculation Required: NO (the pointing solution is on the prediction sheet) + * New Mechanic: ACU automation fault (engine: isAcuAutomationFaulted disables + * program-track / step-track / move-to-target; manual servos still work). + * First scenario where the automation itself is the broken thing. + * + * NICE Framework Alignment: + * Primary Codes: + * - S0424: Skill in executing command line tools (manual operation) + * - T1588: Diagnose faulty system and server hardware + * - S0671: Skill in implementing contingency and recovery plans + * + * Supporting Codes: + * - T0531: Troubleshoot hardware/software interoperability problems + * - S0421: Skill in operating network equipment + * - K1032: Knowledge of satellite-based communication systems + * + * Premise: The ACU automation controller crashed mid-shift. Program-track, + * move-to-target, and step-track are unavailable; the dish is parked where + * automation left it - on TIDEMARK-1. Servos and manual mode still work. The + * operator must switch to manual deliberately, prove pointing against the + * prediction sheet using the beacon (the ACU lock logic is dead, so the + * spectrum is the only truth source), hold the link, and resist rebooting the + * controller while IT works the root cause. + * + * TIDEMARK-1 is GEO and well-behaved, so manual holding is feasible for a + * shift - the lesson is that manual ops is a fallback competency, and that the + * automation was doing real work the operator must now do by hand. + * + * Tone: Crisis-phase. Dana at the console (intro + IT updates), IT rendered as + * SYSTEM ticket notes. All quizzes SYSTEM. 4 clips. + * + * Sim notes: + * - VT-01 antenna starts on TIDEMARK-1 (program-track, locked) but with + * isAcuAutomationFaulted: true. The faulted automation means the operator + * cannot use program-track/step-track; the objective is to commit to + * MANUAL and prove the link there. + * - In manual mode the dish holds position (GEO bird barely moves over a + * shift), so beacon + carrier stay locked once the operator is on target. + */ + +export const scenario23Data: ScenarioData = { + id: 'nats-scenario23', + prerequisiteScenarioIds: ['nats-scenario22'], + url: 'nats/scenarios/nats-scenario23', + imageUrl: 'nats/23/card.png', + number: 23, + title: 'Emergency Bypass', + subtitle: 'Manual Operations During Automation Failure', + duration: '25-30 min', + difficulty: 'advanced', + missionType: 'Contingency Operations', + description: `The ACU automation processor faulted at 1358. Program-track, move-to-target, and step-track are all offline - the dish is sitting exactly where automation left it, which for now is on TIDEMARK-1. The servos still work; it's the brains that died, not the muscles.

IT owns the controller and has no ETA. You own the link. TIDEMARK-1 is GEO and well-behaved, so the pointing solution doesn't change - but with the automation's lock logic dead, the ACU panel can no longer tell you whether you're on the satellite. The beacon on the spectrum is your only proof.

Get into manual cleanly, prove you're on the bird, hold it there, and do not experiment with the broken automation while IT is mid-diagnosis.`, + equipment: [ + '9-meter C-band Antenna (manual servo control)', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'TIDEMARK-1 prediction sheet', + ], + timeLimitSeconds: 30 * 60, + settings: { + isSync: true, + groundStations: [ + { + ...vermontGroundStation, + antennasState: [ + { + // On TIDEMARK-1 where automation left it - but the automation + // controller has faulted. Program-track/step-track are dead; + // manual servo control is the only way to move the dish. + isPowered: true, + azimuth: 161.8 as Degrees, + elevation: 34.2 as Degrees, + polarization: 14 as Degrees, + trackingMode: 'program-track', + isStepTrackEnabled: false, + isBeaconLocked: true, + targetSatelliteId: 61525, + targetAzimuth: 161.8 as Degrees, + targetElevation: 34.2 as Degrees, + targetPolarization: 14 as Degrees, + slewing: false, + beaconCN: 10.4 as dB, + beaconFrequencyHz: 1074.5e6 as Hertz, + isLocked: true, + isAcuAutomationFaulted: true, + } as Partial, + ], + }, + ], + satellites: [tidemark1Satellite, ses10Satellite], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-23?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review the Fault', + description: 'Open the brief and IT ticket NOC-2026-2231.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Frame the Problem', + params: { + character: Character.SYSTEM, + question: 'The ACU automation processor crashed. What is and is not working?', + options: [ + 'Broken: program-track, step-track, move-to-target, lock logic (the automation brain). Working: servos, manual mode, and the RF chain - the dish can still move, you just have to fly it', + 'Everything is down - the antenna is frozen until IT reboots', + 'Only step-track is affected; program-track still works', + 'The RF chain is down; tracking is unaffected', + ], + correctIndex: 0, + explanation: 'Separating the failed subsystem from the healthy ones IS the bypass plan. The muscles work; the brain that usually aims them does not. You become the brain.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: ASSESS THE FAULT + // ============================================================ + { + id: 'confirm-fault-dashboard', + nice: ['T1588', 'T0531'], + title: 'Confirm the Automation Fault', + description: 'Dashboard: confirm the ACU automation alarm and that the RF chain is healthy.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Fault Scope', + params: { + character: Character.SYSTEM, + question: 'The ACU automation fault alarm is up. Before touching anything, what do you record?', + options: [ + 'Current position (Az 161.8 / El 34.2 on TM-1) and that beacon + carrier are still locked - the dish is ON the bird; whatever you do next must not lose that', + 'Nothing - immediately switch to manual', + 'The ACU serial number for the warranty claim', + 'A full spectrum sweep of every transponder', + ], + correctIndex: 0, + explanation: 'The single most valuable asset right now is that the dish is already pointed correctly. Record it before you move, so if a manual input goes wrong you know exactly where home was.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'what-automation-did-quiz', + nice: ['K1032', 'S0671'], + title: 'What the Automation Was Doing', + description: 'Make explicit the work you are about to inherit.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['confirm-fault-dashboard'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Inherited Work', + params: { + character: Character.SYSTEM, + question: 'Which functions does the operator now have to perform by hand?', + options: [ + 'Compute the pointing solution (from the prediction sheet), drive the axes manually, and judge lock from the spectrum - the ACU lock indicator is part of the dead automation and cannot be trusted', + 'Only the slewing - the lock indicator still works', + 'Nothing extra - manual mode automates pointing differently', + 'Re-derive the orbit from first principles', + ], + correctIndex: 0, + explanation: 'The dangerous assumption is trusting the ACU lock light - it is driven by the failed processor. The spectrum analyzer (beacon at the right IF) is the only pointing truth source that does not depend on the broken thing.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'no-reboot-quiz', + nice: ['T1588', 'T0531'], + title: 'Why Not Reboot It', + description: 'IT has not rebooted the controller. Confirm why bypassing beats rebooting right now.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['what-automation-did-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Bypass vs Reboot', + params: { + character: Character.SYSTEM, + question: 'Why is "go manual and hold" the right move instead of power-cycling the ACU yourself?', + options: [ + 'A reboot can return with stale/defaulted axis calibration and destroys the crash state IT needs for root cause - bypass keeps the link up AND preserves the evidence; IT cycles it on their schedule', + 'Rebooting takes too long', + 'The operator is not authorized to use the power switch', + 'A reboot would drop the customer, which manual avoids - and nothing else matters', + ], + correctIndex: 0, + explanation: 'Same discipline as the S8/S16 LNB lesson scaled up: you cycle equipment you own; you coordinate cycles on equipment someone else is debugging. The calibration risk is the technical reason; preserving crash state is the investigative one.', + pointPenalty: 5, + preserveOptionOrder: true, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: COMMIT TO MANUAL + // ============================================================ + { + id: 'switch-to-manual', + nice: ['S0424', 'S0671'], + title: 'Switch to Manual Control', + description: 'Open ACU Control and deliberately select MANUAL - a faulted controller in a half-automatic state is worse than honest manual.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['no-reboot-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Manual Mode Engaged', + params: { trackingMode: 'manual' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'manual-deliberate-quiz', + nice: ['S0424', 'K1032'], + title: 'Why Deliberate Manual', + description: 'Confirm why committing to manual is safer than leaving the dish in a faulted program-track.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['switch-to-manual'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Honest Manual', + params: { + character: Character.SYSTEM, + question: 'Why deliberately select MANUAL rather than leave it in program-track with dead automation?', + options: [ + 'In program-track the panel implies an automation is flying the dish when none is - manual makes the truth explicit: YOU are the controller, the displays mean what they say, and there is no phantom loop to fight', + 'Manual mode is more accurate than program-track', + 'Program-track will resume on its own and undo your inputs', + 'There is no difference - the label is cosmetic', + ], + correctIndex: 0, + explanation: 'A half-failed automatic state is the worst of both worlds: it looks like something is in control and nothing is. Honest manual removes the ambiguity - every control does exactly what it says.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 3: PROVE THE LINK ON INSTRUMENTS + // ============================================================ + { + id: 'prove-beacon', + nice: ['S0424', 'K0773'], + title: 'Prove Pointing on the Beacon', + description: 'Tune the spectrum analyzer to the TM-1 beacon IF (1074.5 MHz) and confirm it - your only pointing truth source.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['manual-deliberate-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum at 1074.5 MHz IF', + params: { + centerFrequency: 1074.5e6, + centerFrequencyTolerance: 1e6, + }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon Present', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -100, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'prove-carrier', + nice: ['S0424', 'T0153'], + title: 'Prove the Carrier', + description: 'Confirm the receiver is locked on the TM-1 carrier with healthy margin - the link is alive under manual control.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['prove-beacon'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'receiver-snr-threshold', + description: 'C/N ≥ 9 dB', + params: { minCNRatio: 9, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'instruments-not-feel-quiz', + nice: ['S0424', 'K1032'], + title: 'Fly Instruments, Not Feel', + description: 'Lock in the verification discipline for manual ops.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['prove-carrier'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Verification Source', + params: { + character: Character.SYSTEM, + question: 'After any manual pointing input during this fault, what confirms you are still on the satellite?', + options: [ + 'The beacon at 1074.5 MHz on the spectrum and receiver lock - the RF truth, independent of the dead ACU automation. Never the ACU lock indicator', + 'The ACU lock indicator turning green', + 'The polar plot showing the dish on target', + 'That the dish stopped moving', + ], + correctIndex: 0, + explanation: 'The ACU lock light is computed by the failed processor - it is exactly the instrument you cannot trust. The beacon and receiver lock come from the RF chain, which is healthy. Fly the instruments that still work.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 4: HOLD AND COORDINATE + // ============================================================ + { + id: 'sustained-manual-hold', + nice: ['S0671', 'T0153'], + title: 'Hold Under Manual Control', + description: 'Sustain the link manually - TIDEMARK-1 is GEO and barely moves, so a steady manual hold is feasible for the shift.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['instruments-not-feel-quiz'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-tracking-mode-set', + description: 'Manual Mode Held', + params: { trackingMode: 'manual' }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Carrier Held', + params: { modemNumber: 1 }, + mustMaintain: true, + maintainDuration: 30, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'geo-feasibility-quiz', + nice: ['K1032', 'S0671'], + title: 'Why Manual Holds (This Time)', + description: 'Confirm what makes a manual hold sustainable here - and when it would not be.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['sustained-manual-hold'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Feasibility', + params: { + character: Character.SYSTEM, + question: 'Manual holding works for this shift. What makes it feasible, and when would it NOT be?', + options: [ + 'TIDEMARK-1 is a well-behaved GEO bird - it barely moves over hours, so a fixed manual point holds. It would NOT work for an inclined bird like AURORA-7, whose figure-8 needs the step-track this fault disabled', + 'Manual always works regardless of the satellite', + 'It works because the LNB compensates for drift', + 'It only works at night', + ], + correctIndex: 0, + explanation: 'The same fault on AURORA-7 would be a genuine crisis - no step-track means chasing a figure-8 by hand. TIDEMARK-1\'s stability is what turns "automation failure" into "annoying but holdable."', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'it-coordination-quiz', + nice: ['T0531', 'S0671'], + title: 'Coordinate the Repair', + description: 'IT is ready to attempt recovery. Set the coordination.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['geo-feasibility-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Repair Coordination', + params: { + character: Character.SYSTEM, + question: 'IT wants to attempt the ACU recovery now. What does the ground operator coordinate?', + options: [ + 'Confirm the link is stable on manual first, agree a window, expect to re-establish program-track and re-verify pointing AFTER recovery - and be ready to fall back to manual if the restart misbehaves', + 'Tell IT to wait until end of shift', + 'Let IT proceed without any ground coordination - it is their system', + 'Switch back to program-track now to help IT test', + ], + correctIndex: 0, + explanation: 'You drive the link; IT drives the box. The link must be on a stable footing (manual, verified) before the box gets touched, and re-verified on the RF after - never assume a recovered automation is pointed correctly just because it says so.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-bypass', + nice: ['K0645', 'S0671'], + title: 'Log the Bypass', + description: 'Record the manual-operations timeline for the shift and for IT.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['it-coordination-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Bypass Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry records the bypass correctly?', + options: [ + 'ACU automation fault 1358 (IT NOC-2026-2231). Bypassed to manual 1404 - TM-1 held on prediction-sheet pointing, beacon + carrier verified on spectrum (ACU lock indicator NOT trusted, driven by failed processor). Link nominal under manual. Controller NOT rebooted - crash state preserved for IT. Recovery to be re-verified on RF post-restart.', + 'ACU broke, switched to manual, link fine.', + 'Automation fault - rebooted ACU, back to program-track.', + 'IT issue, no operator action required.', + ], + correctIndex: 0, + explanation: 'The load-bearing details for the next shift and for IT: pointing source, that the lock light was distrusted, that the controller was deliberately NOT rebooted, and that recovery needs RF re-verification.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Dana, at your console, 14:04] +

+

+ "ACU automation processor faulted at 1358 - program-track, move-to-target, step-track all offline. Dish is parked on TM-1 where it died. Servos and manual still work; it's the brains, not the muscles. IT owns the controller and has no ETA. You own the link. Get into manual cleanly, prove you're on the bird with the spectrum, and hold it. Do NOT play with the broken automation while IT's mid-diagnosis." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/23/intro.mp3'), + }, + objectives: { + 'no-reboot-quiz': { + text: ` +

+ [IT ticket NOC-2026-2231 update, 14:09] +

+

+ "Do NOT power-cycle the ACU - we need the crash dump and the axis calibration is suspect on restart. Hold on manual, we'll coordinate a recovery window once we've pulled the logs. - IT/NOC" +

+ `, + character: Character.SYSTEM, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/23/obj-no-reboot-quiz.mp3'), + }, + 'prove-carrier': { + text: ` +

+ Beacon's where the sheet says, receiver's locked, and you got there without trusting a single readout from the dead controller. That's the whole skill - the automation was never magic, just bookkeeping you can do by hand when you have to. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/23/obj-prove-carrier.mp3'), + }, + 'log-bypass': { + text: ` +

+ Clean bypass, link never dropped, and you left IT everything they need to find the root cause. Log it the way you ran it - especially the part about not trusting the lock light. That's the line that teaches the next person. +

+

+ IT will take the box when they're ready. The link's yours until they hand it back. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/23/obj-log-bypass.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario24.ts b/src/campaigns/nats/scenario24.ts new file mode 100644 index 00000000..25f666eb --- /dev/null +++ b/src/campaigns/nats/scenario24.ts @@ -0,0 +1,841 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, Hertz, IfFrequency, MHz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { aurora7Satellite, ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; + +/** + * NATS Level 24: "Constellation Crisis" + * + * Phase: Crisis Operations (Phase 3 capstone, Scenario 8 of 8) - CAMPAIGN FINALE + * Time Pressure: High - five concurrent tracks, three on clocks you don't control + * Calculation Required: NO (every mechanic was taught; this is orchestration) + * New Value: holding five tracks at once without dropping one - multi-station, + * multi-satellite, multi-customer, plus a board note. No new mechanics; the + * Working Document is the incident-command log / after-action report. + * + * NICE Framework Alignment: + * Primary Codes: + * - S0807: Skill in solving problems + * - T1606: Prepare impact reports + * - T0531: Troubleshoot hardware/software interoperability problems + * + * Supporting Codes: + * - T1538: Resolve customer-reported system incidents + * - S0593: Skill in handling incidents + * - S0677: Skill in recovering failed systems + * - T1429: Prepare trend analysis reports + * - T1144: Implement network backup and recovery procedures + * + * Premise: A compound emergency. A storm front is inbound on VT-01 (S14 + * playbook: heater before the front). ME-02's BUC is running hot, same + * signature as S13 (de-rate). AURORA-7 has a booked SeaLink pass at a marginal, + * end-of-life beacon (S6/S22). Two customers escalate, and the board wants a + * note. The operator holds incident command across both stations and all + * channels, ordering the board and bending the trends, logging it as they go. + * + * The campaign finale: every Phase 1-3 skill, exercised together, under the + * one pressure no single prior scenario applied - holding the whole job in + * your head at once. + * + * Tone: Capstone - the one Phase 2/3 scenario where dialog density expands + * (8-10 clips). All named characters appear. Quizzes SYSTEM. The Working + * Document is the incident-command log. + * + * Sim notes: + * - VT-01: TIDEMARK-1, storm inbound (weather event at T+240, heater off so + * the operator must enable it proactively - S14 discipline). AURORA-7 also + * visible/usable from VT-01 for the pass. + * - ME-02: TIDEMARK-2, BUC over-gain/hot (S13 signature) - de-rate to clear. + * - The Working Document accumulates the incident-command log from the + * triage/decision/board quizzes. + */ + +export const scenario24Data: ScenarioData = { + id: 'nats-scenario24', + prerequisiteScenarioIds: ['nats-scenario23'], + url: 'nats/scenarios/nats-scenario24', + imageUrl: 'nats/24/card.png', + number: 24, + title: 'Constellation Crisis', + subtitle: 'Campaign Capstone', + duration: '40-50 min', + difficulty: 'advanced', + missionType: 'Incident Command', + description: `Everything at once. A storm front tracks onto Vermont inside the hour. Maine's BUC is running hot - the same unit, the same signature as the thermal trend you caught months ago, and the swap never cleared procurement. AURORA-7 has a SeaLink sync pass booked at 0800 on a beacon four dB weaker than the day you qualified. James is already calling. Francis wants a board note by end of morning.

Five tracks. Three are clocks you don't control - the storm, the pass window, the board deadline. Two are trends you can bend - the BUC heat, the customer's confidence.

You've done every piece of this before. Today you do all of it, in the right order, while everyone watches. Incident command is yours. Dana is your resource, not your safety net. This is the exam - fly it like a Tuesday.`, + equipment: [ + '9-meter C-band Antennas (VT-01 + ME-02)', + 'RF Front Ends (both)', + 'Spectrum Analyzers', + 'RX/TX Modems', + 'Incident-command log (Working Document)', + ], + timeLimitSeconds: 50 * 60, + settings: { + isSync: true, + groundStations: [ + // VT-01: TIDEMARK-1, storm inbound, AURORA-7 pass pending. Heater OFF. + { + ...vermontGroundStation, + }, + // ME-02: TIDEMARK-2, BUC over-gain and hot (S13 signature) + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 219.7 as Degrees, + elevation: 26.3 as Degrees, + polarization: -25 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61526, + targetAzimuth: 219.7 as Degrees, + targetElevation: 26.3 as Degrees, + targetPolarization: -25 as Degrees, + slewing: false, + beaconCN: 10.2 as dB, + beaconFrequencyHz: 1070e6 as Hertz, + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // BUC over-gain (33 dB vs 23 operating), heating - the S13 thermal + // trend signature. De-rate to clear. + buc: { + isMuted: false, + isLoopback: false, + loFrequency: 7000 as MHz, + isExtRefLocked: true, + gain: 33 as dB, + saturationPower: 28 as dBm, + temperature: 63, + currentDraw: 4.1, + }, + hpa: { + isHpaEnabled: true, + isHpaSwitchEnabled: true, + backOff: 8 as dB, + }, + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1070e6 as Hertz, + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [ + { + ...vermontGroundStation.transmitters[0], + modems: [ + { + ...vermontGroundStation.transmitters[0].modems[0], + ifSignal: { + ...vermontGroundStation.transmitters[0].modems[0].ifSignal, + signalId: 'TIDEMARK-2-Teleport', + noradId: 61526, + frequency: 1020e6 as IfFrequency, + }, + }, + ], + }, + ], + receivers: [ + { + activeModem: 1, + modems: [ + { + modemNumber: 1, + isPowered: true, + frequency: 1458 as MHz, // TM-2 downlink IF + bandwidth: 36 as MHz, + modulation: 'QPSK', + fec: '3/4', + antenna_id: 1, + }, + ], + }, + ], + }, + ], + satellites: [tidemark1Satellite, tidemark2Satellite, aurora7Satellite, ses10Satellite], + weatherEvents: [ + { + id: 'vermont-front-inbound', + groundStationId: 'VT-01', + type: 'snow', + severity: 'moderate', + startTime: 240, // Front arrives ~4 min in - heater must be on before + duration: 2400, + linkMarginDegradation: 6, + }, + ], + workingDocument: { + title: 'Incident Command Log - Constellation Crisis', + description: 'Live incident command record. Triage calls, decisions, customer commitments, and the board note build here as you work.', + }, + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-24?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION - ASSUME COMMAND, ORDER THE BOARD + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Assume Incident Command', + description: 'Open the brief and the five-track board.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Take Command', + params: { + character: Character.SYSTEM, + question: 'Five concurrent tracks, you hold incident command. What is the first thing command requires?', + options: [ + 'Order the board before touching anything: separate the clocks you do not control (storm ETA, pass window, board deadline) from the trends you can bend (BUC heat, customer confidence), and sequence your attention accordingly', + 'Start fixing the loudest alarm immediately', + 'Escalate everything to Dana since it exceeds one operator', + 'Hand all traffic to whichever station is healthiest and wait out the morning', + ], + correctIndex: 0, + explanation: 'Command is sequencing, not speed. The S16/S20 triage discipline scaled to five tracks: know which clocks are fixed and which trends you own before you spend a single action.', + pointPenalty: 10, + documentSection: 'Command', + documentLine: 'IC assumed. Board ordered: fixed clocks = storm ETA / AURORA pass / board note; bendable trends = ME-02 BUC heat / customer confidence.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'sequencing-quiz', + nice: ['S0807', 'S0593'], + title: 'Set the Sequence', + description: 'Decide the order before acting - the heart of the capstone.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Action Sequence', + params: { + character: Character.SYSTEM, + question: 'Storm hits VT-01 in ~4 minutes; ME-02 BUC is hot but pre-alarm; AURORA pass is at 0800. What order?', + options: [ + 'Protect against the fixed clock first: enable VT-01 heater NOW (one switch, beats the front), then de-rate the ME-02 BUC trend, then set up the AURORA pass before its window - acting on the soonest fixed deadline first, cheap protections before expensive ones', + 'AURORA pass first because customers are watching', + 'ME-02 BUC first because thermal could damage hardware', + 'Whichever customer is loudest on the phone', + ], + correctIndex: 0, + explanation: 'The storm has the nearest fixed deadline and the cheapest protection (one heater switch). Start it, then work the bendable BUC trend, then stage the pass before its window. Sequence by deadline and cost, not by volume.', + pointPenalty: 10, + documentSection: 'Command', + documentLine: 'Sequence: (1) VT-01 heater before front, (2) ME-02 BUC de-rate, (3) AURORA pass setup before 0800.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // TRACK 1: VT-01 STORM PROTECTION (beat the fixed clock) + // ============================================================ + { + id: 'select-vermont', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Vermont first - the storm clock is shortest.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['sequencing-quiz'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'enable-heater-proactive', + nice: ['S0671', 'K0689'], + title: 'Heater Before the Front', + description: 'Enable the feed heater now, before the storm arrives - proactive, the S14 discipline.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'feed-heater-enabled', + description: 'Feed Heater ON (before the front)', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // TRACK 2: ME-02 BUC THERMAL TREND (bend the trend) + // ============================================================ + { + id: 'select-maine', + nice: ['S0421'], + title: 'Open ME-02', + description: 'Heater\'s running unattended at Vermont. Now the BUC trend at Maine.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['enable-heater-proactive'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'recognize-buc-signature', + nice: ['T0531', 'S0807'], + title: 'Recognize the Signature', + description: 'You have seen this BUC before. Name it.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['select-maine'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Signature ID', + params: { + character: Character.SYSTEM, + question: 'ME-02 BUC: gain 10 dB over operating, temperature climbing, current elevated, pre-alarm. What is this and what is the fix?', + options: [ + 'The same over-gain thermal trend from before - the swap that never cleared procurement. Fix is the de-rate: gain back to the 23 dB operating value, which cuts the dissipation at the source', + 'A new BUC hardware failure requiring immediate swap and customer outage', + 'HPA overdrive bleeding heat into the BUC', + 'Ambient temperature in the Maine equipment room', + ], + correctIndex: 0, + explanation: 'Pattern recognition is the payoff of the campaign: you diagnosed this exact signature once. The de-rate addresses the cause without taking the customer down - and you log that the swap is still pending.', + pointPenalty: 5, + documentSection: 'ME-02 / TM-2', + documentLine: 'BUC over-gain thermal trend (recurrence - swap still pending procurement). Action: de-rate to 23 dB operating.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'derate-buc', + nice: ['T0531', 'S0421'], + title: 'De-Rate the BUC', + description: 'Bring the BUC gain back to the 23 dB operating value.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['recognize-buc-signature'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'buc-gain-set', + description: 'BUC Gain at 23 dB', + params: { gain: 23, gainTolerance: 2 }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Still Linear', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'verify-buc-trending-down', + nice: ['T0153', 'T0531'], + title: 'Confirm the Trend Bends', + description: 'Verify the de-rate took: carrier still up, BUC no longer climbing.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['derate-buc'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'receiver-signal-locked', + description: 'TM-2 Carrier Still Locked', + params: { modemNumber: 1 }, + mustMaintain: true, + }, + { + type: 'buc-not-saturated', + description: 'BUC Out of Saturation Risk', + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // TRACK 3: AURORA-7 CUSTOMER PASS (marginal beacon) + // ============================================================ + { + id: 'return-vermont-for-pass', + nice: ['S0421'], + title: 'Back to VT-01 for the Pass', + description: 'Two tracks holding. Now set up the AURORA-7 pass before its window.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['verify-buc-trending-down'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'acquire-aurora', + nice: ['S0421', 'K1032'], + title: 'Acquire AURORA-7', + description: 'Program-track to AURORA-7 (NORAD 28899) for the SeaLink pass.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['return-vermont-for-pass'], + timeLimitSeconds: 4 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Program-Track Mode', + params: { trackingMode: 'program-track' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on AURORA-7', + params: { noradId: 28899 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'step-track-weak-beacon', + nice: ['S0421', 'K1032'], + title: 'Step-Track the Weak Beacon', + description: 'Engage step-track - on an end-of-life beacon, every bit of pointing accuracy counts.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['acquire-aurora'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-tracking-mode-set', + description: 'Step-Track Engaged', + params: { trackingMode: 'step-track' }, + mustMaintain: true, + }, + { + type: 'antenna-beacon-locked', + description: 'Beacon Held', + mustMaintain: true, + maintainDuration: 15, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + { + id: 'pass-go-nogo-quiz', + nice: ['K0721', 'T1538'], + title: 'Pass Go/No-Go', + description: 'Make the call on the booked pass - and time the customer comms right.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['step-track-weak-beacon'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Go/No-Go', + params: { + character: Character.SYSTEM, + question: 'Step-track is holding the weak beacon and C/N is marginal-but-adequate. What is the call to Marcus?', + options: [ + 'GO with a caveat: step-track holding, margin thin on an end-of-life beacon; proceed with the pass but flag that AURORA is on its sunset trajectory - and deliver that BEFORE 0800, not mid-pass', + 'NO-GO - the beacon is too weak, scrub it', + 'GO silently - do not worry the customer with margin details', + 'Defer the decision until the pass is already underway', + ], + correctIndex: 0, + explanation: 'Honest go: the link supports the pass now, the operator says so, and flags the EOL reality (tying back to the S22 recommendation). Communication timing matters - the caveat before the window, not an excuse during it.', + pointPenalty: 5, + documentSection: 'VT-01 / AURORA-7', + documentLine: 'Pass GO (caveat: thin margin, EOL beacon per sunset rec). Step-track holding. Marcus briefed pre-window.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // CUSTOMER + BOARD CHANNELS + // ============================================================ + { + id: 'customer-escalation-quiz', + nice: ['T1538', 'S0593'], + title: 'Manage the Customer Channel', + description: 'James is escalating across both his trunks. Handle the channel without dropping a track.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['pass-go-nogo-quiz'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Customer Comms', + params: { + character: Character.SYSTEM, + question: 'James wants status on TM-1 (storm) and TM-2 (BUC). What do you give him, and how often?', + options: [ + 'Per-trunk cause/action/next-update, in thirty seconds each: TM-1 protected ahead of the storm (heater on, holding), TM-2 stabilized (BUC de-rated, no impact) - with a committed next-update time so he stops calling and you keep working', + 'A full technical briefing on both faults now', + 'Tell him to wait until everything is resolved', + 'Route him to Dana - customer comms is above incident command', + ], + correctIndex: 0, + explanation: 'Command keeps the customer channel short and scheduled: cause, action, next update. A committed update time converts an escalating caller into a manageable one - protecting your attention for the tracks.', + pointPenalty: 5, + documentSection: 'Customer (SeaLink/James)', + documentLine: 'James briefed per-trunk: TM-1 storm-protected (heater on), TM-2 BUC de-rated (no impact). Next update committed.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'storm-hold-check', + nice: ['S0671', 'T0153'], + title: 'Storm Holding', + description: 'Confirm Vermont is riding the front - the proactive heater is doing its job.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['customer-escalation-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Storm Posture', + params: { + character: Character.SYSTEM, + question: 'The front is over Vermont now. Because you enabled the heater early, what is the situation?', + options: [ + 'The feed stays clear as fast as ice tries to form - no accumulation, link holding; the proactive heater turned a potential outage into a non-event you only have to monitor', + 'Vermont is icing and needs an emergency handover', + 'The heater should be cycled off to save power now that the storm is here', + 'The storm requires a repoint to a clearer-sky satellite', + ], + correctIndex: 0, + explanation: 'The whole point of beating the fixed clock: protection in place before the threat means the threat never becomes an incident. This is the S14/S20 lesson paying its dividend under maximum load.', + pointPenalty: 5, + documentSection: 'VT-01 / TM-1', + documentLine: 'Storm over VT-01: no ice accumulation (heater pre-enabled). TM-1 holding, monitor-only.', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'board-note-quiz', + nice: ['T1606', 'T1429'], + title: 'The Board Note', + description: 'Francis wants one paragraph. Write the board-level summary into the command log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['storm-hold-check'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Board Paragraph', + params: { + character: Character.SYSTEM, + question: 'What goes in the one-paragraph board note for Francis?', + options: [ + 'Posture, exposure, action: both stations stable through a concurrent storm + thermal-trend morning, no customer outage; AURORA pass delivered on a sunsetting beacon (reinforces the migration recommendation); residual risk = the pending ME-02 BUC swap. One paragraph, board-level, no jargon', + 'A detailed technical timeline of every action taken', + 'Just "all systems nominal"', + 'A request for more staff', + ], + correctIndex: 0, + explanation: 'Board-level means posture and exposure, not procedure. The note also does institutional work: the AURORA pass on a dying beacon reinforces the S22 sunset case, and the pending BUC swap is named as the residual risk the board controls (procurement).', + pointPenalty: 10, + documentSection: 'Board Note (Martin)', + documentLine: 'Both stations stable through concurrent storm + BUC thermal event, zero customer outage. AURORA pass delivered on EOL beacon (reinforces sunset rec). Residual risk: pending ME-02 BUC swap (procurement).', + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + + // ============================================================ + // CLOSE: REVIEW THE COMMAND LOG, AFTER-ACTION + // ============================================================ + { + id: 'review-command-log', + nice: ['T1606', 'S0807'], + title: 'Review the Command Log', + description: 'Open the incident-command log and confirm it stands as the after-action record.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['board-note-quiz'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'After-Action Check', + params: { + character: Character.SYSTEM, + question: 'What makes this command log a complete after-action record?', + options: [ + 'It shows the ordered board, the sequence and why, each track\'s action and outcome, the customer and board comms, and the residual risk - someone could pick up your shift cold and know exactly what happened and what is still open', + 'It lists every button pressed in order', + 'It concludes that everything is fine', + 'It is brief enough to read in ten seconds', + ], + correctIndex: 0, + explanation: 'The campaign\'s final discipline, scaled to its largest case: the log IS the work made legible. Ordered board, reasoned sequence, per-track outcomes, comms, open risk. The same shape as a good shift log - just holding five tracks at once.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'log-crisis-closed', + nice: ['K0645', 'T1606'], + title: 'Close the Incident', + description: 'Log the capstone closed.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-command-log'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Closing Entry', + params: { + character: Character.SYSTEM, + question: 'Which entry closes the incident?', + options: [ + 'Constellation crisis worked under single-operator incident command: VT-01 storm protected (proactive heater, no ice), ME-02 BUC thermal trend de-rated (no outage), AURORA SeaLink pass delivered on EOL beacon, customers briefed per-trunk, board note filed. Zero customer outage across five concurrent tracks. Residual: ME-02 BUC swap pending. IC closed.', + 'Busy morning, everything handled.', + 'Multiple faults, all fixed, see individual tickets.', + 'Crisis averted.', + ], + correctIndex: 0, + explanation: 'The finale entry: five tracks, one operator, zero customer outage, one open risk named. That sentence is the whole campaign - every skill, held together at once, made legible for whoever comes next.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 15, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Dana, at the door, 07:00] +

+

+ "Everything at once this morning. Storm front onto Vermont inside the hour. Maine's BUC is running hot - same unit, same signature you caught before, swap never cleared procurement. AURORA's got a SeaLink pass at 0800 on a beacon four dB down. James is calling, Francis wants a board note. You've done every piece of this. Today you do all of it, in the right order, while everyone watches. Incident command is yours - I'm your resource, not your safety net." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/intro.mp3'), + }, + objectives: { + 'enable-heater-proactive': { + text: ` +

+ James Okafor, SeaLink. I've got alerts on both my trunks and a storm on your radar - talk to me. Are we about to lose Vermont? +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.CONCERNED, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-enable-heater-proactive.mp3'), + }, + 'recognize-buc-signature': { + text: ` +

+ Catherine here - I'm your hands on the Maine floor today, but you're calling it. I'm looking at the same BUC numbers you are. Tell me the move and I'll back you up, but this one's your read. +

+ `, + character: Character.CATHERINE_VEGA, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-recognize-buc-signature.mp3'), + }, + 'acquire-aurora': { + text: ` +

+ Marcus from Halifax - SeaLink's spun up for the 0800 sync window on AURORA. I know she's tired; I just need to know if we're going. Your call on the beacon - you've flown her more than anyone. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-acquire-aurora.mp3'), + }, + 'pass-go-nogo-quiz': { + text: ` +

+ Copy your GO with the caveat - and noted on the sunset. That's the third time AURORA's come up thin this quarter; your migration recommendation's looking less like planning and more like a schedule. We'll take the pass. Thanks, eh. +

+ `, + character: Character.MARCUS_CHEN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-pass-go-nogo-quiz.mp3'), + }, + 'storm-hold-check': { + text: ` +

+ Okafor again - I'll be quick. Both trunks reading green on my side through all of that. Whatever you did, you did it before I felt it. That's the difference between a vendor and a partner. We're good. +

+ `, + character: Character.JAMES_OKAFOR, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-storm-hold-check.mp3'), + }, + 'board-note-quiz': { + text: ` +

+ Francis Martin. I'm told this morning could have been a very bad day and instead it's a paragraph. That paragraph is what I take to the board. The note on the BUC swap - I hear it; procurement's mine to fix and I will. Good work under real pressure. +

+ `, + character: Character.FRANCIS_MARTIN, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-board-note-quiz.mp3'), + }, + 'log-crisis-closed': { + text: ` +

+ Five tracks, one operator, not a second of customer outage. I sat on my hands the whole morning and you never needed me - which was the exam. +

+

+ Twenty-four scenarios ago you couldn't read an alarm board. Today you ran a constellation through a crisis and wrote it up so clean the board will never know how close it was. That's the job. Welcome to the top of it. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/24/obj-log-crisis-closed.mp3'), + }, + }, + }, +}; diff --git a/src/campaigns/nats/scenario3.ts b/src/campaigns/nats/scenario3.ts index e1515389..691b4d87 100644 --- a/src/campaigns/nats/scenario3.ts +++ b/src/campaigns/nats/scenario3.ts @@ -8,7 +8,7 @@ import { SignalOrigin } from '@app/signal-origin'; import type { dB, dBi, dBm, FECType, Hertz, IfFrequency, MHz, ModulationType, RfFrequency } from '@app/types'; import { getAssetUrl } from '@app/utils/asset-url'; import type { Degrees } from 'ootk'; -import { createRfFrontEnd } from '../rf-front-end-factory'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; import { vermontGroundStation } from './ground-stations'; import { ses10Satellite, tidemark1Satellite } from './satellites'; @@ -640,6 +640,7 @@ export const scenario3Data: ScenarioData = { { type: 'gpsdo-locked', description: 'GPSDO Locked', + params: { requiresObservation: true, observationTab: 'gps-timing' }, mustMaintain: true, }, { @@ -1021,6 +1022,8 @@ export const scenario3Data: ScenarioData = { params: { signalId: 'TIDEMARK-1-Beacon', minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', }, mustMaintain: true, }, @@ -1184,6 +1187,7 @@ export const scenario3Data: ScenarioData = { { type: 'receiver-signal-locked', description: 'Receiver Modem Locked', + params: { requiresObservation: true, observationTab: 'rx-analysis' }, maintainUntilObjectiveComplete: true, }, { @@ -1191,6 +1195,8 @@ export const scenario3Data: ScenarioData = { description: 'C/N Ratio ≥ 10 dB', params: { minCNRatio: 10, + requiresObservation: true, + observationTab: 'rx-analysis', }, maintainUntilObjectiveComplete: true, }, diff --git a/src/campaigns/nats/scenario4.ts b/src/campaigns/nats/scenario4.ts index 4a3ec388..c4e5d391 100644 --- a/src/campaigns/nats/scenario4.ts +++ b/src/campaigns/nats/scenario4.ts @@ -4,7 +4,7 @@ import type { ScenarioData } from '@app/ScenarioData'; import type { dBm, Hertz, MHz } from '@app/types'; import { getAssetUrl } from '@app/utils/asset-url'; import type { Degrees } from 'ootk'; -import { createRfFrontEnd } from '../rf-front-end-factory'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; import { maineGroundStation, vermontGroundStation } from './ground-stations'; import { ses10Satellite, tidemark1Satellite, tidemark2Satellite } from './satellites'; @@ -689,6 +689,8 @@ export const scenario4Data: ScenarioData = { description: 'Receiver Locked', params: { modemNumber: 1, + requiresObservation: true, + observationTab: 'rx-analysis', }, maintainUntilObjectiveComplete: true, }, @@ -698,6 +700,8 @@ export const scenario4Data: ScenarioData = { params: { minCNRatio: 10, modemNumber: 1, + requiresObservation: true, + observationTab: 'rx-analysis', }, maintainUntilObjectiveComplete: true, maintainDuration: 30, diff --git a/src/campaigns/nats/scenario6.ts b/src/campaigns/nats/scenario6.ts index fbaf031c..f1d0dbab 100644 --- a/src/campaigns/nats/scenario6.ts +++ b/src/campaigns/nats/scenario6.ts @@ -5,7 +5,7 @@ import type { ScenarioData } from '@app/ScenarioData'; import type { dB, dBm, Hertz, IfFrequency, MHz } from '@app/types'; import { getAssetUrl } from '@app/utils/asset-url'; import type { Degrees } from 'ootk'; -import { createRfFrontEnd } from '../rf-front-end-factory'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; import { vermontGroundStation } from './ground-stations'; import { aurora7Satellite, ses10Satellite, tidemark1Satellite } from './satellites'; @@ -545,6 +545,8 @@ export const scenario6Data: ScenarioData = { description: 'Receiver Locked', params: { modemNumber: 1, + requiresObservation: true, + observationTab: 'rx-analysis', }, maintainUntilObjectiveComplete: true, }, @@ -554,6 +556,8 @@ export const scenario6Data: ScenarioData = { params: { minCNRatio: 8, modemNumber: 1, + requiresObservation: true, + observationTab: 'rx-analysis', }, maintainUntilObjectiveComplete: true, maintainDuration: 15, diff --git a/src/campaigns/nats/scenario7.ts b/src/campaigns/nats/scenario7.ts index 7526a968..d10a464b 100644 --- a/src/campaigns/nats/scenario7.ts +++ b/src/campaigns/nats/scenario7.ts @@ -4,7 +4,7 @@ import type { ScenarioData } from '@app/ScenarioData'; import { SignalOrigin } from '@app/signal-origin'; import type { dB, dBi, dBm, Hertz, IfFrequency, MHz } from '@app/types'; import { getAssetUrl } from '@app/utils/asset-url'; -import { createRfFrontEnd } from '../rf-front-end-factory'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; import { maineGroundStation, vermontGroundStation } from './ground-stations'; import { ses10Satellite, tidemark1Satellite } from './satellites'; @@ -391,7 +391,7 @@ export const scenario7Data: ScenarioData = { { type: 'antenna-locked', description: 'Antenna Locked on TIDEMARK-1', - params: { satelliteId: 61525 }, + params: { satelliteId: 61525, requiresObservation: true, observationTab: 'acu-control' }, mustMaintain: false, }, { @@ -436,17 +436,19 @@ export const scenario7Data: ScenarioData = { { type: 'equipment-powered', description: 'LNB Powered', - params: { equipment: 'lnb' }, + params: { equipment: 'lnb', requiresObservation: true, observationTab: 'rx-analysis' }, maintainUntilObjectiveComplete: true, }, { type: 'lnb-thermally-stable', description: 'LNB Thermally Stabilized', + params: { requiresObservation: true, observationTab: 'rx-analysis' }, maintainUntilObjectiveComplete: true, }, { type: 'lnb-reference-locked', description: 'LNB Reference Locked', + params: { requiresObservation: true, observationTab: 'rx-analysis' }, maintainUntilObjectiveComplete: true, }, { diff --git a/src/campaigns/nats/scenario8.ts b/src/campaigns/nats/scenario8.ts index 714266fe..e05b409d 100644 --- a/src/campaigns/nats/scenario8.ts +++ b/src/campaigns/nats/scenario8.ts @@ -6,7 +6,7 @@ import { SignalOrigin } from '@app/signal-origin'; import type { dB, dBm, FECType, Hertz, IfFrequency, MHz, ModulationType } from '@app/types'; import { getAssetUrl } from '@app/utils/asset-url'; import type { Degrees } from 'ootk'; -import { createRfFrontEnd } from '../rf-front-end-factory'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; import { vermontGroundStation } from './ground-stations'; import { aurora7Satellite, tidemark1Satellite } from './satellites'; @@ -308,6 +308,7 @@ export const scenario8Data: ScenarioData = { { type: 'gpsdo-locked', description: 'GPSDO Locked', + params: { requiresObservation: true, observationTab: 'gps-timing' }, maintainUntilObjectiveComplete: true, }, { @@ -435,6 +436,7 @@ export const scenario8Data: ScenarioData = { { type: 'lnb-reference-locked', description: 'LNB Reference Locked', + params: { requiresObservation: true, observationTab: 'dashboard' }, mustMaintain: true, }, ], @@ -774,12 +776,13 @@ export const scenario8Data: ScenarioData = { { type: 'receiver-signal-locked', description: 'Receiver Modem Locked', + params: { requiresObservation: true, observationTab: 'rx-analysis' }, mustMaintain: true, }, { type: 'receiver-snr-threshold', description: 'C/N Ratio Adequate', - params: { minCNRatio: 8 }, + params: { minCNRatio: 8, requiresObservation: true, observationTab: 'rx-analysis' }, mustMaintain: true, }, { diff --git a/src/campaigns/nats/scenario9.ts b/src/campaigns/nats/scenario9.ts new file mode 100644 index 00000000..7cad9528 --- /dev/null +++ b/src/campaigns/nats/scenario9.ts @@ -0,0 +1,862 @@ +import type { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from '@app/equipment/antenna/antenna-config-keys'; +import { Receiver } from '@app/equipment/receiver/receiver'; +import { Character, Emotion } from '@app/modal/character-enum'; +import type { Objective } from '@app/objectives/objective-types'; +import type { ScenarioData } from '@app/ScenarioData'; +import type { dB, dBm, Hertz } from '@app/types'; +import { getAssetUrl } from '@app/utils/asset-url'; +import type { Degrees } from 'ootk'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from './ground-stations'; +import { ses10Satellite, tidemark1Satellite, tidemark2Satellite, tidemark3Satellite } from './satellites'; + +/** + * NATS Level 9: "Morning Rounds" + * + * Phase: Qualified Operations (Phase 2, Scenario 1 of 8) + * Time Pressure: None (routine work, per-objective timers only) + * Calculation Required: NO - this is routine verification + * New UI Elements: None - this is a "qualified operator" scenario reusing + * mechanics learned in S1-S8 without introducing anything new. + * + * NICE Framework Alignment: + * Primary Codes: + * - T0153: Monitor network capacity and performance + * - T0431: Check system hardware availability, functionality, integrity + * - S0421: Skill in operating network equipment + * + * Supporting Codes: + * - K0645: Knowledge of standard operating procedures (SOPs) + * - K0740: Knowledge of system performance indicators + * - K0741: Knowledge of system availability measures + * - K0773: Knowledge of telecommunications principles and practices + * - K1032: Knowledge of satellite-based communication systems and software + * + * Premise: First qualified shift after the S8 night-shift solo evaluation. + * Normal morning rounds across three TIDEMARK birds: VT-01 carrying TIDEMARK-1, + * ME-02 carrying TIDEMARK-2 (operational since the S3 weather handover), and + * a quick verification spot-check on the newly commissioned TIDEMARK-3 using + * VT-01's antenna while TM-1 traffic is briefly handed to a sister teleport. + * + * Tone: Player is qualified. No instructional dialog. Dialog is limited to + * a brief intro text from Dana, three short phase-transition check-ins, and + * a sign-off. Every quiz uses Character.SYSTEM (no audio). + * + * Story Continuity: + * - Charlie has transferred to Europe (gone since shortly after S8). + * - Dana Torres is now the primary on-site supervisor. + * - ME-02 fully operational since S3. + * - TIDEMARK-3 was commissioned overnight by another crew. + */ + +export const scenario9Data: ScenarioData = { + id: 'nats-scenario9', + prerequisiteScenarioIds: ['nats-level-8-night-shift'], + url: 'nats/scenarios/nats-scenario9', + imageUrl: 'nats/9/card.png', + number: 9, + title: 'Morning Rounds', + subtitle: 'Multi-Satellite Health Check', + duration: '20-25 min', + difficulty: 'intermediate', + missionType: 'Routine Operations', + description: `First qualified shift. Three birds to check this morning: TIDEMARK-1 from Vermont, TIDEMARK-2 from Maine, and a spot-check on the newly commissioned TIDEMARK-3.

Standard morning rounds - verify each link is healthy, log any anomalies, move on. The overnight commissioning crew brought TIDEMARK-3 online a few hours ago; the only thing on your list is a quick beacon verification before our customers start lighting it up.

No drama today. Just routine.`, + equipment: [ + '9-meter C-band Antenna', + 'RF Front End', + 'Spectrum Analyzer', + 'RX/TX Modems', + 'ME-02: Operational', + ], + timeLimitSeconds: 25 * 60, // 25 minutes + settings: { + isSync: true, + groundStations: [ + // VT-01: healthy, tracking TIDEMARK-1, default morning-shift state + { + ...vermontGroundStation, + }, + // ME-02: operational, tracking TIDEMARK-2 (clone Vermont's RF chain for matching LO) + { + id: 'ME-02', + name: 'Maine Ground Station', + isOperational: true, + location: { + latitude: 45.2538, + longitude: -69.7657, + elevation: 180, + }, + antennas: [ANTENNA_CONFIG_KEYS.C_BAND_9M_VORTEK], + antennasState: [ + { + isPowered: true, + azimuth: 219.7 as Degrees, + elevation: 26.3 as Degrees, + polarization: -25 as Degrees, + trackingMode: 'program-track', + isBeaconLocked: true, + targetSatelliteId: 61526, // TIDEMARK-2 + targetAzimuth: 219.7 as Degrees, + targetElevation: 26.3 as Degrees, + targetPolarization: -25 as Degrees, + slewing: false, + beaconCN: 10.2 as dB, + beaconFrequencyHz: 1070e6 as Hertz, // 5250 LNB LO - 4180 beacon RF + isLocked: true, + } as Partial, + ], + rfFrontEnds: [ + createRfFrontEnd(vermontGroundStation.rfFrontEnds[0], { + // Inherits VT-01 LNB LO 5250 MHz so the TM-2 beacon lands at 1070 MHz IF + // Maine TX is muted - we're not transmitting from ME-02 in this scenario + buc: { isMuted: true }, + hpa: { isHpaEnabled: false, isHpaSwitchEnabled: false }, + }), + ], + spectrumAnalyzers: [ + { + referenceLevel: -91 as dBm, + centerFrequency: 1070e6 as Hertz, // Tuned to TM-2 beacon IF + span: 2e3 as Hertz, + rbw: 1e3 as Hertz, + minAmplitude: -95 as dBm, + maxAmplitude: -75 as dBm, + scaleDbPerDiv: 10 as dB, + screenMode: 'both', + inputUnit: 'MHz', + inputValue: '', + traces: [ + { isVisible: true, isUpdating: true, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + { isVisible: false, isUpdating: false, mode: 'clearwrite' }, + ], + selectedTrace: 1, + }, + ], + transmitters: [], + receivers: [Receiver.getDefaultState()], + }, + ], + satellites: [ + tidemark1Satellite, + tidemark2Satellite, + tidemark3Satellite, + ses10Satellite, + ], + missionBriefUrl: 'https://docs.signalrange.space/campaign-1/scenario-9?content-only=true&dark=true', + isExtraSatellitesVisible: true, + }, + objectives: [ + // ============================================================ + // MISSION PREPARATION + // ============================================================ + { + id: 'review-mission-brief', + nice: ['K0645'], + title: 'Review Shift Brief', + description: 'Open the shift brief and acknowledge you are ready to start rounds.', + groundStation: 'VT-01', + freezesScenarioTimer: true, + prerequisiteObjectiveIds: [], + conditions: [ + { + type: 'mission-brief-opened', + description: 'Shift Brief Opened', + params: { boxId: 'mission-brief' }, + mustMaintain: false, + }, + { + type: 'status-check', + description: 'Ready to Begin Rounds', + params: { + character: Character.SYSTEM, + question: 'Have you reviewed the shift brief and are you ready to begin morning rounds?', + options: ['Yes, brief reviewed. Starting rounds.'], + correctIndex: 0, + explanation: 'Shift clock started. Three birds on the list.', + pointPenalty: 0, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 1: VT-01 / TIDEMARK-1 MORNING CHECK + // ============================================================ + { + id: 'select-vermont-station', + nice: ['S0421'], + title: 'Open VT-01', + description: 'Select the Vermont Ground Station.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['review-mission-brief'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-dashboard-check', + nice: ['T0153', 'K0741'], + title: 'VT-01 Dashboard Sweep', + description: 'Check the Dashboard for active alarms.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['select-vermont-station'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Confirm Alarm Status', + params: { + character: Character.SYSTEM, + question: 'What is the active alarm state on VT-01?', + options: [ + 'No active alarms - all systems nominal', + 'BUC high current draw', + 'LNB reference unlocked', + 'HPA output fault', + ], + correctIndex: 0, + explanation: 'Clean board on the primary station. Moving on to timing.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-gpsdo-spot-check', + nice: ['T0431', 'K0740'], + title: 'VT-01 GPSDO Spot Check', + description: 'Verify the GPSDO is locked and providing a stable reference.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-dashboard-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'GPS Timing Open', + params: { tab: 'gps-timing' }, + mustMaintain: true, + }, + { + type: 'gpsdo-locked', + description: 'GPSDO Locked', + params: { requiresObservation: true, observationTab: 'gps-timing' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'GPSDO Confirmation', + params: { + character: Character.SYSTEM, + question: 'What does the GPSDO lock indicator confirm?', + options: [ + 'Stable 10 MHz reference available to all downstream RF equipment', + 'GPS receiver is searching for satellites', + 'External reference is in holdover', + 'GPSDO is powered off', + ], + correctIndex: 0, + explanation: 'Reference is stable. RX/TX chains can trust their frequencies.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'vt-rx-beacon-check', + nice: ['T0153', 'K1032', 'K0773'], + title: 'VT-01 RX Chain Spot Check', + description: 'Confirm the TIDEMARK-1 beacon is present and the receiver is locked with good C/N.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-gpsdo-spot-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-1 Beacon Detected', + params: { + signalId: 'TIDEMARK-1-Beacon', + minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + { + type: 'receiver-signal-locked', + description: 'Receiver Locked', + params: { modemNumber: 1, requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'vt-tx-hpa-check', + nice: ['T0431', 'K0740'], + title: 'VT-01 TX Chain Spot Check', + description: 'Verify the HPA is enabled and operating with proper backoff.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['vt-rx-beacon-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'TX Chain Open', + params: { tab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-enabled', + description: 'HPA Output Enabled', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'hpa-not-overdriven', + description: 'HPA Within Operating Limits', + params: { requiresObservation: true, observationTab: 'tx-chain' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'HPA Backoff Understanding', + params: { + character: Character.SYSTEM, + question: 'What does the 10 dB HPA backoff during routine ops indicate?', + options: [ + 'Standard operating margin - reduces stress on the amplifier', + 'HPA is faulted and limiting itself', + 'Customer requested low power', + 'Output stage saturated and clipping', + ], + correctIndex: 0, + explanation: 'Headroom is the goal. No drama on TIDEMARK-1.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // PHASE 2: ME-02 / TIDEMARK-2 HEALTH CHECK (PASSIVE) + // ============================================================ + { + id: 'select-maine-station', + nice: ['S0421'], + title: 'Open ME-02', + description: 'Switch to the Maine Ground Station.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['vt-tx-hpa-check'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'ME-02 Selected', + params: { groundStationId: 'ME-02' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'me-dashboard-check', + nice: ['T0153', 'K0741'], + title: 'ME-02 Dashboard Sweep', + description: 'Check the Maine Dashboard for active alarms.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['select-maine-station'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'ME-02 Alarm Status', + params: { + character: Character.SYSTEM, + question: 'What is the alarm state on ME-02?', + options: [ + 'No active alarms - station nominal', + 'Antenna tracking lost', + 'GPSDO holdover', + 'LNB over-temperature', + ], + correctIndex: 0, + explanation: 'Maine is clean. Catherine has the station in good shape.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'me-rx-beacon-check', + nice: ['T0153', 'K1032', 'K0773'], + title: 'ME-02 TIDEMARK-2 Beacon Check', + description: 'Verify the TIDEMARK-2 beacon is present on the spectrum analyzer and the LNB is thermally stable.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-dashboard-check'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'equipment-powered', + description: 'LNB Powered', + params: { equipment: 'lnb', requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'lnb-thermally-stable', + description: 'LNB Thermally Stable', + params: { requiresObservation: true, observationTab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-2 Beacon Detected', + params: { + signalId: 'TIDEMARK-2-Beacon', + minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'me-verify-tracking', + nice: ['K1032', 'T0431'], + title: 'ME-02 Antenna Tracking Check', + description: 'Confirm the Maine antenna is locked on TIDEMARK-2 in program-track.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-rx-beacon-check'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on TIDEMARK-2', + params: { noradId: 61526, requiresObservation: true, observationTab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Tracking Mode for GEO', + params: { + character: Character.SYSTEM, + question: 'Why is program-track the appropriate mode for TIDEMARK-2 right now?', + options: [ + 'GEO satellite holding station - ephemeris is accurate enough; no need to hunt the beacon', + 'Step-track is unavailable on ME-02 hardware', + 'Program-track uses less antenna motion which extends motor life', + 'TIDEMARK-2 is in an inclined orbit and requires program-track', + ], + correctIndex: 0, + explanation: 'For a healthy GEO bird with good TLE, program-track is the right default. Save step-track for inclined birds like AURORA-7.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'me-tx-payload-spot', + nice: ['T0153', 'K0740'], + title: 'ME-02 Customer Traffic Indicator', + description: 'Verify your understanding of how to spot-check downlink data integrity from the dashboard.', + groundStation: 'ME-02', + prerequisiteObjectiveIds: ['me-verify-tracking'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Payload Health Indicators', + params: { + character: Character.SYSTEM, + question: 'On a routine spot check, which combination of indicators confirms the downlink data path is healthy?', + options: [ + 'Frame sync locked + CRC valid + FEC engaged (Reed-Solomon active, no uncorrectables)', + 'High RX power alone is sufficient - if power is good, data is good', + 'Antenna beacon lock is sufficient confirmation of data integrity', + 'Modem temperature within range proves the data path is healthy', + ], + correctIndex: 0, + explanation: 'Beacon lock proves the RF path. Frame sync + CRC + FEC prove the data path. Different layers, different evidence.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + + // ============================================================ + // PHASE 3: TIDEMARK-3 VERIFICATION ON VT-01 + // ============================================================ + { + id: 'switch-to-vermont', + nice: ['S0421'], + title: 'Return to VT-01', + description: 'Switch back to the Vermont Ground Station for the TIDEMARK-3 spot check.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['me-tx-payload-spot'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'ground-station-selected', + description: 'VT-01 Selected', + params: { groundStationId: 'VT-01' }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'repoint-to-tidemark3', + nice: ['S0421', 'K1032', 'T1567'], + title: 'Repoint VT-01 to TIDEMARK-3', + description: 'Command program-track to TIDEMARK-3 (NORAD 61527) at Az 140.5°, El 37.8°. TM-1 traffic is briefly handed to the sister teleport for this check.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['switch-to-vermont'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'ACU Control Open', + params: { tab: 'acu-control' }, + mustMaintain: true, + }, + { + type: 'antenna-tracking-mode-set', + description: 'Program-Track Mode', + params: { trackingMode: 'program-track' }, + mustMaintain: true, + }, + { + type: 'antenna-locked', + description: 'Locked on TIDEMARK-3', + params: { noradId: 61527 }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'tidemark3-beacon-acquire', + nice: ['T0153', 'K0773', 'K1032'], + title: 'TIDEMARK-3 Beacon Verification', + description: 'Tune the spectrum analyzer to the TIDEMARK-3 beacon IF (1078 MHz) and confirm the beacon is present.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['repoint-to-tidemark3'], + timeLimitSeconds: 3 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'RX Analysis Open', + params: { tab: 'rx-analysis' }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum Analyzer at 1078 MHz IF', + params: { + centerFrequency: 1078e6 as Hertz, + centerFrequencyTolerance: 1e6, + }, + mustMaintain: true, + }, + { + type: 'signal-detected', + description: 'TIDEMARK-3 Beacon Detected', + params: { + signalId: 'TIDEMARK-3-Beacon', + minPower: -100 as dBm, + requiresObservation: true, + observationTab: 'rx-analysis', + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + { + id: 'tidemark3-beacon-quality', + nice: ['K0773', 'K1032'], + title: 'Interpret TIDEMARK-3 Beacon', + description: 'Confirm what a clean beacon at 1078 MHz IF tells you about TIDEMARK-3.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['tidemark3-beacon-acquire'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Beacon Significance', + params: { + character: Character.SYSTEM, + question: 'TIDEMARK-3 beacon RF is 4172 MHz. With VT-01 LNB LO at 5250 MHz, seeing the beacon at 1078 MHz IF confirms what?', + options: [ + 'Antenna pointing is correct AND LNB LO is set correctly (5250 - 4172 = 1078)', + 'Only the antenna pointing - LO has no effect on where the beacon appears', + 'Only the LO - antenna pointing is verified by separate means', + 'Nothing meaningful - any noise spike at 1078 would look like this', + ], + correctIndex: 0, + explanation: 'Both have to be right. Wrong pointing = no signal. Wrong LO = signal at a different IF.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'return-to-tidemark1', + nice: ['S0421', 'K1032'], + title: 'Return VT-01 to TIDEMARK-1', + description: 'Repoint program-track back to TIDEMARK-1 and retune the spectrum analyzer to the TM-1 beacon IF (1074.5 MHz).', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['tidemark3-beacon-quality'], + timeLimitSeconds: 2 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'antenna-locked', + description: 'Locked on TIDEMARK-1', + params: { noradId: 61525 }, + mustMaintain: true, + }, + { + type: 'speca-center-frequency', + description: 'Spectrum Analyzer at 1074.5 MHz IF', + params: { + centerFrequency: 1074.5e6 as Hertz, + centerFrequencyTolerance: 1e6, + }, + mustMaintain: true, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + + // ============================================================ + // SHIFT WRAP + // ============================================================ + { + id: 'final-alarm-sweep', + nice: ['T0153', 'K0741'], + title: 'Final Alarm Sweep', + description: 'Do a final dashboard sweep before logging the shift summary.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['return-to-tidemark1'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'tab-active', + hidden: true, + description: 'Dashboard Open', + params: { tab: 'dashboard' }, + mustMaintain: true, + }, + { + type: 'status-check', + description: 'Three-Bird Health Summary', + params: { + character: Character.SYSTEM, + question: 'Summarize the morning round result.', + options: [ + 'TIDEMARK-1 healthy, TIDEMARK-2 healthy, TIDEMARK-3 beacon verified - all three nominal', + 'TIDEMARK-1 degraded, TIDEMARK-2 nominal, TIDEMARK-3 offline', + 'TIDEMARK-1 nominal, TIDEMARK-2 not yet checked, TIDEMARK-3 verified', + 'All three birds in fault state', + ], + correctIndex: 0, + explanation: 'Clean round. Log it and move on.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 5, + }, + { + id: 'log-shift-summary', + nice: ['K0645', 'T0153'], + title: 'Log Shift Summary', + description: 'Select the correct entry for the shift log.', + groundStation: 'VT-01', + prerequisiteObjectiveIds: ['final-alarm-sweep'], + timeLimitSeconds: 1 * 60, + timerStartTrigger: 'on-activate', + conditions: [ + { + type: 'status-check', + description: 'Shift Log Entry', + params: { + character: Character.SYSTEM, + question: 'Which line correctly records this shift in the operations log?', + options: [ + '0700 - Morning rounds complete. VT-01/TM-1, ME-02/TM-2, TM-3 beacon verified via VT-01 spot-check. No anomalies.', + '0700 - Morning rounds incomplete. TM-3 spot-check deferred.', + '0700 - Multiple alarms cleared. See trouble ticket.', + '0700 - Antenna swap performed VT-01 to ME-02.', + ], + correctIndex: 0, + explanation: 'Routine work logged routinely. Next operator picks up with full context.', + pointPenalty: 5, + }, + mustMaintain: false, + }, + ], + conditionLogic: 'AND', + points: 10, + }, + ] as Objective[], + dialogClips: { + intro: { + text: ` +

+ [Text message from Dana at 06:42] +

+

+ "Welcome to your first qualified shift. Three birds today - TM-1, TM-2, TM-3. Overnight commissioning crew brought TM-3 up a few hours ago; just need a beacon spot-check before customers light it up. Standard morning rounds. Coffee's in the kitchen." +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/9/intro.mp3'), + }, + objectives: { + 'select-maine-station': { + text: ` +

+ VT-01 looks clean. Catherine's run Maine since the handover stuck - shouldn't be anything to surprise you over there. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/9/obj-select-maine-station.mp3'), + }, + 'switch-to-vermont': { + text: ` +

+ Maine confirmed nominal. Sister teleport is taking TM-1 customer traffic for the next few minutes while you spot-check TM-3. No transmit needed - just a beacon look. Antenna's yours. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.NEUTRAL, + audioUrl: getAssetUrl('/assets/campaigns/nats/9/obj-switch-to-vermont.mp3'), + }, + 'return-to-tidemark1': { + text: ` +

+ TM-3 looks clean. Bring VT-01 back to TM-1 and we'll pull customer traffic back. Sister teleport's been notified. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.CONFIDENT, + audioUrl: getAssetUrl('/assets/campaigns/nats/9/obj-return-to-tidemark1.mp3'), + }, + 'log-shift-summary': { + text: ` +

+ That's the morning. Routine work logged routinely. Welcome to qualified ops - this is what most days look like. +

+

+ See you tomorrow. +

+ `, + character: Character.DANA_TORRES, + emotion: Emotion.HAPPY, + audioUrl: getAssetUrl('/assets/campaigns/nats/9/obj-log-shift-summary.mp3'), + }, + }, + }, +}; diff --git a/src/dev-menu/dev-menu-box.ts b/src/dev-menu/dev-menu-box.ts index 2009b576..e63ac444 100644 --- a/src/dev-menu/dev-menu-box.ts +++ b/src/dev-menu/dev-menu-box.ts @@ -3,9 +3,9 @@ * @description Draggable box providing dev tools: auto-skip dialogs, complete objective, set timer */ -import { DraggableBox } from '@engine/ui/draggable-box'; -import { html } from '@engine/utils/development/formatter'; -import { getEl, showEl } from '@engine/utils/get-el'; +import { DraggableBox } from '@app/engine/ui/draggable-box'; +import { html } from '@app/engine/utils/development/formatter'; +import { getEl, showEl } from '@app/engine/utils/get-el'; import { ObjectivesManager } from '@app/objectives/objectives-manager'; import { Header } from '@app/pages/layout/header/header'; import { ScenarioSelectionPage } from '@app/pages/scenario-selection'; diff --git a/src/equipment/antenna/antenna-core.ts b/src/equipment/antenna/antenna-core.ts index 17d35a40..61ea7843 100644 --- a/src/equipment/antenna/antenna-core.ts +++ b/src/equipment/antenna/antenna-core.ts @@ -1,14 +1,14 @@ import { EventBus } from "@app/events/event-bus"; import { SignalOrigin } from "@app/signal-origin"; import { Degrees } from "ootk"; -import { Events } from "../../events/events"; -import { SimulationManager } from "../../simulation/simulation-manager"; -import { dB, dBm, Hertz, RfSignal } from "../../types"; -import { AlarmStatus, BaseEquipment } from '../base-equipment'; -import { TapPoint } from "../rf-front-end/coupler-module/tap-points"; -import { RFFrontEndCore } from "../rf-front-end/rf-front-end-core"; -import { Satellite } from "../satellite/satellite"; -import { Transmitter } from "../transmitter/transmitter"; +import { Events } from "@app/events/events"; +import { SimulationManager } from "@app/simulation/simulation-manager"; +import { dB, dBm, Hertz, RfSignal } from "@app/types"; +import { AlarmStatus, BaseEquipment } from '@app/equipment/base-equipment'; +import { TapPoint } from "@app/equipment/rf-front-end/coupler-module/tap-points"; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { Satellite } from "@app/equipment/satellite/satellite"; +import { Transmitter } from "@app/equipment/transmitter/transmitter"; import { ANTENNA_CONFIG_KEYS } from "./antenna-config-keys"; import { ANTENNA_CONFIGS, AntennaConfig } from "./antenna-configs"; import { StepTrackController } from "./step-track-controller"; @@ -108,6 +108,11 @@ export interface AntennaState { precipitationDetected: boolean; /** Ice accumulation on feed horn in dB (0 = no ice) */ iceAccumulation_dB: number; + /** Elevated sky-noise degradation in dB on the receive path (e.g., sun transit). 0 = nominal sky. */ + skyNoiseDegradation_dB: number; + /** ACU automation controller fault: program-track, step-track, and target + * slewing are unavailable; manual/stow/maintenance servo control still works. */ + isAcuAutomationFaulted: boolean; // === ACU Identification === /** ACU model number */ @@ -233,6 +238,8 @@ export abstract class AntennaCore extends BaseEquipment { isRainBlowerEnabled: false, precipitationDetected: false, iceAccumulation_dB: 0, + skyNoiseDegradation_dB: 0, + isAcuAutomationFaulted: false, // ACU Identification acuModel: this.config.acuModel ?? 'Kratos NGC-2200', acuSerialNumber: this.config.acuSerialNumber ?? 'ACU-01', @@ -324,9 +331,15 @@ export abstract class AntennaCore extends BaseEquipment { } update(): void { + // ACU automation fault: the automation processor (program-track, + // step-track, lock logic) is offline. Servos and manual control still + // work - updateSlew_ keeps running so manual/stow/maintenance moves do. + const automationAvailable = !this.state.isAcuAutomationFaulted; + // Update program-track position continuously when tracking a satellite // This is the base tracking - always follows ephemeris - if (this.state.trackingMode === 'program-track' && + if (automationAvailable && + this.state.trackingMode === 'program-track' && this.state.targetSatelliteId !== null && this.state.isPowered && this.state.isOperational) { @@ -334,7 +347,7 @@ export abstract class AntennaCore extends BaseEquipment { } // Update step-track controller if step-track optimization is enabled (within program-track) - if (this.state.isStepTrackEnabled && this.state.isPowered && this.state.isOperational) { + if (automationAvailable && this.state.isStepTrackEnabled && this.state.isPowered && this.state.isOperational) { this.stepTrackController_.update(); } @@ -342,7 +355,8 @@ export abstract class AntennaCore extends BaseEquipment { this.updateSlew_(); // Check for program-track lock when antenna arrives near target - if (this.state.trackingMode === 'program-track' && + if (automationAvailable && + this.state.trackingMode === 'program-track' && this.state.targetSatelliteId !== null && !this.state.isSlewing) { this.checkProgramTrackLock_(); @@ -901,6 +915,16 @@ export abstract class AntennaCore extends BaseEquipment { this.notifyStateChange_(); } + /** + * Update elevated sky-noise degradation on the receive path (called by + * WeatherManager for sun-transit events). RX-only: the uplink is unaffected. + * @param degradation_dB - Current sky-noise degradation in dB (0 = nominal) + */ + updateSkyNoiseDegradation(degradation_dB: number): void { + this.state.skyNoiseDegradation_dB = degradation_dB; + this.notifyStateChange_(); + } + // ======================================================================== // FINE ADJUSTMENT METHODS // ======================================================================== @@ -1332,6 +1356,23 @@ export abstract class AntennaCore extends BaseEquipment { alarms.push({ severity: 'warning', message: `HIGH POLARIZATION (${this.state.polarization}°)` }); } + // ACU automation controller fault + if (this.state.isAcuAutomationFaulted) { + alarms.push({ severity: 'error', message: 'ACU AUTOMATION FAULT - MANUAL CONTROL ONLY' }); + } + + // Elevated sky-noise warning (sun transit) + if (this.state.skyNoiseDegradation_dB > 0.5) { + const sky = this.state.skyNoiseDegradation_dB; + if (sky >= 6) { + alarms.push({ severity: 'error', message: `SUN TRANSIT: SKY NOISE CRITICAL (${sky.toFixed(1)} dB)` }); + } else if (sky >= 2) { + alarms.push({ severity: 'warning', message: `SUN TRANSIT: ELEVATED SKY NOISE (${sky.toFixed(1)} dB)` }); + } else { + alarms.push({ severity: 'info', message: `ELEVATED SKY NOISE (${sky.toFixed(1)} dB)` }); + } + } + // Ice accumulation warning if (this.state.iceAccumulation_dB > 0) { const ice = this.state.iceAccumulation_dB; @@ -1790,8 +1831,9 @@ export abstract class AntennaCore extends BaseEquipment { const Latm = this.calculateAtmosphericLoss_(frequency, elevation); const Lfeed = this.feedLossAt_(frequency) + (this.config.rxChainLoss_dB ?? 0); - // Ice accumulation adds to feed loss (ice on feed horn acts as lossy medium) - const Lice = this.state.iceAccumulation_dB; + // Ice accumulation adds to feed loss (ice on feed horn acts as lossy medium). + // Elevated sky noise (sun transit) is modeled as equivalent RX-path loss. + const Lice = this.state.iceAccumulation_dB + this.state.skyNoiseDegradation_dB; const LfeedTotal = Lfeed + Lice; const Tant = Tsky + this.noiseFromLossK_(Latm, 260); // Atm ~260 K slab @@ -1856,8 +1898,10 @@ export abstract class AntennaCore extends BaseEquipment { // Use pattern gain (accounts for off-axis angle) instead of just peak gain const Grx_dBi = this.patternGain_dBi_(offAxis_deg, f_Hz); - // Feed loss (frequency-dependent) + ice accumulation on feed horn - const feedLoss = this.feedLossAt_(f_Hz) + this.state.iceAccumulation_dB; + // Feed loss (frequency-dependent) + ice accumulation on feed horn + + // elevated sky noise (sun transit) as equivalent RX loss + const feedLoss = this.feedLossAt_(f_Hz) + this.state.iceAccumulation_dB + + this.state.skyNoiseDegradation_dB; // Pointing loss (if any off-axis error from wind/jitter) const pointingLoss = this.pointingLoss_dB_(offAxis_deg, f_Hz); diff --git a/src/equipment/antenna/antenna-ui-basic.ts b/src/equipment/antenna/antenna-ui-basic.ts index 6d781d3f..cf5f633e 100644 --- a/src/equipment/antenna/antenna-ui-basic.ts +++ b/src/equipment/antenna/antenna-ui-basic.ts @@ -1,8 +1,8 @@ import { PowerSwitch } from "@app/components/power-switch/power-switch"; import { RotaryKnob } from "@app/components/rotary-knob/rotary-knob"; import { Degrees } from "ootk"; -import { html } from "../../engine/utils/development/formatter"; -import { qs } from "../../engine/utils/query-selector"; +import { html } from "@app/engine/utils/development/formatter"; +import { qs } from "@app/engine/utils/query-selector"; import { ANTENNA_CONFIG_KEYS } from "./antenna-config-keys"; import { AntennaCore, AntennaState } from "./antenna-core"; import './antenna.css'; diff --git a/src/equipment/antenna/antenna-ui-modern.ts b/src/equipment/antenna/antenna-ui-modern.ts index 5b95fc6e..54b3ca54 100644 --- a/src/equipment/antenna/antenna-ui-modern.ts +++ b/src/equipment/antenna/antenna-ui-modern.ts @@ -1,8 +1,8 @@ import { HelpButton } from "@app/components/help-btn/help-btn"; import { PolarPlot } from "@app/components/polar-plot/polar-plot"; import { Degrees } from "ootk"; -import { html } from "../../engine/utils/development/formatter"; -import { qs } from "../../engine/utils/query-selector"; +import { html } from "@app/engine/utils/development/formatter"; +import { qs } from "@app/engine/utils/query-selector"; import { ANTENNA_CONFIG_KEYS } from "./antenna-config-keys"; import { AntennaCore, AntennaState } from "./antenna-core"; import './antenna-ui-modern.css'; diff --git a/src/equipment/antenna/antenna-ui-standard.ts b/src/equipment/antenna/antenna-ui-standard.ts index 70d664e9..5256cff6 100644 --- a/src/equipment/antenna/antenna-ui-standard.ts +++ b/src/equipment/antenna/antenna-ui-standard.ts @@ -4,8 +4,8 @@ import { PowerSwitch } from "@app/components/power-switch/power-switch"; import { RotaryKnob } from "@app/components/rotary-knob/rotary-knob"; import { ToggleSwitch } from "@app/components/toggle-switch/toggle-switch"; import { Degrees } from "ootk"; -import { html } from "../../engine/utils/development/formatter"; -import { qs } from "../../engine/utils/query-selector"; +import { html } from "@app/engine/utils/development/formatter"; +import { qs } from "@app/engine/utils/query-selector"; import { ANTENNA_CONFIG_KEYS } from "./antenna-config-keys"; import { AntennaCore, AntennaState } from "./antenna-core"; import './antenna.css'; diff --git a/src/equipment/antenna/step-track-controller.ts b/src/equipment/antenna/step-track-controller.ts index a574f7bd..0c2e7f29 100644 --- a/src/equipment/antenna/step-track-controller.ts +++ b/src/equipment/antenna/step-track-controller.ts @@ -1,7 +1,7 @@ import { Hertz } from "@app/types"; import { Degrees } from "ootk"; import { SimulationManager } from "@app/simulation/simulation-manager"; -import { TapPoint } from "../rf-front-end/coupler-module/tap-points"; +import { TapPoint } from "@app/equipment/rf-front-end/coupler-module/tap-points"; import { AntennaCore } from "./antenna-core"; /** diff --git a/src/equipment/base-equipment.ts b/src/equipment/base-equipment.ts index 5876c1d7..54e435a7 100644 --- a/src/equipment/base-equipment.ts +++ b/src/equipment/base-equipment.ts @@ -1,11 +1,11 @@ import { generateUuid } from '@app/engine/utils/uuid'; -import { EventBus } from '../events/event-bus'; -import { EventMap, Events } from '../events/events'; +import { EventBus } from '@app/events/event-bus'; +import { EventMap, Events } from '@app/events/events'; import { AntennaState } from './antenna'; -import { RealTimeSpectrumAnalyzerState } from './real-time-spectrum-analyzer/real-time-spectrum-analyzer'; -import { ReceiverState } from './receiver/receiver'; -import { RFFrontEndState } from './rf-front-end/rf-front-end-core'; -import { TransmitterState } from './transmitter/transmitter'; +import { RealTimeSpectrumAnalyzerState } from '@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; +import { ReceiverState } from '@app/equipment/receiver/receiver'; +import { RFFrontEndState } from '@app/equipment/rf-front-end/rf-front-end-core'; +import { TransmitterState } from '@app/equipment/transmitter/transmitter'; import './base-equipment.css'; diff --git a/src/equipment/lcd-screen/example.ts b/src/equipment/lcd-screen/example.ts index 9312f634..10667e33 100644 --- a/src/equipment/lcd-screen/example.ts +++ b/src/equipment/lcd-screen/example.ts @@ -1,4 +1,4 @@ -import { html } from "../../engine/utils/development/formatter"; +import { html } from "@app/engine/utils/development/formatter"; html`
diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control-box.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control-box.ts index 3a9906cd..b0bea387 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control-box.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control-box.ts @@ -1,6 +1,6 @@ -import { DraggableBox } from "../../engine/ui/draggable-box"; -import { html } from "../../engine/utils/development/formatter"; -import { getEl } from "../../engine/utils/get-el"; +import { DraggableBox } from "@app/engine/ui/draggable-box"; +import { html } from "@app/engine/utils/development/formatter"; +import { getEl } from "@app/engine/utils/get-el"; import { AnalyzerControl } from "./analyzer-control"; import { RealTimeSpectrumAnalyzer } from "./real-time-spectrum-analyzer"; diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control.ts index a86325e2..69b436e9 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control.ts @@ -3,25 +3,25 @@ import { Logger } from '@app/logging/logger'; import { Sfx } from '@app/sound/sfx-enum'; import SoundManager from '@app/sound/sound-manager'; import { parseLocalizedNumber } from '@app/utils/parse-number'; -import { BaseElement } from '../../components/base-element'; -import { html } from "../../engine/utils/development/formatter"; -import { qs, qsa } from "../../engine/utils/query-selector"; +import { BaseElement } from '@app/components/base-element'; +import { html } from "@app/engine/utils/development/formatter"; +import { qs, qsa } from "@app/engine/utils/query-selector"; import './analyzer-control.css'; -import { ACAmptBtn } from './analyzer-control/ac-ampt-btn/ac-ampt-btn'; -import { ACBWBtn } from './analyzer-control/ac-bw-btn/ac-bw-btn'; -import { ACFreqBtn } from './analyzer-control/ac-freq-btn/ac-freq-btn'; -import { ACGhzBtn } from './analyzer-control/ac-ghz-btn/ac-ghz-btn'; -import { ACHzBtn } from './analyzer-control/ac-hz-btn/ac-hz-btn'; -import { ACKhzBtn } from './analyzer-control/ac-khz-btn/ac-khz-btn'; -import { ACMhzBtn } from './analyzer-control/ac-mhz-btn/ac-mhz-btn'; -import { ACMkrBtn } from './analyzer-control/ac-mkr-btn/ac-mkr-btn'; -import { ACMkr2Btn } from './analyzer-control/ac-mkr2-btn/ac-mkr2-btn'; -import { ACModeBtn } from './analyzer-control/ac-mode-btn/ac-mode-btn'; -import { ACSaveBtn } from './analyzer-control/ac-save-btn/ac-save-btn'; -import { ACSpanBtn } from './analyzer-control/ac-span-btn/ac-span-btn'; -import { ACSweepBtn } from './analyzer-control/ac-sweep-btn/ac-sweep-btn'; -import { ACTraceBtn } from './analyzer-control/ac-trace-btn/ac-trace-btn'; -import { BaseControlButton } from './analyzer-control/base-control-button'; +import { ACAmptBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ampt-btn/ac-ampt-btn'; +import { ACBWBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-bw-btn/ac-bw-btn'; +import { ACFreqBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-freq-btn/ac-freq-btn'; +import { ACGhzBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ghz-btn/ac-ghz-btn'; +import { ACHzBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-hz-btn/ac-hz-btn'; +import { ACKhzBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-khz-btn/ac-khz-btn'; +import { ACMhzBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mhz-btn/ac-mhz-btn'; +import { ACMkrBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr-btn/ac-mkr-btn'; +import { ACMkr2Btn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr2-btn/ac-mkr2-btn'; +import { ACModeBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mode-btn/ac-mode-btn'; +import { ACSaveBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-save-btn/ac-save-btn'; +import { ACSpanBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-span-btn/ac-span-btn'; +import { ACSweepBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-sweep-btn/ac-sweep-btn'; +import { ACTraceBtn } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn'; +import { BaseControlButton } from '@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button'; import { RealTimeSpectrumAnalyzer } from './real-time-spectrum-analyzer'; export interface AnalyzerControlOptions { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ampt-btn/ac-ampt-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ampt-btn/ac-ampt-btn.ts index f61d9335..826ae20b 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ampt-btn/ac-ampt-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ampt-btn/ac-ampt-btn.ts @@ -1,6 +1,6 @@ import { dB } from "@app/types"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-ampt-btn.css'; export class ACAmptBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-bw-btn/ac-bw-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-bw-btn/ac-bw-btn.ts index 634f824c..74315b97 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-bw-btn/ac-bw-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-bw-btn/ac-bw-btn.ts @@ -1,7 +1,7 @@ import { Logger } from "@app/logging/logger"; import { Hertz } from "@app/types"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-bw-btn.css'; export class ACBWBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra1-btn/ac-extra1-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra1-btn/ac-extra1-btn.ts index 530df86c..5ea8a708 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra1-btn/ac-extra1-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra1-btn/ac-extra1-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-extra1-btn.css'; export class ACExtra1Btn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra2-btn/ac-extra2-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra2-btn/ac-extra2-btn.ts index 22aecbf7..41a0f7fd 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra2-btn/ac-extra2-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra2-btn/ac-extra2-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-extra2-btn.css'; export class ACExtra2Btn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra3-btn/ac-extra3-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra3-btn/ac-extra3-btn.ts index 3134d124..14e95c2b 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra3-btn/ac-extra3-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra3-btn/ac-extra3-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-extra3-btn.css'; export class ACExtra3Btn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra4-btn/ac-extra4-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra4-btn/ac-extra4-btn.ts index 83a6457f..7e009afa 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra4-btn/ac-extra4-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-extra4-btn/ac-extra4-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-extra4-btn.css'; export class ACExtra4Btn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-freq-btn/ac-freq-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-freq-btn/ac-freq-btn.ts index 0934f5ff..403a05e9 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-freq-btn/ac-freq-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-freq-btn/ac-freq-btn.ts @@ -1,8 +1,8 @@ import { Logger } from "@app/logging/logger"; import { Hertz } from "@app/types"; import { parseLocalizedNumber } from "@app/utils/parse-number"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-freq-btn.css'; export class ACFreqBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ghz-btn/ac-ghz-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ghz-btn/ac-ghz-btn.ts index 77e47214..a4621a62 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ghz-btn/ac-ghz-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-ghz-btn/ac-ghz-btn.ts @@ -1,6 +1,6 @@ import { parseLocalizedNumber } from "@app/utils/parse-number"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-ghz-btn.css'; export class ACGhzBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-hz-btn/ac-hz-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-hz-btn/ac-hz-btn.ts index 2858a433..b351497c 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-hz-btn/ac-hz-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-hz-btn/ac-hz-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-hz-btn.css'; export class ACHzBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-khz-btn/ac-khz-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-khz-btn/ac-khz-btn.ts index 264bf255..bb0a4ccb 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-khz-btn/ac-khz-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-khz-btn/ac-khz-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-khz-btn.css'; export class ACKhzBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-meas-btn/ac-meas-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-meas-btn/ac-meas-btn.ts index dab1419b..c1b77999 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-meas-btn/ac-meas-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-meas-btn/ac-meas-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-meas-btn.css'; export class ACMeasBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mhz-btn/ac-mhz-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mhz-btn/ac-mhz-btn.ts index c811cefb..f1d39cd8 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mhz-btn/ac-mhz-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mhz-btn/ac-mhz-btn.ts @@ -1,6 +1,6 @@ import { parseLocalizedNumber } from "@app/utils/parse-number"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-mhz-btn.css'; export class ACMhzBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr-btn/ac-mkr-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr-btn/ac-mkr-btn.ts index f6954f1c..fd4c98ec 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr-btn/ac-mkr-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr-btn/ac-mkr-btn.ts @@ -1,6 +1,6 @@ import { Logger } from "@app/logging/logger"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-mkr-btn.css'; export class ACMkrBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr2-btn/ac-mkr2-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr2-btn/ac-mkr2-btn.ts index 31714d7e..7c3deb0d 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr2-btn/ac-mkr2-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mkr2-btn/ac-mkr2-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-mkr2-btn.css'; export class ACMkr2Btn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mode-btn/ac-mode-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mode-btn/ac-mode-btn.ts index 80951e2b..218fd773 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mode-btn/ac-mode-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-mode-btn/ac-mode-btn.ts @@ -1,7 +1,7 @@ import { EventBus } from "@app/events/event-bus"; import { Events } from "@app/events/events"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-mode-btn.css'; export class ACModeBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-save-btn/ac-save-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-save-btn/ac-save-btn.ts index 0f182e6f..ca03b14f 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-save-btn/ac-save-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-save-btn/ac-save-btn.ts @@ -1,6 +1,6 @@ import { getEl } from "@app/engine/utils/get-el"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-save-btn.css'; export class ACSaveBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-span-btn/ac-span-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-span-btn/ac-span-btn.ts index 1c796fe4..4fcad727 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-span-btn/ac-span-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-span-btn/ac-span-btn.ts @@ -1,7 +1,7 @@ import { Logger } from "@app/logging/logger"; import { Hertz } from "@app/types"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-span-btn.css'; export class ACSpanBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-sweep-btn/ac-sweep-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-sweep-btn/ac-sweep-btn.ts index 5edc8346..c364369f 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-sweep-btn/ac-sweep-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-sweep-btn/ac-sweep-btn.ts @@ -1,5 +1,5 @@ -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-sweep-btn.css'; export class ACSweepBtn extends BaseControlButton { diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn.ts index 20b00ada..1b92155a 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn.ts @@ -1,6 +1,6 @@ import { Logger } from "@app/logging/logger"; -import { AnalyzerControl } from "../../analyzer-control"; -import { BaseControlButton } from "../base-control-button"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; +import { BaseControlButton } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button"; import './ac-trace-btn.css'; export type TraceMode = 'clearwrite' | 'hold' | 'maxhold' | 'minhold' | 'average'; diff --git a/src/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button.ts b/src/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button.ts index b193f377..6caa5d70 100644 --- a/src/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button.ts +++ b/src/equipment/real-time-spectrum-analyzer/analyzer-control/base-control-button.ts @@ -5,7 +5,7 @@ import { Events } from "@app/events/events"; import { Logger } from "@app/logging/logger"; import { Sfx } from "@app/sound/sfx-enum"; import SoundManager from "@app/sound/sound-manager"; -import { AnalyzerControl } from "../analyzer-control"; +import { AnalyzerControl } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control"; export abstract class BaseControlButton { private readonly html_: string; diff --git a/src/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer.ts b/src/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer.ts index 5802d79c..2e0b724b 100644 --- a/src/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer.ts +++ b/src/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer.ts @@ -2,18 +2,18 @@ import { HelpButton } from "@app/components/help-btn/help-btn"; import { qs } from "@app/engine/utils/query-selector"; import { EventBus } from "@app/events/event-bus"; import { Logger } from "@app/logging/logger"; -import { html } from "../../engine/utils/development/formatter"; -import { Events } from "../../events/events"; -import { dB, Hertz, IfSignal, RfSignal } from "../../types"; -import { BaseEquipment } from '../base-equipment'; -import { TapPoint } from "../rf-front-end/coupler-module/tap-points"; -import { RFFrontEndCore } from "../rf-front-end/rf-front-end-core"; +import { html } from "@app/engine/utils/development/formatter"; +import { Events } from "@app/events/events"; +import { dB, Hertz, IfSignal, RfSignal } from "@app/types"; +import { BaseEquipment } from '@app/equipment/base-equipment'; +import { TapPoint } from "@app/equipment/rf-front-end/coupler-module/tap-points"; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; import { AnalyzerControlBox } from "./analyzer-control-box"; -import type { TraceMode } from "./analyzer-control/ac-trace-btn/ac-trace-btn"; +import type { TraceMode } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn"; import { defaultSpectrumAnalyzerState } from "./defaultSpectrumAnalyzerState"; import './real-time-spectrum-analyzer.css'; -import { SpectralDensityPlot } from './rtsa-screen/spectral-density-plot'; -import { WaterfallDisplay } from "./rtsa-screen/waterfall-display"; +import { SpectralDensityPlot } from '@app/equipment/real-time-spectrum-analyzer/rtsa-screen/spectral-density-plot'; +import { WaterfallDisplay } from "@app/equipment/real-time-spectrum-analyzer/rtsa-screen/waterfall-display"; import { SpectrumDataProcessor } from "./spectrum-data-processor"; type MarkerPoint = { x: number; y: number; signal: number }; diff --git a/src/equipment/real-time-spectrum-analyzer/rtsa-screen/rtsa-screen.ts b/src/equipment/real-time-spectrum-analyzer/rtsa-screen/rtsa-screen.ts index 9ebb6638..0b84121a 100644 --- a/src/equipment/real-time-spectrum-analyzer/rtsa-screen/rtsa-screen.ts +++ b/src/equipment/real-time-spectrum-analyzer/rtsa-screen/rtsa-screen.ts @@ -1,5 +1,5 @@ import { Logger } from "@app/logging/logger"; -import { RealTimeSpectrumAnalyzer } from "../real-time-spectrum-analyzer"; +import { RealTimeSpectrumAnalyzer } from "@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer"; export abstract class RTSAScreen { // Canvas elements diff --git a/src/equipment/real-time-spectrum-analyzer/rtsa-screen/spectral-density-plot.ts b/src/equipment/real-time-spectrum-analyzer/rtsa-screen/spectral-density-plot.ts index c5d0cd75..56ec9557 100644 --- a/src/equipment/real-time-spectrum-analyzer/rtsa-screen/spectral-density-plot.ts +++ b/src/equipment/real-time-spectrum-analyzer/rtsa-screen/spectral-density-plot.ts @@ -1,8 +1,8 @@ import { SimulationManager } from "@app/simulation/simulation-manager"; -import { Hertz, IfSignal, MHz, RfSignal } from "../../../types"; -import type { TraceMode } from "../analyzer-control/ac-trace-btn/ac-trace-btn"; -import { RealTimeSpectrumAnalyzer } from "../real-time-spectrum-analyzer"; -import { SpectrumDataProcessor } from "../spectrum-data-processor"; +import { Hertz, IfSignal, MHz, RfSignal } from "@app/types"; +import type { TraceMode } from "@app/equipment/real-time-spectrum-analyzer/analyzer-control/ac-trace-btn/ac-trace-btn"; +import { RealTimeSpectrumAnalyzer } from "@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer"; +import { SpectrumDataProcessor } from "@app/equipment/real-time-spectrum-analyzer/spectrum-data-processor"; import { RTSAScreen } from "./rtsa-screen"; /** diff --git a/src/equipment/real-time-spectrum-analyzer/rtsa-screen/waterfall-display.ts b/src/equipment/real-time-spectrum-analyzer/rtsa-screen/waterfall-display.ts index e4cee62c..cda9d4d5 100644 --- a/src/equipment/real-time-spectrum-analyzer/rtsa-screen/waterfall-display.ts +++ b/src/equipment/real-time-spectrum-analyzer/rtsa-screen/waterfall-display.ts @@ -1,6 +1,6 @@ -import { Hertz } from "../../../types"; -import { RealTimeSpectrumAnalyzer, RealTimeSpectrumAnalyzerState } from "../real-time-spectrum-analyzer"; -import { SpectrumDataProcessor } from "../spectrum-data-processor"; +import { Hertz } from "@app/types"; +import { RealTimeSpectrumAnalyzer, RealTimeSpectrumAnalyzerState } from "@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer"; +import { SpectrumDataProcessor } from "@app/equipment/real-time-spectrum-analyzer/spectrum-data-processor"; import { RTSAScreen } from "./rtsa-screen"; export class WaterfallDisplay extends RTSAScreen { diff --git a/src/equipment/real-time-spectrum-analyzer/spectrum-data-processor.ts b/src/equipment/real-time-spectrum-analyzer/spectrum-data-processor.ts index df932e39..319412dc 100644 --- a/src/equipment/real-time-spectrum-analyzer/spectrum-data-processor.ts +++ b/src/equipment/real-time-spectrum-analyzer/spectrum-data-processor.ts @@ -1,4 +1,4 @@ -import { Hertz, IfSignal, RfSignal } from "../../types"; +import { Hertz, IfSignal, RfSignal } from "@app/types"; import { RealTimeSpectrumAnalyzer } from "./real-time-spectrum-analyzer"; /** diff --git a/src/equipment/receiver/receiver.ts b/src/equipment/receiver/receiver.ts index d8c2dd1d..1acbfb12 100644 --- a/src/equipment/receiver/receiver.ts +++ b/src/equipment/receiver/receiver.ts @@ -1,13 +1,13 @@ import { PowerSwitch } from "@app/components/power-switch/power-switch"; import { EventBus } from "@app/events/event-bus"; -import { html } from "../../engine/utils/development/formatter"; -import { qs } from "../../engine/utils/query-selector"; -import { Events } from "../../events/events"; -import { dBm, FECType, Hertz, IfSignal, MHz, ModulationType } from "../../types"; -import { AntennaCore } from "../antenna"; -import { AlarmStatus, BaseEquipment } from "../base-equipment"; -import { TapPoint } from "../rf-front-end/coupler-module/tap-points"; -import { RFFrontEndCore } from "../rf-front-end/rf-front-end-core"; +import { html } from "@app/engine/utils/development/formatter"; +import { qs } from "@app/engine/utils/query-selector"; +import { Events } from "@app/events/events"; +import { dBm, FECType, Hertz, IfSignal, MHz, ModulationType } from "@app/types"; +import { AntennaCore } from "@app/equipment/antenna"; +import { AlarmStatus, BaseEquipment } from "@app/equipment/base-equipment"; +import { TapPoint } from "@app/equipment/rf-front-end/coupler-module/tap-points"; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; import { ADCDegradationResult, calculateADCDegradation } from './adc-degradation'; import './receiver.css'; diff --git a/src/equipment/rf-front-end/agc-module/agc-module-core.ts b/src/equipment/rf-front-end/agc-module/agc-module-core.ts index 2d3720b8..5312d50d 100644 --- a/src/equipment/rf-front-end/agc-module/agc-module-core.ts +++ b/src/equipment/rf-front-end/agc-module/agc-module-core.ts @@ -1,7 +1,7 @@ import { SignalOrigin } from "@app/signal-origin"; import { dB, dBm, IfSignal } from '@app/types'; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule, RFFrontEndModuleState } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule, RFFrontEndModuleState } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * AGC module state diff --git a/src/equipment/rf-front-end/agc-module/agc-module-factory.ts b/src/equipment/rf-front-end/agc-module/agc-module-factory.ts index 60f4f60c..d0d03cbf 100644 --- a/src/equipment/rf-front-end/agc-module/agc-module-factory.ts +++ b/src/equipment/rf-front-end/agc-module/agc-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { AGCModuleCore, AGCState } from './agc-module-core'; import { AGCModuleUIHeadless } from './agc-module-ui-headless'; diff --git a/src/equipment/rf-front-end/agc-module/agc-module-ui-headless.ts b/src/equipment/rf-front-end/agc-module/agc-module-ui-headless.ts index 791007f2..b4048992 100644 --- a/src/equipment/rf-front-end/agc-module/agc-module-ui-headless.ts +++ b/src/equipment/rf-front-end/agc-module/agc-module-ui-headless.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { AGCModuleCore, AGCState } from './agc-module-core'; /** diff --git a/src/equipment/rf-front-end/buc-module/buc-module-core.ts b/src/equipment/rf-front-end/buc-module/buc-module-core.ts index b01104ed..03013efa 100644 --- a/src/equipment/rf-front-end/buc-module/buc-module-core.ts +++ b/src/equipment/rf-front-end/buc-module/buc-module-core.ts @@ -1,7 +1,7 @@ import { SignalOrigin } from "@app/signal-origin"; import { dB, dBm, Hertz, IfFrequency, IfSignal, MHz, RfFrequency, RfSignal } from '@app/types'; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule, RFFrontEndModuleState } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule, RFFrontEndModuleState } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * Spurious output from mixer products diff --git a/src/equipment/rf-front-end/buc-module/buc-module-factory.ts b/src/equipment/rf-front-end/buc-module/buc-module-factory.ts index f2643bc9..afe05137 100644 --- a/src/equipment/rf-front-end/buc-module/buc-module-factory.ts +++ b/src/equipment/rf-front-end/buc-module/buc-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { BUCModuleCore, BUCState } from './buc-module-core'; import { BUCModuleUIStandard } from './buc-module-ui-standard'; diff --git a/src/equipment/rf-front-end/buc-module/buc-module-ui-standard.ts b/src/equipment/rf-front-end/buc-module/buc-module-ui-standard.ts index 527fdf8c..c5252d23 100644 --- a/src/equipment/rf-front-end/buc-module/buc-module-ui-standard.ts +++ b/src/equipment/rf-front-end/buc-module/buc-module-ui-standard.ts @@ -3,7 +3,7 @@ import { RotaryKnob } from '@app/components/rotary-knob/rotary-knob'; import { ToggleSwitch } from '@app/components/toggle-switch/toggle-switch'; import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { BUCModuleCore, BUCState } from './buc-module-core'; import './buc-module.css'; diff --git a/src/equipment/rf-front-end/coupler-module/coupler-module-factory.ts b/src/equipment/rf-front-end/coupler-module/coupler-module-factory.ts index af67b4c1..44bce5dc 100644 --- a/src/equipment/rf-front-end/coupler-module/coupler-module-factory.ts +++ b/src/equipment/rf-front-end/coupler-module/coupler-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { CouplerModule, CouplerState } from './coupler-module'; export type CouplerModuleUIType = 'standard' | 'basic' | 'headless'; diff --git a/src/equipment/rf-front-end/coupler-module/coupler-module.ts b/src/equipment/rf-front-end/coupler-module/coupler-module.ts index 61f16f3e..3c38d084 100644 --- a/src/equipment/rf-front-end/coupler-module/coupler-module.ts +++ b/src/equipment/rf-front-end/coupler-module/coupler-module.ts @@ -2,8 +2,8 @@ import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; import { SignalPathManager } from "@app/simulation/signal-path-manager"; import { IfFrequency, RfFrequency } from "@app/types"; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule } from '@app/equipment/rf-front-end/rf-front-end-module'; import './coupler-module.css'; import { TapPoint } from "./tap-points"; diff --git a/src/equipment/rf-front-end/filter-module/filter-module-core.ts b/src/equipment/rf-front-end/filter-module/filter-module-core.ts index 835b5b88..16193c1d 100644 --- a/src/equipment/rf-front-end/filter-module/filter-module-core.ts +++ b/src/equipment/rf-front-end/filter-module/filter-module-core.ts @@ -1,7 +1,7 @@ import { SignalOrigin } from "@app/signal-origin"; import { dBm, Hertz, IfSignal, MHz } from '@app/types'; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * Filter bandwidth configuration diff --git a/src/equipment/rf-front-end/filter-module/filter-module-factory.ts b/src/equipment/rf-front-end/filter-module/filter-module-factory.ts index b229d310..2cf54ce6 100644 --- a/src/equipment/rf-front-end/filter-module/filter-module-factory.ts +++ b/src/equipment/rf-front-end/filter-module/filter-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { IfFilterBankModuleCore, IfFilterBankState } from './filter-module-core'; import { IfFilterBankModuleUIStandard } from './filter-module-ui-standard'; diff --git a/src/equipment/rf-front-end/filter-module/filter-module-ui-standard.ts b/src/equipment/rf-front-end/filter-module/filter-module-ui-standard.ts index 90dd2266..17de7291 100644 --- a/src/equipment/rf-front-end/filter-module/filter-module-ui-standard.ts +++ b/src/equipment/rf-front-end/filter-module/filter-module-ui-standard.ts @@ -2,7 +2,7 @@ import { HelpButton } from '@app/components/help-btn/help-btn'; import { RotaryKnob } from "@app/components/rotary-knob/rotary-knob"; import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { FILTER_BANDWIDTH_CONFIGS, IfFilterBankModuleCore, IfFilterBankState } from './filter-module-core'; import './filter-module.css'; diff --git a/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-core.ts b/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-core.ts index 7fcfc0ae..db3369a0 100644 --- a/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-core.ts +++ b/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-core.ts @@ -1,7 +1,7 @@ import { SimulationManager } from '@app/simulation/simulation-manager'; import { clamp } from 'ootk'; -import { RFFrontEndCore } from '../rf-front-end-core'; -import { RFFrontEndModule } from '../rf-front-end-module'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; +import { RFFrontEndModule } from '@app/equipment/rf-front-end/rf-front-end-module'; import { defaultGpsdoState, GPSDOState } from './gpsdo-state'; /** diff --git a/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-factory.ts b/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-factory.ts index c56e026a..938fa236 100644 --- a/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-factory.ts +++ b/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { GPSDOModuleCore } from './gpsdo-module-core'; import { GPSDOModuleUIStandard } from './gpsdo-module-ui-standard'; import { GPSDOState } from './gpsdo-state'; diff --git a/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-ui-standard.ts b/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-ui-standard.ts index 8cfd9149..f2bbfb57 100644 --- a/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-ui-standard.ts +++ b/src/equipment/rf-front-end/gpsdo-module/gpsdo-module-ui-standard.ts @@ -6,7 +6,7 @@ import { qs } from "@app/engine/utils/query-selector"; import { EventBus } from '@app/events/event-bus'; import { Events } from '@app/events/events'; import { GPSDOState } from '.'; -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { GPSDOModuleCore } from './gpsdo-module-core'; import './gpsdo-module.css'; diff --git a/src/equipment/rf-front-end/hpa-module/hpa-module-core.ts b/src/equipment/rf-front-end/hpa-module/hpa-module-core.ts index da10fb31..84283179 100644 --- a/src/equipment/rf-front-end/hpa-module/hpa-module-core.ts +++ b/src/equipment/rf-front-end/hpa-module/hpa-module-core.ts @@ -1,7 +1,7 @@ import { SignalOrigin } from "@app/signal-origin"; import type { dB, dBm, dBW, RfSignal } from '@app/types'; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * High Power Amplifier module state diff --git a/src/equipment/rf-front-end/hpa-module/hpa-module-factory.ts b/src/equipment/rf-front-end/hpa-module/hpa-module-factory.ts index fe2831b0..3be47088 100644 --- a/src/equipment/rf-front-end/hpa-module/hpa-module-factory.ts +++ b/src/equipment/rf-front-end/hpa-module/hpa-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { HPAModuleCore, HPAState } from './hpa-module-core'; import { HPAModuleUIStandard } from './hpa-module-ui-standard'; diff --git a/src/equipment/rf-front-end/hpa-module/hpa-module-ui-standard.ts b/src/equipment/rf-front-end/hpa-module/hpa-module-ui-standard.ts index a60e76b9..bda3e1e0 100644 --- a/src/equipment/rf-front-end/hpa-module/hpa-module-ui-standard.ts +++ b/src/equipment/rf-front-end/hpa-module/hpa-module-ui-standard.ts @@ -5,7 +5,7 @@ import { SecureToggleSwitch } from '@app/components/secure-toggle-switch/secure- import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; import type { dBW } from '@app/types'; -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { HPAModuleCore, HPAState } from './hpa-module-core'; import './hpa-module.css'; diff --git a/src/equipment/rf-front-end/lnb-module/lnb-module-core.ts b/src/equipment/rf-front-end/lnb-module/lnb-module-core.ts index 4f019a4a..8a6b1da0 100644 --- a/src/equipment/rf-front-end/lnb-module/lnb-module-core.ts +++ b/src/equipment/rf-front-end/lnb-module/lnb-module-core.ts @@ -1,7 +1,7 @@ import { SignalOrigin } from "@app/signal-origin"; import { dB, Hertz, IfFrequency, IfSignal, MHz, RfFrequency, RfSignal } from '@app/types'; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule, RFFrontEndModuleState } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule, RFFrontEndModuleState } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * Low Noise Block converter module state diff --git a/src/equipment/rf-front-end/lnb-module/lnb-module-factory.ts b/src/equipment/rf-front-end/lnb-module/lnb-module-factory.ts index e148cf91..8d05d385 100644 --- a/src/equipment/rf-front-end/lnb-module/lnb-module-factory.ts +++ b/src/equipment/rf-front-end/lnb-module/lnb-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { LNBModuleCore, LNBState } from './lnb-module-core'; import { LNBModuleUIStandard } from './lnb-module-ui-standard'; diff --git a/src/equipment/rf-front-end/lnb-module/lnb-module-ui-standard.ts b/src/equipment/rf-front-end/lnb-module/lnb-module-ui-standard.ts index 9d5a5ecb..0572ccf6 100644 --- a/src/equipment/rf-front-end/lnb-module/lnb-module-ui-standard.ts +++ b/src/equipment/rf-front-end/lnb-module/lnb-module-ui-standard.ts @@ -3,7 +3,7 @@ import { RotaryKnob } from "@app/components/rotary-knob/rotary-knob"; import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; import { IfFrequency } from "@app/types"; -import { RFFrontEndCore } from "../rf-front-end-core"; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; import { LNBModuleCore, LNBState } from './lnb-module-core'; import './lnb-module.css'; diff --git a/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-core.ts b/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-core.ts index 94288ab7..a0629251 100644 --- a/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-core.ts +++ b/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-core.ts @@ -1,7 +1,7 @@ import { SignalOrigin } from "@app/signal-origin"; import { dB, dBm, IfSignal, MHz } from '@app/types'; -import { RFFrontEndCore } from "../rf-front-end-core"; -import { RFFrontEndModule, RFFrontEndModuleState } from '../rf-front-end-module'; +import { RFFrontEndCore } from "@app/equipment/rf-front-end/rf-front-end-core"; +import { RFFrontEndModule, RFFrontEndModuleState } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * Single notch configuration diff --git a/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-factory.ts b/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-factory.ts index 44c37bb0..de5966b1 100644 --- a/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-factory.ts +++ b/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { NotchFilterModuleCore, NotchFilterState } from './notch-filter-module-core'; import { NotchFilterModuleUIHeadless } from './notch-filter-module-ui-headless'; diff --git a/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-ui-headless.ts b/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-ui-headless.ts index 40e81c2d..b5534672 100644 --- a/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-ui-headless.ts +++ b/src/equipment/rf-front-end/notch-filter-module/notch-filter-module-ui-headless.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { NotchFilterModuleCore, NotchFilterState } from './notch-filter-module-core'; /** diff --git a/src/equipment/rf-front-end/omt-module/omt-module-factory.ts b/src/equipment/rf-front-end/omt-module/omt-module-factory.ts index a0e6c08f..6a078fd9 100644 --- a/src/equipment/rf-front-end/omt-module/omt-module-factory.ts +++ b/src/equipment/rf-front-end/omt-module/omt-module-factory.ts @@ -1,4 +1,4 @@ -import { RFFrontEndCore } from '../rf-front-end-core'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; import { OMTModule, OMTState } from './omt-module'; export type OMTModuleUIType = 'standard' | 'basic' | 'headless'; diff --git a/src/equipment/rf-front-end/omt-module/omt-module.ts b/src/equipment/rf-front-end/omt-module/omt-module.ts index efd9843c..313dbd45 100644 --- a/src/equipment/rf-front-end/omt-module/omt-module.ts +++ b/src/equipment/rf-front-end/omt-module/omt-module.ts @@ -3,9 +3,9 @@ import { html } from "@app/engine/utils/development/formatter"; import { Logger } from '@app/logging/logger'; import { SignalOrigin } from "@app/signal-origin"; import type { dBi, dBm, RfSignal } from '@app/types'; -import { dB } from '../../../types'; -import { RFFrontEndCore } from '../rf-front-end-core'; -import { RFFrontEndModule } from '../rf-front-end-module'; +import { dB } from '@app/types'; +import { RFFrontEndCore } from '@app/equipment/rf-front-end/rf-front-end-core'; +import { RFFrontEndModule } from '@app/equipment/rf-front-end/rf-front-end-module'; /** * Polarization types for OMT/Duplexer diff --git a/src/equipment/rf-front-end/rf-front-end-core.ts b/src/equipment/rf-front-end/rf-front-end-core.ts index 20d16015..bfb50a7c 100644 --- a/src/equipment/rf-front-end/rf-front-end-core.ts +++ b/src/equipment/rf-front-end/rf-front-end-core.ts @@ -1,21 +1,21 @@ import { EventBus } from "@app/events/event-bus"; -import { Events, HpaNoiseAmplificationData } from "../../events/events"; -import { SignalPathManager } from '../../simulation/signal-path-manager'; -import { dBm, Hertz, IfFrequency, RfFrequency } from "../../types"; -import { AntennaCore } from "../antenna"; -import { AlarmStatus, BaseEquipment } from "../base-equipment"; -import { Transmitter } from '../transmitter/transmitter'; -import { AGCModuleCore, AGCState } from './agc-module/agc-module-core'; -import { BUCModuleCore, BUCState } from './buc-module/buc-module-core'; -import { CouplerModule, CouplerState } from './coupler-module/coupler-module'; -import { TapPoint } from "./coupler-module/tap-points"; -import { IfFilterBankModuleCore, IfFilterBankState } from './filter-module/filter-module-core'; -import { GPSDOModuleCore } from './gpsdo-module/gpsdo-module-core'; -import { defaultGpsdoState, GPSDOState } from "./gpsdo-module/gpsdo-state"; -import { HPAModuleCore, HPAState } from './hpa-module/hpa-module-core'; -import { LNBModuleCore, LNBState } from './lnb-module/lnb-module-core'; -import { NotchFilterModuleCore, NotchFilterState } from './notch-filter-module/notch-filter-module-core'; -import { OMTModule, OMTState } from './omt-module/omt-module'; +import { Events, HpaNoiseAmplificationData } from "@app/events/events"; +import { SignalPathManager } from '@app/simulation/signal-path-manager'; +import { dBm, Hertz, IfFrequency, RfFrequency } from "@app/types"; +import { AntennaCore } from "@app/equipment/antenna"; +import { AlarmStatus, BaseEquipment } from "@app/equipment/base-equipment"; +import { Transmitter } from '@app/equipment/transmitter/transmitter'; +import { AGCModuleCore, AGCState } from '@app/equipment/rf-front-end/agc-module/agc-module-core'; +import { BUCModuleCore, BUCState } from '@app/equipment/rf-front-end/buc-module/buc-module-core'; +import { CouplerModule, CouplerState } from '@app/equipment/rf-front-end/coupler-module/coupler-module'; +import { TapPoint } from "@app/equipment/rf-front-end/coupler-module/tap-points"; +import { IfFilterBankModuleCore, IfFilterBankState } from '@app/equipment/rf-front-end/filter-module/filter-module-core'; +import { GPSDOModuleCore } from '@app/equipment/rf-front-end/gpsdo-module/gpsdo-module-core'; +import { defaultGpsdoState, GPSDOState } from "@app/equipment/rf-front-end/gpsdo-module/gpsdo-state"; +import { HPAModuleCore, HPAState } from '@app/equipment/rf-front-end/hpa-module/hpa-module-core'; +import { LNBModuleCore, LNBState } from '@app/equipment/rf-front-end/lnb-module/lnb-module-core'; +import { NotchFilterModuleCore, NotchFilterState } from '@app/equipment/rf-front-end/notch-filter-module/notch-filter-module-core'; +import { OMTModule, OMTState } from '@app/equipment/rf-front-end/omt-module/omt-module'; /** * Complete RF Front-End state diff --git a/src/equipment/rf-front-end/rf-front-end-ui-standard.ts b/src/equipment/rf-front-end/rf-front-end-ui-standard.ts index 318b6b14..e9a0d0af 100644 --- a/src/equipment/rf-front-end/rf-front-end-ui-standard.ts +++ b/src/equipment/rf-front-end/rf-front-end-ui-standard.ts @@ -1,30 +1,30 @@ import { EventBus } from '@app/events/event-bus'; import { Events } from '@app/events/events'; -import { html } from "../../engine/utils/development/formatter"; -import { qs } from "../../engine/utils/query-selector"; -import { AGCModuleCore, AGCState } from './agc-module/agc-module-core'; -import { createAGC } from './agc-module/agc-module-factory'; -import { BUCModuleCore, BUCState } from './buc-module/buc-module-core'; -import { createBUC } from './buc-module/buc-module-factory'; -import { CouplerModule, CouplerState } from "./coupler-module/coupler-module"; -import { createCoupler } from './coupler-module/coupler-module-factory'; -import { IfFilterBankModuleCore, IfFilterBankState } from './filter-module/filter-module-core'; -import { createIfFilterBank } from './filter-module/filter-module-factory'; -import { IfFilterBankModuleUIStandard } from './filter-module/filter-module-ui-standard'; +import { html } from "@app/engine/utils/development/formatter"; +import { qs } from "@app/engine/utils/query-selector"; +import { AGCModuleCore, AGCState } from '@app/equipment/rf-front-end/agc-module/agc-module-core'; +import { createAGC } from '@app/equipment/rf-front-end/agc-module/agc-module-factory'; +import { BUCModuleCore, BUCState } from '@app/equipment/rf-front-end/buc-module/buc-module-core'; +import { createBUC } from '@app/equipment/rf-front-end/buc-module/buc-module-factory'; +import { CouplerModule, CouplerState } from "@app/equipment/rf-front-end/coupler-module/coupler-module"; +import { createCoupler } from '@app/equipment/rf-front-end/coupler-module/coupler-module-factory'; +import { IfFilterBankModuleCore, IfFilterBankState } from '@app/equipment/rf-front-end/filter-module/filter-module-core'; +import { createIfFilterBank } from '@app/equipment/rf-front-end/filter-module/filter-module-factory'; +import { IfFilterBankModuleUIStandard } from '@app/equipment/rf-front-end/filter-module/filter-module-ui-standard'; import { GPSDOModuleCore } from './gpsdo-module'; -import { createGPSDO } from './gpsdo-module/gpsdo-module-factory'; -import { GPSDOModuleUIStandard } from './gpsdo-module/gpsdo-module-ui-standard'; -import { GPSDOState } from './gpsdo-module/gpsdo-state'; -import { HPAModuleCore, HPAState } from './hpa-module/hpa-module-core'; -import { createHPA } from './hpa-module/hpa-module-factory'; -import { HPAModuleUIStandard } from './hpa-module/hpa-module-ui-standard'; -import { LNBModuleCore, LNBState } from './lnb-module/lnb-module-core'; -import { createLNB } from './lnb-module/lnb-module-factory'; -import { LNBModuleUIStandard } from './lnb-module/lnb-module-ui-standard'; -import { NotchFilterModuleCore, NotchFilterState } from './notch-filter-module/notch-filter-module-core'; -import { createNotchFilter } from './notch-filter-module/notch-filter-module-factory'; -import { OMTModule, OMTState } from "./omt-module/omt-module"; -import { createOMT } from "./omt-module/omt-module-factory"; +import { createGPSDO } from '@app/equipment/rf-front-end/gpsdo-module/gpsdo-module-factory'; +import { GPSDOModuleUIStandard } from '@app/equipment/rf-front-end/gpsdo-module/gpsdo-module-ui-standard'; +import { GPSDOState } from '@app/equipment/rf-front-end/gpsdo-module/gpsdo-state'; +import { HPAModuleCore, HPAState } from '@app/equipment/rf-front-end/hpa-module/hpa-module-core'; +import { createHPA } from '@app/equipment/rf-front-end/hpa-module/hpa-module-factory'; +import { HPAModuleUIStandard } from '@app/equipment/rf-front-end/hpa-module/hpa-module-ui-standard'; +import { LNBModuleCore, LNBState } from '@app/equipment/rf-front-end/lnb-module/lnb-module-core'; +import { createLNB } from '@app/equipment/rf-front-end/lnb-module/lnb-module-factory'; +import { LNBModuleUIStandard } from '@app/equipment/rf-front-end/lnb-module/lnb-module-ui-standard'; +import { NotchFilterModuleCore, NotchFilterState } from '@app/equipment/rf-front-end/notch-filter-module/notch-filter-module-core'; +import { createNotchFilter } from '@app/equipment/rf-front-end/notch-filter-module/notch-filter-module-factory'; +import { OMTModule, OMTState } from "@app/equipment/rf-front-end/omt-module/omt-module"; +import { createOMT } from "@app/equipment/rf-front-end/omt-module/omt-module-factory"; import { RFFrontEndCore, RFFrontEndState } from './rf-front-end-core'; import './rf-front-end.css'; diff --git a/src/equipment/transmitter/transmitter.ts b/src/equipment/transmitter/transmitter.ts index 94b7f256..07ba9adf 100644 --- a/src/equipment/transmitter/transmitter.ts +++ b/src/equipment/transmitter/transmitter.ts @@ -1,12 +1,12 @@ import { ToggleSwitch } from "@app/components/toggle-switch/toggle-switch"; import { EventBus } from "@app/events/event-bus"; import { SignalOrigin } from "@app/signal-origin"; -import { PowerSwitch } from '../../components/power-switch/power-switch'; -import { html } from "../../engine/utils/development/formatter"; -import { qs } from "../../engine/utils/query-selector"; -import { Events } from "../../events/events"; -import { dBi, dBm, FECType, Hertz, IfFrequency, IfSignal, ModulationType } from "../../types"; -import { AlarmStatus, BaseEquipment } from "../base-equipment"; +import { PowerSwitch } from '@app/components/power-switch/power-switch'; +import { html } from "@app/engine/utils/development/formatter"; +import { qs } from "@app/engine/utils/query-selector"; +import { Events } from "@app/events/events"; +import { dBi, dBm, FECType, Hertz, IfFrequency, IfSignal, ModulationType } from "@app/types"; +import { AlarmStatus, BaseEquipment } from "@app/equipment/base-equipment"; import './transmitter.css'; export interface TransmitterModem { diff --git a/src/events/event-bus.ts b/src/events/event-bus.ts index 52d2507c..b3033540 100644 --- a/src/events/event-bus.ts +++ b/src/events/event-bus.ts @@ -1,4 +1,4 @@ -import { Logger } from "../logging/logger"; +import { Logger } from "@app/logging/logger"; import { EventMap, Events } from "./events"; type EventHandlersMap = Partial void>>>; diff --git a/src/events/events.ts b/src/events/events.ts index 11c8d5b7..efbb3560 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -19,11 +19,11 @@ import type { CryptoZeroizedData, } from "@app/equipment/crypto/crypto-types"; import { Milliseconds } from "ootk"; -import { ReceiverModemState } from "../equipment/receiver/receiver"; -import { TransmitterModem } from "../equipment/transmitter/transmitter"; -import { ConditionState, Objective, ObjectiveState } from "../objectives/objective-types"; -import { OpsLogEntry } from "../ops-log/ops-log-types"; -import { RfSignal } from "../types"; +import { ReceiverModemState } from "@app/equipment/receiver/receiver"; +import { TransmitterModem } from "@app/equipment/transmitter/transmitter"; +import { ConditionState, Objective, ObjectiveState } from "@app/objectives/objective-types"; +import { OpsLogEntry } from "@app/ops-log/ops-log-types"; +import { RfSignal } from "@app/types"; // Antenna Event specific interfaces export interface AntennaLoopbackChangedData { @@ -236,7 +236,7 @@ export interface AlarmStateChangedData { export interface WeatherEventData { id: string; groundStationId: string; - type: 'snow' | 'rain' | 'fog' | 'wind' | 'dust' | 'hail' | 'ice' | 'storm'; + type: 'snow' | 'rain' | 'fog' | 'wind' | 'dust' | 'hail' | 'ice' | 'storm' | 'sun-transit'; severity: 'minor' | 'moderate' | 'severe'; startTime: number; duration: number; diff --git a/src/interference/interference-manager.ts b/src/interference/interference-manager.ts new file mode 100644 index 00000000..ecc98bf6 --- /dev/null +++ b/src/interference/interference-manager.ts @@ -0,0 +1,123 @@ +/** + * @file InterferenceManager - Scheduled, time-windowed RF interference + * @description Injects interference signals into a satellite's uplink path on + * a configurable duty cycle. Because injection happens at the satellite + * (externalSignal), the transponder relays the interferer to EVERY receiving + * station - modeling uplink interference/jamming, which is the + * discrimination-relevant case (local terrestrial interference would appear + * at one station only). + * + * Scenarios configure via `settings.interferenceEvents`. The windowed on/off + * pattern is the training signal: deliberate interference has a duty cycle; + * accidents are continuous or random. + */ + +import { EventBus } from '@app/events/event-bus'; +import { Events } from '@app/events/events'; +import { ScenarioManager } from '@app/scenario-manager'; +import { SignalOrigin } from '@app/signal-origin'; +import { SimulationManager } from '@app/simulation/simulation-manager'; +import type { dBi, dBm, FECType, Hertz, ModulationType, RfFrequency, RfSignal } from '@app/types'; +import type { Milliseconds } from 'ootk'; + +export interface InterferenceEventConfig { + id: string; + /** NORAD ID of the satellite whose transponder relays the interferer */ + satelliteNoradId: number; + /** Interferer RF center frequency (uplink, Hz) */ + frequency: number; + /** Interferer bandwidth (Hz) */ + bandwidth: number; + /** Interferer power at the transponder input (dBm) */ + power: number; + /** Uplink polarization - must match the victim transponder to route */ + polarization: 'H' | 'V'; + /** Seconds since mission start when the event envelope opens */ + startTime: number; + /** Total envelope duration (s); on/off windows repeat inside it */ + duration: number; + /** Window cycle period (s) */ + periodSeconds: number; + /** Transmit-on time per period (s) */ + onSeconds: number; +} + +export class InterferenceManager { + private static instance_: InterferenceManager | null = null; + + private events_: InterferenceEventConfig[] = []; + private missionStartTime_ = 0; + /** Currently-injected signalIds (subset of events) */ + private readonly activeSignalIds_ = new Set(); + private readonly boundUpdateHandler_: (dt: Milliseconds) => void; + + private constructor() { + this.missionStartTime_ = Date.now(); + this.boundUpdateHandler_ = this.update_.bind(this); + this.events_ = ScenarioManager.getInstance().settings.interferenceEvents ?? []; + EventBus.getInstance().on(Events.UPDATE, this.boundUpdateHandler_); + } + + static getInstance(): InterferenceManager { + this.instance_ ??= new InterferenceManager(); + return this.instance_; + } + + static destroy(): void { + if (this.instance_) { + EventBus.getInstance().off(Events.UPDATE, this.instance_.boundUpdateHandler_); + this.instance_ = null; + } + } + + /** Whether the event's interferer is currently transmitting */ + isEventActive(eventId: string): boolean { + return this.activeSignalIds_.has(InterferenceManager.signalIdFor(eventId)); + } + + static signalIdFor(eventId: string): string { + return `INTERFERER-${eventId}`; + } + + private update_(): void { + const elapsed = (Date.now() - this.missionStartTime_) / 1000; + const sim = SimulationManager.getInstance(); + + for (const event of this.events_) { + const signalId = InterferenceManager.signalIdFor(event.id); + const inEnvelope = elapsed >= event.startTime && elapsed < event.startTime + event.duration; + const phase = (elapsed - event.startTime) % event.periodSeconds; + const shouldTransmit = inEnvelope && phase < event.onSeconds; + const isInjected = this.activeSignalIds_.has(signalId); + + if (shouldTransmit === isInjected) continue; + + const satellite = sim.satellites.find(s => s.noradId === event.satelliteNoradId); + if (!satellite) continue; + + if (shouldTransmit) { + const signal: RfSignal = { + signalId, + serverId: 1, + noradId: event.satelliteNoradId, + frequency: event.frequency as RfFrequency, + polarization: event.polarization, + power: event.power as dBm, + bandwidth: event.bandwidth as Hertz, + modulation: 'null' as ModulationType, // Noise-like, unmodulated blob + fec: 'null' as FECType, + feed: '', + isDegraded: false, + origin: SignalOrigin.SATELLITE_RX, + noiseFloor: null, + gainInPath: 0 as dBi, + }; + satellite.externalSignal.push(signal); + this.activeSignalIds_.add(signalId); + } else { + satellite.externalSignal = satellite.externalSignal.filter(s => s.signalId !== signalId); + this.activeSignalIds_.delete(signalId); + } + } + } +} diff --git a/src/modal/hint-modal.ts b/src/modal/hint-modal.ts index e2ce3ada..89ab361e 100644 --- a/src/modal/hint-modal.ts +++ b/src/modal/hint-modal.ts @@ -5,10 +5,10 @@ import { EventBus } from '@app/events/event-bus'; import { Events, HintShownData } from '@app/events/events'; -import { DraggableModal } from '@engine/ui/draggable-modal'; -import { html } from '@engine/utils/development/formatter'; -import { getEl } from '@engine/utils/get-el'; -import bulbPng from '../assets/icons/bulb.png'; +import { DraggableModal } from '@app/engine/ui/draggable-modal'; +import { html } from '@app/engine/utils/development/formatter'; +import { getEl } from '@app/engine/utils/get-el'; +import bulbPng from '@app/assets/icons/bulb.png'; import { HintManager } from './hint-manager'; import './hint-modal.css'; diff --git a/src/modal/objective-failed-modal.ts b/src/modal/objective-failed-modal.ts index 9701b2a6..26dbd3a0 100644 --- a/src/modal/objective-failed-modal.ts +++ b/src/modal/objective-failed-modal.ts @@ -6,7 +6,7 @@ import { ProgressSaveManager } from '@app/user-account/progress-save-manager'; import { DialogManager } from './dialog-manager'; import { PendingQuizIndicator } from './pending-quiz-indicator'; import { QuizModal } from './quiz-modal'; -import stopwatchPng from '../assets/icons/stopwatch.png'; +import stopwatchPng from '@app/assets/icons/stopwatch.png'; import './objective-failed-modal.css'; interface FailureModalOptions { diff --git a/src/modal/quiz-modal.ts b/src/modal/quiz-modal.ts index d1dfab64..49c1da2c 100644 --- a/src/modal/quiz-modal.ts +++ b/src/modal/quiz-modal.ts @@ -5,9 +5,9 @@ import { EventBus } from '@app/events/event-bus'; import { Events, QuizAnsweredData, QuizCompletedData, QuizDismissedData, QuizPassedData, QuizShowData } from '@app/events/events'; -import { DraggableBox } from '@engine/ui/draggable-box'; -import { html } from '@engine/utils/development/formatter'; -import { getEl, showEl } from '@engine/utils/get-el'; +import { DraggableBox } from '@app/engine/ui/draggable-box'; +import { html } from '@app/engine/utils/development/formatter'; +import { getEl, showEl } from '@app/engine/utils/get-el'; import { Character, CharacterAvatars, CharacterNames, Emotion, getCharacterAvatarUrl } from './character-enum'; import './quiz-modal.css'; diff --git a/src/objectives/objective-types.ts b/src/objectives/objective-types.ts index 4713700d..66ceff25 100644 --- a/src/objectives/objective-types.ts +++ b/src/objectives/objective-types.ts @@ -233,6 +233,10 @@ export interface ConditionParams { character?: Character; /** For status-check: if true, options will not be randomized (use for "All of the above" questions) */ preserveOptionOrder?: boolean; + /** For status-check: line appended to the Working Document panel when this quiz is passed */ + documentLine?: string; + /** For status-check: Working Document section the documentLine belongs to (default: "Notes") */ + documentSection?: string; /** For signal-detected/signal-level-correct: signal identifier to match */ signalId?: string; /** For signal-detected/signal-level-correct: minimum power level in dBm */ @@ -274,6 +278,24 @@ export interface ConditionParams { /** For fault-active/fault-cleared: fault ID to check */ faultId?: string; + // Observation-gating parameters + /** + * If true, this (typically passive) condition does not count as satisfied + * from ambient simulation state alone. It latches satisfied only after the + * underlying value is true WHILE the operator is viewing the observation + * context (params.observationTab). Once observed it stays satisfied even if + * the operator navigates away. Use for checks that would otherwise show + * pre-ticked before the player ever looked (signal-detected, receiver lock, + * beacon lock, gpsdo lock, etc.). + */ + requiresObservation?: boolean; + /** + * For requiresObservation: the tab that must be active to "observe" the + * value (e.g. 'rx-analysis', 'tx-chain', 'acu-control', 'gps-timing', + * 'dashboard'). Matched by exact id or prefix, like the tab-active condition. + */ + observationTab?: string; + /** Additional context-specific parameters */ [key: string]: unknown; } @@ -300,6 +322,13 @@ export interface Condition { description: string; /** Hint or tip to help achieve the condition (optional) */ hint?: string; + /** + * If true, this condition is still enforced for objective completion but is + * NOT rendered as a row in the checklist. Used to require the operator be on + * the correct tab without spelling out which tab (qualified-operator scenarios + * expect the player to know where to look). + */ + hidden?: boolean; /** Parameters specific to this condition type */ params?: ConditionParams; /** Whether this condition must be maintained (true) or just achieved once (false) */ @@ -398,4 +427,10 @@ export interface ConditionState { lostTimestamps?: number[]; /** Whether a hint was requested for this condition */ hintRequested?: boolean; + /** + * For requiresObservation conditions: latches true once the value has been + * observed on the correct tab. After that the condition counts as satisfied + * regardless of tab or live value. + */ + observed?: boolean; } diff --git a/src/objectives/objectives-manager.ts b/src/objectives/objectives-manager.ts index fcae639d..363544d6 100644 --- a/src/objectives/objectives-manager.ts +++ b/src/objectives/objectives-manager.ts @@ -17,7 +17,7 @@ import { TabbedCanvas } from '@app/pages/mission-control/tabbed-canvas'; import { SimulationManager } from '@app/simulation/simulation-manager'; import { TrafficControlManager } from '@app/traffic/traffic-control-manager'; import { Milliseconds } from 'ootk'; -import bulbPng from '../assets/icons/bulb.png'; +import bulbPng from '@app/assets/icons/bulb.png'; import { Condition, ConditionParams, @@ -318,6 +318,7 @@ export class ObjectivesManager { condState.isSatisfied = true; condState.isMaintenanceComplete = true; condState.satisfiedAt = Date.now(); + condState.observed = true; // a force-completed objective counts as observed } // Mark objective as complete @@ -391,6 +392,7 @@ export class ObjectivesManager { condState.satisfiedAt = undefined; condState.maintainedDuration = 0; condState.isMaintenanceComplete = false; + condState.observed = false; // re-doing the objective requires re-observing } // Remove from collapsed set @@ -692,6 +694,7 @@ export class ObjectivesManager { currentCondState.satisfiedAt = savedCondState.satisfiedAt; currentCondState.maintainedDuration = savedCondState.maintainedDuration; currentCondState.isMaintenanceComplete = savedCondState.isMaintenanceComplete; + currentCondState.observed = savedCondState.observed; currentCondState.lostTimestamps = savedCondState.lostTimestamps || []; } }); @@ -792,6 +795,14 @@ export class ObjectivesManager { for (let i = 0; i < objective.conditions.length; i++) { const condition = objective.conditions[i]; + + // Hidden conditions are still enforced for completion but not shown + // (e.g. the tab-active requirement on qualified-operator scenarios, + // where we don't spell out which tab to open). + if (condition.hidden) { + continue; + } + const conditionState = objectiveState.conditionStates[i]; const conditionCompleted = conditionState.isMaintenanceComplete; @@ -1035,6 +1046,7 @@ export class ObjectivesManager { condState.satisfiedAt = undefined; condState.maintainedDuration = 0; condState.isMaintenanceComplete = false; + condState.observed = false; // re-observe after a prerequisite reset } // Recursively deactivate objectives that depend on this one @@ -1058,7 +1070,22 @@ export class ObjectivesManager { } const wasSatisfied = conditionState.isSatisfied; - const isNowSatisfied = this.evaluateCondition_(conditionState.condition, objectiveState); + let isNowSatisfied = this.evaluateCondition_(conditionState.condition, objectiveState); + + // Observation gate: a flagged passive condition does not count from + // ambient simulation state alone. It must be seen on the correct tab + // once, after which it latches satisfied (stays checked even if the + // operator navigates away or the live value changes). + const condParams = conditionState.condition.params; + if (condParams?.requiresObservation && condParams?.observationTab) { + if (conditionState.observed) { + isNowSatisfied = true; // already observed - latched + } else if (isNowSatisfied && this.isObservationContextActive_(conditionState.condition)) { + conditionState.observed = true; // observed for the first time - latch + } else { + isNowSatisfied = false; // value not yet observed on the right tab + } + } // Update satisfied state conditionState.isSatisfied = isNowSatisfied; @@ -1164,6 +1191,19 @@ export class ObjectivesManager { return equipmentArray.some(checker); } + /** + * Whether the operator is currently viewing the observation context (tab) + * required to "observe" a requiresObservation condition. Mirrors the + * tab-active matching (exact id or prefix). + */ + private isObservationContextActive_(condition: Condition): boolean { + const targetTab = condition.params?.observationTab; + if (!targetTab) return false; + const activeTab = TabbedCanvas.getActiveTab(); + if (!activeTab) return false; + return activeTab === targetTab || activeTab.startsWith(`${targetTab}-`); + } + /** * Evaluate a single condition and return whether it's currently satisfied */ @@ -2093,6 +2133,8 @@ export class ObjectivesManager { return activeTab === targetTab || activeTab.startsWith(`${targetTab}-`); } + // (observation-context helper isObservationContextActive_ lives below) + // ═══════════════════════════════════════════════════════════════ // FEC Conditions // ═══════════════════════════════════════════════════════════════ diff --git a/src/pages/base-page.ts b/src/pages/base-page.ts index e4656ad3..3a2e32db 100644 --- a/src/pages/base-page.ts +++ b/src/pages/base-page.ts @@ -14,6 +14,9 @@ import { OpsLogManager } from "@app/ops-log/ops-log-manager"; import { NavigationOptions, Router } from "@app/router"; import { ScenarioManager } from "@app/scenario-manager"; import { ScenarioDialogManager } from "@app/scenarios/scenario-dialog-manager"; +import { WorkingDocumentManager } from "@app/scenarios/working-document-manager"; +import { InterferenceManager } from "@app/interference/interference-manager"; +import { WeatherManager } from "@app/weather/weather-manager"; import { ScenarioCompletionHandler } from "@app/scoring/scenario-completion-handler"; import { ScoreCalculator } from "@app/scoring/score-calculator"; import { SimulationManager } from "@app/simulation/simulation-manager"; @@ -92,6 +95,21 @@ export abstract class BasePage extends BaseElement { // Initialize scenario dialog manager for objective completion dialogs ScenarioDialogManager.getInstance().initialize(); + // Initialize the Working Document panel (no-op unless the scenario + // declares settings.workingDocument) + WorkingDocumentManager.getInstance().initialize(); + + // Warm up the weather manager so scheduled events (rain, sun transit) + // tick from scenario start rather than first ACU-tab render + if ((scenario.settings.weatherEvents?.length ?? 0) > 0) { + WeatherManager.getInstance(); + } + + // Start scheduled interference events (uplink jammers etc.) + if ((scenario.settings.interferenceEvents?.length ?? 0) > 0) { + InterferenceManager.getInstance(); + } + // Initialize quiz modal for status-check objective conditions QuizModal.getInstance(); @@ -247,7 +265,21 @@ export abstract class BasePage extends BaseElement { if (!scenarioId) return null; try { - const progress = await getUserDataService().getScenarioProgress(scenarioId); + // Cap how long the remote progress check can block scenario start. The + // request retries failed fetches with exponential backoff (~7s total), + // and this call is awaited before the screen transitions - so a slow or + // unreachable backend (offline, or a CORS-blocked dev origin) would stall + // "Start" for several seconds. If the check does not answer quickly, + // proceed into the scenario; the worst case is replaying an + // already-complete scenario, which is harmless. + const COMPLETION_CHECK_TIMEOUT_MS = 1500; + const progress = await Promise.race([ + // Swallow the eventual rejection so that when the timeout wins the race + // the still-pending request (which keeps retrying in the background) + // does not surface as an unhandled promise rejection. + getUserDataService().getScenarioProgress(scenarioId).catch(() => null), + new Promise((resolve) => setTimeout(() => resolve(null), COMPLETION_CHECK_TIMEOUT_MS)), + ]); // Return null if no progress or not completed if (!progress?.completedAt) return null; diff --git a/src/pages/campaign-selection.ts b/src/pages/campaign-selection.ts index 48e684b1..1f07e4c4 100644 --- a/src/pages/campaign-selection.ts +++ b/src/pages/campaign-selection.ts @@ -6,7 +6,7 @@ import { Router } from "@app/router"; import { Auth } from "@app/user-account/auth"; import { getUserDataService } from "@app/user-account/user-data-service"; import { getAssetUrl } from "@app/utils/asset-url"; -import { html } from "../engine/utils/development/formatter"; +import { html } from "@app/engine/utils/development/formatter"; import { BasePage } from "./base-page"; import "./campaign-selection.css"; @@ -56,7 +56,7 @@ export class CampaignSelectionPage extends BasePage { private async loadUserDataAndUpdate_(): Promise { try { // Wait for auth to be initialized before trying to load user data - const { App } = await import('../app'); + const { App } = await import('@app/app'); await App.authReady; // Check if user is logged in and update the warning visibility diff --git a/src/pages/layout/body/body.ts b/src/pages/layout/body/body.ts index efab7e1b..19ac7144 100644 --- a/src/pages/layout/body/body.ts +++ b/src/pages/layout/body/body.ts @@ -1,5 +1,5 @@ import { BaseElement } from "@app/components/base-element"; -import { html } from "../../../engine/utils/development/formatter"; +import { html } from "@app/engine/utils/development/formatter"; import './body.css'; /** diff --git a/src/pages/layout/footer/footer.ts b/src/pages/layout/footer/footer.ts index d6f1d332..63bff394 100644 --- a/src/pages/layout/footer/footer.ts +++ b/src/pages/layout/footer/footer.ts @@ -1,6 +1,6 @@ import { BaseElement } from "@app/components/base-element"; import { qs } from "@app/engine/utils/query-selector"; -import { html } from "../../../engine/utils/development/formatter"; +import { html } from "@app/engine/utils/development/formatter"; import './footer.css'; /** diff --git a/src/pages/layout/header/header.ts b/src/pages/layout/header/header.ts index 26e6f46f..0bcfd335 100644 --- a/src/pages/layout/header/header.ts +++ b/src/pages/layout/header/header.ts @@ -13,7 +13,7 @@ import { ModalProfile } from "@app/user-account/modal-profile"; import { isSupabaseApprovedDomain } from "@app/user-account/supabase-client"; import '@app/user-account/user-account.css'; import type { User } from "@supabase/supabase-js"; -import { html } from "../../../engine/utils/development/formatter"; +import { html } from "@app/engine/utils/development/formatter"; import './header.css'; /** diff --git a/src/pages/mission-control/asset-tree-sidebar.ts b/src/pages/mission-control/asset-tree-sidebar.ts index c9ee2993..8ec2fc22 100644 --- a/src/pages/mission-control/asset-tree-sidebar.ts +++ b/src/pages/mission-control/asset-tree-sidebar.ts @@ -1,4 +1,5 @@ import { GroundStation } from "@app/assets/ground-station/ground-station"; +import { WorkingDocumentManager } from '@app/scenarios/working-document-manager'; import { BaseElement } from "@app/components/base-element"; import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; @@ -15,16 +16,16 @@ import { ObjectivesManager } from "@app/objectives"; import { OpsLogModal } from "@app/ops-log/ops-log-modal"; import { ScenarioManager } from "@app/scenario-manager"; import { SimulationManager } from "@app/simulation/simulation-manager"; -import activityPng from '../../assets/icons/activity.png'; -import antennaPng from '../../assets/icons/antenna.png'; -import checklistPng from "../../assets/icons/checklist.png"; -import dashboardPng from '../../assets/icons/dashboard.png'; -import historyPng from '../../assets/icons/history.png'; -import layoutSidebarLeftCollapsePng from '../../assets/icons/layout-sidebar-left-collapse.png'; -import layoutSidebarLeftExpandPng from '../../assets/icons/layout-sidebar-left-expand.png'; -import satelliteOffPng from '../../assets/icons/satellite-off.png'; -import satellitePng from '../../assets/icons/satellite.png'; -import targetArrowPng from '../../assets/icons/target-arrow.png'; +import activityPng from '@app/assets/icons/activity.png'; +import antennaPng from '@app/assets/icons/antenna.png'; +import checklistPng from "@app/assets/icons/checklist.png"; +import dashboardPng from '@app/assets/icons/dashboard.png'; +import historyPng from '@app/assets/icons/history.png'; +import layoutSidebarLeftCollapsePng from '@app/assets/icons/layout-sidebar-left-collapse.png'; +import layoutSidebarLeftExpandPng from '@app/assets/icons/layout-sidebar-left-expand.png'; +import satelliteOffPng from '@app/assets/icons/satellite-off.png'; +import satellitePng from '@app/assets/icons/satellite.png'; +import targetArrowPng from '@app/assets/icons/target-arrow.png'; import './asset-tree-sidebar.css'; /** @@ -184,6 +185,21 @@ export class AssetTreeSidebar extends BaseElement { this.addChecklistListener_(); this.addDialogHistoryListener_(); this.addOpsLogListener_(); + this.addWorkingDocListener_(); + } + + private addWorkingDocListener_(): void { + const btn = qs('.working-doc-icon', this.dom_); + if (!btn) return; + + // Only surface the icon for scenarios that declare a working document + if (WorkingDocumentManager.isEnabled()) { + (btn as HTMLElement).style.display = ''; + } + + btn.addEventListener('click', () => { + WorkingDocumentManager.getInstance().open(); + }); } private addMissionBriefListener_(): void { @@ -367,6 +383,12 @@ export class AssetTreeSidebar extends BaseElement { Checklist + Dialog History diff --git a/src/pages/mission-control/mission-control-page.ts b/src/pages/mission-control/mission-control-page.ts index bb703816..b8713eb9 100644 --- a/src/pages/mission-control/mission-control-page.ts +++ b/src/pages/mission-control/mission-control-page.ts @@ -11,13 +11,16 @@ import { ObjectivesManager } from "@app/objectives/objectives-manager"; import { NavigationOptions } from "@app/router"; import { ScenarioManager } from "@app/scenario-manager"; import { ScenarioDialogManager } from "@app/scenarios/scenario-dialog-manager"; +import { WorkingDocumentManager } from "@app/scenarios/working-document-manager"; +import { InterferenceManager } from "@app/interference/interference-manager"; +import { WeatherManager } from "@app/weather/weather-manager"; import { AlarmService } from "@app/services/alarm-service"; import { SimulationManager } from "@app/simulation/simulation-manager"; import { syncEquipmentWithStore } from "@app/sync"; import { AppState, syncManager } from "@app/sync/storage"; import { Auth } from "@app/user-account/auth"; -import { BasePage } from "../base-page"; -import { Body } from "../layout/body/body"; +import { BasePage } from "@app/pages/base-page"; +import { Body } from "@app/pages/layout/body/body"; import { AssetTreeSidebar } from "./asset-tree-sidebar"; import { GlobalCommandBar } from "./global-command-bar"; import './mission-control-page.css'; @@ -282,6 +285,9 @@ export class MissionControlPage extends BasePage { SimulationManager.destroy(); ObjectivesManager.destroy(); ScenarioDialogManager.reset(); + WorkingDocumentManager.reset(); + WeatherManager.destroy(); + InterferenceManager.destroy(); QuizModal.destroy(); PendingQuizIndicator.destroy(); EventBus.destroy(); diff --git a/src/pages/mission-control/tabbed-canvas.ts b/src/pages/mission-control/tabbed-canvas.ts index 2fa730d7..5442da41 100644 --- a/src/pages/mission-control/tabbed-canvas.ts +++ b/src/pages/mission-control/tabbed-canvas.ts @@ -11,13 +11,13 @@ import { EventBus } from "@app/events/event-bus"; import { Events } from "@app/events/events"; import { SimulationManager } from "@app/simulation/simulation-manager"; import './tabbed-canvas.css'; -import { ACUControlTab } from './tabs/acu-control-tab'; -import { DashboardTab } from './tabs/dashboard-tab'; -import { GPSTimingTab } from './tabs/gps-timing-tab'; -import { MissionOverviewTab } from './tabs/mission-overview-tab'; -import { RxAnalysisTab } from './tabs/rx-analysis-tab'; -import { SatelliteDashboardTab } from './tabs/satellite-dashboard-tab'; -import { TxChainTab } from './tabs/tx-chain-tab'; +import { ACUControlTab } from '@app/pages/mission-control/tabs/acu-control-tab'; +import { DashboardTab } from '@app/pages/mission-control/tabs/dashboard-tab'; +import { GPSTimingTab } from '@app/pages/mission-control/tabs/gps-timing-tab'; +import { MissionOverviewTab } from '@app/pages/mission-control/tabs/mission-overview-tab'; +import { RxAnalysisTab } from '@app/pages/mission-control/tabs/rx-analysis-tab'; +import { SatelliteDashboardTab } from '@app/pages/mission-control/tabs/satellite-dashboard-tab'; +import { TxChainTab } from '@app/pages/mission-control/tabs/tx-chain-tab'; /** * TabbedCanvas - Dynamic tabbed interface for ground station equipment diff --git a/src/pages/mission-control/tabs/acu-control-tab.ts b/src/pages/mission-control/tabs/acu-control-tab.ts index a5fa9384..037cfdaf 100644 --- a/src/pages/mission-control/tabs/acu-control-tab.ts +++ b/src/pages/mission-control/tabs/acu-control-tab.ts @@ -719,11 +719,18 @@ export class ACUControlTab extends BaseElement { if (modelEl) modelEl.textContent = state.acuModel; if (serialEl) serialEl.textContent = `(${state.acuSerialNumber})`; - // Sync tracking mode buttons + // Sync tracking mode buttons. Under an ACU automation fault the + // program-track button is disabled - the automation processor is down; + // servos (manual/stow/maintenance) still work. + const acuFaulted = state.isAcuAutomationFaulted === true; const modeButtons = this.qsa_('btn-tracking'); modeButtons.forEach(btn => { const mode = (btn as HTMLElement).dataset.mode; btn.classList.toggle('active', mode === state.trackingMode); + if (mode === 'program-track') { + (btn as HTMLButtonElement).disabled = acuFaulted; + btn.classList.toggle('disabled', acuFaulted); + } }); // Show/hide tracking sections based on mode @@ -861,10 +868,19 @@ export class ACUControlTab extends BaseElement { } } - // Sync move-to-target button disabled state + // Sync move-to-target button disabled state (also disabled under ACU fault) const moveToTargetBtn = this.qs_('move-to-target-btn'); if (moveToTargetBtn) { - moveToTargetBtn.disabled = state.trackingMode !== 'program-track' || state.targetSatelliteId === null; + moveToTargetBtn.disabled = acuFaulted || + state.trackingMode !== 'program-track' || state.targetSatelliteId === null; + } + const satelliteSelectEl = this.qs_('satellite-select'); + if (satelliteSelectEl) { + satelliteSelectEl.disabled = acuFaulted; + } + const stepTrackToggleEl = this.qs_('step-track-toggle'); + if (stepTrackToggleEl) { + stepTrackToggleEl.disabled = acuFaulted; } // Sync satellite dropdown selection (skip if user is interacting) diff --git a/src/pages/mission-control/tabs/dashboard-tab.ts b/src/pages/mission-control/tabs/dashboard-tab.ts index b56f2cd3..7f3ace97 100644 --- a/src/pages/mission-control/tabs/dashboard-tab.ts +++ b/src/pages/mission-control/tabs/dashboard-tab.ts @@ -4,10 +4,10 @@ import { html } from "@app/engine/utils/development/formatter"; import { qs } from "@app/engine/utils/query-selector"; import { EventBus } from "@app/events/event-bus"; import { Events } from "@app/events/events"; -import antennaPng from '../../../assets/icons/antenna.png'; -import modemPng from '../../../assets/icons/radio.png'; -import receiverPng from '../../../assets/icons/arrow-big-down-lines.png'; -import transmitterPng from '../../../assets/icons/arrow-big-up-lines.png'; +import antennaPng from '@app/assets/icons/antenna.png'; +import modemPng from '@app/assets/icons/radio.png'; +import receiverPng from '@app/assets/icons/arrow-big-down-lines.png'; +import transmitterPng from '@app/assets/icons/arrow-big-up-lines.png'; import './dashboard-tab.css'; interface AlarmEntry { diff --git a/src/pages/mission-control/tabs/mission-overview-tab.ts b/src/pages/mission-control/tabs/mission-overview-tab.ts index 63f853c4..af1fb3aa 100644 --- a/src/pages/mission-control/tabs/mission-overview-tab.ts +++ b/src/pages/mission-control/tabs/mission-overview-tab.ts @@ -6,8 +6,8 @@ import { Satellite } from "@app/equipment/satellite/satellite"; import { EventBus } from "@app/events/event-bus"; import { Events } from "@app/events/events"; import { SimulationManager } from "@app/simulation/simulation-manager"; -import antennaPng from '../../../assets/icons/antenna.png'; -import satellitePng from '../../../assets/icons/satellite.png'; +import antennaPng from '@app/assets/icons/antenna.png'; +import satellitePng from '@app/assets/icons/satellite.png'; import './mission-overview-tab.css'; /** diff --git a/src/pages/mission-control/tabs/satellite-dashboard-tab.ts b/src/pages/mission-control/tabs/satellite-dashboard-tab.ts index 73f504f2..beb5df3c 100644 --- a/src/pages/mission-control/tabs/satellite-dashboard-tab.ts +++ b/src/pages/mission-control/tabs/satellite-dashboard-tab.ts @@ -7,7 +7,7 @@ import { Events } from "@app/events/events"; import { ScenarioManager } from "@app/scenario-manager"; import { SimulationManager } from "@app/simulation/simulation-manager"; import { TrafficControlManager } from "@app/traffic/traffic-control-manager"; -import satellitePng from '../../../assets/icons/satellite.png'; +import satellitePng from '@app/assets/icons/satellite.png'; import './satellite-dashboard-tab.css'; /** diff --git a/src/pages/sandbox-page.ts b/src/pages/sandbox-page.ts index e5240653..2886e71c 100644 --- a/src/pages/sandbox-page.ts +++ b/src/pages/sandbox-page.ts @@ -5,15 +5,18 @@ import { Logger } from "@app/logging/logger"; import { NavigationOptions } from "@app/router"; import { ScenarioManager } from "@app/scenario-manager"; import { ScenarioDialogManager } from "@app/scenarios/scenario-dialog-manager"; +import { WorkingDocumentManager } from "@app/scenarios/working-document-manager"; +import { InterferenceManager } from "@app/interference/interference-manager"; +import { WeatherManager } from "@app/weather/weather-manager"; import { SimulationManager } from "@app/simulation/simulation-manager"; import { QuizModal } from "@app/modal/quiz-modal"; import { AppState, syncManager } from "@app/sync/storage"; import { ObjectivesManager } from "@app/objectives/objectives-manager"; -import { html } from "../engine/utils/development/formatter"; -import { clearPersistedStore, syncEquipmentWithStore } from '../sync/storage'; +import { html } from "@app/engine/utils/development/formatter"; +import { clearPersistedStore, syncEquipmentWithStore } from '@app/sync/storage'; import { BasePage } from "./base-page"; -import { Body } from "./layout/body/body"; -import { Equipment } from './sandbox/equipment'; +import { Body } from "@app/pages/layout/body/body"; +import { Equipment } from '@app/pages/sandbox/equipment'; /** * Student page implementation @@ -174,6 +177,9 @@ export class SandboxPage extends BasePage { SimulationManager.destroy(); ObjectivesManager.destroy(); ScenarioDialogManager.reset(); + WorkingDocumentManager.reset(); + WeatherManager.destroy(); + InterferenceManager.destroy(); QuizModal.destroy(); EventBus.destroy(); const container = getEl(SandboxPage.containerId); diff --git a/src/pages/sandbox/equipment.ts b/src/pages/sandbox/equipment.ts index e8e7d256..44fb3649 100644 --- a/src/pages/sandbox/equipment.ts +++ b/src/pages/sandbox/equipment.ts @@ -11,11 +11,11 @@ import { DraggableHtmlBox } from "@app/modal/draggable-html-box"; import { ObjectivesManager } from "@app/objectives"; import { ScenarioManager, SimulationSettings } from "@app/scenario-manager"; import { SimulationManager } from "@app/simulation/simulation-manager"; -import { html } from "../../engine/utils/development/formatter"; -import { RealTimeSpectrumAnalyzer } from '../../equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; -import { Receiver } from '../../equipment/receiver/receiver'; -import { Transmitter } from '../../equipment/transmitter/transmitter'; -import { SandboxPage } from "../sandbox-page"; +import { html } from "@app/engine/utils/development/formatter"; +import { RealTimeSpectrumAnalyzer } from '@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; +import { Receiver } from '@app/equipment/receiver/receiver'; +import { Transmitter } from '@app/equipment/transmitter/transmitter'; +import { SandboxPage } from "@app/pages/sandbox-page"; import './equipment.css'; /** diff --git a/src/pages/scenario-selection.ts b/src/pages/scenario-selection.ts index 83bb8969..8fbfa51a 100644 --- a/src/pages/scenario-selection.ts +++ b/src/pages/scenario-selection.ts @@ -9,7 +9,7 @@ import { ScenarioData } from '@app/ScenarioData'; import { clearPersistedStore } from "@app/sync/storage"; import { getUserDataService } from "@app/user-account/user-data-service"; import { getAssetUrl } from "@app/utils/asset-url"; -import { html } from "../engine/utils/development/formatter"; +import { html } from "@app/engine/utils/development/formatter"; import { BasePage } from "./base-page"; import "./scenario-selection.css"; @@ -96,7 +96,7 @@ export class ScenarioSelectionPage extends BasePage { private async loadCheckpointsAndUpdate_(): Promise { try { // Wait for auth to be initialized before trying to load user data - const { App } = await import('../app'); + const { App } = await import('@app/app'); await App.authReady; const userDataService = getUserDataService(); diff --git a/src/router.ts b/src/router.ts index 2979a2f1..5d9a6171 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,15 +1,15 @@ -import { CampaignManager } from "./campaigns/campaign-manager"; -import { ccsCampaignData, geolocationCampaignData, hamSdrCampaignData, natsCampaignData, natsEuCampaignData } from "./campaigns/nats/campaign-data"; -import { EventBus } from "./events/event-bus"; -import { Events } from "./events/events"; -import { CampaignSelectionPage } from "./pages/campaign-selection"; -import { Footer } from "./pages/layout/footer/footer"; -import { Header } from "./pages/layout/header/header"; -import { MissionControlPage } from "./pages/mission-control/mission-control-page"; -import { SandboxPage } from "./pages/sandbox-page"; -import { ScenarioSelectionPage } from "./pages/scenario-selection"; +import { CampaignManager } from "@app/campaigns/campaign-manager"; +import { ccsCampaignData, geolocationCampaignData, hamSdrCampaignData, natsCampaignData, natsEuCampaignData } from "@app/campaigns/nats/campaign-data"; +import { EventBus } from "@app/events/event-bus"; +import { Events } from "@app/events/events"; +import { CampaignSelectionPage } from "@app/pages/campaign-selection"; +import { Footer } from "@app/pages/layout/footer/footer"; +import { Header } from "@app/pages/layout/header/header"; +import { MissionControlPage } from "@app/pages/mission-control/mission-control-page"; +import { SandboxPage } from "@app/pages/sandbox-page"; +import { ScenarioSelectionPage } from "@app/pages/scenario-selection"; import { ScenarioManager } from "./scenario-manager"; -import { SimulationManager } from "./simulation/simulation-manager"; +import { SimulationManager } from "@app/simulation/simulation-manager"; /** * Navigation options for router diff --git a/src/scenario-manager.ts b/src/scenario-manager.ts index 0cc5336b..ddd80c4e 100644 --- a/src/scenario-manager.ts +++ b/src/scenario-manager.ts @@ -1,32 +1,48 @@ -import { GroundStationConfig } from './assets/ground-station/ground-station-state'; -import { PreviousShiftLogEntry } from './ops-log/ops-log-types'; -import { scenario1Data } from './campaigns/nats/scenario1'; -import { scenario2Data } from "./campaigns/nats/scenario2"; -import { scenario3Data } from './campaigns/nats/scenario3'; -import { scenario4Data } from './campaigns/nats/scenario4'; -import { scenario5Data } from './campaigns/nats/scenario5'; -import { scenario6Data } from './campaigns/nats/scenario6'; -import { scenario7Data } from './campaigns/nats/scenario7'; -import { scenario8Data } from './campaigns/nats/scenario8'; -import { sandboxData as natsSandboxData } from './campaigns/nats/sandbox'; -import { AntennaState } from './equipment/antenna'; -import { ANTENNA_CONFIG_KEYS } from "./equipment/antenna/antenna-config-keys"; -import { defaultSpectrumAnalyzerState } from './equipment/real-time-spectrum-analyzer/defaultSpectrumAnalyzerState'; -import { RealTimeSpectrumAnalyzerState } from './equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; -import { Receiver, ReceiverState } from './equipment/receiver/receiver'; -import { BUCModuleCore } from './equipment/rf-front-end/buc-module'; -import { CouplerModule } from './equipment/rf-front-end/coupler-module/coupler-module'; -import { IfFilterBankModuleCore } from './equipment/rf-front-end/filter-module'; -import { defaultGpsdoState } from './equipment/rf-front-end/gpsdo-module/gpsdo-state'; -import { HPAModuleCore } from './equipment/rf-front-end/hpa-module'; -import { LNBModuleCore } from './equipment/rf-front-end/lnb-module'; -import { OMTModule } from './equipment/rf-front-end/omt-module/omt-module'; -import { RFFrontEndState } from './equipment/rf-front-end/rf-front-end-core'; -import { Satellite } from './equipment/satellite/satellite'; -import { Transmitter, TransmitterState } from './equipment/transmitter/transmitter'; -import { Character, Emotion } from './modal/character-enum'; +import { GroundStationConfig } from '@app/assets/ground-station/ground-station-state'; +import { PreviousShiftLogEntry } from '@app/ops-log/ops-log-types'; +import { scenario1Data } from '@app/campaigns/nats/scenario1'; +import { scenario2Data } from "@app/campaigns/nats/scenario2"; +import { scenario3Data } from '@app/campaigns/nats/scenario3'; +import { scenario4Data } from '@app/campaigns/nats/scenario4'; +import { scenario5Data } from '@app/campaigns/nats/scenario5'; +import { scenario6Data } from '@app/campaigns/nats/scenario6'; +import { scenario7Data } from '@app/campaigns/nats/scenario7'; +import { scenario8Data } from '@app/campaigns/nats/scenario8'; +import { scenario9Data } from '@app/campaigns/nats/scenario9'; +import { scenario10Data } from '@app/campaigns/nats/scenario10'; +import { scenario11Data } from '@app/campaigns/nats/scenario11'; +import { scenario12Data } from '@app/campaigns/nats/scenario12'; +import { scenario13Data } from '@app/campaigns/nats/scenario13'; +import { scenario14Data } from '@app/campaigns/nats/scenario14'; +import { scenario15Data } from '@app/campaigns/nats/scenario15'; +import { scenario16Data } from '@app/campaigns/nats/scenario16'; +import { scenario17Data } from '@app/campaigns/nats/scenario17'; +import { scenario18Data } from '@app/campaigns/nats/scenario18'; +import { scenario19Data } from '@app/campaigns/nats/scenario19'; +import { scenario20Data } from '@app/campaigns/nats/scenario20'; +import { scenario21Data } from '@app/campaigns/nats/scenario21'; +import { scenario22Data } from '@app/campaigns/nats/scenario22'; +import { scenario23Data } from '@app/campaigns/nats/scenario23'; +import { scenario24Data } from '@app/campaigns/nats/scenario24'; +import { sandboxData as natsSandboxData } from '@app/campaigns/nats/sandbox'; +import { AntennaState } from '@app/equipment/antenna'; +import { ANTENNA_CONFIG_KEYS } from "@app/equipment/antenna/antenna-config-keys"; +import { defaultSpectrumAnalyzerState } from '@app/equipment/real-time-spectrum-analyzer/defaultSpectrumAnalyzerState'; +import { RealTimeSpectrumAnalyzerState } from '@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; +import { Receiver, ReceiverState } from '@app/equipment/receiver/receiver'; +import { BUCModuleCore } from '@app/equipment/rf-front-end/buc-module'; +import { CouplerModule } from '@app/equipment/rf-front-end/coupler-module/coupler-module'; +import { IfFilterBankModuleCore } from '@app/equipment/rf-front-end/filter-module'; +import { defaultGpsdoState } from '@app/equipment/rf-front-end/gpsdo-module/gpsdo-state'; +import { HPAModuleCore } from '@app/equipment/rf-front-end/hpa-module'; +import { LNBModuleCore } from '@app/equipment/rf-front-end/lnb-module'; +import { OMTModule } from '@app/equipment/rf-front-end/omt-module/omt-module'; +import { RFFrontEndState } from '@app/equipment/rf-front-end/rf-front-end-core'; +import { Satellite } from '@app/equipment/satellite/satellite'; +import { Transmitter, TransmitterState } from '@app/equipment/transmitter/transmitter'; +import { Character, Emotion } from '@app/modal/character-enum'; import { ScenarioData } from './ScenarioData'; -import { sandboxData } from './scenarios/sandbox'; +import { sandboxData } from '@app/scenarios/sandbox'; declare global { interface Window { @@ -58,7 +74,7 @@ export interface SimulationSettings { weatherEvents?: Array<{ id: string; groundStationId: string; - type: "snow" | "rain" | "fog" | "wind" | "dust" | "hail" | "ice" | "storm"; + type: "snow" | "rain" | "fog" | "wind" | "dust" | "hail" | "ice" | "storm" | "sun-transit"; severity: "minor" | "moderate" | "severe"; /** Seconds since mission start */ startTime: number; @@ -67,6 +83,32 @@ export interface SimulationSettings { /** dB degradation to link margin */ linkMarginDegradation: number; }>; + /** Scheduled, duty-cycled RF interference injected at a satellite's + * transponder (relayed to all stations - uplink interference). */ + interferenceEvents?: Array<{ + id: string; + satelliteNoradId: number; + /** Interferer RF center frequency (uplink, Hz) */ + frequency: number; + bandwidth: number; + /** Power at the transponder input (dBm) */ + power: number; + polarization: 'H' | 'V'; + /** Seconds since mission start when the envelope opens */ + startTime: number; + /** Envelope duration (s); on/off windows repeat inside it */ + duration: number; + /** Window cycle period (s) */ + periodSeconds: number; + /** Transmit-on seconds per period */ + onSeconds: number; + }>; + /** Working Document panel: an in-scenario document that accumulates a line + * per passed quiz whose condition declares params.documentLine. */ + workingDocument?: { + title: string; + description?: string; + }; /** Traffic ownership configuration for handover scenarios */ trafficOwnership?: Array<{ /** Satellite NORAD ID */ @@ -156,6 +198,22 @@ export const SCENARIOS: ScenarioData[] = [ scenario6Data, scenario7Data, scenario8Data, + scenario9Data, + scenario10Data, + scenario11Data, + scenario12Data, + scenario13Data, + scenario14Data, + scenario15Data, + scenario16Data, + scenario17Data, + scenario18Data, + scenario19Data, + scenario20Data, + scenario21Data, + scenario22Data, + scenario23Data, + scenario24Data, ]; export function isScenarioLocked(scenario: ScenarioData, completedScenarioIds: string[]): boolean { diff --git a/src/scenarios/sandbox.ts b/src/scenarios/sandbox.ts index 3c09bb9c..5a22499d 100644 --- a/src/scenarios/sandbox.ts +++ b/src/scenarios/sandbox.ts @@ -2,9 +2,9 @@ import type { AntennaState } from '@app/equipment/antenna'; import type { ScenarioData } from '@app/ScenarioData'; import type { dB, dBm, Hertz, MHz } from '@app/types'; import type { Degrees } from 'ootk'; -import { createRfFrontEnd } from '../campaigns/rf-front-end-factory'; -import { vermontGroundStation } from '../campaigns/nats/ground-stations'; -import { aurora7Satellite, tidemark1Satellite } from '../campaigns/nats/satellites'; +import { createRfFrontEnd } from '@app/campaigns/rf-front-end-factory'; +import { vermontGroundStation } from '@app/campaigns/nats/ground-stations'; +import { aurora7Satellite, tidemark1Satellite } from '@app/campaigns/nats/satellites'; /** * Sandbox Mode diff --git a/src/scenarios/working-document-manager.ts b/src/scenarios/working-document-manager.ts new file mode 100644 index 00000000..f0ddf201 --- /dev/null +++ b/src/scenarios/working-document-manager.ts @@ -0,0 +1,141 @@ +/** + * @file WorkingDocumentManager - Accumulating in-scenario document panel + * @description Builds a visible operator document (quick-reference card, + * impact report, incident timeline) line-by-line as the player answers + * status-check quizzes correctly. A status-check condition opts in by + * setting `params.documentLine` (and optionally `params.documentSection`); + * the line is appended to the document when QUIZ_PASSED fires for it. + * + * Scenarios enable the panel via `settings.workingDocument: { title, description? }`. + */ + +import { EventBus } from '@app/events/event-bus'; +import { Events, QuizPassedData } from '@app/events/events'; +import { DraggableHtmlBox } from '@app/modal/draggable-html-box'; +import { ScenarioManager } from '@app/scenario-manager'; + +interface DocumentEntry { + section: string; + line: string; +} + +const DEFAULT_SECTION = 'Notes'; + +export class WorkingDocumentManager { + private static instance_: WorkingDocumentManager | null = null; + + private entries_: DocumentEntry[] = []; + private box_: DraggableHtmlBox | null = null; + private readonly boundQuizPassedHandler_: (data: QuizPassedData) => void; + + private constructor() { + this.boundQuizPassedHandler_ = this.handleQuizPassed_.bind(this); + } + + static getInstance(): WorkingDocumentManager { + this.instance_ ??= new WorkingDocumentManager(); + return this.instance_; + } + + /** Whether the current scenario uses the working document panel */ + static isEnabled(): boolean { + return !!ScenarioManager.getInstance().settings.workingDocument; + } + + /** Call once per scenario load (after ScenarioManager is ready). */ + initialize(): void { + this.entries_ = []; + this.box_ = null; + EventBus.getInstance().off(Events.QUIZ_PASSED, this.boundQuizPassedHandler_); + if (WorkingDocumentManager.isEnabled()) { + EventBus.getInstance().on(Events.QUIZ_PASSED, this.boundQuizPassedHandler_); + } + } + + /** Open (or create) the document panel. */ + open(): void { + if (!WorkingDocumentManager.isEnabled()) return; + this.ensureBox_(); + this.render_(); + this.box_?.open(); + } + + /** Number of lines accumulated so far (used by completeness checks). */ + getEntryCount(): number { + return this.entries_.length; + } + + private handleQuizPassed_(data: QuizPassedData): void { + const scenario = ScenarioManager.getInstance().data; + const objective = scenario.objectives?.find(o => o.id === data.objectiveId); + const condition = objective?.conditions?.[data.conditionIndex]; + const line = condition?.params?.documentLine; + if (!line) return; + + const section = condition?.params?.documentSection ?? DEFAULT_SECTION; + + // Idempotence: a re-fired event for the same line must not duplicate it + if (this.entries_.some(e => e.line === line && e.section === section)) return; + + const isFirstEntry = this.entries_.length === 0; + this.entries_.push({ section, line }); + + this.ensureBox_(); + this.render_(); + + // Auto-show on the first entry so the player sees the document begin to + // build; afterwards only update content and let them manage the window. + if (isFirstEntry) { + this.box_?.open(); + } + } + + private ensureBox_(): void { + if (this.box_) return; + const config = ScenarioManager.getInstance().settings.workingDocument; + this.box_ = new DraggableHtmlBox(config?.title ?? 'Working Document', 'working-document', '', 'app-shell-page'); + } + + private render_(): void { + if (!this.box_) return; + const config = ScenarioManager.getInstance().settings.workingDocument; + + // Group entries by section, preserving first-appearance order + const sections = new Map(); + for (const entry of this.entries_) { + const lines = sections.get(entry.section) ?? []; + lines.push(entry.line); + sections.set(entry.section, lines); + } + + const sectionsHtml = [...sections.entries()].map(([section, lines]) => ` +
+
${section}
+
    + ${lines.map(l => `
  • ✓ ${l}
  • `).join('')} +
+
+ `).join(''); + + const emptyHtml = ` +

+ (Document is empty - entries are added as you make the calls during the shift.) +

+ `; + + this.box_.updateContent(` +
+ ${config?.description ? `

${config.description}

` : ''} + ${this.entries_.length > 0 ? sectionsHtml : emptyHtml} +
${this.entries_.length} entr${this.entries_.length === 1 ? 'y' : 'ies'}
+
+ `); + } + + static reset(): void { + if (this.instance_) { + EventBus.getInstance().off(Events.QUIZ_PASSED, this.instance_.boundQuizPassedHandler_); + this.instance_ = null; + } + } +} diff --git a/src/simulation/default-signal-data.ts b/src/simulation/default-signal-data.ts index 75357e5e..826a72ba 100644 --- a/src/simulation/default-signal-data.ts +++ b/src/simulation/default-signal-data.ts @@ -1,6 +1,6 @@ // @ts-nocheck -import { FECType, Hertz, ModulationType, RfFrequency, RfSignal } from "../types"; +import { FECType, Hertz, ModulationType, RfFrequency, RfSignal } from "@app/types"; const meteoSat9Signals: RfSignal[] = [ { diff --git a/src/simulation/simulation-manager.ts b/src/simulation/simulation-manager.ts index fe21a8df..93efe4f7 100644 --- a/src/simulation/simulation-manager.ts +++ b/src/simulation/simulation-manager.ts @@ -14,7 +14,7 @@ import { ScenarioManager } from '@app/scenario-manager'; import { ProgressSaveManager } from '@app/user-account/progress-save-manager'; import { UserDataService } from '@app/user-account/user-data-service'; import { Degrees, Milliseconds } from 'ootk'; -import { RfSignal } from './../types'; +import { RfSignal } from '@app/types'; export class SimulationManager { private static instance_: SimulationManager; @@ -50,7 +50,14 @@ export class SimulationManager { this.satelliteSignals = this.satellites.flatMap(sat => sat.txSignal); this.lastFrameTime = Date.now(); - this.gameLoop_(); + // Start the loop on the NEXT frame, not synchronously. Running update() + // inside the constructor emits Events.UPDATE before getInstance() has + // assigned `instance_`; any condition/handler that calls + // SimulationManager.getInstance() during that first tick would re-enter + // this constructor and recurse infinitely (Maximum call stack size + // exceeded). Deferring to requestAnimationFrame lets the constructor + // return and `instance_` be assigned before the first tick fires. + this.animationFrameId_ = requestAnimationFrame(this.gameLoop_.bind(this)); } static getInstance(): SimulationManager { diff --git a/src/sync/storage.ts b/src/sync/storage.ts index bb7f76b6..d3d5a77b 100644 --- a/src/sync/storage.ts +++ b/src/sync/storage.ts @@ -7,9 +7,9 @@ import { GroundStation } from '@app/assets/ground-station/ground-station'; import { SimulationManager } from '@app/simulation/simulation-manager'; -import { EventBus } from '../events/event-bus'; -import { Events } from '../events/events'; -import type { Equipment } from '../pages/sandbox/equipment'; +import { EventBus } from '@app/events/event-bus'; +import { Events } from '@app/events/events'; +import type { Equipment } from '@app/pages/sandbox/equipment'; import { StorageProviderFactory, StorageProviderType } from './storage-provider-factory'; import { SyncManager, type AppState } from './sync-manager'; import './webpack-hot-module'; diff --git a/src/sync/sync-manager.ts b/src/sync/sync-manager.ts index a390c6f2..92c30943 100644 --- a/src/sync/sync-manager.ts +++ b/src/sync/sync-manager.ts @@ -4,13 +4,13 @@ import { AntennaState } from '@app/equipment/antenna'; import { RFFrontEndState } from '@app/equipment/rf-front-end/rf-front-end-core'; import { ObjectiveState } from '@app/objectives'; import { SimulationManager } from '@app/simulation/simulation-manager'; -import { RealTimeSpectrumAnalyzerState } from '../equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; -import { ReceiverState } from '../equipment/receiver/receiver'; -import { TransmitterState } from '../equipment/transmitter/transmitter'; -import { Logger } from '../logging/logger'; -import { OpsLogManager } from '../ops-log/ops-log-manager'; -import { OpsLogState } from '../ops-log/ops-log-types'; -import type { Equipment } from '../pages/sandbox/equipment'; +import { RealTimeSpectrumAnalyzerState } from '@app/equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer'; +import { ReceiverState } from '@app/equipment/receiver/receiver'; +import { TransmitterState } from '@app/equipment/transmitter/transmitter'; +import { Logger } from '@app/logging/logger'; +import { OpsLogManager } from '@app/ops-log/ops-log-manager'; +import { OpsLogState } from '@app/ops-log/ops-log-types'; +import type { Equipment } from '@app/pages/sandbox/equipment'; import type { StorageProvider } from './storage-provider'; /** diff --git a/src/user-account/supabase-client.ts b/src/user-account/supabase-client.ts index 46125362..d9b96f89 100644 --- a/src/user-account/supabase-client.ts +++ b/src/user-account/supabase-client.ts @@ -5,6 +5,7 @@ export const SUPABASE_ANON_KEY = process.env.PUBLIC_SUPABASE_ANON_KEY; export const isSupabaseApprovedDomain = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || window.location.hostname.endsWith('signalrange.space') || window.location.hostname.endsWith('keeptrack.space') || window.location.hostname.endsWith('workers.dev'); diff --git a/src/weather/weather-manager.ts b/src/weather/weather-manager.ts index d60d0d53..6363b690 100644 --- a/src/weather/weather-manager.ts +++ b/src/weather/weather-manager.ts @@ -98,6 +98,43 @@ export class WeatherManager { // Update ice accumulation for all antennas this.updateIceAccumulation_(dtSeconds); + + // Update sun-transit sky-noise degradation for all antennas + this.updateSunTransit_(elapsedSeconds); + } + + /** + * Apply sun-transit sky-noise degradation to affected ground stations. + * + * The Sun crossing the antenna boresight raises the noise floor following a + * smooth rise-peak-fall profile: sin^2(pi * progress) scaled by the event's + * linkMarginDegradation (peak dB). Degradation is RX-only and clears + * automatically when the event ends - there is no operator mitigation, by + * design: the training point is to anticipate, ride through, and document. + */ + private updateSunTransit_(elapsedSeconds: number): void { + const sim = SimulationManager.getInstance(); + + for (const gs of sim.groundStations) { + const sunEvent = this.weatherEvents_.find(e => + e.groundStationId === gs.state.id && + e.type === 'sun-transit' && + e.isActive + ); + + let degradation = 0; + if (sunEvent) { + const progress = (elapsedSeconds - sunEvent.startTime) / sunEvent.duration; + const profile = Math.sin(Math.PI * Math.min(1, Math.max(0, progress))) ** 2; + degradation = sunEvent.linkMarginDegradation * profile; + } + + for (const antenna of gs.antennas) { + if (antenna.state.skyNoiseDegradation_dB !== degradation) { + antenna.updateSkyNoiseDegradation(degradation); + } + } + } } /** Check and update which weather events are active */ diff --git a/test/app.test.ts b/test/app.test.ts index 4151bb68..0dde262b 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -2,6 +2,7 @@ import { vi } from 'vitest'; import { App } from "../src/app"; import { EventBus } from "../src/events/event-bus"; import { Events } from "../src/events/events"; +import { SimulationManager } from "../src/simulation/simulation-manager"; // Tests for App class @@ -19,24 +20,24 @@ describe('App class', () => { // Ensure a clean DOM root for BaseElement.init_ calls document.body.innerHTML = '
'; - // Prevent requestAnimationFrame from scheduling the loop repeatedly - // App.gameLoop_ calls requestAnimationFrame at the end; we stub it so it doesn't call the callback. - // The first frame still runs because App.create() calls gameLoop_() synchronously. - // @ts-ignore - global.requestAnimationFrame = vi.fn(() => 1); + // The game loop now lives in SimulationManager, which defers its first tick + // to requestAnimationFrame (see SimulationManager constructor). Stub rAF so it + // records the scheduled callback but never auto-runs it - tests drive the tick + // manually. Use vi.stubGlobal (not `global.x =`) so the stub overrides the + // binding jsdom module code actually resolves against. + vi.stubGlobal('requestAnimationFrame', vi.fn(() => 1)); }); afterEach(() => { // Clear module registry so singletons are reset between tests vi.resetModules(); - // @ts-ignore - global.requestAnimationFrame = undefined; + vi.unstubAllGlobals(); // Clear any global set by App // @ts-ignore delete (global as any).signalRange; }); - it('create() should instantiate App, set window.signalRange and emit UPDATE and DRAW once', () => { + it('create() instantiates App, sets window.signalRange, and emits ROUTE_CHANGED', () => { const emitSpy = vi.spyOn(EventBus.getInstance(), 'emit'); const app = App.create(); @@ -45,9 +46,39 @@ describe('App class', () => { // @ts-ignore expect((global as any).signalRange).toBe(app); - // EventBus.emit should have been called for UPDATE and DRAW during the first gameLoop_ run + // create() wires up the app and routes to the initial page. The game loop is + // NOT running after create(): SimulationManager is constructed during init but + // immediately torn down when the router navigates to campaign selection + // (Router.showPage -> SimulationManager.destroy). UPDATE/DRAW only flow once a + // scenario page spins up its own SimulationManager - see the game-loop test below. const calledEvents = emitSpy.mock.calls.map((c: any[]) => c[0]); + expect(calledEvents).toContain(Events.ROUTE_CHANGED); + + emitSpy.mockRestore(); + }); + + it('SimulationManager game loop emits UPDATE and DRAW on a tick', () => { + // Capture the most recently scheduled frame so we can drive ticks manually. + let frame: FrameRequestCallback | undefined; + vi.stubGlobal('requestAnimationFrame', vi.fn((cb: FrameRequestCallback) => { + frame = cb; + return 1; + })); + // App.create() sets window.signalRange (required by SimulationManager.getInstance) + // and then the router destroys the init-time simulation. Construct a fresh, live + // one to exercise the loop without the router tearing it down. + App.create(); + SimulationManager.destroy(); + + const emitSpy = vi.spyOn(EventBus.getInstance(), 'emit'); + SimulationManager.getInstance(); + + // Run a single game-loop tick (the constructor scheduled it via rAF). + expect(frame).toBeDefined(); + frame?.(0); + + const calledEvents = emitSpy.mock.calls.map((c: any[]) => c[0]); expect(calledEvents).toEqual(expect.arrayContaining([Events.UPDATE, Events.DRAW])); emitSpy.mockRestore(); diff --git a/test/weather/weather-manager.test.ts b/test/weather/weather-manager.test.ts index 4dd94836..5c0f04a8 100644 --- a/test/weather/weather-manager.test.ts +++ b/test/weather/weather-manager.test.ts @@ -34,11 +34,16 @@ const createMockAntenna = (uuid: string, isHeaterEnabled = false, iceAccumulatio uuid, isHeaterEnabled, iceAccumulation_dB, + skyNoiseDegradation_dB: 0, }, updateIceAccumulation: vi.fn((value: number) => { // Update the mock state when called mockAntennas.find(a => a.state.uuid === uuid)!.state.iceAccumulation_dB = value; }), + updateSkyNoiseDegradation: vi.fn((value: number) => { + // Update the mock state when called (sun-transit sky-noise path) + mockAntennas.find(a => a.state.uuid === uuid)!.state.skyNoiseDegradation_dB = value; + }), }); let mockAntennas: ReturnType[] = [];