Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Extending MyAstroBoard

MyAstroBoard is an opinionated core, not a plugin platform (see ROADMAP.md - Architecture direction). You extend it through a small set of narrow, well-bounded extension points. Each is a self-contained unit a contributor can add, and a maintainer can review, without having to learn the whole codebase.

If what you want to add is a whole new vertical feature - its own tab, its own storage, its own i18n namespace, its own cache jobs - that is a core change, not an extension. Open a discussion first.

Extension pointWhat it addsStatusRecipe
TranslationA new UI language, or fixes to an existing oneAvailable7.TRANSLATIONS.md
Target catalogueCross-references or standalone deep-sky objects in SkyTonightAvailablebelow
ConnectorA bridge to an external astronomy tool, surfaced in the app tabs it declaresAvailableCONNECTORS.md - Adding a new connector
Export formatterA new “export Plan My Night / SkyTonight as X” format (Stellarium, SkySafari, NINA…)Planned for v2.1 - no stable contract yet-

Every extension follows the project-wide rules in CONTRIBUTING.md: English only, all 6 i18n files, centralized logging, tests that mirror the backend layout, and the validation block before you call it done.


Target catalogue

Two shapes, depending on whether your catalogue tags existing objects or adds new ones.

Cross-reference catalogue

Adds a catalogue_names entry and a source_catalogues tag to existing NGC/IC records. It never adds coordinates and never changes the preferred display name (OpenNGC / Messier / Caldwell always win). Examples: Herschel 400, Pensack 500, LBN, Arp.

  1. Add backend/catalogues/<name>.json, in one of the two existing formats:
    • a map { "NGC 1234": "<Cat> 5", ... } (LBN, Arp style), or
    • a list [ "NGC 1234", "IC 5678", ... ] when the source name is not needed (Pensack 500 style).
  2. Register it in _build_cross_ref_map() in backend/skytonight/skytonight_catalogue_builder.py - one _load_json_catalogue(...) block modelled on the existing ones, keying with _ngc_ic_match_key().
  3. Add the catalogue id to SKYTONIGHT_PREFERRED_NAME_ORDER in backend/utils/constants.py. Position matters: put it after the authoritative catalogues, near the other cross-refs.
  4. Add a row to the catalogue table and a source note in SKYTONIGHT.md.
  5. Add a test in tests/skytonight/test_skytonight_catalogue_builder.py: a known NGC record gains the tag.

Standalone catalogue

Creates new target records with their own coordinates, for objects with no NGC/IC identifier. Examples: Sharpless, Barnard, vdB, Abell PNe.

  1. Add backend/catalogues/<name>.json as a list of objects:
    {
      "name": "Sh2-155",
      "ra_hours": 22.951,
      "dec_degrees": 62.616,
      "size_arcmin": 50.0,
      "type": "Emission Nebula",
      "description": "Cave Nebula",
      "mag": null,
      "constellation": "Cepheus",
      "extra_catalogues": []
    }
    
  2. Register it with _build_standalone_targets_from_json('<name>.json', '<Cat>') in skytonight_catalogue_builder.py (next to the existing standalone_* calls).
  3. Steps 3-5 are the same as for a cross-reference catalogue.

After either

python scripts/build_skytonight_catalogue.py      # or POST /api/skytonight/dataset/rebuild
pytest tests/skytonight/

Catalogue data must be verifiable against a published source - cite it in the SKYTONIGHT.md note and in the PR description.


Connector

A connector is a Python class extending BaseConnector (backend/connectors/base_connector.py), registered in backend/connectors/__init__.py, exposing one or more independently-toggleable modules. Its data appears in the app tabs it names in target_modules - ["observatory"] for AllSky, and legitimately [] for a self-contained connector that feeds no existing tab. Full recipe: CONNECTORS.md - Adding a new connector.

A connector is the safest kind of third-party contribution because its contract is narrow: talk to an external system, return data. It does not own UI, user storage, or an i18n namespace of its own beyond its settings labels.

There is no public SDK or external plugin repository planned - that idea was dropped (see ROADMAP.md - Architecture direction). New connectors land directly in backend/connectors/ by PR, same as AllSky and MyAstroShine.

A connector is a connector. MyAstroShine (MYASTROSHINE.md) is bidirectional and owns UI in the Astrodex tab, but it is still an optional bridge to an external service that the user switches on or off - so it is a BaseConnector in the REGISTRY, listed by GET /api/connectors, and rendered by the same card as AllSky. What differs is carried declaratively: SECRET_FIELDS for its credentials, CONFIG_FIELDS for its extra settings, target_modules = ["astrodex"] for where its data lands, and an empty MODULES because the round-trip has no independently-toggleable parts.

Anything that writes back into MyAstroBoard data or owns its own screen still needs a core change alongside the connector - open a discussion first.

A connector may also push rather than be polled: the MQTT / Home Assistant connector (HOME_ASSISTANT.md) owns a background publisher thread (connectors/mqtt_publisher.py, started from app.py like the other schedulers) and reaches feature packages only through lazy imports, because cache/ and observation/ already import connectors/ at module level.


Export formatter

Planned for v2.1. Today, Plan My Night has two hard-coded export routes (/api/plan-my-night/export.csv, /api/plan-my-night/export.pdf). v2.1 introduces a small registry of formatters (Stellarium .stel, SkySafari .skylist, NINA target-list XML, extended CSV) so a new format is one formatter class plus a test. This section will carry the recipe once that contract exists.


The one hard rule: no new cross-feature dependency cycles

The backend is split into per-domain packages (skytonight/, observation/, equipment/, weather/, astroweather/, space/, cache/, utils/). A handful of import cycles between these already exist and are being unwound one at a time. Do not add new ones.

  • A shared helper two features both need belongs in utils/ (or a new small shared module), not imported feature-to-feature.
  • If feature A genuinely must call into feature B at request time and B does not already depend on A, a lazy import inside the function is acceptable as a deliberate, commented exception (this is how plan_my_night reaches equipment_profiles today). A new module-level import that closes a cycle is not.
  • When in doubt, ask in the PR - a reviewer will tell you whether the edge is already there or you are creating one.

This keeps every feature reviewable and testable on its own, which is the whole point of the extension-point model above.

Last updated 2026-11-13