Skip to content

Architecture (EDOS workspace)

Audience: Agent

This document describes how the standalone projects in this workspace fit together at runtime and which contracts matter when changing behavior. For a folder-by-folder inventory, see REPOS.md. For message-level detail, see IPC.md.

Design principles

  1. Not a monorepo — each top-level folder is an independent unit (own dependencies, build, deploy). Integration is via published npm packages (@howfe/...) and HTTP / browser APIs (postMessage, fetch).
  2. Journal data stays local — the hosted “remote journal reader” does not upload journal files to a server; it uses the File System Access API in the browser and forwards parsed events to an opener window via postMessage.
  3. Shared typing@howfe/elite-dangerous-event-types is the single source of truth for journal JSON shapes and type guards (isEliteEvent, isStatus, …).
  4. Shared IPC helper@howfe/inter-frame-messenger wraps window.postMessage with allowlists and a small vocabulary of MessageTypeEnum values.
  5. Community APIs via cache — Calls to third-party Elite Dangerous community APIs (SPANSH, EDSM, and similar) must go through the API cache server, not direct fetch to those endpoints from apps. Those services are run by volunteers in their spare time and typically have no DDoS or abuse protection. The cache is the fair-use layer: deduplicate requests, spread load, and avoid hammering upstream when many users or tools run at once.

Major subsystems

Subsystem Responsibility
Event types TypeScript types + guards for journal lines and Status.json.
Inter-frame messenger Typed postMessage envelope, origin checks, helpers for EliteEvent / Status.
Remote journal reader (web UI) User picks Status.json and journal .log locally; polls files; emits events to window.opener.
Surface map (web UI) Map + external API loaders; opens journal reader popup; subscribes to journal/status events.
API cache server Required front for community third-party HTTP APIs: proxy + cache + queue so apps do not stress volunteer-run endpoints (see principle 5 above).

Runtime topology

Typical production setup (URLs are hardcoded in app code today; see REPOS.md):

  • Surface map is loaded in the user’s browser (origin varies by deployment).
  • Remote journal reader is loaded in a popup at https://edjr.howfe.org (opened from surface map).
  • API cache at https://api-cache.howfe.org fronts calls to SPANSH, EDSM, etc.
flowchart LR subgraph browser["User browser"] SM["Surface map"] POP["Journal reader popup\n(edjr)"] end SM -->|"window.open"| POP POP -->|"postMessage\nEliteEvent / Status"| SM SM -->|"HTTPS"| CACHE["api-cache"] CACHE -->|"HTTPS"| UP["Third-party APIs"]

Journal and status pipeline

  1. Elite Dangerous writes *.log (one JSON object per line) and Status.json under the player’s Saved Games path (OS-specific).
  2. In the journal reader UI, the user grants file access via the File System Access API (@vueuse/core useFileSystemAccess).
  3. JournalWatcher (elite-dangerous-remote-journal-reader/ui/src/JournalWatcher.ts) reads text, splits lines, JSON.parses each line, filters with isEliteEvent, deduplicates with an in-memory eventHistory of raw lines, then sends new events via the messenger.
  4. StatusWatcher (StatusWatcher.ts) parses the whole JSON file, compares timestamp to avoid duplicate sends, and sends Status when it changes.
  5. Both watchers use InterFrameMessenger with target = window.opener so events flow from popup → parent (send uses target.postMessage).

The surface map does not read journal files directly; it receives EliteEvent and Status only through useJournalReader (elite-dangerous-surface-map/ui/src/composables/useJournalReader.ts), which listens on the parent InterFrameMessenger instance bound to the popup window.

Surface map consumption of events

  • useJournalReader is a module-level singleton pattern: one shared ifm and callback lists. readGalaxyDataFromEvents() registers onEliteEvent handlers for scans, location, codex, organics, etc. useCommanderState registers onStatus for commander/vehicle state.
  • startJournalReader() (e.g. from InteractionButtons.vue) opens the popup and constructs the parent-side InterFrameMessenger with listen types EliteEvent and Status, allowedSenderOrigins: ['*'], allowedListenerOrigins: ['https://edjr.howfe.org'].

Third-party HTTP data (non-journal)

  • Rule: Any new or existing app that calls community-maintained HTTP APIs must route those calls through the cache server (same pattern as fetchWithCache). Direct client→upstream calls for those APIs are not acceptable: they increase load on services that lack capacity planning and DDoS defenses.
  • Current pattern: Loaders under elite-dangerous-surface-map/ui/src/logic/externalDataLoaders/ use fetchWithCache (fetchWithCache.ts), which requests https://api-cache.howfe.org?url=... so responses are deduplicated and cached server-side (api-cache-server/api/src/app.ts). Self-hosted deployments should point at their cache instance with the same contract, not at upstream APIs from every browser tab.

Relationship to original-concept.md

original-concept.md lists general WebView / postMessage risks (origin validation, performance, legacy WebViews). This codebase implements origin filtering in InterFrameMessenger (allowedSenderOrigins / allowedListenerOrigins). For exact rules per side, see IPC.md.

Sequence: open popup and receive events

See IPC.md for sequence diagrams (parent/child roles and message envelope).

Gaps / TODO

Topic Follow-up
elite-dangerous-local-journal-reader No source in this workspace checkout — architecture TBD when the repo is populated.
Host-driven journal commands Child listens for OpenJournal / PollJournal / etc.; surface map parent currently uses empty sendMessageTypes — remote open is via UI inside the popup, not from the map. TODO: Verify if you add parent→popup automation.
Configurable service URLs edjr / api-cache hosts are fixed strings in TS — consider env-based configuration for self-hosting.