Skip to content

Latest commit

 

History

History
196 lines (147 loc) · 7.5 KB

File metadata and controls

196 lines (147 loc) · 7.5 KB

Project instructions for Claude Code

Code Conventions

  • Private properties and methods end with _ suffix (e.g., domCache_, syncDomWithState_())
  • Use readonly on properties set in constructor that should never change (e.g., containerEl, boundHandlers)
  • Use protected (not private) for properties that subclasses or UI layers need to access
  • Branded types (Hertz, dB, Degrees) require explicit casting - check type definitions early
  • State handlers typed as (state: Partial<T>) => void, never Function | null
  • Don't use bracket notation (obj['method']()) to access methods - make them public instead

CSS/Tabler

  • CSS custom properties use --mc-* namespace for Mission Control semantics
  • Import order: @tabler/coretabler-overrides.cssindex.css
  • Use Tabler utility classes (d-flex, justify-content-between, mb-2, fw-bold, font-monospace)
  • Cards: card h-100 for equal heights, with card-header and card-body
  • Forms: form-range for sliders, form-check form-switch for toggles

Architecture Patterns

  • Equipment modules use Core/UI separation: *-core.ts (business logic) and *-ui-standard.ts (DOM/UI)
  • Adapters: DOM caching (domCache_ Map), extract handlers to private methods (not inline), dispose() cleanup
  • UI components created BEFORE super() call; components needing uniqueId created AFTER
  • RotaryKnob uses callback-in-constructor; PowerSwitch/ToggleSwitch use addEventListeners() method
  • Factories return base Core type for polymorphism

EventBus Events

  • Events.UPDATE - Fires on each simulation tick; use for periodic state sync in adapters
  • Events.DRAW - Fires for canvas rendering only; do NOT use for DOM updates
  • When using .bind(this) for event handlers, store the bound reference to properly remove it later:
private readonly boundUpdateHandler_: () => void;

constructor() {
  this.boundUpdateHandler_ = this.syncDomWithState_.bind(this);
  EventBus.getInstance().on(Events.UPDATE, this.boundUpdateHandler_);
}

dispose(): void {
  EventBus.getInstance().off(Events.UPDATE, this.boundUpdateHandler_);
}

Adapter Throttling Pattern

For adapters listening to Events.UPDATE, throttle DOM updates to avoid performance issues:

private static readonly UPDATE_INTERVAL_MS = 1000;
private lastSyncTime_: number = 0;

private throttledSync_(): void {
  const now = Date.now();
  if (now - this.lastSyncTime_ < ClassName.UPDATE_INTERVAL_MS) return;
  this.lastSyncTime_ = now;
  this.syncDomWithState_();
}

Direct user actions (button clicks, toggles) should bypass throttling for immediate feedback.

Protecting Input Fields During DOM Sync

When syncDomWithState_() updates input fields, it can overwrite what the user is typing. Always check document.activeElement before updating inputs, selects, or textareas:

// Skip updating if user is focused on this input
const input = qs<HTMLInputElement>('#my-input', this.dom_);
if (input && document.activeElement !== input) {
  input.value = state.someValue.toString();
}

Apply this pattern to all user-editable fields in sync methods.

Equipment Adjust Controls (Phase 6+)

For numeric equipment controls (frequency, gain, power, etc.), use the equip-adjust-control pattern:

HTML Structure

<div class="equip-adjust-control">
  <label class="equip-adjust-label">CONTROL NAME</label>
  <div class="equip-adjust-row">
    <div class="equip-adjust-buttons equip-adjust-decrease">
      <button id="xxx-dec-coarse" class="btn-equip">-N</button>
      <button id="xxx-dec-fine" class="btn-equip">-n</button>
    </div>
    <div class="equip-adjust-display">
      <input type="number" id="xxx-value" class="equip-adjust-input" />
    </div>
    <div class="equip-adjust-buttons equip-adjust-increase">
      <button id="xxx-inc-fine" class="btn-equip">+n</button>
      <button id="xxx-inc-coarse" class="btn-equip">+N</button>
    </div>
    <span class="equip-adjust-unit">UNIT</span>
  </div>
</div>

Adapter Staged Values Pattern

RF equipment changes must use staged values + Apply button to prevent accidental changes:

private stagedValue_: number = DEFAULT;

private adjustStagedValue_(delta: number): void {
  this.stagedValue_ = Math.max(MIN, Math.min(MAX, this.stagedValue_ + delta));
  const input = this.domCache_.get('valueInput') as HTMLInputElement;
  if (input) input.value = this.stagedValue_.toString();
}

private applyHandler_(): void {
  this.module.handleValueChange(this.stagedValue_);
  this.syncDomWithState_(this.module.state);
}

Key Rules

  • Input field IS the display - don't create separate display elements
  • Use fixed width: 3.5rem on .btn-equip for vertical alignment
  • Status indicators use text spans (e.g., "Locked"/"Unlocked"), not LED circles
  • Toggle handlers must check state before calling toggle methods: if (state !== isChecked)
  • The qs() function throws on missing elements - remove refs when removing HTML elements
  • Use cacheElement_(htmlId, cacheKey) helper when HTML IDs differ from cache keys

Satellite Constructor

The Satellite class constructor takes signal arrays in a specific order:

new Satellite(
  noradId: number,
  uplinkSignals: RfSignal[],    // First array: signals satellite RECEIVES
  downlinkSignals: RfSignal[],  // Second array: signals satellite TRANSMITS directly
  config: { az, el, rotation, frequencyOffset }
)

Key Points:

  • First array (uplinks): Use origin: SignalOrigin.SATELLITE_RX - these are signals the satellite receives and transponds
  • Second array (downlinks): Use origin: SignalOrigin.TRANSMITTER - these are signals the satellite transmits directly (e.g., beacons)
  • Transponder: Automatically converts uplink signals to downlinks using frequencyOffset
    • Example: Uplink at 5943 MHz with frequencyOffset: 2.225e9 → Downlink at 3718 MHz
  • Don't duplicate: If a signal is in the uplink array, the transponder creates the downlink automatically. Don't add it to both arrays.

TypeScript Type Checking

Always use the npm script to check for TypeScript errors:

npm run type-check

Do NOT run tsc directly on individual files:

# WRONG - will fail with module resolution errors
npx tsc --noEmit src/campaigns/nats/scenario5.ts

This project uses @app/* path aliases (e.g., @app/types, @app/equipment/...) that require the full tsconfig.json configuration. Running tsc on individual files bypasses this and produces false "Cannot find module" errors.

Git Commits

  • Do NOT add Co-Authored-By lines to commit messages
  • Use conventional commit format: type(scope): description
  • Use emoji in commit titles for clarity:
    • Example: feat: ✨ Add new frequency adjustment control
    • :sparkles: for new features
    • 🐛 :bug: for bug fixes
    • ♻️ :recycle: for refactoring
    • 📝 :memo: for documentation changes
  • Common types: feat, fix, refactor, test, docs, chore

Planning

When you use Plan Mode or create multi-step plans in this repo:

  • Store each plan as a Markdown file under ./plans/ in this project.

    • Filename convention: phase-<n>-<short-topic>-plan.md
    • Example: phase-1-auth-refactor-plan.md
  • After completing a phase:

    • Write a brief retrospective to ./retrospectives/ in this project.
    • Filename convention: phase-<n>-<short-topic>-retro.md
    • Include sections: What worked, What didn’t, What to change next time.
  • Never write plans or retrospectives into the home directory; always use project-relative paths.