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¶
- 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). - 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. - Shared typing —
@howfe/elite-dangerous-event-typesis the single source of truth for journal JSON shapes and type guards (isEliteEvent,isStatus, …). - Shared IPC helper —
@howfe/inter-frame-messengerwrapswindow.postMessagewith allowlists and a small vocabulary ofMessageTypeEnumvalues. - 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
fetchto 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.orgfronts calls to SPANSH, EDSM, etc.
Journal and status pipeline¶
- Elite Dangerous writes
*.log(one JSON object per line) andStatus.jsonunder the player’s Saved Games path (OS-specific). - In the journal reader UI, the user grants file access via the File System Access API (
@vueuse/coreuseFileSystemAccess). JournalWatcher(elite-dangerous-remote-journal-reader/ui/src/JournalWatcher.ts) reads text, splits lines,JSON.parses each line, filters withisEliteEvent, deduplicates with an in-memoryeventHistoryof raw lines, then sends new events via the messenger.StatusWatcher(StatusWatcher.ts) parses the whole JSON file, comparestimestampto avoid duplicate sends, and sendsStatuswhen it changes.- Both watchers use
InterFrameMessengerwithtarget = window.openerso events flow from popup → parent (sendusestarget.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¶
useJournalReaderis a module-level singleton pattern: one sharedifmand callback lists.readGalaxyDataFromEvents()registersonEliteEventhandlers for scans, location, codex, organics, etc.useCommanderStateregistersonStatusfor commander/vehicle state.startJournalReader()(e.g. fromInteractionButtons.vue) opens the popup and constructs the parent-sideInterFrameMessengerwith listen typesEliteEventandStatus,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/usefetchWithCache(fetchWithCache.ts), which requestshttps://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. |