diff --git a/README.md b/README.md
index 9fae8532..4c9df95e 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \
| Plugin | Description | Preview |
|--------|-------------|---------|
| [News Ticker](./plugins/news/) | RSS news headlines from ESPN, NCAA, custom sources | |
-| [Of The Day](./plugins/of-the-day/) | Daily quotes, Bible verses, word of the day | |
+| [Of The Day](./plugins/of-the-day/) | Daily quotes, Bible verses, word of the day |
|
### Integrations (2)
diff --git a/docs/assets/of-the-day/auto-fit.png b/docs/assets/of-the-day/auto-fit.png
new file mode 100644
index 00000000..9b0d79c7
Binary files /dev/null and b/docs/assets/of-the-day/auto-fit.png differ
diff --git a/docs/assets/of-the-day/categories.png b/docs/assets/of-the-day/categories.png
new file mode 100644
index 00000000..6840b7bf
Binary files /dev/null and b/docs/assets/of-the-day/categories.png differ
diff --git a/docs/assets/of-the-day/customization.png b/docs/assets/of-the-day/customization.png
new file mode 100644
index 00000000..5255d8ef
Binary files /dev/null and b/docs/assets/of-the-day/customization.png differ
diff --git a/docs/assets/of-the-day/hero.png b/docs/assets/of-the-day/hero.png
new file mode 100644
index 00000000..9daebdff
Binary files /dev/null and b/docs/assets/of-the-day/hero.png differ
diff --git a/docs/assets/of-the-day/panel-sizes.png b/docs/assets/of-the-day/panel-sizes.png
new file mode 100644
index 00000000..d3928324
Binary files /dev/null and b/docs/assets/of-the-day/panel-sizes.png differ
diff --git a/docs/assets/of-the-day/shots.json b/docs/assets/of-the-day/shots.json
new file mode 100644
index 00000000..46217260
--- /dev/null
+++ b/docs/assets/of-the-day/shots.json
@@ -0,0 +1,277 @@
+{
+ "plugin": "of-the-day",
+ "defaults": {
+ "width": 128,
+ "height": 32,
+ "scale": 6,
+ "freeze_time": "2026-09-02T16:00:00+00:00",
+ "config": {
+ "enabled": true,
+ "categories": {
+ "word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/word_of_the_day.json",
+ "display_name": "Word of the Day"
+ }
+ },
+ "category_order": [
+ "word_of_the_day"
+ ]
+ }
+ },
+ "shots": [
+ {
+ "name": "hero",
+ "height": 64,
+ "scale": 6
+ },
+ {
+ "name": "cat-english",
+ "config": {
+ "categories": {
+ "word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/word_of_the_day.json",
+ "display_name": "Word of the Day"
+ }
+ },
+ "category_order": [
+ "word_of_the_day"
+ ]
+ },
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "cat-slovenian",
+ "config": {
+ "categories": {
+ "slovenian_word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/slovenian_word_of_the_day.json",
+ "display_name": "Slovenian Word of the Day"
+ }
+ },
+ "category_order": [
+ "slovenian_word_of_the_day"
+ ]
+ },
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "fit-on",
+ "config": {
+ "auto_fit_text": true,
+ "customization": {
+ "body_text": {
+ "font_size": 12
+ }
+ }
+ },
+ "standalone": false,
+ "height": 64,
+ "scale": 6
+ },
+ {
+ "name": "fit-off",
+ "config": {
+ "auto_fit_text": false,
+ "customization": {
+ "body_text": {
+ "font_size": 12
+ }
+ }
+ },
+ "standalone": false,
+ "height": 64,
+ "scale": 6
+ },
+ {
+ "name": "col-default",
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "col-amber",
+ "config": {
+ "customization": {
+ "title_text": {
+ "text_color": [
+ 255,
+ 176,
+ 0
+ ]
+ },
+ "body_text": {
+ "text_color": [
+ 200,
+ 130,
+ 0
+ ]
+ }
+ }
+ },
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "col-cyan",
+ "config": {
+ "customization": {
+ "title_text": {
+ "text_color": [
+ 0,
+ 229,
+ 255
+ ]
+ },
+ "body_text": {
+ "text_color": [
+ 120,
+ 180,
+ 200
+ ]
+ }
+ }
+ },
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "col-big",
+ "config": {
+ "customization": {
+ "title_text": {
+ "font_size": 10
+ },
+ "body_text": {
+ "font_size": 8
+ }
+ }
+ },
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "size-64x32",
+ "width": 64,
+ "height": 32,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "size-128x32",
+ "width": 128,
+ "height": 32,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "size-128x64",
+ "width": 128,
+ "height": 64,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "size-256x32",
+ "width": 256,
+ "height": 32,
+ "scale": 4,
+ "standalone": false
+ }
+ ],
+ "composites": [
+ {
+ "name": "categories",
+ "columns": 1,
+ "cells": [
+ {
+ "shot": "cat-english",
+ "label": "word_of_the_day",
+ "sublabel": "the bundled English list, 365 entries"
+ },
+ {
+ "shot": "cat-slovenian",
+ "label": "slovenian_word_of_the_day",
+ "sublabel": "the bundled Slovenian list"
+ }
+ ]
+ },
+ {
+ "name": "auto-fit",
+ "columns": 1,
+ "cells": [
+ {
+ "shot": "fit-on",
+ "label": "auto_fit_text: true",
+ "sublabel": "the default; body font_size 12 is stepped down until the whole definition fits"
+ },
+ {
+ "shot": "fit-off",
+ "label": "auto_fit_text: false",
+ "sublabel": "held at 12, so the text is cut to the lines that fit and ellipsized"
+ }
+ ]
+ },
+ {
+ "name": "customization",
+ "columns": 2,
+ "cells": [
+ {
+ "shot": "col-default",
+ "label": "The defaults",
+ "sublabel": "white title, grey body"
+ },
+ {
+ "shot": "col-amber",
+ "label": "Amber",
+ "sublabel": "title_text.text_color and body_text.text_color"
+ },
+ {
+ "shot": "col-cyan",
+ "label": "Cyan",
+ "sublabel": ""
+ },
+ {
+ "shot": "col-big",
+ "label": "Larger sizes",
+ "sublabel": "title_text.font_size 10, body_text.font_size 8"
+ }
+ ]
+ },
+ {
+ "name": "panel-sizes",
+ "columns": 1,
+ "cells": [
+ {
+ "shot": "size-64x32",
+ "label": "64 x 32",
+ "sublabel": "only the title and a fragment fit"
+ },
+ {
+ "shot": "size-128x32",
+ "label": "128 x 32",
+ "sublabel": "title plus one truncated body line"
+ },
+ {
+ "shot": "size-128x64",
+ "label": "128 x 64",
+ "sublabel": "the definition wraps and fits in full"
+ },
+ {
+ "shot": "size-256x32",
+ "label": "256 x 32",
+ "sublabel": "a long chain; width buys a longer body line"
+ }
+ ]
+ }
+ ]
+}
diff --git a/plugins.json b/plugins.json
index f900d57c..e383ff41 100644
--- a/plugins.json
+++ b/plugins.json
@@ -653,10 +653,10 @@
"plugin_path": "plugins/of-the-day",
"stars": 0,
"downloads": 0,
- "last_updated": "2026-08-06",
+ "last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
- "latest_version": "1.4.1"
+ "latest_version": "1.4.2"
},
{
"id": "olympics",
diff --git a/plugins/of-the-day/README.md b/plugins/of-the-day/README.md
index 9300bf12..58f39801 100644
--- a/plugins/of-the-day/README.md
+++ b/plugins/of-the-day/README.md
@@ -1,286 +1,360 @@
-# Of The Day Display Plugin
+# Of The Day
-Display daily featured content like Word of the Day, Bible verses, or custom daily items with automatic rotation and multi-category support.
+A different word, quote, verse or fact each day, from JSON files you control.
+Ships with an English and a Slovenian word-of-the-day list, and takes any
+number of your own.
-## Features
+
-- **Multiple Categories**: Word of the Day, Bible verses, custom content
-- **Automatic Daily Updates**: Loads new content each day
-- **Rotating Display**: Alternates between title and content
-- **Multi-line Text Wrapping**: Handles long definitions
-- **Configurable Data Sources**: Load from JSON files
-- **Category Rotation**: Cycles through enabled categories
+*Every image in this README is real plugin output, rendered at the true panel
+size against a frozen clock — "Opportune" is genuinely the entry for 2 September
+in the bundled list. Nothing here is a mockup.*
-## Configuration
+---
-### Web UI Configuration
+## Table of Contents
-This plugin is fully configurable through the LEDMatrix web interface:
+1. [What's On Screen](#whats-on-screen)
+2. [Installation](#installation)
+3. [Adding a Category](#adding-a-category)
+4. [The Data File Format](#the-data-file-format)
+5. [Configuration Reference](#configuration-reference)
+ - [Rotation and timing](#rotation-and-timing)
+ - [auto_fit_text](#auto_fit_text)
+ - [Fonts and colours](#fonts-and-colours)
+6. [Panel Sizes](#panel-sizes)
+7. [Troubleshooting](#troubleshooting)
+8. [Development](#development)
+9. [Support](#support)
-1. Open the LEDMatrix web interface (`http://your-pi-ip:5000`)
-2. Open the **Plugin Manager** tab and install **Of The Day Display**
- from the **Plugin Store** section if it isn't already
-3. Open the **Of The Day Display** tab in the second nav row
-4. Adjust settings using the auto-generated form (enable/disable
- categories, update intervals, rotation timings, display duration)
-5. Click **Save**
+---
-The web UI form is generated directly from
-[`config_schema.json`](config_schema.json), including the nested
-category configurations with collapsible sections.
+## What's On Screen
-### Example Configuration
+A title, underlined, with body text beneath it:
-```json
-{
- "enabled": true,
- "update_interval": 3600,
- "display_rotate_interval": 20,
- "subtitle_rotate_interval": 10,
- "category_order": ["word_of_the_day", "bible_verse_of_the_day"],
- "categories": {
- "word_of_the_day": {
- "enabled": true,
- "data_file": "of_the_day/word_of_the_day.json",
- "display_name": "Word of the Day"
- },
- "bible_verse_of_the_day": {
- "enabled": false,
- "data_file": "of_the_day/bible_verse_of_the_day.json",
- "display_name": "Bible Verse"
- }
- },
- "display_duration": 40
-}
+```text
+ Opportune <- title, from the entry's "title"
+──────────────────
+ Well-chosen or <- body, from "subtitle" then "description"
+favorable or appropriate
```
-### Configuration Options
+The body rotates between the entry's `subtitle` and `description` every
+`display_rotate_interval` seconds, so a definition and an example sentence both
+get their turn without needing a taller panel.
-- `enabled`: Enable/disable the plugin
-- `update_interval`: Seconds between checking for new day (default: 3600)
-- `display_rotate_interval`: Seconds between category rotations (default: 20)
-- `subtitle_rotate_interval`: Seconds between title/content rotation (default: 10)
-- `auto_fit_text`: Shrink the body font automatically when a long definition or subtitle can't fit the panel at the configured size (default: true). Wrapping is font-aware either way — line breaks and line count follow the actual font metrics, so custom fonts/sizes wrap correctly. Text that can't fit even at the smallest size is shortened with `...`
-- `category_order`: Order to display categories
-- `categories`: Dictionary of category configurations
-- `display_duration`: Total display duration in seconds
+With more than one category configured, the plugin also rotates between
+categories — a word today, a quote next turn — in `category_order`.
-## Plugin Structure
+---
-This plugin is fully self-contained. All data files are stored within the plugin directory:
+## Installation
-```
-of-the-day/
-├── manifest.json
-├── config_schema.json
-├── manager.py
-├── requirements.txt
-├── README.md
-└── of_the_day/ # Data directory
- ├── word_of_the_day.json
- ├── slovenian_word_of_the_day.json
- └── bible_verse_of_the_day.json
-```
+**From the Plugin Store (recommended).** Open the LEDMatrix web interface at
+`http://:5000`, go to **Plugin Manager**, find **Of The Day** in the
+**Plugin Store** section, and click **Install**.
-When you download/install this plugin, everything it needs is included. When you delete it, no traces remain.
+**Manually.** Copy this directory into your LEDMatrix `plugin-repos/` and
+restart the display service.
-## Data File Format
+> **A fresh install shows "No Data".** Two things are off by default:
+> `enabled` is `false`, and — more surprisingly — **the `categories` block is
+> empty**. The bundled data files exist, but nothing points at them until you
+> add a category. See below.
-Each category uses a JSON file with daily entries:
+---
-### Word of the Day Format
+## Adding a Category
-```json
-{
- "2025-10-11": {
- "word": "Ephemeral",
- "pronunciation": "ih-FEM-er-uhl",
- "type": "adjective",
- "definition": "Lasting for a very short time; transitory",
- "example": "The beauty of cherry blossoms is ephemeral, lasting only a week or two."
- }
-}
-```
+The easiest route is the plugin's own tab in the web interface, which lists the
+bundled files, lets you upload your own, and writes the config for you.
-### Bible Verse Format
+By hand, a category is an entry in the `categories` object keyed by an id of
+your choosing:
```json
{
- "2025-10-11": {
- "title": "John 3:16",
- "content": "For God so loved the world that he gave his one and only Son...",
- "reference": "John 3:16 (NIV)"
+ "of-the-day": {
+ "enabled": true,
+ "categories": {
+ "word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/word_of_the_day.json",
+ "display_name": "Word of the Day"
+ }
+ },
+ "category_order": ["word_of_the_day"]
}
}
```
-### Custom Format
+| Key | Type | Default | What it does |
+|-----|------|---------|--------------|
+| `enabled` | boolean | `true` | Include this category in the rotation |
+| `data_file` | string | — | Path to the JSON file, relative to the plugin directory |
+| `display_name` | string | — | Label shown in the configuration UI |
+
+Two lists ship with the plugin:
-You can create custom categories with any structure containing:
-- `word` or `title`: Main heading
-- `definition` or `content` or `text`: Main content
-- Optional: `pronunciation`, `type`, `example`, etc.
+
-## Usage
+| File | Contents |
+|------|----------|
+| `of_the_day/word_of_the_day.json` | An English word a day, with definition and example |
+| `of_the_day/slovenian_word_of_the_day.json` | A Slovenian word a day, with its English meaning |
-### Adding New Categories
+`category_order` controls the order categories are shown in. A category listed
+in `category_order` but missing from `categories` is skipped, and one present
+in `categories` but absent from `category_order` is appended after the
+listed ones.
-1. Create a JSON data file in the plugin's `of_the_day/` directory
- - Path: `plugins/of-the-day/of_the_day/my_custom.json`
-2. Add entries for each date in YYYY-MM-DD format
-3. Add category to config:
+---
+
+## The Data File Format
+
+A data file is a JSON object keyed by **day of the year as a string**, `"1"`
+through `"365"`:
```json
{
- "categories": {
- "my_custom_category": {
- "enabled": true,
- "data_file": "of_the_day/my_custom.json",
- "display_name": "My Daily Item"
- }
+ "1": {
+ "title": "Abstruse",
+ "subtitle": "Difficult to understand; obscure",
+ "description": "The philosopher's work was filled with abstruse concepts."
},
- "category_order": ["my_custom_category", "word_of_the_day"]
+ "245": {
+ "title": "Opportune",
+ "subtitle": "Well-chosen or particularly favorable or appropriate",
+ "description": "She waited for an opportune moment to raise the subject."
+ }
}
```
-### Display Behavior
+| Field | Shown as |
+|-------|----------|
+| `title` | The underlined heading |
+| `subtitle` | The first body line — a definition or short gloss |
+| `description` | The second body line, rotated in after `display_rotate_interval` |
+
+Selection is by calendar date, not random: day 245 of any year shows entry
+`"245"`. That makes the display stable — it will not change halfway through the
+day — and means a file needs 365 entries for full coverage. Missing days are
+skipped rather than erroring.
+
+Because it is keyed by day rather than date, the same file works every year.
+For a leap year, day 366 has no entry unless you add one.
+
+> Earlier documentation described these files as keyed by `YYYY-MM-DD`. They are
+> not — the bundled files and the loader both use the day-of-year number. A file
+> written with date keys will load without error and then show nothing, because
+> no key ever matches.
+
+### Writing your own list
+
+- **Keep the subtitle short.** It is the line most often shown, and on a 128×32
+ panel roughly 24 characters survive before truncation. The description can be
+ longer since it only needs to fit when it takes its turn.
+- **Use the same fields throughout.** An entry missing `subtitle` or
+ `description` simply shows less; it does not fall back to another field.
+- **Cover all 365 days** if you want the category to appear every day. Gaps are
+ skipped, so a sparse file means the category quietly disappears on the missing
+ days.
+- **Generate rather than type.** The format is a flat JSON object, so a
+ dictionary API, an RSS feed or a spreadsheet export can all be turned into one
+ with a short script. Nothing in the plugin cares how the file was produced —
+ drop the result in via the file manager.
+
+---
+
+## Configuration Reference
+
+| Option | Type | Default | What it does |
+|--------|------|---------|--------------|
+| `enabled` | boolean | `false` | Whether the plugin runs at all |
+| `categories` | object | *(empty)* | Your categories — see [above](#adding-a-category) |
+| `category_order` | array | `["word_of_the_day", "slovenian_word_of_the_day"]` | Display order |
+| `display_duration` | number | `40` | Seconds each category holds the panel |
+| `display_rotate_interval` | number | `20` | Seconds between body elements within a category |
+| `subtitle_rotate_interval` | number | `10` | Seconds between subtitle variants |
+| `update_interval` | integer | `3600` | Seconds between checks for a new day |
+| `auto_fit_text` | boolean | `true` | Shrink the body font so long text fits |
+| `customization` | object | *(defaults)* | Fonts, sizes, colours and offsets |
+| `file_manager` | — | — | Not a setting; the data-file widget in the web UI |
+
+### Rotation and timing
+
+Three intervals stack, from slowest to fastest:
+
+- **`display_duration`** (default `40`) — how long the whole category holds the
+ panel before the display controller moves on.
+- **`display_rotate_interval`** (default `20`) — within that turn, how often the
+ body swaps between the entry's subtitle and description. At the defaults, a
+ 40-second turn shows each of the two for 20 seconds.
+- **`subtitle_rotate_interval`** (default `10`) — how often the subtitle line
+ itself cycles, for entries carrying more than one.
+
+If you shorten `display_duration` below `display_rotate_interval`, the second
+body element never appears — the turn ends first.
+
+`update_interval` is only a check for the date rolling over. Since entries
+change once a day, the hourly default is already far more often than needed.
+
+### `auto_fit_text`
+
+When the body text will not fit at the configured size, this steps the font
+down until it does — largest size that fits wins. Only scalable fonts can
+shrink; a bitmap font is left alone. Text that still cannot fit at the smallest
+size is cut to the lines that fit, with the last one ellipsized.
+
+
+
+It has no visible effect when the text already fits, or when the panel has room
+for only one body line whatever the size — on a 128×32 panel the result is the
+same either way. It earns its keep on taller panels and with larger body fonts.
+
+### Fonts and colours
+
+Two elements are styled independently under `customization`: `title_text` and
+`body_text`.
+
+
+
+| Element | `font` | `font_size` | `text_color` |
+|---------|--------|-------------|--------------|
+| `title_text` | `PressStart2P-Regular.ttf` | `8` (4–16) | `[255, 255, 255]` |
+| `body_text` | `4x6-font.ttf` | `6` (4–12) | `[200, 200, 200]` |
+
+```json
+{
+ "customization": {
+ "title_text": { "text_color": [255, 176, 0], "font_size": 10 },
+ "body_text": { "text_color": [200, 130, 0], "font_size": 8 }
+ }
+}
+```
-The plugin rotates through:
-1. **Category rotation**: Changes every `display_rotate_interval` seconds
-2. **Content rotation**: Within each category, alternates between:
- - Title/word display with category name
- - Definition/content display with word wrapping
+> **The key names are `font_size` and `text_color`.** The schema declares these
+> elements in an `x-style-elements` block that uses the short names `size` and
+> `color`, but that is the *declaration* format — the core expands it into the
+> config properties `font`, `font_size` and `text_color`. Writing `size` or
+> `color` in your config is silently ignored, with no warning and no visible
+> change. It is an easy mistake to make from reading the schema.
-### Built-in Categories
+Both elements also accept `x_offset` and `y_offset` for nudging position.
-**word_of_the_day**
-- Displays vocabulary words with definitions
-- Shows pronunciation and part of speech
-- Great for learning and education
+Customization needs a LEDMatrix core with the element-style system. On an older
+core the section is not offered and the classic styling above is used, which is
+why an untouched config renders identically either way.
-**bible_verse_of_the_day**
-- Displays daily Bible verses
-- Shows reference and translation
-- Perfect for inspiration
+---
-**slovenian_word_of_the_day**
-- Example of foreign language words
-- Can be adapted for any language
+## Panel Sizes
-## Plugin Isolation
+
-This plugin is designed to be completely self-contained:
+Height matters far more than width here, because the body wraps:
-- ✅ All data files stored in plugin directory
-- ✅ No external dependencies on main project structure
-- ✅ Can be installed/uninstalled cleanly
-- ✅ No traces left after deletion
-- ✅ Easy to backup (just copy the plugin folder)
-- ✅ Easy to share (distribute the entire plugin directory)
+- **64×32** fits the title and a fragment of the definition.
+- **128×32** fits the title and one truncated body line.
+- **128×64** is where the plugin comes into its own — the definition wraps and
+ fits in full.
+- **256×32** buys a longer single body line, but is still one line.
-Data files are automatically loaded from `of_the_day/` subdirectory within the plugin.
+If the definitions matter to you, 64 rows is worth more than 256 columns.
-## Creating Data Files
+---
-### Template Structure
+## Troubleshooting
-```json
-{
- "2025-01-01": {
- "word": "Example",
- "definition": "A representative form or pattern",
- "pronunciation": "ig-ZAM-puhl",
- "type": "noun",
- "example": "This is an example sentence."
- },
- "2025-01-02": {
- "word": "Another",
- "definition": "One more; an additional",
- ...
- }
-}
-```
+**It says "No Data".**
+The most likely cause is that no category is configured — the `categories`
+block is empty on a fresh install even though the data files ship with the
+plugin. Add one from the plugin's tab in the web UI, or by hand as
+[shown above](#adding-a-category).
-### Tips for Creating Content
+**Nothing appears at all.**
+`enabled` defaults to `false`.
-1. **Date Format**: Always use YYYY-MM-DD
-2. **Keep it Short**: Definitions should fit on 4-5 lines
-3. **Clear Pronunciation**: Use phonetic spelling
-4. **Good Examples**: Make them memorable and relevant
-5. **Consistent Structure**: Use same fields across all entries
+**A category is configured but never shows.**
+Check its own `enabled` flag, that `data_file` resolves relative to the plugin
+directory, and that the file has an entry for today's day-of-year number.
-## Display Examples
+**The definition is cut off.**
+That is the panel height. Turn on `auto_fit_text` (it is on by default), reduce
+`body_text.font_size`, or use a taller panel — see
+[Panel Sizes](#panel-sizes).
-### Title Display
-```
- Word of the Day
-
- Ephemeral
-
- (ih-FEM-er-uhl)
-```
+**I changed a colour or size and nothing happened.**
+Check the key names: they are `font_size` and `text_color`, not `size` and
+`color`. See [Fonts and colours](#fonts-and-colours).
-### Content Display
-```
-Lasting for a very
-short time; transitory
-The beauty of cherry
-blossoms is ephemeral
-```
+**The example sentence never appears.**
+`display_duration` is probably shorter than `display_rotate_interval`, so the
+turn ends before the body rotates. Raise the former or lower the latter.
-## Advanced Usage
+**The word did not change at midnight.**
+`update_interval` decides how often the date is re-checked; at the default it
+can be up to an hour late.
-### Multiple Data Sources
+---
-You can maintain separate data files for different purposes:
-- Educational words
-- Technical terms
-- Inspirational quotes
-- Historical facts
-- Daily trivia
+## Development
-### Automated Updates
+### Project structure
-Data files can be updated automatically:
-- From APIs (Merriam-Webster, etc.)
-- From RSS feeds
-- From custom scripts
-- From database exports
+```text
+of-the-day/
+├── manifest.json # Plugin metadata and version history
+├── manager.py # OfTheDayPlugin
+├── config_schema.json # Settings schema; source of truth for defaults
+├── of_the_day/ # Bundled data files
+│ ├── word_of_the_day.json
+│ └── slovenian_word_of_the_day.json
+├── scripts/ # Backend actions for the web UI file manager
+├── web_ui/ # The file-manager interface
+├── test/ # Harness config and golden images
+└── README.md
+```
-## Troubleshooting
+The `scripts/` directory holds the actions the file manager calls —
+`list_files`, `get_file`, `save_file`, `create_file`, `delete_file`,
+`upload_file`, `toggle_category` and `update_config`. They are invoked by the
+web UI rather than run directly.
-**No content displayed:**
-- Verify data file path is correct
-- Check that today's date exists in JSON file
-- Ensure category is enabled in config
-- Check file permissions
+### Tests
-**Content cut off:**
-- Reduce text length in data file
-- Adjust display dimensions
-- Shorten definitions or examples
+```bash
+python plugins/of-the-day/test_text_fitting.py
+python plugins/of-the-day/test_element_styles.py
+```
-**Category not showing:**
-- Verify it's in `category_order`
-- Check that `enabled` is `true`
-- Ensure data file exists and is valid JSON
+### Regenerating the images in this README
-**Wrong date content:**
-- Check system date/time
-- Verify date format in JSON (YYYY-MM-DD)
-- Check timezone settings
+```bash
+python scripts/render_docs_assets.py --plugin of-the-day
+```
-## Integration with Education
+`--check` verifies the committed images still match what the plugin renders.
+The clock is frozen in the shot list, which is what pins the entry to
+"Opportune" — without that the images would change daily.
-Perfect for:
-- Vocabulary building
-- Language learning
-- Daily inspiration
-- Historical facts
-- Science concepts
-- Programming terms
+---
-## License
+## Support
-GPL-3.0 License - see main LEDMatrix repository for details.
+- YouTube:
+- Instagram:
+- Discord:
+- Sponsor: [GitHub Sponsors](https://github.com/sponsors/ChuckBuilds) ·
+ [Buy Me a Coffee](https://buymeacoffee.com/chuckbuilds) ·
+ [Ko-fi](https://ko-fi.com/chuckbuilds/)
+Released under the GNU General Public License v3.0 — see [LICENSE](LICENSE).
diff --git a/plugins/of-the-day/manifest.json b/plugins/of-the-day/manifest.json
index 61c589a4..c8fcf6c4 100644
--- a/plugins/of-the-day/manifest.json
+++ b/plugins/of-the-day/manifest.json
@@ -1,7 +1,7 @@
{
"id": "of-the-day",
"name": "Of The Day Display",
- "version": "1.4.1",
+ "version": "1.4.2",
"author": "ChuckBuilds",
"description": "Display daily featured content like Word of the Day, Bible verses, or custom daily items. Supports multiple categories with rotating display and configurable data sources.",
"category": "information",
@@ -21,6 +21,13 @@
"of_the_day"
],
"versions": [
+ {
+ "version": "1.4.2",
+ "released": "2026-09-02",
+ "ledmatrix_min_version": "2.0.0",
+ "notes": "Documentation only, no behaviour change. Rewrites the README around real rendered screenshots and documents all 13 settings. Leads with the reason a fresh install shows 'No Data': the categories block is empty by default, so the bundled data files exist but nothing points at them until a category is added. Corrects the data-file format, which the old README described as keyed by YYYY-MM-DD -- the files and the loader both use the day-of-year number, so a file written with date keys loads cleanly and then shows nothing. Documents that the customization keys are font_size and text_color, not the size and color used by the schema's x-style-elements declaration; the short names are silently ignored. Also separates the three rotation intervals and notes that auto_fit_text only has a visible effect where shrinking can gain a line.",
+ "changelog": "Documentation only, no behaviour change. Rewrites the README around real rendered screenshots and documents all 13 settings. Leads with the reason a fresh install shows 'No Data': the categories block is empty by default, so the bundled data files exist but nothing points at them until a category is added. Corrects the data-file format, which the old README described as keyed by YYYY-MM-DD -- the files and the loader both use the day-of-year number, so a file written with date keys loads cleanly and then shows nothing. Documents that the customization keys are font_size and text_color, not the size and color used by the schema's x-style-elements declaration; the short names are silently ignored. Also separates the three rotation intervals and notes that auto_fit_text only has a visible effect where shrinking can gain a line."
+ },
{
"version": "1.4.1",
"released": "2026-08-06",
@@ -66,7 +73,7 @@
"ledmatrix_min": "2.0.0"
}
],
- "last_updated": "2026-08-06",
+ "last_updated": "2026-09-02",
"stars": 0,
"downloads": 0,
"verified": true,