{"id":1189,"date":"2026-03-07T12:38:12","date_gmt":"2026-03-07T17:38:12","guid":{"rendered":"https:\/\/scootercam.net\/blog\/?p=1189"},"modified":"2026-03-07T12:38:13","modified_gmt":"2026-03-07T17:38:13","slug":"admin-guide","status":"publish","type":"post","link":"https:\/\/scootercam.net\/blog\/admin-guide\/","title":{"rendered":"Admin guide"},"content":{"rendered":"\n<p><strong>Contents<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-overview\">System Overview<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-infrastructure\">Infrastructure at a Glance<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-directories\">Key Directories<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-config\">Configuration File<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-config-blocks\">Config Blocks<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-credentials\">API Credentials<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-cron\">Cron Jobs<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-cron-tempest\">Tempest Weather Cron<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-cron-moon\">Moon Data Cron<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-cron-camera\">Camera \/ Timelapse Cron<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-cache\">Cache Files<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-classes\">PHP Classes<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-class-tempest\">ScooterCam_Tempest<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-class-sunset\">ScooterCam_Sunset_Predictor<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-class-moonset\">ScooterCam_Moonset_Predictor<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-class-sunmoon\">ScooterCam_SunMoon_Display<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-widgets\">Display Widgets<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-alert-banner\">Alert Banner<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-alerts\">Weather Alert System<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-windrose\">Wind Rose Data Accumulator<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-scoring\">Moon &amp; Sunset Scoring<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-sunset-factors\">Sunset Score Factors<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-moonset-criteria\">Notable Moonset Criteria<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-troubleshooting\">Troubleshooting<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-common-issues\">Common Issues<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-cron-health\">Checking Cron Health<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-cache-health\">Checking Cache File Health<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-logs\">Log Files<\/a><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><a href=\"#sc-admin-succession\">Notes for Future Administrators<\/a>\n<ol class=\"wp-block-list\">\n<li><a href=\"#sc-admin-likely-issues\">Most Likely Things to Need Attention<\/a><\/li>\n\n\n\n<li><a href=\"#sc-admin-dont-change\">What NOT to Change<\/a><\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-overview\">1. System Overview<\/h2>\n\n\n\n<p>ScooterCam is a custom PHP-based weather monitoring and sunset photography system serving the Lake Michigan shoreline between South Haven and Saugatuck, Michigan. It is hosted on GlowHost shared hosting with Cloudflare as the DNS and CDN layer.<\/p>\n\n\n\n<p>The system consists of a Raspberry Pi 5 at the shoreline running a Python scheduler (camctl), two Reolink IP cameras, a Tempest weather station, and a PHP web application on the server. A WordPress installation runs separately in the <code>\/blog\/<\/code> subdirectory and is largely independent of the main application.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-infrastructure\">1.1 Infrastructure at a Glance<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Component<\/th><th>Details<\/th><\/tr><\/thead><tbody><tr><td><strong>Web host<\/strong><\/td><td>GlowHost shared hosting<\/td><\/tr><tr><td><strong>CDN \/ DNS<\/strong><\/td><td>Cloudflare<\/td><\/tr><tr><td><strong>Server path<\/strong><\/td><td><code>\/home\/scooterc\/public_html<\/code><\/td><\/tr><tr><td><strong>WordPress<\/strong><\/td><td><code>\/home\/scooterc\/public_html\/blog\/<\/code><\/td><\/tr><tr><td><strong>Pi access<\/strong><\/td><td>Raspberry Pi Connect (remote) + SSHFS (file mount)<\/td><\/tr><tr><td><strong>Dev environment<\/strong><\/td><td>Mac + BBEdit with SSHFS mount<\/td><\/tr><tr><td><strong>PHP template system<\/strong><\/td><td>Custom: header.php, footer.php, $page_config<\/td><\/tr><tr><td><strong>Timezone<\/strong><\/td><td>America\/Detroit (all timestamps)<\/td><\/tr><tr><td><strong>Coordinates<\/strong><\/td><td>42.5593, \u221286.2361 (South Haven\/Saugatuck area)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-directories\">1.2 Key Directories<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Path<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>includes\/<\/code><\/td><td>PHP classes, config, widgets, alert banner<\/td><\/tr><tr><td><code>includes\/widgets\/<\/code><\/td><td>Individual display widgets (chart, pressure, wind rose, etc.)<\/td><\/tr><tr><td><code>cache\/<\/code><\/td><td>All JSON cache files written by cron and API classes<\/td><\/tr><tr><td><code>cron\/<\/code><\/td><td>All cron scripts \u2014 run via server crontab<\/td><\/tr><tr><td><code>data\/tempest\/<\/code><\/td><td>Tempest API response cache (MD5-keyed JSON files)<\/td><\/tr><tr><td><code>logs\/<\/code><\/td><td>Cron output logs<\/td><\/tr><tr><td><code>timelapse\/<\/code><\/td><td>Timelapse video output<\/td><\/tr><tr><td><code>live\/<\/code><\/td><td>Live camera image staging<\/td><\/tr><tr><td><code>blog\/<\/code><\/td><td>WordPress installation (independent)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-config\">2. Configuration File<\/h2>\n\n\n\n<p>All site-wide settings live in a single file: <code>includes\/config.php<\/code>. This file returns a PHP array and is loaded by every page and cron script. Never hardcode credentials or paths anywhere else.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>includes\/config.php is the single source of truth<\/strong> for all API keys, credentials, paths, and settings. If something stops working, check here first.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-config-blocks\">2.1 Config Blocks<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Block<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>tempest<\/code><\/td><td>Tempest API token, station ID, device ID, cache durations, alert and wind rose cache paths<\/td><\/tr><tr><td><code>visual_crossing<\/code><\/td><td>Visual Crossing API key and location string for 15-day forecast<\/td><\/tr><tr><td><code>openweather<\/code><\/td><td>OpenWeatherMap API key, lat\/lon for marine and moonset weather data<\/td><\/tr><tr><td><code>sunmoon<\/code><\/td><td>All sun\/moon\/sunset scoring config: API keys, home + horizon coordinates, moon data file path, cache expiry<\/td><\/tr><tr><td><code>site<\/code><\/td><td>Site name, tagline, timezone, location name<\/td><\/tr><tr><td><code>paths<\/code><\/td><td>Absolute filesystem paths for cameras, images, timelapse, logs, data directories<\/td><\/tr><tr><td><code>periods<\/code><\/td><td>Time period definitions for timelapse (morning, afternoon, evening, night)<\/td><\/tr><tr><td><code>cameras<\/code><\/td><td>Per-camera configuration: name, display name, enabled flag, capture timing, capabilities<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-credentials\">2.2 API Credentials<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Service<\/th><th>Key location<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>Tempest WeatherFlow<\/td><td><code>config[tempest][token]<\/code><\/td><td>Personal use token \u2014 does not expire<\/td><\/tr><tr><td>Visual Crossing<\/td><td><code>config[visual_crossing][api_key]<\/code><\/td><td>15-day forecast<\/td><\/tr><tr><td>OpenWeatherMap<\/td><td><code>config[openweather][api_key]<\/code><\/td><td>Used by sunset + moonset scoring<\/td><\/tr><tr><td>RapidAPI (moon-phase)<\/td><td><code>config[sunmoon][moon_api_key]<\/code><\/td><td>Called <strong>only<\/strong> by cron \u2014 never at page load<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u26a0\ufe0f <strong>The RapidAPI moon-phase key must never be called from a page load.<\/strong> In March 2026 a caching failure caused 3,200% quota overrun and significant unexpected charges. Moon data is now fetched exclusively by <code>cron\/moon-cache-cron.php<\/code> once daily.<\/p>\n<\/blockquote>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-cron\">3. Cron Jobs<\/h2>\n\n\n\n<p>All time-sensitive data is populated by cron scripts, never at page load time. This is critical for performance on shared hosting. Run <code>crontab -l<\/code> to verify all entries are active.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-cron-tempest\">3.1 Tempest Weather Cron<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>cron\/tempest-cache-cron.php<\/code><br><strong>Schedule:<\/strong> Every 5 minutes \u2014 <code>*\/5 * * * *<\/code><br><strong>Log:<\/strong> <code>logs\/tempest-cron.log<\/code><\/p>\n\n\n\n<p>This is the main data cron. It runs six steps in sequence, each reusing data from earlier steps to avoid redundant API calls:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Step<\/th><th>What it does<\/th><th>Output<\/th><\/tr><\/thead><tbody><tr><td><strong>1<\/strong><\/td><td>Fetch current conditions from Tempest station API<\/td><td>$current observation array<\/td><\/tr><tr><td><strong>2<\/strong><\/td><td>Fetch 10-day daily forecast<\/td><td>data\/tempest\/ cache files<\/td><\/tr><tr><td><strong>3<\/strong><\/td><td>Fetch 24-hour hourly forecast<\/td><td>data\/tempest\/ cache files<\/td><\/tr><tr><td><strong>4<\/strong><\/td><td>Fetch 24 hours of historical observations<\/td><td>$historical array<\/td><\/tr><tr><td><strong>5<\/strong><\/td><td>Derive weather alerts from $current<\/td><td>cache\/sc_alerts.json<\/td><\/tr><tr><td><strong>6<\/strong><\/td><td>Append wind direction\/speed to rolling 30-day log<\/td><td>cache\/sc_wind_rose.json<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-cron-moon\">3.2 Moon Data Cron<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>cron\/moon-cache-cron.php<\/code><br><strong>Schedule:<\/strong> Once daily at 1 AM \u2014 <code>0 1 * * *<\/code><br><strong>Log:<\/strong> <code>logs\/moon-cron.log<\/code><\/p>\n\n\n\n<p>Fetches moon phase data from RapidAPI (<code>moon-phase.p.rapidapi.com<\/code>) and sun data via PHP&#8217;s built-in <code>date_sun_info()<\/code>. Writes a normalized JSON file to <code>cache\/sc_moon_data.json<\/code>. All moon and sun display on the site reads this file \u2014 zero API calls happen at page load.<\/p>\n\n\n\n<p>A lock file (<code>sys_get_temp_dir()\/sc_moon_cron.lock<\/code>) prevents duplicate runs within the same hour in case of crontab misconfiguration. If the cache file is more than 36 hours old, a warning is written to the PHP error log; the old data continues to display rather than showing an error.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-cron-camera\">3.3 Camera \/ Timelapse Cron (Pi-side)<\/h3>\n\n\n\n<p>The Raspberry Pi 5 runs its own Python scheduler (camctl) which handles image capture, timelapse compilation, and FTP upload to the server. This is separate from the server-side cron jobs above. See the camera system documentation for details.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-cache\">4. Cache Files<\/h2>\n\n\n\n<p>All cache files live in the <code>cache\/<\/code> directory at the web root. The directory must be writable by the web server user. Files are written atomically (tmp file then rename) to prevent partial reads.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>File<\/th><th>Written by<\/th><th>Read by<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>sc_alerts.json<\/code><\/td><td>Tempest cron Step 5<\/td><td>includes\/alert-banner.php<\/td><td>Refreshed every 5 min. Stale after 10 min.<\/td><\/tr><tr><td><code>sc_wind_rose.json<\/code><\/td><td>Tempest cron Step 6<\/td><td>widget-wind-rose.php<\/td><td>Append-only, 30-day rolling window.<\/td><\/tr><tr><td><code>sc_moon_data.json<\/code><\/td><td>moon-cache-cron.php<\/td><td>ScooterCam_SunMoon_Display, ScooterCam_Moonset_Predictor<\/td><td>Refreshed daily at 1 AM.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Tempest API response files are cached in <code>data\/tempest\/<\/code> using MD5-keyed filenames, managed automatically by the ScooterCam_Tempest class. OpenWeatherMap responses for the moonset predictor are cached in <code>sys_get_temp_dir()<\/code> for 30 minutes using the pattern <code>sc_owm_{md5}.json<\/code>.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-classes\">5. PHP Classes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-class-tempest\">5.1 ScooterCam_Tempest<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>includes\/tempest.php<\/code><\/p>\n\n\n\n<p>The main weather data class. Wraps all Tempest WeatherFlow API calls with file-based caching. Key methods:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Method<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>get_current()<\/code><\/td><td>Returns latest station observation array. Cached per config[tempest][cache_current].<\/td><\/tr><tr><td><code>get_historical($hours)<\/code><\/td><td>Returns obs array for past N hours from device endpoint.<\/td><\/tr><tr><td><code>get_forecast_daily()<\/code><\/td><td>Returns 10-day daily forecast array.<\/td><\/tr><tr><td><code>get_forecast_hourly($hours)<\/code><\/td><td>Returns N-hour hourly forecast array.<\/td><\/tr><tr><td><code>get_alerts($cache_path)<\/code><\/td><td>Derives 5 alert types from current obs. Reads\/writes sc_alerts.json for onset tracking.<\/td><\/tr><tr><td><code>get_temperature_trend($hours)<\/code><\/td><td>Returns trend direction and change amount over past N hours.<\/td><\/tr><tr><td><code>degrees_to_cardinal($deg)<\/code><\/td><td>Converts wind degrees to 16-point compass label.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Alert thresholds are defined as class constants and can be adjusted without touching logic:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Constant<\/th><th>Default<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td><code>HIGH_WIND_THRESHOLD_MPH<\/code><\/td><td>25<\/td><td>Gust speed that triggers a high wind alert<\/td><\/tr><tr><td><code>FREEZE_THRESHOLD_F<\/code><\/td><td>32<\/td><td>Temperature at or below which freeze alert fires<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-class-sunset\">5.2 ScooterCam_Sunset_Predictor<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>includes\/class-sunset-predictor.php<\/code><\/p>\n\n\n\n<p>Scores tonight&#8217;s sunset quality 0\u2013100 using two OpenWeatherMap calls: one for the observer&#8217;s location and one for a horizon point ~20 miles west over the lake (42.5593, \u221286.6461). Scoring factors: cloud coverage (40%), visibility (25%), humidity (20%), wind speed (15%), weather type (15%), horizon contrast (20%). Weights are normalized in <code>calculateScore()<\/code>.<\/p>\n\n\n\n<p><code>getCurrentScore()<\/code> returns an array with <code>score<\/code> (int), <code>grade<\/code> (letter), <code>description<\/code> (string), and <code>breakdown<\/code> (per-factor array). The <code>is_sunset_score_window()<\/code> method on <code>ScooterCam_SunMoon_Display<\/code> gates display, returning true only between noon and sunset.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-class-moonset\">5.3 ScooterCam_Moonset_Predictor<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>includes\/class-moonset-predictor.php<\/code><\/p>\n\n\n\n<p>Determines whether today features a notable moonset over Lake Michigan (full moon \u226580% illumination, setting 5\u20139 AM, in the western sky azimuth 225\u2013315\u00b0). If notable, scores viewing conditions using OWM cloud\/visibility data.<\/p>\n\n\n\n<p>Moon phase data is read exclusively from <code>cache\/sc_moon_data.json<\/code> written by the daily cron. No RapidAPI calls occur at page load. OWM weather calls are cached 30 minutes in the system temp directory.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-class-sunmoon\">5.4 ScooterCam_SunMoon_Display<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>includes\/class-sunmoon-display.php<\/code><\/p>\n\n\n\n<p>Renders all sun and moon HTML components: sunset score card, moonset score card, moon phase calendar, sun arc SVG, moon arc SVG, and current moon phase details. All time formatting uses the configured timezone via the private <code>format_timestamp()<\/code> helper to prevent UTC display bugs.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-widgets\">6. Display Widgets<\/h2>\n\n\n\n<p>Widgets are self-contained PHP includes in <code>includes\/widgets\/<\/code>. Each locates its own data source and renders HTML. Drop into any page template with a single include line.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Widget file<\/th><th>Usage<\/th><th>Data source<\/th><\/tr><\/thead><tbody><tr><td><code>widget-tempest-chart.php<\/code><\/td><td>Temperature + wind 24-hour chart<\/td><td>Tempest get_historical(24) \u2014 already cached<\/td><\/tr><tr><td><code>widget-pressure-trend.php<\/code><\/td><td>Pressure sparkline + trend badge<\/td><td>Tempest get_historical(6) \u2014 already cached<\/td><\/tr><tr><td><code>widget-wind-rose.php<\/code><\/td><td>SVG 16-point wind rose, 30 days<\/td><td>cache\/sc_wind_rose.json<\/td><\/tr><tr><td><code>widget-sunset-quality.php<\/code><\/td><td>Sunset score + conditions<\/td><td>ScooterCam_Sunset_Predictor + Tempest (pressure, wind direction)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>All widgets check for a <code>$tempest<\/code> instance in scope before creating one, and check for <code>$historical<\/code> if already loaded, to prevent redundant instantiation when multiple widgets appear on the same page. Widget CSS is scoped with a widget-specific prefix to prevent conflicts with the main stylesheet or WordPress.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-alert-banner\">6.1 Alert Banner<\/h3>\n\n\n\n<p><strong>File:<\/strong> <code>includes\/alert-banner.php<\/code><br><strong>Usage:<\/strong> <code>&lt;?php include __DIR__ . '\/includes\/alert-banner.php'; ?&gt;<\/code><\/p>\n\n\n\n<p>Reads <code>cache\/sc_alerts.json<\/code> and renders colored dismissible banners for active weather events. Stale cache protection: condition-based alerts are suppressed if the cache is older than 10 minutes. Lightning alerts bypass this check since they use their own epoch-based expiry.<\/p>\n\n\n\n<p>Dismissal is tracked in <code>sessionStorage<\/code> keyed by event epoch\/onset, so a new event of the same type (a new lightning strike, for example) re-displays even if the previous one was dismissed.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-alerts\">7. Weather Alert System<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Alert type<\/th><th>Trigger condition<\/th><th>Clears when<\/th><th>CSS class<\/th><\/tr><\/thead><tbody><tr><td><strong>Lightning<\/strong><\/td><td>Strike detected within past 60 min<\/td><td>Automatically, 60 min after last strike epoch<\/td><td><code>.sc-alert--lightning<\/code><\/td><\/tr><tr><td><strong>Rain<\/strong><\/td><td>precip_type = 1 or precip rate &gt; 0<\/td><td>precip_type returns to 0<\/td><td><code>.sc-alert--rain<\/code><\/td><\/tr><tr><td><strong>Hail<\/strong><\/td><td>precip_type = 2<\/td><td>precip_type returns to 0<\/td><td><code>.sc-alert--hail<\/code><\/td><\/tr><tr><td><strong>High Wind<\/strong><\/td><td>wind_gust \u2265 25 mph<\/td><td>Gust drops below threshold<\/td><td><code>.sc-alert--high_wind<\/code><\/td><\/tr><tr><td><strong>Freeze<\/strong><\/td><td>air_temp \u2264 32\u00b0F (0\u00b0C)<\/td><td>Temperature rises above threshold<\/td><td><code>.sc-alert--freeze<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>All alert data is derived from the Tempest station observation endpoint \u2014 the same endpoint used for current conditions. No separate alert API exists. Onset timestamps are preserved across cron runs by reading the previous <code>sc_alerts.json<\/code> before writing the new one, allowing the banner to show &#8220;started 23 minutes ago&#8221; accurately.<\/p>\n\n\n\n<p>To adjust thresholds, change the <code>HIGH_WIND_THRESHOLD_MPH<\/code> and <code>FREEZE_THRESHOLD_F<\/code> constants at the top of the <code>ScooterCam_Tempest<\/code> class in <code>includes\/tempest.php<\/code>.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-windrose\">8. Wind Rose Data Accumulator<\/h2>\n\n\n\n<p>Step 6 of the Tempest cron appends one record to <code>cache\/sc_wind_rose.json<\/code> every 5 minutes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{ \"t\": epoch, \"spd\": mph, \"gst\": mph, \"dir\": degrees }<\/code><\/pre>\n\n\n\n<p>Records older than 30 days are pruned on each cron run. Calm entries (0\/0 speed) are skipped. After 30 days the file contains approximately 8,640 entries at roughly 300KB.<\/p>\n\n\n\n<p>The wind rose widget bins these into 16 compass sectors (22.5\u00b0 each) and four speed bands, then renders a pure SVG rose scaled so the busiest direction fills 88% of the maximum radius. No JavaScript is required.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-scoring\">9. Moon &amp; Sunset Scoring<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-sunset-factors\">9.1 Sunset Score Factors<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Factor<\/th><th>Weight<\/th><th>Optimal condition<\/th><\/tr><\/thead><tbody><tr><td>Cloud coverage<\/td><td>40 pts<\/td><td>25\u201355% sky cover (partial clouds catch light best)<\/td><\/tr><tr><td>Visibility<\/td><td>25 pts<\/td><td>10+ km (clear air = vivid color)<\/td><\/tr><tr><td>Humidity<\/td><td>20 pts<\/td><td>Below 45% RH (dry air = saturated color)<\/td><\/tr><tr><td>Wind speed<\/td><td>15 pts<\/td><td>5\u201315 mph (light movement)<\/td><\/tr><tr><td>Weather type<\/td><td>15 pts<\/td><td>Partly cloudy<\/td><\/tr><tr><td>Horizon contrast<\/td><td>20 pts<\/td><td>Different conditions at observer vs. horizon point over lake<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-moonset-criteria\">9.2 Notable Moonset Criteria<\/h3>\n\n\n\n<p>A moonset is flagged as notable only when <strong>all three<\/strong> conditions are met simultaneously:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Moon illumination is 80% or greater (nearly or fully full)<\/li>\n\n\n\n<li>Moonset time is between 5:00 AM and 9:00 AM local (America\/Detroit)<\/li>\n\n\n\n<li>Moon azimuth at setting is between 225\u00b0 and 315\u00b0 (western sky, over the lake)<\/li>\n<\/ul>\n\n\n\n<p>This event occurs roughly 2\u20133 times per year. All moonset logic reads from <code>cache\/sc_moon_data.json<\/code> \u2014 no runtime API calls.<\/p>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-troubleshooting\">10. Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-common-issues\">10.1 Common Issues<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Symptom<\/th><th>Most likely cause<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td>Alert banner not showing<\/td><td>sc_alerts.json missing or older than 10 min<\/td><td>Check Tempest cron is running. Run manually: <code>php cron\/tempest-cache-cron.php<\/code><\/td><\/tr><tr><td>Wind rose shows &#8220;not enough data&#8221;<\/td><td>sc_wind_rose.json missing or fewer than 10 entries<\/td><td>Normal if Step 6 was recently added. Data accumulates over 30 days.<\/td><\/tr><tr><td>Moon phase shows stale data<\/td><td>moon-cache-cron.php not running<\/td><td>Check crontab. Run manually and check logs\/moon-cron.log<\/td><\/tr><tr><td>Sudden high API charges (RapidAPI)<\/td><td>Moon cron being called at page load<\/td><td>Verify ScooterCam_Moonset_Predictor has no direct API calls. getMoonPhaseData() must only read the cache file.<\/td><\/tr><tr><td>PHP fatal: Cannot redeclare method<\/td><td>Patch was appended instead of replacing original<\/td><td>Open the class file, search for duplicate method names, delete the old version.<\/td><\/tr><tr><td>Sunset score not displaying<\/td><td>Outside display window (before noon or after sunset)<\/td><td>Normal behavior. The widget is gated by is_sunset_score_window().<\/td><\/tr><tr><td>Timezone displayed wrong<\/td><td>UTC timestamp formatted without timezone conversion<\/td><td>Use format_timestamp() helper in SunMoon_Display, or <code>new DateTime('@'.$ts)-&gt;setTimezone($tz)<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-cron-health\">10.2 Checking Cron Health<\/h3>\n\n\n\n<p>To verify all crons are registered:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>crontab -l<\/code><\/pre>\n\n\n\n<p>Expected entries:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>*\/5 * * * *<\/code> \u2014 Tempest cache cron (tempest-cache-cron.php)<\/li>\n\n\n\n<li><code>0 1 * * *<\/code> \u2014 Moon data cron (moon-cache-cron.php)<\/li>\n<\/ul>\n\n\n\n<p>To run a cron manually and see its output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>php \/home\/scooterc\/public_html\/cron\/tempest-cache-cron.php\nphp \/home\/scooterc\/public_html\/cron\/moon-cache-cron.php<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-cache-health\">10.3 Checking Cache File Health<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>cache\/sc_alerts.json<\/code> \u2014 should be less than 10 minutes old during normal operation<\/li>\n\n\n\n<li><code>cache\/sc_wind_rose.json<\/code> \u2014 should grow by one entry every 5 minutes<\/li>\n\n\n\n<li><code>cache\/sc_moon_data.json<\/code> \u2014 should be less than 25 hours old<\/li>\n\n\n\n<li><code>data\/tempest\/*.json<\/code> \u2014 MD5-keyed files, auto-managed by ScooterCam_Tempest class<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-logs\">10.4 Log Files<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Log file<\/th><th>Contents<\/th><\/tr><\/thead><tbody><tr><td><code>logs\/tempest-cron.log<\/code><\/td><td>Tempest cron output \u2014 timestamps, step completion, any errors<\/td><\/tr><tr><td><code>logs\/moon-cron.log<\/code><\/td><td>Moon cron output \u2014 phase name, moonrise\/set times, HTTP status<\/td><\/tr><tr><td>PHP error log<\/td><td>Stale cache warnings, API errors, class instantiation failures<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sc-admin-succession\">11. Notes for Future Administrators<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>This section is written for whoever takes over maintenance of ScooterCam. No prior knowledge of the codebase is assumed.<\/p>\n<\/blockquote>\n\n\n\n<p>ScooterCam has been designed with succession in mind. The goal is that a reasonably capable person \u2014 not necessarily a developer \u2014 can keep it running with straightforward maintenance. Here is the philosophy:<\/p>\n\n\n\n<p><strong>One config file.<\/strong> All credentials, paths, and settings are in <code>includes\/config.php<\/code>. If an API key expires or a path changes, that is the only file that needs updating.<\/p>\n\n\n\n<p><strong>Cron-first data.<\/strong> Nothing that costs money (API calls) happens at page load. All paid API calls run on a schedule via cron scripts. Pages only read files. This prevents runaway costs from traffic spikes.<\/p>\n\n\n\n<p><strong>Flat JSON caches.<\/strong> Data is stored as plain JSON files in the <code>cache\/<\/code> directory. You can open and read these files in any text editor to verify what the site is currently seeing.<\/p>\n\n\n\n<p><strong>Graceful degradation.<\/strong> Every widget checks whether its data file exists and is fresh before displaying. If something is missing, the widget shows a friendly notice rather than a PHP error.<\/p>\n\n\n\n<p><strong>Atomic file writes.<\/strong> Cache files are written to a <code>.tmp<\/code> file first, then renamed. This means a page load can never catch a file mid-write.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-likely-issues\">11.1 Most Likely Things to Need Attention<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>API keys expire or need rotation.<\/strong> Check <code>includes\/config.php<\/code> for the relevant key.<\/li>\n\n\n\n<li><strong>The Raspberry Pi loses connectivity.<\/strong> Access via Raspberry Pi Connect. The camctl scheduler auto-restarts on reboot.<\/li>\n\n\n\n<li><strong>A cron job stops running.<\/strong> Use <code>crontab -l<\/code> to check, then re-add the missing line.<\/li>\n\n\n\n<li><strong>GlowHost or Cloudflare settings change.<\/strong> DNS is managed through Cloudflare; hosting through the GlowHost control panel.<\/li>\n\n\n\n<li><strong>WordPress blog at \/blog\/ needs updates.<\/strong> Plugin and security updates are independent of the main site.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sc-admin-dont-change\">11.2 What NOT to Change<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Do not move <code>cache\/<\/code> outside the web root without updating all cache path references in <code>config.php<\/code> and the cron scripts.<\/li>\n\n\n\n<li>Do not change the JSON structure written by the Tempest cron (Steps 5 and 6) without also updating the widgets that read those files.<\/li>\n\n\n\n<li>Do not add RapidAPI calls anywhere except inside <code>cron\/moon-cache-cron.php<\/code>.<\/li>\n\n\n\n<li>Do not modify the obs_st array indices used in ScooterCam_Tempest without verifying against the current Tempest API documentation.<\/li>\n<\/ul>\n\n\n\n<p><a href=\"#sc-admin-overview\">\u2191 Back to top<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Contents 1. System Overview ScooterCam is a custom PHP-based weather monitoring and sunset photography system serving the Lake Michigan shoreline between South Haven and Saugatuck, Michigan. It is hosted on GlowHost shared hosting with Cloudflare as the DNS and CDN layer. The system consists of a Raspberry Pi 5 at the shoreline running a Python &#8230; <a title=\"Admin guide\" class=\"read-more\" href=\"https:\/\/scootercam.net\/blog\/admin-guide\/\" aria-label=\"Read more about Admin guide\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[24],"tags":[],"class_list":["post-1189","post","type-post","status-publish","format-standard","hentry","category-guides"],"_links":{"self":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts\/1189","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/comments?post=1189"}],"version-history":[{"count":1,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts\/1189\/revisions"}],"predecessor-version":[{"id":1190,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts\/1189\/revisions\/1190"}],"wp:attachment":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/media?parent=1189"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/categories?post=1189"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/tags?post=1189"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}