VFOX-DT Ticker System — Design Overview
---------------------------------------
This document describes the architecture for the automated and manual ticker
system used in OBS. The ticker page is responsible for:

- Auto-activating when NOAA/EC alerts exist
- Auto-deactivating when alerts expire
- Allowing manual override for program interruptions, schedule changes, and promos
- Displaying smooth CSS-based scrolling text
- Remaining cross-platform (Windows, Linux, macOS)
- Running independently inside OBS browser sources



---------------------------------------
SECTION: OBS Broadcast Chain Overview
---------------------------------------
Purpose:  
Clarifies which HTML components are intended to appear on-air inside OBS, and which components are strictly for operator control. This prevents confusion during future upgrades or multi-region deployments.

Core idea:  
Only presentation components go into OBS.
Control components never appear on-air.

Files Loaded Into OBS (On-Air Graphics)
These are the only files that should ever be added as OBS Browser Sources:

map.html — Displays the county map, alert colours, and alert summaries.

ticker.html — Displays the scrolling text crawl (auto alerts or manual messages).

Both pages:

run independently

hide their manual controls when OBS is detected

update automatically

respond to displayMode and manualOverride

Files Not Loaded Into OBS (Off-Air Control)
These files must never be added to OBS:

control.html — The master control panel for display mode, manual text, and notifications.

config files (optional) — Region definitions, county lists, or shared JSON.

development/test pages — Anything used for debugging or previewing components.

These pages:

contain operator UI

may include buttons, text boxes, or debug output

are intended for browser use only

should remain hidden from OBS at all times

Broadcast Chain Summary
control.html → Operator chooses what goes on‑air

map.html → OBS shows the visual geography

ticker.html → OBS shows the scrolling crawl

OBS Scene → Combines map + ticker based on displayMode

This mirrors a real broadcast workflow:
Control room → Graphics engines → On‑air output

---------------------------------------
SECTION: Notification Triggers (Producer Alerts)
------------------------------------------------
Purpose:
Alerts the operator when new NOAA/EC alerts are detected, without automatically
activating the ticker or map. This allows the operator to decide whether to show
the ticker, the map, both, or neither.

Use When:
You want to be notified of weather alerts, but retain full manual control over
what appears in OBS.

Core idea:
Automatic detection, manual activation. The system notifies you, but you choose
what goes on-air.

State Variables:
let alertsExist = false;        // True if NOAA/EC alerts are active
let notifyPending = false;      // True if new alerts arrived since last check
let displayMode = "none";       // "none", "map", "ticker", "both"

Notification Logic:
JS:
function checkForAlerts() {
    const previous = alertsExist;
    alertsExist = (noaaAlertsExist || ecAlertsExist);

    // If alerts now exist and previously did not, trigger a notification
    if (alertsExist && !previous) {
        notifyPending = true;
        showNotification("Weather alerts detected. Choose display mode.");
    }
}

Notification UI:
HTML:
<div id="notification" class="hidden">
  <p id="notification-text"></p>
  <button onclick="activateMap()">Show Map</button>
  <button onclick="activateTicker()">Show Ticker</button>
  <button onclick="activateBoth()">Show Both</button>
  <button onclick="dismissNotification()">Dismiss</button>
</div>

JS:
function showNotification(msg) {
    document.getElementById("notification-text").innerText = msg;
    document.getElementById("notification").classList.remove("hidden");
}

function dismissNotification() {
    notifyPending = false;
    document.getElementById("notification").classList.add("hidden");
}

Display Mode Control:
JS:
function activateMap() {
    displayMode = "map";
    updateOBSDisplay();
    dismissNotification();
}

function activateTicker() {
    displayMode = "ticker";
    updateOBSDisplay();
    dismissNotification();
}

function activateBoth() {
    displayMode = "both";
    updateOBSDisplay();
    dismissNotification();
}

function updateOBSDisplay() {
    // OBS browser source checks displayMode and shows/hides elements accordingly
}

Notes:
- Notifications do not activate the ticker automatically.
- Manual override still supersedes all modes.
- Notifications only appear when new alerts arrive, not on every refresh.
- This system mirrors real broadcast workflows where producers decide what goes on-air.


---------------------------------------
SECTION: Display Mode Control (Map / Ticker / Both / None)
---------------------------------------
Purpose:  
Allows the operator to choose what appears in OBS when alerts exist or when manual mode is active. This separates alert detection (automatic) from on‑air presentation (manual), mirroring real broadcast workflows.

Modes:

none — nothing shown

map — map only

ticker — ticker only

both — map + ticker

Core idea:  
Alerts notify you, but you decide what goes on-air.

State Variable:
JS:
let displayMode = "none"; // "map", "ticker", "both"



Control Functions:
function activateMap() {
    displayMode = "map";
    updateOBSDisplay();
}

function activateTicker() {
    displayMode = "ticker";
    updateOBSDisplay();
}

function activateBoth() {
    displayMode = "both";
    updateOBSDisplay();
}

function deactivateAll() {
    displayMode = "none";
    updateOBSDisplay();
}

Notes:

Display mode is independent of alert severity.

Manual override still supersedes all modes.

Notifications only inform you — they do not activate anything automatically.

This system mirrors a real producer’s control panel in a broadcast control room.

---------------------------------------
SECTION: Custom Ticker Input (Extended Version)
---------------------------------------
Purpose:  
Allows the operator to inject custom text into the ticker for non-weather purposes, such as:

program interruptions

schedule changes

community messages

promos

breaking news

website plugs

Core idea:  
Manual text forces the ticker on, regardless of alerts.

HTML:
<textarea id="custom-message"></textarea>
<button onclick="setCustomTicker()">Set Custom Message</button>



JS:
function setCustomTicker() {
    const text = document.getElementById("custom-message").value;
    manualText = text;
    manualOverride = true;

    updateTicker(text, null, "none");  // Replace auto text
    updateTickerVisibility();          // Force ticker on
}


Notes:

Manual text remains active until manual override is disabled.

Auto alerts are ignored while manual override is true.

This integrates cleanly with the three-state system (Silent / Auto / Manual).


---------------------------------------
SECTION: OBS Display Logic
---------------------------------------
Purpose:  
Controls what OBS browser sources show based on displayMode. This is the bridge between your dashboard logic and your on-air presentation.

Core idea:  
OBS does not decide anything.
The browser source page decides what to show based on displayMode.

JS:
function updateOBSDisplay() {
    const map = document.getElementById("map");
    const ticker = document.getElementById("ticker");

    switch (displayMode) {
        case "map":
            map.style.display = "block";
            ticker.style.display = "none";
            break;

        case "ticker":
            map.style.display = "none";
            ticker.style.display = "block";
            break;

        case "both":
            map.style.display = "block";
            ticker.style.display = "block";
            break;

        case "none":
        default:
            map.style.display = "none";
            ticker.style.display = "none";
            break;
    }
}

Notes:

This function is called whenever displayMode changes.

OBS browser sources simply render the HTML/CSS/JS — no OBS scripting required.

This keeps your system cross-platform and self-contained.

Works seamlessly with manual override and auto alert detection.

---------------------------------------
SECTION: Ticker Activation Logic
--------------------------------
Purpose:
Controls when the ticker is visible. The ticker page decides visibility internally.

Use When:
You want the ticker to auto-appear when alerts exist and auto-hide when they don’t.

Core idea:  
The ticker page decides visibility internally using CSS classes. OBS does nothing.

Code:
JS:
if (noaaAlertsExist || ecAlertsExist) {
    document.body.classList.add("show-ticker");
} else {
    document.body.classList.remove("show-ticker");
}

CSS:
body:not(.show-ticker) #ticker {
    display: none;
}


---------------------------------------
SECTION: Auto-Adjust Scroll Speed
---------------------------------
Purpose:
Keeps crawl readable regardless of message length.

Use When:
Ticker text varies in length.

Core idea:  
Longer text → slower scroll.
Shorter text → faster scroll.

Code:
JS:
const length = combined.length;
const duration = Math.max(20, length / 10);
ticker.style.animationDuration = `${duration}s`;





CSS:
#ticker {
    display: none;
}
body.active #ticker {
    display: block;
}

---------------------------------------
SECTION: Manual Override Toggle
-------------------------------
Purpose:
Allows forcing the ticker on/off regardless of alerts.

Use When:
Showing program interruptions, schedule changes, or custom messages.

Core idea:  
Manual override always wins over auto mode.

Code:
JS:
let manualOverride = false;

HTML:
<button onclick="manualOverride = !manualOverride">
  Toggle Ticker
</button>

Logic:
JS:
if (!manualOverride && alertsExist) {
    document.body.classList.add("active");
} else {
    document.body.classList.remove("active");
}

---------------------------------------
SECTION: Manual Ticker Text
---------------------------
Purpose:
Allows custom messages to scroll (announcements, promos, etc.)

Use when:  
You want to display non-alert messages like:
“Tonight’s movie begins at 8:30 PM”
“Visit raspberrypi.lan  | vfox-dt.thefurrycollective.ca”

Core idea:  
Manual text replaces auto text until you disable override.

HTML:
<textarea id="manual-text"></textarea>
<button onclick="setManualTicker()">Set Manual Ticker</button>

JS:
function setManualTicker() {
    const text = document.getElementById("manual-text").value;
    updateTicker(text, null, "none");
    manualOverride = true;
}

---------------------------------------
SECTION: Ticker Modes
---------------------
Purpose:
Defines the three operating states.

Modes:
- Silent Mode: no alerts, no manual override
- Auto Mode: alerts exist
- Manual Mode: manual override active

Core idea:  
A simple state machine controls everything.

State Variables:
let autoAlertsExist = false;
let manualOverride = false;
let manualText = "";


---------------------------------------
SECTION: updateTickerVisibility()
---------------------------------
Purpose:
Central logic that decides whether ticker is shown.

Use when:  
You change manualOverride or autoAlertsExist.

Core idea:  
This is the “brain” of the ticker.

Code:
JS:
function updateTickerVisibility() {
    if (manualOverride) {
        document.body.classList.add("active");
        return;
    }

    if (autoAlertsExist) {
        document.body.classList.add("active");
    } else {
        document.body.classList.remove("active");
    }
}

---------------------------------------
SECTION: Manual Controls (Hidden in OBS)
----------------------------------------
Purpose:
Allows manual ticker control outside OBS, hides manual controls when running inside OBS.

Use when:  
You want a clean, presentation-only view in OBS.

Core idea:  
OBS browser sources include “OBS” in the user agent.

HTML:
<div id="manual-controls">
  <textarea id="manual-input"></textarea>
  <button onclick="activateManualTicker()">Activate Manual Ticker</button>
  <button onclick="deactivateManualTicker()">Disable Manual Ticker</button>
</div>

JS:
function activateManualTicker() {
    manualText = document.getElementById("manual-input").value;
    manualOverride = true;
    updateTicker(manualText, null, "none");
    updateTickerVisibility();
}

function deactivateManualTicker() {
    manualOverride = false;
    updateTickerVisibility();
}

Hide in OBS:
JS:
if (navigator.userAgent.includes("OBS")) {
    document.getElementById("manual-controls").style.display = "none";
}

---------------------------------------
SECTION: Ticker Priority
------------------------
1. Manual Override (always wins)
2. Auto Alerts
3. Silent Mode

This prevents confusion later when debugging.

---------------------------------------
SECTION: Ticker Update Pipeline
-------------------------------
1. Fetch NOAA + EC alerts
2. Build summary strings
3. Determine if alerts exist
4. Update autoAlertsExist
5. Build combined ticker text
6. Adjust scroll speed
7. Call updateTickerVisibility()

This is the mental model of the whole system.

---------------------------------------
SECTION: Ticker Reset Behavior
------------------------------
This helps when alerts expire or manual mode ends.

When alerts expire or manual mode ends:
- Clear ticker text
- Remove alert classes
- Reset scroll speed
- Remove .active class
- Set manualOverride = false
- Set autoAlertsExist = false

This ensures the ticker fully disappears.

---------------------------------------
SECTION: Ticker Text Formatting Rules
-------------------------------------

Rules:

Prefix with “From the VFOX-DT Weather Center —”

Group counties by alert type

Use uppercase event names

Use your alert color classes

Separate segments with |

Keep the crawl under ~300 characters when possible

If all counties in the viewing area are affected by the same alert type,
replace the county list with “OUR ENTIRE VIEWING AREA”.

This keeps the crawl short and readable during widespread events.

JS:
const viewingArea = ["Wayne","Oakland","Macomb","Washtenaw","Livingston","Monroe","Lenawee","Saint_Clair"];

if (affectedCounties.length === viewingArea.length) {
    countyText = "OUR ENTIRE VIEWING AREA";
} else {
    countyText = affectedCounties.join(", ");
}

And for our Canadian veiwing area, which is only slightly smaller, and with three counties instead of twelve:

JS:

const canadianViewingArea = [
  "WINDSOR-ESSEX-PELEE",
  "CHATHAM-KENT-MORAVIANTOWN",
  "LAMBTON COUNTY"
];

---------------------------------------
SECTION: Viewing Area Definitions
---------------------------------

United States (Core Detroit Viewing Area):
- Wayne
- Oakland
- Macomb
- Washtenaw
- Livingston
- Monroe
- Lenawee
- Saint_Clair

Canada (Southwestern Ontario Viewing Area):
- Windsor-Essex-Pelee
- Chatham-Kent-Moraviantown
- Lambton County

Notes:
Outer counties such as Shiawassee, Genesee, Lapeer, Sanilac, Hillsdale, Jackson, and Ingham are excluded because they are outside the primary VFOX-DT viewing area and rarely relevant to Windsor/Detroit weather.


---------------------------------------
SECTION: Extended Storm Watch Zone (Toledo DMA)
-----------------------------------------------
Counties:
- Lucas County (Toledo)
- Wood County
- Fulton County
- Ottawa County

Purpose:
These counties are not part of the VFOX-DT viewing area, but storms frequently
approach Windsor/Detroit from the southwest. Alerts in these counties can be
used as early indicators for potential activation of the ticker or manual override.

Usage:
- Do NOT include these counties in the main ticker crawl.
- Use them to trigger optional messages such as:
  “Storms approaching from the southwest — Toledo area under a Severe Thunderstorm Watch.”
- Use them to anticipate activation of the ticker before local alerts are issued.


---------------------------------------
SECTION: Manual Message Examples
--------------------------------
PROGRAM INTERRUPTION — We are experiencing technical difficulties.
SCHEDULE CHANGE — Tonight’s movie begins at 8:30 PM.
COMMUNITY MESSAGE — Stay safe, Windsor! Roads are icy tonight.
VISIT US — raspberrypi.lan | vfox-dt.thefurrycollective.ca

---------------------------------------
APPENDIX: Original Two-State Logic
---------------------------------------

----

The original idea was to have a two-state toggle: active and hidden, with no manual override.  its JS and CSS logic was formatted as such:

JS:
if (alertsExist) {
    document.body.classList.add("active");
} else {
    document.body.classList.remove("active");
}


CSS:
#ticker {
    display: none;
}
body.active #ticker {
    display: block;
}

This meant:

State A — Alerts exist → show ticker
State B — No alerts → hide ticker

But since we want the ability to hide alerts manually, or to manually insert our own text crawl, we needed to expand on this greatly, hence expanding to a three-state system, much like the LEDs for our map alert system.

----

So, we don't need the above snippet if we're doing a three-state system.  If we just want a basic on-off toggle, this would actually do the trick, however!

---------------------------------------
APPENDIX: Simple Notification Trigger
---------------------------------------

----

When alerts are detected, trigger a non-intrusive notification:

JS:
if (alertsExist && !manualOverride) {
    showNotification("Weather alerts detected. Choose display mode.");
}

This could be: a popup, blinking icon, a sound a dashboard highlight.  You'd get notified but nothing activates until you say so.

This is kept as an example, as a more advanced version exists near the beginning of the page.

---


---------------------------------------
APPENDIX: Manual Display Control Panel
---------------------------------------

---

We can also add buttons or toggles like:

HTML:
<button onclick="activateMap()">Show Map</button>
<button onclick="activateTicker()">Show Ticker</button>
<button onclick="activateBoth()">Show Both</button>
<button onclick="deactivateAll()">Hide All</button>

Each button sets a display mode:
JS:
let displayMode = "none"; // "map", "ticker", "both"

function activateMap() {
    displayMode = "map";
    updateOBSDisplay();
}

function activateTicker() {
    displayMode = "ticker";
    updateOBSDisplay();
}

function activateBoth() {
    displayMode = "both";
    updateOBSDisplay();
}

function deactivateAll() {
    displayMode = "none";
    updateOBSDisplay();
}

Then your OBS browser source logic checks displayMode and shows/hides accordingly.

----

This is also fairly simple, but a more advanced version exists in the main part of this document.

---------------------------------------
APPENDIX: Custom Ticker Text Input
---------------------------------------

---

Since we have manual override logic, we can extend it to mon-weather messages as well with a new input:

HTML:
<textarea id="custom-message"></textarea>
<button onclick="setCustomTicker()">Set Custom Message</button>

and with JavaScript logic:
JS:
function setCustomTicker() {
    const text = document.getElementById("custom-message").value;
    manualText = text;
    manualOverride = true;
    updateTicker(text, null, "none");
    updateTickerVisibility();
}

This allows you to display promos, announcements, community mesasges, breaking news, text like "visit our website", without even touching the weather system.
