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

Session Analytics and Wishlist

Session Analytics (v1.5) answers two questions from data MyAstroBoard already stores: “how am I progressing?” and “how am I optimizing?”. The Wishlist that ships with it answers a third: “what do I still want to capture?”

They live as two sub-tabs of the Astrodex main tab, alongside Astrodex, Plan My Night, the Observation Log, Photo Map and Catalogue Collection - so the whole Plan -> Observe -> Log -> Astrodex loop and everything derived from it stays under one navigational roof.


Table of Contents

  1. The four analytics sections
  2. Wishlist
  3. What “best months” does and does not say
  4. What is computed, and from what
  5. Why the Observation Log is the primary source
  6. Resolving coordinates
  7. Scale directions
  8. Performance and caching
  9. API endpoints
  10. Out of scope
  11. Related documentation

The four analytics sections

1. Headline totals

Integration lifetime / this year / this month, sessions and nights, distinct objects captured, constellations, average rating, and the Astrodex collection size.

“This month” is the observer’s month: the date is resolved in the active location’s timezone, not the server’s, so a session logged at 01:30 local on the 1st lands where the user expects it.

2. Where your time goes

Integration over time (a dense monthly series - a month with no activity renders as a zero bar rather than closing up the axis), object types, constellation spread, and equipment usage.

1b. How the headline figures are rendered

Through the shared .stat-plate component (DOMUtils.buildStatPlate()), not a grid of cards - see the Headline Figures Standard in .github/instructions/copilot.instructions.md. The Astrodex and Observation Log tabs render theirs the same way.

A zero duration still carries its unit: at the hero’s size, a bare “0” under “Light collected” is the first thing a new user reads, and it does not say zero of what.

2b. Colours under the red night-vision theme

Both palettes go through _saPalette() / _saQualityPalette(), which return a single-hue red ramp when html[data-theme="red"] is in force - the categories are then separated by lightness rather than by hue. A blue or green dot is exactly the light that theme exists to keep out of a dark-adapted eye, and this page puts a large chart in front of one.

3. Sky coverage

Every captured object placed on an RA/Dec grid: right ascension on the x axis, reversed as on any sky chart, declination on the y axis. Colour by object type or by capture year.

It is drawn as a sky chart rather than as a scatter plot, so the dots have something to sit against:

  • the Milky Way is filled behind the data, from the galactic equator transformed into equatorial coordinates (J2000 pole at RA 192.85948 deg, Dec 27.12825 deg). Several faint layers at widening galactic latitudes are filled over one another, so the glow falls off instead of stepping;
  • the ecliptic is drawn dashed - the path the Sun, Moon and planets follow;
  • the celestial equator is marked at Dec 0;
  • a dot grows with the integration time behind its object, on a square root so one very long project does not flatten everything else;
  • the top axis reads the same right ascension as a month: an object culminates near local midnight when the Sun sits half a sky away from it. The reading is anchored on the equinoxes and solstices rather than on one straight line through the year, which would name the wrong month for an object near a boundary.

A shaded band marks the declinations that never rise at the active location (dec < -(90 - lat) in the northern hemisphere, mirrored in the southern), so “where have I been” is read against where the observer can actually go.

Objects whose coordinates cannot be resolved at all are counted and named, not silently dropped - the same honest accounting the Photo Map does with its ungeotagged pictures.

Everything painted on the canvas takes its colour from custom properties declared in static/css/bs_session_analytics.css (--sky-chart-*), read back with getComputedStyle at draw time: a canvas cannot carry a class, and the red night-vision theme redefines all of them. The plot area stays dark under the light theme on purpose - a sky chart reads as a window onto the sky, and inverting it would only make the objects harder to place.

4. Conditions and results

Every rated entry joined to its night’s seeing, transparency, SQM and Moon illumination, plotted as four scatters with bucket averages underneath.

This is descriptive, not predictive. Each bucket reports its own sample count next to its average, and a bucket with fewer than five samples is shown as “not enough data yet” rather than plotted. There is no trend line and no correlation coefficient: over a handful of nights, one would be noise dressed as insight.


Wishlist

A wishlist item records intent: an object you want to capture. It is the only new persistent state v1.5 introduces - everything else is derived.

Storage is the wishlist_items database table (one row per item), validated before every write like the Observation Log. Wishlists are permanently private, and they are included in the admin backup ZIP.

Two rules

  1. “Captured” is never stored. It is recomputed on every read from the Observation Log entries and the Astrodex items. A stored boolean would go stale the moment an entry is edited or a session deleted, and silently-wrong progress is worse than no progress bar.
  2. Coordinates are resolved and frozen at add time, through the resolver (see Resolving coordinates).

Behaviour

  • Deduplication is cross-catalogue: adding “NGC 224” when “M 31” is already on the list is reported as a duplicate, not added twice. Aliases count too.
  • Captured items stay on the list and are shown as done, so “12 of 20” keeps its denominator. POST /api/wishlist/archive-captured is the explicit opt-in for removing them.
  • Moving targets (planets, comets) are accepted as wishes but never get a frozen position: a fixed RA/Dec would be wrong within weeks, so they simply report no window, the same distinction the v1.4 visibility calendar draws.
  • Priorities are high / normal / low; the list can also be sorted by visibility (the default) or name.
  • Cap: MAX_WISHLIST_ITEMS = 500 per user.

Where objects are added from

SurfaceNotes
SkyTonight result tablesThrough each row’s More popup, which covers deep-sky objects, bodies and comets in one place
Catalogue Collection cardsBrowse a whole catalogue and queue what you want
Beginner Catalog cardsThe v1.1 tie-in the roadmap asks for

All three payloads carry an in_wishlist flag, computed from a preloaded index so annotating a thousand SkyTonight rows stays one file read.

The SkyTonight result tables themselves are built as HTML strings - the legacy path the project style guide says not to extend - which is why the table’s entry point is its per-row More popup rather than a new column.

Next visibility

GET /api/wishlist attaches, per item, the observable hours on the nearest sampled night and the best of the next three sampled months. That is the cheap ranking signal the default sort uses; the precise twelve-month answer stays one click away behind the v1.4 per-object visibility calendar, which every placed item links to.

?visibility=0 skips the ephemeris pass entirely for a fast render.


What “best months” does and does not say

The chart plots two clearly separate series:

  • Available - mean dark hours and moonless dark hours per night for that calendar month at the active location, computed live from the Sun and Moon. This is the site’s own nightly budget.
  • Logged - the month total the user actually recorded, across all years, plus the nights logged and mean rating.

The two are different units - a nightly average against a cumulative total - so they get separate y axes, each labelled. On one shared axis a well-used month climbs above the available darkness and reads as an impossibility.

It is not a weather statistic. MyAstroBoard stores no historical weather: every weather cache is a short-TTL forecast snapshot that is overwritten in place, and nothing accumulates a climatology. The chart therefore answers “when is it worth planning to be out here, and when have I actually been out” - never “when is it clear here”.

A weather climatology (multi-year cloud cover from an archive API, behind its own scheduler cache job) was specified and deliberately deferred; see feature.md for the full design if it is ever picked up.

If the ephemeris cannot be computed for a location, the personal half still renders and the chart says the sky figures are missing, rather than showing a half-empty chart that looks complete.


What is computed, and from what

SectionSource
Totals, monthly series, object/constellation/equipment breakdownsObservation Log sessions, nights and entries
Sky coverage mapObservation Log entries + Astrodex items, placed via the SkyTonight dataset
Conditions and resultsObservation Log entries (rating) joined to their night (seeing, transparency, SQM, Moon)
Best monthsEphemeris at the active location + the user’s own logged months
WishlistIts own per-user store, plus derived captured state

Everything is strictly self-scoped: a user only ever sees aggregates of their own log and their own Astrodex. Observation Log sessions are permanently private, so the analytics built on them are too. Read-only accounts may read every analytics route; only the wishlist’s write routes require the user role.

All the aggregation is pure: backend/observation/session_analytics.py takes already-loaded sessions and returns a payload. Loading files, resolving the request’s active location and reaching across feature packages is the blueprint layer’s job (backend/blueprints/session_analytics.py), the same split the Observation Log uses.


Why the Observation Log is the primary source

Attaching a photo to an entry is a manual, optional step, so Astrodex picture metadata covers an unknown subset of what was actually captured. Every logged entry, by contrast, carries its own frame_count / sub_exposure_seconds / integration_minutes.

So: every “hours” and “objects captured” figure comes from the Observation Log. The Astrodex collection size is reported as its own, separately-labelled figure - never folded into the logged totals - and Astrodex items are used on the sky coverage map only to cover objects that were added straight to the gallery without a session entry.

This was decided when v1.3 shipped; see docs/OBSERVATION_LOG.md.

Which figures come from where

  • integration_minutes is the one guaranteed-summable field. When it is absent but both frame_count and sub_exposure_seconds were recorded, their product is used instead.
  • An entry counts as captured when it has frames or integration time - the same threshold the Observation Log itself uses to auto-register a target in Astrodex. A planned-but-clouded-out entry is logged but is not a capture.
  • Entries are attributed to their night’s date, never to created_at: a session written up three weeks later still lands in the month it happened.
  • Distinct objects are counted on the dataset’s cross-catalogue catalogue_group_id when it resolves, so “M 31” and “NGC 224” are one object, not two.
  • Equipment hours follow the effective combination of each entry: its own override if it has one, else the session’s. Both are frozen snapshots on the records themselves, so renaming or deleting a combination never rewrites history. Entries with no equipment recorded get an explicit bucket rather than being dropped, so the hours always add up to the headline total.

Resolving coordinates

The sky coverage map and the wishlist both need numeric RA/Dec, and neither can take the stored ra/dec at face value. Plan My Night and the Observation Log copy those fields straight from whatever the client sent, and the two SkyTonight frontend paths disagree: the target cards send coordinates.ra_hours (a decimal-hours float) while the result tables send the formatted ra_hms string ("21h 31m 48.32s"). Astrodex items are worse - ra/dec are stripped on every write.

backend/observation/target_coordinates.py is the single resolver, applying one order everywhere (first hit wins):

  1. The SkyTonight dataset lookup, which carries canonical numeric ra_deg/dec_deg plus the object’s cross-catalogue group_id. Display labels such as "M31 - Andromeda Galaxy" are reduced to their identifier before the lookup.
  2. The record’s own stored values, parsed tolerantly: a number (RA in decimal hours, Dec in degrees) or a sexagesimal string in h m s, d m s, deg ' " or colon form. A negative-zero degrees field (-00 30 00) keeps its sign.
  3. Give up - and the caller counts the miss.

The wishlist stamps the resolved values onto the item at add time, so the visibility pass is a pure numeric loop that never re-learns this.


Scale directions

Three of the recorded conditions run in different directions. Every bucket the API emits is ordered best first and carries its own numeric bounds, so no consumer has to re-derive which way a scale goes:

FieldDirectionBest band
seeing1 = best .. 8 = worst (7Timer ASTRO)1-3
transparency1 = worst .. 8 = best (7Timer ASTRO)6-8
sqmhigher is darker, therefore better>= 20.5
moon_illumination_percentlower is better0-33
rating0-5 in 0.5 steps, higher is better-

A reversed seeing axis is a correctness bug, not a cosmetic one, and has its own test.


Performance and caching

No scheduler cache job is registered. The analytics are per-user aggregations over that user’s own JSON files - a few milliseconds of dict work even for a heavy log.

The only expensive part is the ephemeris, and it is cached the way v1.4 already caches this class of work, with bounded in-process LRUs:

CacheKeyWhy
Monthly dark hours(location_id, year)Changes once a year
Night contexts(location_id, date)A night’s Sun/Moon grid is identical for every target, so repeated wishlist loads do no fresh ephemeris work

That is also why the wishlist’s visibility pass scales: the grid is built once per sampled night and every target is folded through it at O(1) trig per time step, so a 500-item wishlist costs the same few grids as a single object would.


API endpoints

All routes require login and are self-scoped. Write routes additionally require the user role.

MethodRouteNotes
GET/api/session-analytics/summaryTotals, monthly series, type/constellation/equipment breakdowns, top targets. ?year= selects the “this year” bucket
GET/api/session-analytics/sky-coveragePlaced points, unplaced count and names, never-visible declination for the active location
GET/api/session-analytics/conditionsPer-entry samples plus bucketed averages with sample counts
GET/api/session-analytics/best-monthsAstronomical availability and the user’s own logged months. ?year=
GET/api/wishlistItems with derived captured state, progress counters and next visibility. ?sort=visibility|priority|name, ?visibility=0
POST/api/wishlistBody {"targets": [...]} - always a list. Returns added items plus skip counts
PATCH/api/wishlist/<item_id>priority, notes
DELETE/api/wishlist/<item_id>Remove one item
POST/api/wishlist/archive-capturedRemove every item currently derived as captured

The four analytics routes are fetched in parallel when the sub-tab opens, so the ephemeris-backed one never holds up the three that are instant.


Out of scope

  • Weather climatology - see above.
  • Per-filter breakdown of integration time. The Observation Log keeps one aggregate integration_minutes per entry (already out of scope in v1.3).
  • Cross-user or club leaderboards. Sessions are private; analytics stay self-scoped. Public profiles are a v2.1 concern.
  • A true all-sky projection for the coverage map. v1.5 ships the rectangular RA/Dec grid with the Milky Way and the ecliptic drawn on it; the projected, imagery-backed view arrives with the v2.0 sky chart rather than being built twice.
  • Named stars and constellation figures on the coverage map. The landmarks it carries are computed, not catalogued - drawing real stars means shipping a star catalogue to the browser, which belongs with the v2.0 sky chart.
  • Guiding-quality trends per equipment combination - that needs the v2.2 PHD2 import.
  • PDF export of the dashboard.

Last updated 2026-11-13