Skip to contents

EpiSODIC 0.24.0

New

  • Each detection run nowcasts every stream carrying an open cluster, from the stream’s own reporting delays (surveillance::nowcast(), bayes.trunc), configured under forecast.nowcast
  • Each run’s nowcast is stored in episodic_forecast and episodic_forecast_value (schema version 11), with the reason when a stream could not be nowcast
  • The outbreak and Epidemics dossier curves draw the nowcast’s median and 90% interval on the days and weeks still being reported
  • The Performance screen scores stored nowcasts once reporting is complete: interval coverage, weighted interval score, and the error of taking the reported count as final
  • Each detection run gives every open outbreak of a transmissible pathogen the probability that it is over, from the serial interval, the pathogen’s offspring distribution and the stream’s reporting completeness, configured under forecast.outbreak_end
  • The pathogen CSV takes end_r and end_k, the offspring mean and dispersion, shipped empty for every pathogen
  • The outbreak dossier shows the probability beside the case-free days, with the date it reaches forecast.outbreak_end.probability_threshold, or why there is none
  • Each run’s outbreak-end probability is stored in episodic_cluster_forecast and episodic_cluster_forecast_value (schema version 12)
  • The Performance screen scores stored outbreak-end probabilities once the case-free window after the last case is reported: mean probability, share over, and Brier score
  • The Pathogen screen’s parameter panel shows a transmissible pathogen’s offspring distribution
  • Each detection run estimates every open epidemic’s weekly growth rate, the chance it is growing and the chance it is past its peak so far, on the nowcast-corrected weekly counts, configured under forecast.epidemic_direction
  • The Epidemics dossier shows the weekly growth with its doubling or halving time and the weeks it covers, and the chance the epidemic is past its peak, or why there is none
  • Each run’s epidemic direction is stored in episodic_cluster_forecast and episodic_cluster_forecast_estimate (schema version 13)
  • The Performance screen scores stored epidemic directions once their weeks are fully reported: mean probability, share true, and Brier score
  • A seasonal epidemic stores the typical past season from the MEM fit that set its thresholds: its curve, start week and length, in episodic_epidemic_season and episodic_epidemic_typical_week (schema version 13)
  • The Epidemics dossier draws the typical past season on the weekly curve, aligned to the epidemic’s onset, and shows the typical length and how much earlier or later this epidemic started
  • The typical past season’s curve is the median of the past seasons by default, set by mem.typical_curve (median, geometric_mean or arithmetic_mean)
  • The tuning vignette’s reference covers the notifications keys
  • Each detection run forecasts every open epidemic’s weekly counts over the next weeks, with the chance of reaching each MEM band, as the quantile average of a negative binomial surveillance::hhh4() model of the stream’s history and the extended trend of its last weeks, configured under forecast.epidemic_outlook
  • Each run’s epidemic outlook is stored in episodic_cluster_forecast_week (schema version 14)
  • The Epidemics dossier shows a pathogen’s outlook, as a table and on the weekly curve, only once its outlooks have been scored on forecast.epidemic_outlook.min_scored weeks and beat expecting no change, and states that record
  • The Performance screen scores stored outlooks per horizon: interval coverage, weighted interval score, and the score relative to expecting no change

Changed

  • episodic_demo() replays the last replay_days days of its synthetic data as daily runs with a one-week reporting horizon, so the demo has measured reporting delays and a nowcast, and gives its transmissible pathogens an illustrative offspring distribution, so an outbreak shows its end-of-outbreak probability

  • The outbreak dossier shows the end-of-outbreak tile only with a probability, or when the forecast failed

  • The Epidemics dossier’s intensity tile names the peak so far only when its band differs from the current week’s

  • The Performance screen’s outbreaks-only note is limited to its detection and assessment metrics, since its nowcast section covers epidemics too

  • A stat tile can carry a line between its caption and its value saying what the value measures

  • The Epidemics dossier’s “Outbreaks during” tile is “Linked outbreaks”, with a line saying what links them

Fixed

  • The outbreak dossier’s case-free days count from the latest run’s date instead of today, so a day without a run, a replay or a backfill no longer adds case-free days

EpiSODIC 0.23.0

New

  • Five severity_* palette roles colour both verdicts and MEM intensity bands, overridable through EPISODIC_STYLE
  • The Epidemics dossier header shows the current MEM band as an “Intensity” chip
  • Each seasonal epidemic in the Epidemics list shows its current MEM band as an “Intensity” chip
  • Text on a filled chip or selected picker button takes ink, white or black, whichever reads on the fill
  • The Pathogen and Epidemics weekly curves shade the MEM intensity bands behind the bars
  • Each detection run stores every open seasonal epidemic’s weekly counts and intensity bands in episodic_epidemic_week (schema version 10)
  • An assessed cluster’s assessment pane says which cluster it would otherwise be filed under, with a link to it
  • The weekly curve’s note explains why there is no Low band when MEM’s medium threshold is at or below the start threshold

Changed

  • The weekly curve on the Pathogen screen and the Epidemics dossier is titled “Confirmed cases per week” instead of “Weekly incidence”, with no y-axis title
  • Verdicts, intensity chips, intensity stats and the Pathogen chart’s intensity lines take their colours from the severity scale
  • Synthetic Influenza A and RSV come in winter waves of varying size and timing, so the demo always has a MEM season with intensity bands
  • MEM threshold lines are thin and solid, and the weekly curve’s legend, titled “Epidemic Severity” in every language, names the shaded intensity bands on the right, with the start and end thresholds, the only lines drawn, below them at one width
  • The Epidemics dossier and list read a seasonal epidemic’s thresholds, latest week and bands from what the detection runs stored instead of refitting MEM
  • suppression.parent_min_flagged_children ships as 1, so a parent absorbs a single flagged child holding under half its cases

Fixed

  • MEM intensity bands are read from mem::memmodel()’s intensity.thresholds, so they are no longer always missing
  • A MEM fit without intensity bands says why in the run log
  • A seasonal epidemic closes on the trough backstop only once its count is back at or below the pre-epidemic threshold, so an out-of-season epidemic is no longer closed while still rising

EpiSODIC 0.22.2

New

  • A generated outbreak report version in the dossier links to its report in a new tab for signed-in users, while its file still exists

Changed

  • episodic_check_cases() advises on pc values missing from the configured EPISODIC_GEO_DATA instead of on pc values that are not four digits
  • Country-specific terms (BSN, GGD, Gesundheitsamt) replaced by generic ones in messages, translations and documentation
  • The unused wpg_notifiable, ggd_informed and ggd_note columns are dropped from episodic_assessment_event where they hold no data (schema version 9)
  • A schema migration drops a column only when nothing uses it and it holds no data

EpiSODIC 0.22.1

New

  • EPISODIC_MAIL_TEMPLATE_NEW_CLUSTERS and EPISODIC_MAIL_TEMPLATE_REPORT set a Quarto template for the body of the new-clusters and scheduled-report emails
  • episodic_mail_template_preview() renders an email template against example data

Changed

  • Outbreak reports and emails take their colours and font from the instance palette
  • An unusable EPISODIC_STYLE file or value falls back to the shipped palette and is reported instead of stopping the app

EpiSODIC 0.22.0

New

  • A detection run logs the time its stream loop spent per stage
  • MEM fits are cached per stream in episodic_detector_cache and reused only when their exact input is unchanged (schema version 8)
  • episodic_run_cron() migrates a database behind the installed package before running, unless database.auto_migrate is false
  • episodic_db_migrate() copies a SQLite database beside itself before the first step (backup = TRUE)
  • A run that refuses a database records the refusal as a failed run
  • A run that stops before it begins (database refused, case data failing its checks) sends the run_failure notification
  • The run log names the database server and version it connected to (SQLite, MariaDB or MySQL)

Changed

  • The same_place scan is linear in a place’s case count instead of quadratic
  • Each stream takes its cases from an index built once per run instead of filtering the whole case table
  • Farrington’s weekly bins, the MEM anchor, seasonality statistic and season matrix are counted per distinct date instead of per case
  • Baseline exclusions, patient-days and trend row counts are read once per run instead of once per stream
  • The Farrington detector and the trend cache share one fit per stream instead of fitting the same week twice
  • Reconciliation reads assessment events once per run and ages a stream’s undetected clusters in one statement

Fixed

  • A case import naming more distinct patients than one SQL statement allows no longer fails the run
  • Farrington with institution patient-days no longer fails the run under surveillance 1.26
  • Lattice suppression no longer suppresses a child cluster behind a parent it shares no case with, nor re-suppresses one already suppressed

EpiSODIC 0.21.0

New

  • The Epidemics dossier shows the latest complete week against the week before, the peak so far, the current MEM intensity and the latest Rt
  • The Epidemics dossier has a choropleth of the epidemic’s cases, beside the postcode breakdown
  • The Epidemics dossier compares the epidemic’s season with earlier seasons on one axis
  • The Epidemics dossier shows Rt, age and sex against the area’s own baseline, and the care lines cases were found in
  • Outbreaks during an epidemic are listed in the cluster table, with their state, row hover and click-through
  • Lattice suppression writes progress lines during a detection run
  • MEM log lines name a stream’s region code as well as its pathogen and level
  • The Archive search combines terms with AND and OR
  • The Epidemics dossier shows its detection settings
  • The Epidemics dossier warns when the area’s case history is missing cases linked to the epidemic, naming both catchment codes where they differ

Changed

  • Outbreak and epidemic identifiers take their prefix from the reader’s language (e.g. U-/E- in Dutch, A-/E- in German)
  • Lattice suppression reads case sets and assessments in two batched queries instead of per cluster
  • The Outbreaks and Epidemics rails leave closed clusters out in the database query
  • The epidemic dossier reads the stream’s case history once and its lead-in, thresholds, Rt and baseline come from the epidemic’s own area
  • Contributing institutions on the Epidemics dossier are counted from the epidemic’s own cases
  • The season overlay assigns season weeks per distinct day instead of per case
  • Positivity is drawn on an axis scaled to the series instead of a fixed 0-100%
  • Every chart’s y axis leaves 25% headroom above the highest value and none below the lowest
  • Axes of case and test counts are labelled in whole numbers only
  • Rt is drawn on a log2 axis everywhere
  • The Pathogens screen’s selector lists pathogens alphabetically, case-insensitively
  • Screen-level loading spinners are three to four times larger and are not dimmed with the content
  • ggplot2 (>= 3.5.0) is required
  • The phenotypic resistance profile placeholder panel is removed from the Outbreaks dossier
  • The Epidemics dossier shows one map instead of two, labelled with postcode and count, and Rt and positivity at full width
  • A closed epidemic’s curve ends four weeks after its last case, and its dossier reads its intensity at the peak
  • Every screen shares one neutral background and one accent colour (primary)
  • Charts draw their main series in primary, earlier seasons in fading greys, and test volume in a light primary
  • The Archive search box is up to 44rem wide and full width on narrow screens
  • The rails show a care-line chip for ‘other’
  • Care-line chips differ in form as well as colour: first line filled, second tinted, third outlined, other dashed
  • The detection-settings card is titled for its outbreak or epidemic and reports the run that last detected it
  • Translation files list their keys in sorted order

Fixed

  • The positivity feed is summed per ISO week, so period starts in the same week no longer produce duplicate week labels
  • A positivity above 100% is left out of the chart instead of drawn at 100%
  • Outbreaks are linked to the epidemics they ran during whatever either cluster’s state, so historical epidemics get their outbreaks
  • A cluster whose first or last day cannot be read as a date is left out of the during links and named in the run log, instead of failing the run
  • An epidemic’s length is counted from its first to its last case instead of to today
  • The line list and outbreak report show sex as a translated word instead of its stored code
  • The Pathogens screen’s detection-parameters panel italicises the pathogen name in its title and text
  • Map areas with the fewest cases are tinted instead of drawn in the case-free colour
  • A map’s frame no longer extends past the reference geometry by more than its minimum margin, so a widespread cluster’s map fills its box
  • devtools::load_all() no longer replaces the session’s EPISODIC_CONFIG with the test suite’s configuration

EpiSODIC 0.20.6

Changed

  • The Archive reads the database once per data change; searching and the level chips filter in memory
  • The Archive shows closed clusters 100 per page
  • The dashboard sends its shell and rail before the first dossier, which follows on the next flush
  • Cluster tables resolve the configuration once per table instead of twice per row
  • A configuration file is re-read and re-hashed only when its size or modification time changes
  • Cluster states for the rail and the Archive are derived in one pass instead of one scan per cluster
  • The Archive builds place labels once per stream and closure authors once per user
  • Batched reads over many clusters or streams run in chunks

Fixed

  • The Archive search box keeps what was typed when the results redraw
  • An Archive search containing a character such as ( or [ no longer stops the screen
  • Batched reads no longer exceed MySQL’s 65,535-placeholder limit on instances with many clusters

EpiSODIC 0.20.5

Fixed

  • Opening an epidemic dossier on a MariaDB/MySQL instance no longer aborts the R session; the epidemic object is built before the dossier’s first query rather than inside it
  • Every query now evaluates its parameters before the statement is prepared, so no query parameter that itself queries the database can abort the R session on MariaDB/MySQL

EpiSODIC 0.20.4

Fixed

  • episodic_add_manual_cluster()’s level argument pointed at vignette("architecture"), a vignette that no longer exists; it now links to the pkgdown article that documents the five lattice levels
  • The pkgdown deploy step purges the published site before every rebuild (clean: true, plus pkgdown::clean_site()), so a page whose source vignette is removed no longer lingers on the live site

EpiSODIC 0.20.3

Fixed

  • Links between vignettes point at the pkgdown articles instead of the repository’s source files

EpiSODIC 0.20.2

Changed

  • The Chinese outbreak and epidemic unit words carry a measure word (起暴发, 场流行病), like the other zh units

EpiSODIC 0.20.1

New

  • The Instance screen’s Archive card counts outbreaks and epidemics separately, and its Performance card shows the overall PPV

Changed

  • The Instance cards’ count lines are set in the primary dark colour

EpiSODIC 0.20.0

New

  • An Epidemics screen with a rail of open epidemics, a dossier with seasonal evidence, and the assessment pane a season is declared in
  • The navigation bar carries four links: Outbreaks, Epidemics, Pathogen, Instance
  • An outbreak’s dossier header carries a “During E-123” chip for each epidemic it ran during, which opens that epidemic
  • The Epidemics screen carries a notes panel and the full assessment form, so an epidemic is assessed and annotated like any other signal
  • The epidemic dossier shows weekly case curve with MEM threshold overlay, tests and positivity, contributing institutions with concentration, and linked outbreaks
  • The Clusters rail and Performance screen filter to outbreaks only (L1-L3)
  • Epidemiologists can record seasonal declarations (season_started, season_not_yet, season_ended) on seasonal epidemics
  • Cluster references render as O-{id} for outbreaks and E-{id} for epidemics, uniform across all eight languages
  • episodic_object_ref() helper renders the scale-aware reference from either a scale string or a lattice level
  • User-facing “cluster” wording replaced throughout: outbreak-only contexts say “outbreak”, mixed contexts say “outbreaks and epidemics”, detection contexts say “signal”
  • Full i18n vocabulary sweep across all eight language files for the outbreak/epidemic terminology
  • Report filenames changed from cluster-{id} to outbreak-{id}

Changed

  • The Archive is reached from a card on the Instance screen instead of its own link, so the navigation bar fits on a phone width
  • access.require_login now ships as false: a freshly installed instance reads open, and an operator closes it deliberately
  • The Clusters screen is called Outbreaks
  • The Epidemics screen holds its three panes in the app shell, like the Outbreaks screen, so its rail keeps its scroll position and gains the phone-tier pane switcher
  • The Epidemics rail is the Outbreaks rail’s markup, with more room per row
  • The seasonal declaration is offered inside the epidemic’s assessment form rather than as a form of its own, alongside the ordinary classification verdicts
  • The classification form and the notes panel namespace their element ids, so both surveillance screens can carry one
  • An outbreak’s “Linked to” chips cover the outbreak scale only; the epidemic it ran during is a “During” chip instead
  • The confirmed-outbreak classification’s explanation no longer names one scale while its label names the other
  • The mobile pane-switcher’s active tab, the recalculating spinner, the Outbreaks screen’s recommendation box, the epi curve/trend/Rt/map/weekly-cases charts, and every cluster table’s row-link colour now read the current nav section’s own colour instead of always primary; the age/sex pyramid keeps its own colour-coding unchanged, and buttons and chips are untouched

Fixed

  • The shared cluster table (Pathogen screen, Archive, and the dossier’s related and similar-clusters panels) always prefixed ids with O-; each row now reads its own scale via episodic_object_ref(), so an epidemic-scale row reads E-{id}
  • Screen titles (Pathogen activity, Archive, Activity, Performance, Info, Streams) were sized in a fixed pixel value and no longer scaled with the mobile font-size bump; a shared rem-based .episodic-screen-title class replaces the inline style
  • episodic_geography_nests() no longer fails for region-level epidemics after the generic refactoring removed the L5 fast path
  • The Pathogens screen’s weekly-cases chart now uses the section accent colour, consistent with the Rt and geo charts on the same screen

EpiSODIC 0.19.0

New

  • An Instance screen, holding the Streams, Activity, Performance, Info and Settings screens the navigation bar no longer carries
  • Per-output loading spinners in the primary colour, so each panel, chart and table says it is the one being waited for
  • A “Last 3 months” period on the Pathogen screen, left of “Last 12 months”
  • An open-by-number box in the rail header, which opens any cluster the instance will show, closed and archived ones included
  • A row about a cluster on the Activity screen opens that cluster
  • Schema version 5: two composite indexes on episodic_case, for the dashboard’s date-bounded and institution-bounded pathogen reads
  • MEM derives its season anchor and seasonality eligibility from the data, so it fires in any ISO week for any pathogen whose cases show a season, in either hemisphere
  • MEM runs at province (L4) and region (L5) level, configurable via mem.levels
  • mem_mode replaces mem_applicable: a three-state override (auto, yes, no) per pathogen
  • Schema version 6: mem_mode column on episodic_pathogen_config
  • Full-year MEM seasons with no NA off-season period
  • Schema version 7: scale discriminator on episodic_cluster, episodic_epidemic_season satellite table, episodic_cluster_link relation table, and declaration verdicts on episodic_assessment_event
  • Configurable scale boundary (scale.epidemic_levels) determines which lattice levels produce epidemics rather than outbreaks
  • Verdict labels derive the outbreak/epidemic boundary from configuration rather than a hardcoded constant
  • Clusters opened at epidemic-level streams receive scale = 'epidemic'; all others receive scale = 'outbreak'
  • A seasonal satellite row (episodic_epidemic_season) is written when a new epidemic cluster opens from a MEM detection
  • Seasonal epidemics close automatically when the weekly case count drops below the post-epidemic threshold or the evaluation week enters the trough
  • Epidemic identity is preserved across the season anchor rollover
  • Outbreaks are linked to epidemics they occur during, based on pathogen, time overlap and geographic nesting (episodic_cluster_link)
  • Lattice suppression works across the outbreak/epidemic scale boundary

Changed

  • The navigation bar is four links and is visible at every width, with no collapsed menu anywhere in the app
  • Every screen is rendered once and shown or hidden, instead of being torn out and rebuilt from the database on every navigation
  • Navigation, pane and cluster state live in three data attributes on the app shell, and every highlight is derived from them by the stylesheet
  • All navigation behaviour moved into one cached script, replacing the inline event handlers on nav links, rail rows, cluster table rows and chips
  • The clusters screen’s three panes carry their own geometry, so each scrolls inside itself instead of scrolling the document
  • The rail row is a button beside its bulk-select checkbox rather than a div wrapped around it
  • The phone-tier pane switcher names the open cluster from the server’s selection instead of copying it out of the rail’s markup
  • Every navigation control is at least 44px on a touch viewport and answers a press before the screen changes
  • shiny is required at version 1.14.0 or newer, for observer-based output visibility tracking
  • The header navigation renders once per access state instead of on every navigation, so it no longer arrives with the screen it exists to navigate away from
  • The resolved YAML configuration is cached per file content, instead of being re-read, re-validated and re-merged at every episodic_geography_config() call
  • The denominator panel reads only the weeks it plots, rather than a pathogen’s whole recorded history
  • Weekly case counts are computed by binary search rather than a pass over every date per week
  • The demography baseline query is a correlated NOT EXISTS rather than a nested NOT IN
  • The Activity screen resolves a row’s cluster by lookup rather than by scanning every cluster per row
  • The open-cluster list is no longer invalidated by leaving the Clusters screen and coming back
  • Opening a cluster builds its cluster object once instead of three times, and its completion curve once instead of twice
  • A ward stream’s cases and an area stream’s postcodes are narrowed by the database rather than after every case of the pathogen has reached R
  • A run log line saying a component produced nothing it could have produced is marked as a warning, and one saying something failed outright as an error
  • The Activity screen’s run modal marks a failed or partial run, rows the run skipped, and a run that had no eligible stream to detect on

Fixed

  • Saving a cluster note that repeats the note already on file no longer records a version in which nothing changed, and an empty note on a cluster that never had one is not recorded at all
  • A cluster id that names nothing viewable no longer selects an absent cluster in the dossier
  • The epidemic dossier’s weekly curve errored instead of drawing, its weekly counts carrying no completeness flag
  • The epidemic dossier’s statistics stacked vertically instead of forming a row, from a class the stylesheet does not draw
  • The screen no longer stays behind when a cluster is opened from the Epidemics screen after the reader has returned to a screen the server already believed they were on
  • An outbreak’s “during” links no longer name epidemics that lattice suppression has folded into another

EpiSODIC 0.18.0

New

  • The first run against a database reports the whole case history it is given, so importing years of history fills the Archive instead of leaving an empty dashboard
  • Clusters a backfill opens whose last case is past reconciliation.stale_open_days close in that same run, and go straight to the Archive
  • episodic_run_cron(backfill =) overrides that decision; episodic_validate_detection() passes FALSE, so a replay still measures detection delay rather than an import
  • Schema version 4: episodic_cluster.opened_in_backfill and episodic_detection_run.is_backfill

Changed

  • The Performance screen’s time-to-detection excludes clusters a backfill opened, since their opened_at is the day the archive was imported
  • Farrington tests every week its streams can carry on a backfill run, rather than the max_weeks_tested catch-up cap
  • A run states the span of the case history it holds, and says outright when the newest case is older than every rule-based detector’s lookback window
  • same_place and rare_trigger report how many hits their lookback window kept out, so a run that reports nothing says which kind of nothing it found
  • The Farrington history-shortfall line says how many of the eligible streams it fitted on, not only how many it skipped

Fixed

  • The Pathogen screen wrote a pathogen’s serial interval and incubation range in R’s own marks rather than the session language’s
  • The Streams screen’s Farrington tooltip and a notification’s “and N more” line ignored the session language the same way

EpiSODIC 0.17.2

Changed

  • episodic_check_cases(stop_on_problem = TRUE) runs its checks once and throws on that report, instead of running the whole check suite twice

Fixed

  • episodic_run_cron() failed on any case data at all, reading the case data itself where the check report was expected

EpiSODIC 0.17.1

New

  • episodic_report_version_claim registers a report’s version number before the render rather than after it, so two concurrent renders of one cluster never collide
  • Schema version 3: episodic_report_version_claim, and a unique index on episodic_report_render(cluster_id, version_no)
  • episodic_format_number() writes every number a reader sees in the session language’s own marks, from four misc.decimal.mark/misc.thousands.* keys per language
  • Hindi numbers group the Indian way (12,34,567) and Spanish starts grouping at five digits (2000, but 12.345), both from those keys
  • misc.language.<code> names all eight languages in every language file, so a message can say “Dutch” where it used to say nl
  • EPISODIC_LANGUAGE takes regional variants: en-US writes January 7, 2025 and es-419 writes 1,234.5
  • en-GB and es-ES are accepted as names for en and es, and a region that is not shipped (nl-BE) now falls back to its language rather than to English
  • Four date.format.* keys per language, so German puts a point after the day, Spanish two “de”s in, and Chinese writes 2025年1月7日

Changed

  • A reporting delay that could not be measured is now NA rather than 0, and the epi curve, the pathogen curve, Rt and the doubling time each say so instead of assuming complete reporting
  • Timestamps on the dashboard and in reports are written in the session language rather than as %d-%m-%Y
  • The report’s similar-clusters table applies the same small-count suppression as the concentration table
  • The synthetic generators put the caller’s random stream back when they are done, instead of leaving their own behind
  • The Info screen names the language it renders in, with the code beside it, rather than printing the code alone
  • The unsupported-language warning lists each code with the language it stands for

Fixed

  • The dossier failed to render at all on a database whose last run recorded no package versions, taking eleven tests down with it
  • The dossier crashed on a cluster with no case linked to it, where the case-free tile compared against a day count that does not exist
  • Detector names were joined with the Dutch word “en” in all eight languages
  • The Streams screen’s Farrington tooltip was written in English on every language
  • An NA in an epi curve’s incomplete flag silently dropped that day’s bar from the chart
  • The Pathogen screen counted a comparison period that predates the database as zero cases rather than as no data
  • Chart axes wrote 1.234,5 in Arabic, Hindi and Chinese, which all write 1,234.5 with the digits this package renders
  • Dates were written day-month-year in every language, including the three that do not write them that way
  • The English translation wrote “License” where British English writes “licence”
  • Numbers on every screen ignored the session language: an R-formatted 1,234.5 reached a Dutch, German, French or Spanish reader unchanged
  • episodic_report_diff() and the scheduled-report mail picked their previous version by row order when two shared a version number
  • A failed file.copy() of a rendered report was recorded as a successful render
  • A synthetic window too short to produce a baseline case returned a list where a data frame was expected

EpiSODIC 0.17.0

New

  • episodic_validate_detection() replays generated history week by week and measures the detectors against the outbreaks that were injected into it
  • episodic_validate_comparator() runs a naive same-place rule or a 2-SD Shewhart limit over the same data, through the same metrics
  • episodic_validate_rethreshold() re-matches a validation result at different thresholds without replaying anything
  • episodic_synthetic_ground_truth() returns what episodic_synthetic_cases() injected, per outbreak and per case
  • episodic_synthetic_cases() gains outbreaks, for a history with none injected, and outbreak_offsets, to disperse them across the window
  • config$<detector>$enabled switches any of the four detectors off, and is part of config_hash
  • data-raw/validation/ runs the full multi-seed study and writes the numbers a paper would quote
  • CI runs the suite against a real MariaDB server, so DDL a server rejects fails the build
  • CI also runs the suite against a real MySQL server, which is a different server: the two disagree about column-level foreign keys

Fixed

  • The MariaDB schema was never accepted by a real server: the tables are declared in reading order, not foreign-key order, so creation failed on the first statement
  • On MySQL the schema created no foreign keys at all: inline column-level REFERENCES are parsed and discarded there, so every constraint was silently absent
  • episodic_db_exists() treated a schema shared with another application as an existing EpiSODIC database, so a first run refused and sent the operator to episodic_db_migrate(), which stamped it current with two of its twenty-four tables
  • episodic_db_runs() bound a double to LIMIT, which MySQL refuses, so the Activity screen failed on that dialect
  • Half the lattice resolved its geography from the run’s own configuration and half from EPISODIC_CONFIG, so a run given a config path silently matched no case to any geographic stream
  • episodic_db_create() leaked the connection it opened when refusing a database that already had tables
  • With EPISODIC_PC_PROVINCE_MAP unset, a run said the shipped Dutch province ranges were in use, which they no longer are
  • Scheduled reports, on-demand renders and config exports derived their output directory from dirname() of the database path, which for a MariaDB DSN turned the database password into a directory name on disk; a new report.output_dir config key is now required for a DSN and refuses outright when unset

EpiSODIC 0.16.0

New

  • episodic_db_migrate() brings an existing database up to the current schema; the schema now carries a recorded version and episodic_db_connect() refuses a database it does not recognise
  • config$geography names the whole-catchment code and the area-code rule, so the two coarsest lattice levels are no longer hardcoded to one country
  • config$same_place$lookback_days and config$rare_trigger$lookback_days bound how far back the rule-based detectors report hits
  • config$report$small_count_threshold is documented in the shipped defaults instead of only existing in code
  • An instance configuration is validated against the shipped defaults: an unknown key, a wrong type, or a null where a value is needed stops the run and names the key
  • episodic_add_user() gains must_change, for an account that does not need a forced password change
  • Refused sign-ins are recorded in episodic_app_login_failure with the username tried and which of the three reasons it was
  • The Activity screen gains category filter chips: assessments, closures, mutes, sign-ins, detection runs

Changed

  • BREAKING: access.require_login now defaults to true, so a freshly installed instance is closed to anonymous visitors
  • BREAKING: EPISODIC_CONFIG, EPISODIC_STYLE and EPISODIC_QUARTO_REPORT set to a path that does not exist are now errors rather than silent fallbacks
  • BREAKING: EPISODIC_GEO_DATA no longer falls back to the bundled Netherlands geometry; without it the dashboard shows its bar-chart fallback
  • BREAKING: EPISODIC_PC_PROVINCE_MAP no longer falls back to Dutch province ranges; without it the province level of the lattice stays empty
  • BREAKING: the whole-catchment region code and the GEBIED- area prefix are now config$geography, which changes every geographic stream key
  • BREAKING: episodic_demo() refuses a db_path that already exists and refuses a MariaDB DSN; overwrite = TRUE rebuilds a demo in place
  • An unsupported EPISODIC_LANGUAGE warns once and falls back to English instead of erroring on every render
  • The dashboard sets lang and dir on the document, and the stylesheet uses logical properties, so Arabic renders right to left
  • Mail headers are RFC 2047 encoded and message bodies base64 encoded, so non-ASCII subjects and reports survive every relay
  • episodic_demo() configures its geography through the documented environment variables rather than relying on built-in defaults
  • episodic_demo() dates its run from the last day a supplied cases extract covers, and says so; episodic_run_cron() still dates every run from the system date
  • An explicit YAML null now keeps its key during the configuration merge instead of removing it
  • Sign-in rows on the Activity screen are withheld from a reader who has not signed in
  • episodic_run_cron(debug = TRUE) no longer prints per-query SQL and encoding dumps; it reports phase and per-stream progress

Fixed

  • same_place and rare_trigger re-emitted every hit in the entire case history on every run, which reset runs_since_detected so close_after_runs could never fire and grew episodic_detection without bound
  • Farrington aggregated to the week containing run_date, testing a partial week against full-week baselines, so it was near-blind on every day except Sunday
  • episodic_run_cron(run_date = ...) was ignored by reconciliation, which judged staleness against the wall clock
  • An already-closed cluster was closed again on every subsequent run, filling episodic_cluster_state with duplicate closures
  • A closed cluster that was re-detected went on absorbing cases silently instead of returning to the board for reassessment
  • A merge into an already-assessed cluster did not flag it as changed since assessment
  • The forced password change was skipped for any account named demo whose password was demo
  • Lattice suppression could hide every detected cluster behind a manually added one, which holds no episodic_case rows so scored zero overlap with all of them
  • Lattice suppression could chain, putting a cluster behind one that was itself suppressed and showing the far end of the chain on no dossier at all
  • The reporting-completion curve skipped every lag at which nothing had arrived yet, so short lags read as far more complete than they were and the incompleteness zone sized from it was too narrow
  • Deduplication grouped on patient_key and pathogen glued together with no separator, so two patients whose keys and pathogens concatenate alike were treated as one and a positive was silently dropped
  • A suppression parent’s case list was re-read once per overlapping child
  • The institution activity feed compared the operator’s own institution_key against the stored hash of it, so every row was skipped and patient-day normalisation never engaged on any real deployment
  • A manual cluster’s detector agreement was scored out of one detector instead of four, so every manual cluster outranked a comparable detected one
  • The dossier narrative could state that positivity had risen, or stayed flat, for a cluster whose positivity was never measured at all
  • A first positive test was reported as positivity rising
  • episodic_html_escape() left single quotes unescaped, and every attribute in the notification HTML is single-quoted
  • same_place merged its hit windows on gaps in index rather than gaps in time, so two outbreaks years apart at one place became a single window spanning both
  • same_place and rare_trigger reported cases sampled after run_date, so a replayed run reported outbreaks from its own future
  • episodic_app_data_asof() took its date from a UTC timestamp and compared it against a local one, putting the incompleteness window a day out for the hours after local midnight, and read the wrong date entirely for a backfilled run

EpiSODIC 0.15.0

New

  • The assessment form gets a “close this cluster” checkbox, submitted alongside the classification, so closure is always a deliberate, individual decision rather than implied by a verdict
  • The checkbox is pre-ticked (with a highlighted outline) when artefact/expected variation is chosen, but can always be unticked to leave the cluster open for a second opinion
  • Submitting an assessment now asks for confirmation, naming the verdict and stating plainly whether it will close the cluster
  • A cluster explicitly closed and then reassessed shows a banner naming who closed it, when, and that new cases have come in since
  • reconciliation.autoclose_unassessed (default true) controls whether the cron auto-closes a cluster nobody ever assessed once it goes stale
  • The Archive now names who closed each cluster (a person’s full name, or “System” for an automatic closure)
  • A closed cluster’s assessment form is hidden behind a “Re-open” button instead of staying open for a new assessment, reachable the same way from the rail, the Archive, or any related/similar-clusters panel

Changed

  • No verdict, including artefact/expected variation, closes a cluster by itself any more - closure is always a separate, deliberate act, whether taken by a person or by the cron’s unassessed-and-stale rule
  • A cluster explicitly closed reopens into “Reassessment needed” the moment new cases arrive on its stream, rather than staying closed regardless
  • The "closable" cluster state is removed; a classified, unclosed cluster now reads as “Monitoring” regardless of verdict
  • The dossier’s large case-count stat now reads “Cases” instead of “Confirmed”
  • Cluster notes and the config-change audit log now show the acting user’s full name, never their login name

Fixed

  • The cluster notes history modal showed the login name instead of the full name
  • The scheduled report panel’s “set by” line showed the login name instead of the full name
  • The assessment form’s rationale field, close checkbox and submit button are now disabled until a verdict is picked, so recording an assessment without one is no longer possible

EpiSODIC 0.14.1

Changed

  • Chart panels now name the specific R package(s) at fault instead of showing the raw “Graphics API version mismatch” error when the graphics-rendering stack (e.g. ragg, systemfonts, textshaping) is installed but incompatible with the running R version, in all shipped languages
  • A distinct message covers the rarer case where no PNG graphics device works at all (a build-time R configuration issue), rather than wrongly telling the operator to reinstall a package

EpiSODIC 0.14.0

New

  • The dashboard now adapts to phone and tablet screens: the clusters screen’s rail goes off-canvas below 1200px, and below 768px shows one of rail, dossier or assessment at a time through a sticky segmented control that also names the open cluster
  • The header’s navigation collapses behind a menu button below 1200px
  • Tables, charts and controls throughout the dashboard get a horizontal scroll, larger tap targets and iOS-safe input sizing below 1200px

Changed

  • .episodic-body/.episodic-streams-screen no longer size themselves with a hardcoded calc(100vh - 44px); the app shell is now a column flex so the header can grow past its usual height without breaking either screen’s layout
  • The dossier’s interpretation/notes pair and the Pathogen screen’s care-line/institution pair now stack below 768px instead of staying side by side

EpiSODIC 0.13.2

Changed

EpiSODIC 0.13.1

Changed

  • Renamed the verloop.* and statusverloop.* translation keys to timeline.* and status_trajectory.*, and the matching episodic-verloop* CSS classes to episodic-timeline*: every translation key and CSS class name is now full English, regardless of language content

EpiSODIC 0.13.0

New

  • Clusters can now carry a scheduled-report subscription: an epidemiologist sets an every-N-days cadence and a list of recipient email addresses from the Reports panel, for colleagues without an EpiSODIC account (or only a viewer one) who need updates without opening the dashboard
  • episodic_run_cron() sends every due scheduled report after each run, as a self-contained HTML attachment through an already-configured, email-capable notification channel (SMTP, sendmail or Microsoft 365); every attempt, sent or failed, is logged
  • A schedule sends its first report on the very next cron run, then every N days after the last successful send; it stops automatically, after one final report, once its cluster closes, merges, or is suppressed
  • Every outbreak report now shows what changed since the previous version - new cases, and the priority score, ratio and case period deltas - in Quarto callout boxes
  • New vignette("scheduled-reports") documenting the cadence, closure and delivery rules

Changed

  • The dossier’s Reports panel gained a “Scheduled reports” section, gated to the epidemiologist role like every other write action on it

EpiSODIC 0.12.1

Changed

  • The notes panel has a History button showing every past version of the note as a word-level diff, with the saving user and timestamp per version
  • Saving a cluster note now only redraws the notes panel, instead of the whole dossier including every plot

EpiSODIC 0.12.0

New

  • Every cluster now has a notes panel next to the interpretation panel: a free-text, markdown-formatted scratchpad open to any signed-in role, stored as plain text and rendered on display
  • New episodic_add_manual_cluster() adds clusters detected by another algorithm or system, vectorised over any number of clusters in one call, never connected to this instance’s own case data - for output from another programming language or system, fed in as already-parsed R values

Changed

  • bslib, cli, commonmark, htmltools and jsonlite moved from Imports to Suggests: shiny (a hard Import) already Imports every one of them, so they were never an independent dependency in the first place
  • vignette("data-format") documents episodic_add_manual_cluster() in a new “Clusters from another system” section

EpiSODIC 0.11.0

New

  • New access.require_login configuration key (default false, keeping the current behaviour): when true, an anonymous visitor gets a sign-in prompt and nothing else, because the server renders no navigation, no status strip and no screen for them rather than hiding it client-side
  • The Settings screen has a read-only “Dashboard access” panel reporting which way access.require_login is set; it is deliberately not editable in-app, so a login wall cannot be switched off from inside the app by a single account
  • A cluster’s dossier can now be opened by URL: ?cluster=<id> on the dashboard address selects that cluster on the Clusters screen
  • New-cluster notifications now link each cluster id straight to its dossier when dashboard_url is configured
  • The geography panel and the outbreak report now name the province each postcode falls in, alongside the postcode itself
  • The Info screen has a “Reference data” panel reporting, per EPISODIC_* variable, whether the instance’s own file was read, was rejected, or was read and matched nothing, and what it delivered
  • The reference-data panel reports how many of the case data’s own postcodes resolve to a province, which is what distinguishes a EPISODIC_PC_PROVINCE_MAP that was never read from one whose postcode format does not match the case data
  • episodic_default_style.yaml (and EPISODIC_STYLE) now also carries typography: font (a CSS font-family stack) and font_size_base, which scales every other font size in the dashboard, since those are now expressed in rem relative to it
  • Every cluster table now has a “Case days” column: the number of distinct dates with at least one case, as its own figure next to the case count and the calendar duration - a cluster can run 90 days with cases on only 10 of them (a sharp peak) or on 39 of them (a flat, sustained rise), the same duration and case count, two different epidemic shapes
  • episodic_add_user() is now exported, so accounts can be provisioned from a script without reaching into the package’s internals

Changed

  • Trimmed the exported API surface to what an operator actually needs: episodic_care_lines(), episodic_case_columns(), episodic_config_hash(), episodic_config_resolve(), episodic_institution_types(), episodic_interpretation_slots(), episodic_provision_user(), episodic_resolve_data(), episodic_sex_codes(), episodic_tr() and episodic_validate_cases() are no longer exported (still used internally)
  • The shipped default config, style, pathogen and report files are renamed to self-explanatory names: default.yaml -> episodic_default_config.yaml, palette.yaml -> episodic_default_style.yaml, pathogen_config.csv -> episodic_default_pathogen_config.csv, cluster_report.qmd -> episodic_default_report.qmd
  • Every table of clusters (Archive, Pathogen screen, the dossier’s related and similar-clusters panels, the outbreak report and the new-cluster notification) now shares one implementation with the same columns in the same order: cluster id, then context, then the case period (first case - last case, as one range), cases, case days, then what was decided (classification/state/closure date/shared cases), then duration and priority
  • Every table of clusters is now sorted on the last case day, most recent first, with priority and cluster id breaking ties
  • A cluster row that does not open a dossier, because lattice suppression folded it into another, now shows its id in the warning colour with a tooltip explaining why, instead of a link that does nothing
  • The per-screen cluster table column translation keys are replaced by one column.* family shared by every cluster table
  • episodic_config_hash() now also excludes the access section, for the same reason it excludes notifications: it governs how the instance is operated, never what a run computes
  • EPISODIC_PC_PROVINCE_MAP pointing at a missing, unreadable, empty, wrongly-columned or duplicate-postcode CSV is now a run-stopping error naming the file and the problem, instead of a silent fall back to the shipped Dutch demo ranges
  • A detection run in which no postcode resolves to a province now says so in its trace, instead of leaving province-level detection silently empty
  • The dashboard’s area/province/region place label now shows region_code verbatim instead of cosmetically reformatting it, so an operator can see at a glance, character for character, whether EPISODIC_PC_PROVINCE_MAP resolved the code they expected, without risking a mangled real place name (e.g. the hyphen in “Noord-Holland”)

Fixed

  • Opening a cluster from a table row or a “linked to #N” chip now moves the rail’s own highlight to match; it used to leave the rail showing whichever cluster was selected before, because the rail deliberately does not re-render on every selection
  • Opening the dashboard via a ?cluster=<id> link no longer crashes with “Can’t access reactive value ‘url_search’ outside of reactive consumer”
  • episodic_demo() no longer leaves EPISODIC_CONFIG/EPISODIC_DB/EPISODIC_GEO_DATA permanently overwritten for the rest of the R session once it returns

EpiSODIC 0.10.3

Fixed

  • Email/notification messages for new clusters and run failures are now rendered in EPISODIC_LANGUAGE, in all 8 shipped languages, instead of always in English
  • The new-clusters email’s numeric columns are now horizontally centred, and its dates are formatted the same way the dashboard formats them
  • microsoft365 notifications under the cached-login flow (Option C) now honour from: when set, mail is sent from that mailbox via Microsoft365R::get_business_outlook(shared_mbox_email = from) instead of always sending as the signed-in account
  • Fixed a grepl(..., ignore.case = TRUE, fixed = TRUE) warning when matching a Microsoft 365 tenant against cached Azure AD logins; the match is now done case-insensitively without triggering the warning

EpiSODIC 0.10.2

New

EpiSODIC 0.10.1

Fixed

  • episodic_demo()’s startup message once again mentions “demo account”, after an unrelated styling commit reworded it away from that phrase

Changed

  • Redesigned the Settings screen: notification channels now sit in a compact wrapping card grid with a toggle switch per channel instead of full-width stacked blocks, and the read-only detection configuration is grouped by section with readable field labels instead of one dense grid of dotted, all-caps keys
  • Documentation updates

EpiSODIC 0.10.0

New

  • Implemented various ways to send notifications of new clusters: ntfy, SMTP, sendmail, Microsoft 365, Teams (Power Automate Workflow), Slack
  • Added vignette("notifications") with setup steps per channel
  • Added an is_admin account property and an in-app Settings screen for managing notification channels, dashboard accounts, and viewing (read-only) detection configuration, without needing server access
  • Added episodic_config_export() to export the resolved configuration as a zip file, with notification secrets masked by default

Fixed

  • Fixed pkgdown build failure from three undocumented notification topics

Changed

  • Documented every episodic_default_config.yaml config key with a short inline explanation
  • Every write action now re-resolves the signed-in account against the database immediately before writing (episodic_auth_refresh_user()), so deactivating or demoting an account from the Settings screen takes effect on its already-open sessions too, not only on its next sign-in
  • An is_admin account can no longer revoke its own admin access or deactivate its own account from the Settings screen (self-lockout guard)

EpiSODIC 0.8.17

Changed

  • Minor UI changes

EpiSODIC 0.8.16

Fixed

  • Fixed duplicate-key crash when an institution had two care lines: batch insert now keeps one row per key, matching prior per-row behaviour
  • Fixed two lattice streams with colliding concatenated grouping keys silently merging, causing one stream to disappear without error

Changed

  • Denominator feed now loads in one batched read/insert/update instead of a query and write per row (1,246 to 2 round trips for 623 rows)
  • Removed six dead functions superseded by batched writers or duplicated logic; tests now use episodic_test_institution() fixture helper

EpiSODIC 0.8.15

Changed

  • MEM’s min_seasons moved from a hard-coded function default into config (mem.min_seasons), keeping cron and app in agreement
  • Chart axes now use the reading language’s number format via episodic_chart_number_labels(), fixing Rt misread as e.g. “1.4” vs 1,400

Fixed

  • Removed three unused schema columns (revoked_at, left_at, last_login_at) describing capabilities that never existed
  • Removed two unused internal readers and two unused function arguments (institutions, level) that made call sites pass dead values

EpiSODIC 0.8.14

Fixed

  • Fixed n_cases counting wrong cases for ward/area-level clusters; now uses one shared function, episodic_db_cases_for_stream_id()
  • Fixed stream muting doing nothing; a run now actually suppresses new detections on muted streams and reports how many were suppressed
  • Reconciliation functions no longer swallow database errors via tryCatch(); only a genuinely missing stream still falls back safely

EpiSODIC 0.8.13

Fixed

  • Fixed Farrington detector being silently inactive on <4 years of history; now reports shortfall and farrington.b default lowered to 2

Changed

  • Removed unbounded episodic_reporting_triangle table; delay curve now derived on the fly from existing case/run data
  • episodic_detection_run gains a run_date column representing the run’s business date (distinct from started_at)
  • Batched remaining per-row database writes in a run; round trips dropped from 1,868 to 203 on a synthetic run
  • Streams screen baseline-window queries reduced from 52 per page to 3 via episodic_baseline_excluded_windows_many()

EpiSODIC 0.8.12

Changed

  • episodic_run_cron() now batches writes instead of row-by-row, cutting MariaDB round trips from 12,549 to 1,868 on a synthetic run
  • Note: chunked dbBind() is not a true batch; single multi-row statements via new episodic_db_write_many() are what collapse round trips

EpiSODIC 0.8.11

Changed

  • Pathogen, Archive and dossier similar-clusters screens now derive cluster state in bulk instead of per-cluster, fixing slow MariaDB loads
  • Query counts now flat rather than linear in cluster count (e.g. Pathogen screen: 259 to 7 queries on 63 clusters); output unchanged

EpiSODIC 0.8.10

Fixed

  • Fixed R session crash on MariaDB caused by re-entrant connection use when a scoring closure ran inside dbExecute(); now computed eagerly
  • Fixed institution ids read back as integer64 on MariaDB being corrupted to 0 on insert, collapsing institution-level streams together

Changed

  • MariaDB connections now explicitly set utf8mb4 character encoding instead of relying on server defaults

EpiSODIC 0.8.9

Changed

  • Reverted 0.8.2’s explicit NA-typing of cron-write defaults; column types already enforce this and the scaffolding proved unnecessary

EpiSODIC 0.8.8

Changed

  • episodic_run_cron(debug = TRUE) now logs SQL and bound parameters before each call, aiding the ongoing MariaDB crash investigation

EpiSODIC 0.8.7

Changed

  • Author list trimmed to its actual contributors and copyright holders: Matthijs Berends, Certe Medical Diagnostics & Advice Foundation, and University Medical Center Groningen (ROR-identified, same format as Certe)
  • Every language listing (README, vignettes, roxygen docs) now orders English first, then the rest alphabetically, instead of leading with Dutch
  • Geography is documented as operator-supplied throughout, not Netherlands-specific: the “Geographic reference data” section in vignette("data-format") is substantially expanded, now also covering EPISODIC_PC_PROVINCE_MAP (L4 stream detection) alongside the map data, and the FAQ’s geography entry does the same
  • vignette("overview")’s screen-by-screen section no longer stops at three screens (“a third screen closes the loop”) - it now covers all seven (Clusters, Pathogen, Performance, Archive, Streams, Activity, Info)
  • episodic_check_denominators() and episodic_check_institution_activity() are now mentioned alongside episodic_check_cases() everywhere the docs discuss checking input data before a run (vignette("data-format"), vignette("deployment"), vignette("faq"), README)

Fixed

  • care_line’s "third" (tertiary care) value was missing from every column-requirements table and roxygen doc listing its allowed values - episodic_care_lines already included it, the docs just did not

EpiSODIC 0.8.6

Changed

  • Reverted 0.8.5’s gc(full = TRUE) forcing in episodic_run_cron(debug = TRUE): a second lab reproduction, with that forcing in place, showed a gc() completing cleanly immediately after episodic_growth_slope() and then the crash landing on the very next call, episodic_spatial_concentration() - ruling out delayed-onset corruption from an earlier call and pointing at that one call on that specific data instead, which the extra gc() calls were never able to help with and were only slowing every run down (~600-1000ms added per stream, a ~5 minute run turning into “10-20 seconds on SQLite” by the lab’s own comparison)
  • episodic_spatial_concentration() (the isolated crash site) no longer uses table()/factor(), which sort their levels and so invoke locale-aware string collation - not guaranteed safe against a string whose declared encoding does not match its actual bytes, which is exactly what a client/server character-set mismatch can hand back from a MariaDB connection. Counts via tabulate(match(pc, unique(pc))) instead, which only ever hashes and byte-compares the strings themselves and sorts nothing but the resulting integer codes
  • episodic_run_cron(debug = TRUE) now dumps each reconciliation candidate’s pc values (encoding, validity, raw bytes) immediately before episodic_spatial_concentration() runs, in place of the removed gc() forcing - cheap (a handful of values at most) and aimed squarely at confirming or ruling out the character-encoding hypothesis above

EpiSODIC 0.8.5

Changed

  • episodic_run_cron(debug = TRUE) now forces a full garbage collection (gc(full = TRUE)) immediately after every database round trip inside the per-stream reconciliation loop. A fatal crash from corrupted memory typically surfaces later than its actual cause - whenever R’s own, otherwise lazily scheduled, garbage collector happens to stumble on the damage - so forcing one right after each call is what lets the trace log land on the actual culprit instead of a downstream symptom several calls away. Confirmed necessary: a lab reproduction of the ongoing MariaDB crash showed the trace stopping at a different line on each run (once after episodic_growth_slope(), once mid-episodic_app_density()) despite hitting the exact same stream and data, the signature of exactly this kind of delayed-onset corruption. Noisy and slow; debug = TRUE was already meant only for chasing a failure like this, not for routine runs

EpiSODIC 0.8.4

Fixed

  • episodic_db_truncate() against MariaDB/MySQL crashed with Error: bad_weak_ptr right after successfully truncating every table. on.exit(..., add = TRUE) appends to the end of the exit-handler list, so the handler that restores FOREIGN_KEY_CHECKS (registered after the connection’s own on.exit(dbDisconnect(con))) ran after the connection had already been closed - RMariaDB’s response to a query against a freed connection is a C++ abort, not a catchable R error. Now restores foreign-key checks explicitly right after truncating, with the on.exit handler only as a safety net for the error path, registered with after = FALSE so it runs before the disconnect even then

EpiSODIC 0.8.3

New

  • episodic_db_truncate(): empties every EpiSODIC table (including dashboard accounts) while keeping the schema itself, for a hard reset back to “freshly created, no data” without a schema migration. Refuses to run outside an interactive session, and requires typing the database’s own name back at a prompt before it deletes anything

EpiSODIC 0.8.2

Fixed

  • Every cron-side database write function’s optional parameters defaulted to a bare, untyped NA (R’s logical NA) rather than the column’s own type (NA_integer_, NA_real_, NA_character_). RSQLite coerces this silently; RMariaDB does not treat a logical NA bound to a non-logical column the same way as a properly typed one, which is a documented source of crashes rather than a catchable error. Most notably, episodic_db_detection_insert()’s cluster_id - untyped, and hit on every single detection insert in every run, since a detection is always written before a cluster is attached to it - now defaults to NA_integer_

EpiSODIC 0.8.1

Changed

  • episodic_run_cron(debug = TRUE) traces every sub-step inside a stream’s own reconciliation (triangle update, MEM/Farrington detection, baseline exclusion, population vector, trend caching, detection insert, and each reconciliation candidate’s priority-score components) instead of only the stream as a whole - narrows a crash or a stall down to the exact call inside a single stream’s processing, not just which stream

EpiSODIC 0.8.0

New

  • episodic_check_denominators() and episodic_check_institution_activity(): the same non-throwing pre-flight report episodic_check_cases() gives the case feed, now available for the optional positivity and hospital-activity feeds too
  • episodic_run_cron() now refuses on a malformed institution_activity feed before writing anything (when handed as a plain data frame), instead of only mid-transaction
  • episodic_run_cron() writes a timestamped message() at every phase of a run (config resolution, DB connect, each feed load, lattice enumeration, each detector, and each stream’s reconciliation), always on - useful in any interactive load-through and in a real cron job’s own log. A new debug = TRUE argument adds sessionInfo(), package/driver versions, memory snapshots, and per-stream detail on top, for chasing a failure (including a crashed session) that leaves no R-level error behind

Changed

  • L4 (province) stream derivation supports an operator-supplied PC-to-province lookup via EPISODIC_PC_PROVINCE_MAP, falling back to the bundled Northern Netherlands demo ranges as before - a deployment outside that demo region previously got no L4 streams at all, silently
  • Archive screen sorts by Period (descending) instead of by closing date
  • Dossier top card reorders to confirmed, unique patients, duration (new), doubling time, case-free days, priority, with ratio/incidence density (where shown) moved after priority
  • Pathogen screen’s detection parameter table shows a long source_ref reference in the “What it does” column instead of “Value”
  • Pathogen screen’s positivity chart no longer renders a percentage axis past 100%
  • Pathogen screen’s weekly incidence and season/year comparison charts share the same x-axis span for any period that falls within a single season/year
  • Geography map charts drop their fill legend (redundant with the on-map count labels) so the render height computed for the map’s own aspect ratio is not shortened by legend space, removing the whitespace above/below the map
  • .shiny-plot-output centres its contents vertically and horizontally; the geo-panel map group centres horizontally as well

EpiSODIC 0.7.0

This release includes a database schema migration (episodic_case gains a required lab_number column) - existing databases need to be recreated or migrated before running against this version.

New

  • Case data requirements gain a required lab_number column: your laboratory’s own specimen/culture number, distinct from source_key and, unlike it, not required to be unique - two rows may share one when a single culture yields more than one reported result. Shown on the line list alongside patient_key, replacing the previously-shown (and not useful) source_key
  • Similar-clusters panel on the dossier gains a cluster ID column and opens the same way every other cluster table does

Changed

  • episodic_db_case_insert_new() batches its existence check and insert into chunked round trips instead of one query pair per row, a large speedup for larger imports

EpiSODIC 0.6.*

New

  • Unassessed clusters auto-close once their last case day is older than config$reconciliation$stale_open_days (default 60 days)
  • Run-detail modal shows how many clusters a run auto-closed
  • Info screen shows the app’s version, description, license and website, read live from DESCRIPTION
  • Line list gains patient_key as its first column

Changed

  • Rail sorts by last case day (newest first) instead of priority score
  • Assessment rationale is now optional, not required
  • Rail’s bulk-select checkbox sits inline with the pathogen name
  • Version number moved from the navbar to the Info screen

Fixed

  • Modal close buttons (dead under Bootstrap 5)
  • Several Shiny performance issues in the rail and colour palette

EpiSODIC 0.5.*

New

  • episodic_check_cases() reports all case data problems at once, with row-level detail and fixes
  • Lattice suppression collapses one outbreak seen at multiple levels into a single dossier
  • Linked clusters show e.g. “Linked to #123” and a Related clusters panel
  • episodic_case_data documents every column’s type, nullability and accepted values
  • Deduplication now checks against the most recently stored episode, not just the current batch
  • “Isolate” renamed to “positive” throughout the app, reports and validation advice
  • New “Frequently asked questions” vignette
  • Rail cards show the cluster’s care line as a colour-coded chip (1st/2nd/3rd line) beside the pathogen name, and the cluster id in small type next to it
  • The geography panel shows a second, uncropped choropleth alongside the existing cropped one, for region-wide context
  • episodic_care_lines gains "third" (tertiary care)

Changed

  • Accounts now have exactly two roles: "epidemiologist" (read/write) and "viewer" (read-only)
  • episodic_validate_cases() reports all problems in one error, not just the first
  • episodic_run_cron() checks feeds before writing and stops cleanly on failure
  • Status strip and Activity screen show why a run failed
  • Farrington now retests every week since the last completed run, capped at 8 weeks
  • config$effect_size_floor now gates new-cluster candidates against borderline signals
  • episodic_synthetic_cases() demo data rebalanced to avoid spurious chance clusters
  • lang now defaults correctly to EPISODIC_LANGUAGE everywhere; unset means English
  • Empty strings in required columns are now treated as invalid, like NA
  • Cluster status chips render filled (colour background, white text) throughout, not just outlined
  • Rail card dates spell the month out when the range falls within a single month ("2-12 January 2025"), abbreviated otherwise ("12 Jan. - 4 Feb. 2025")
  • Rail card’s last line shows case count and priority score; ratio dropped
  • Bar chart labels widen from ~30 to ~50 characters before truncating, with a mouse-over tooltip for the full value

Fixed

  • Region-level streams no longer report the whole catchment’s counts under one region’s name
  • Cluster line lists no longer include unrelated cases from the same ward or catchment
  • Reconciliation no longer opens duplicate dossiers for an ageing cluster
  • Translation placeholders no longer break on backslashes
  • Epidemic curve thousands separator now follows the language by default
  • episodic_run_cron() no longer corrupts sample_date/receipt_date/period_start/period_end when the operator supplies them as Date columns (as episodic_case_data explicitly allows) - RSQLite was binding them by their raw numeric epoch value instead of YYYY-MM-DD text, so a later read failed with “character string is not in a standard unambiguous format”
  • episodic_check_cases()’s allowed-value messages (care_line, institution_type, sex) now quote each value, and its ?episodic_case_data references are clickable in terminals/RStudio that support it
  • episodic_db_create() against MySQL no longer fails with “BLOB/TEXT column can’t have a default value” [1101] - episodic_stream.denominator and episodic_case.care_line are now widened to VARCHAR under the mariadb dialect, since MySQL (unlike MariaDB and SQLite) rejects a DEFAULT on TEXT columns outright
  • episodic_db_create() against MySQL no longer fails with “BLOB/TEXT column … used in key specification without a key length” [1170] - every remaining TEXT column used in a PRIMARY KEY, composite PRIMARY KEY, or CREATE INDEX (pathogen, patient_key, sample_date, run_date, week_start, period_start, care_line, area_code) is now widened to a bounded VARCHAR under the mariadb dialect, since MySQL requires an explicit key length on TEXT/BLOB columns used as a key
  • episodic_denominators_load() against MySQL no longer fails to write a denominator row with no area stratum (area_code = NA, which episodic_validate_denominators() has always allowed) - episodic_denominator’s composite PRIMARY KEY included the nullable area_code, and MySQL implicitly forces every PRIMARY KEY column NOT NULL; it now has a surrogate denominator_id primary key with the natural key as a UNIQUE constraint instead, which permits multiple NULLs in both MySQL and SQLite
  • episodic_db_create() against MySQL no longer fails with “you have an error in your SQL syntax … near ‘trigger’” [1064] - trigger is a reserved word in MySQL; episodic_cluster_state.trigger and the raw SQL that reads/writes it are now backtick-quoted
  • episodic_detect_same_place() no longer hardcodes care_line = NA on every stream it creates or upserts - it is the detector responsible for the institution-level streams (LTC, out-of-hours, general practice) that lattice enumeration never creates on its own, and for hospital ward-level streams, so this had silently hidden the care line on almost every stream capable of showing one
  • Logo no longer shows an opaque white background outside the hexagon

EpiSODIC 0.4.0

Changed

  • Renamed data interface from “ingestion” to case-based naming (R/cases*.R, episodic_cases_load(), etc.), no deprecation shims
  • Documented data frames/tibbles as normal input; zero-argument functions still accepted
  • care_line now accepts NA, normalised to "unknown"
  • Corrected care_line’s documented values to first, second, other, unknown
  • A run that skips rows now finishes partial, not success
  • Skipped institution activity rows now warn with counts and unmatched keys
  • Denominators and institution activity validated like cases, with clear errors

New

  • Every case column documents type, nullability and permitted values
  • Exported episodic_care_lines, episodic_institution_types, episodic_sex_codes
  • episodic_validate_cases() enforces the full data requirements
  • episodic_detection_run records per-feed supply/dedup/insert/skip counts
  • Activity screen shows per-run arrival and skip counts

Fixed

  • Institution activity rows with unmatched keys no longer silently dropped
  • Missing translations for running/partial run statuses added in all languages

EpiSODIC 0.3.1

New

  • New Pathogen screen: weekly incidence, seasonal comparison, Rt, positivity, age/sex, geography, signals
  • MEM thresholds drawn against the season curve with intensity bands
  • Rt now estimated per pathogen, not only per cluster
  • mem detector documented on the Info screen

Fixed

  • expected, excess and ratio now correctly persisted and carried through reconciliation
  • Priority score now computes all seven components instead of defaulting most to zero
  • Missing components no longer drag the score down
  • Doubling time now uses a Poisson fit to daily counts instead of cumulative log-regression
  • Positivity now compares region-wide cases and tests over the same week
  • Reporting lag now measured from cluster’s last case day, not run date
  • Rt no longer overestimated for clusters younger than one serial interval
  • episodic_mem_status() no longer returns ambiguous NULL off-season
  • MEM now evaluates the last complete week and fits only on full seasons
  • Concentration now a share of cases with known postcode, not all cases
  • Demographic baseline now excludes the cluster being compared
  • Top navigation now correctly highlights the active screen

Changed

  • Choropleth cropped to relevant areas instead of the full reference dataset
  • Geography panel now spans the full dossier width
  • Standardised terminology to “pathogen” across all languages
  • Axis labels and legend text standardised to 11pt secondary grey
  • Season overlay stops at the current week instead of running flat to zero
  • Dutch terminology standardised to “casus”
  • case_free_days now translated instead of shown as a raw column name
  • Weekly charts now show ISO week number with month/year, thinning past ~18 months
  • Cluster ids now shown beside pathogen name in dossier and Pathogen screen
  • Pathogen screen’s cluster table renamed “Clusters in this period” and links through properly
  • Archive now links through and includes a cluster id column
  • Top navigation now follows links out of the Pathogen screen
  • Pathogen picker’s count now labelled as an all-time case count

EpiSODIC 0.3.0

New

  • Dashboard translated into Spanish, French, German, Mandarin, Hindi and Arabic, alongside Dutch and English
  • EPISODIC_LANGUAGE sets the default language, falling back to English

EpiSODIC 0.2.0

Changed

  • Documentation rewritten for an epidemiologist audience
  • Internal episode_ prefix renamed to episodic_ throughout

New

  • EPISODIC_DB accepts a mysql:// DSN for MariaDB/MySQL
  • Package version shown in the app header
  • README screenshots now ship in man/figures/

EpiSODIC 0.1.0

First development version.

  • Detection: lattice enumeration, five detectors, cluster reconciliation, cross-lattice suppression, baseline feedback
  • Interface: bilingual (NL/EN) Shiny app, dossier view, audit-trailed classification, Performance screen
  • Reporting: versioned Quarto outbreak reports with small-count suppression
  • SQLite backend, with optional EpiEstim/mem/quarto/sf
  • episodic_demo() runs the whole system against bundled synthetic data