# DefApp Weather Dashboard

**System:** `rickweb.test/weatherdash`  
**Database:** `intel145_weather`  
**Version:** 1.0.0  
**Last Updated:** October 2026

---

## Table of Contents

1. [Overview](#1-overview)
2. [File Structure](#2-file-structure)
3. [Installation](#3-installation)
4. [Configuration](#4-configuration)
5. [Going Live — Replacing Mock Data](#5-going-live--replacing-mock-data)
6. [API Endpoint Contract](#6-api-endpoint-contract)
7. [Naming Conventions](#7-naming-conventions)
8. [Cards Reference](#8-cards-reference)
9. [JavaScript API](#9-javascript-api)
10. [CSS Variables & Theming](#10-css-variables--theming)
11. [Dark Mode](#11-dark-mode)
12. [Alerts System](#12-alerts-system)
13. [Responsive Behavior](#13-responsive-behavior)
14. [Print Stylesheet](#14-print-stylesheet)
15. [Extending the Dashboard](#15-extending-the-dashboard)
16. [Troubleshooting](#16-troubleshooting)

---

## 1. Overview

The DefApp Weather Dashboard is a fully self-contained Bootstrap 5 / AdminLTE-style weather display built for the `rickweb.test/weatherdash` environment. It reads from the `intel145_weather` database via a set of `weather_station` REST endpoints and renders 10 metric cards in a responsive grid layout.

### Key features

| Feature | Detail |
|---|---|
| Data source | `intel145_weather` DB via `/api/weather_station/` |
| Station support | Multi-station selector (Alpha, Beta, Gamma) |
| Auto-refresh | Every 60 seconds (configurable) |
| Dark mode | Toggle saved to `localStorage` as `wsPrefs_theme` |
| Charts | Chart.js 4.5.1 — sparkline + bar + 24h modal |
| Icons | Lucide (unpkg CDN) + custom inline SVGs |
| Mock mode | Ships in mock mode; one config flag to go live |
| Print support | Clean print stylesheet — sidebar/navbar hidden |

---

## 2. File Structure

```
defapp/
└── weather/
    ├── weather.index.php          Entry point (standalone URL)
    ├── weather.view.php           Main HTML/PHP view template
    ├── weather.config.php         All configuration (DB, stations, paths)
    └── assets/
        ├── css/
        │   └── weather.css        All dashboard styles (ws- prefix)
        └── js/
            └── weather.js         All JS — data, UI, charts, events
```

**URL when running standalone:**
```
https://rickweb.test/weatherdash/weather/
```

**To embed inside an existing AdminLTE page:**
```php
require_once '/path/to/weather/weather.view.php';
```

---

## 3. Installation

### Step 1 — Copy files

Drop the `weather/` folder into your DefApp root:

```
/var/www/rickweb.test/weatherdash/weather/
```

### Step 2 — Verify asset path

Open `weather.config.php` and confirm `asset_base` matches your server path:

```php
'asset_base' => '/weatherdash/assets/',
```

### Step 3 — Verify CDN access

The dashboard loads these from CDN. Confirm your server has outbound access:

| Library | CDN URL |
|---|---|
| Chart.js 4.5.1 | `cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js` |
| Lucide Icons | `unpkg.com/lucide@latest` |
| Inter Font | `fonts.googleapis.com` |

### Step 4 — Test in mock mode

Visit `https://rickweb.test/weatherdash/weather/` — you should see the dashboard with simulated data cycling every 60 seconds. No database connection is required in mock mode.

### Step 5 — (Optional) Auth hook

In `weather.index.php`, uncomment and connect to your DefApp auth system:

```php
require_once '../_system/auth.php';
defapp_require_login();
```

---

## 4. Configuration

All configuration lives in `weather.config.php` as a single `WS_CONFIG` constant array.

### Key settings

```php
define('WS_CONFIG', [

    // Toggle mock vs. live data
    'mock_mode'        => true,         // false = use real API

    // API base path
    'api_base'         => '/api/weather_station/',

    // Auto-refresh interval (seconds)
    'refresh_interval' => 60,

    // Default station key
    'default_station'  => 'alpha',

    // Station definitions
    'stations' => [
        'alpha' => [
            'name'       => 'Station Alpha',
            'location'   => 'West End, NC',
            'coords'     => '35.1936°N, 79.5937°W',
            'temp_base'  => 72,          // mock only
            'humid_base' => 68,          // mock only
        ],
        // beta, gamma ...
    ],

    // Display overrides
    'display_user'     => 'Rick',
    'display_location' => 'West End, NC',

    // Asset path (must match web root)
    'asset_base'       => '/weatherdash/assets/',

    // Footer metadata
    'firmware_version' => 'v3.8.2',
    'calibration_date' => 'Sep 15, 2026',

    // Internal navigation links
    'links' => [
        'history'  => '/weatherdash/history/',
        'settings' => '/weatherdash/settings/',
    ],
]);
```

### PHP → JS config bridge

`weather.view.php` serializes the config to a JS global at the bottom of the page:

```php
<script>
const WS_PHP_CONFIG = <?= json_encode([
    'default_station'  => $ws_default,
    'stations'         => $ws_stations,
    'api_base'         => WS_CONFIG['api_base'],
    'mock_mode'        => WS_CONFIG['mock_mode'],
    'refresh_interval' => WS_CONFIG['refresh_interval'],
]) ?>;
</script>
```

`weather.js` reads `WS_PHP_CONFIG` at startup. You never need to duplicate config between PHP and JS.

---

## 5. Going Live — Replacing Mock Data

### Single flag

Set in `weather.config.php`:

```php
'mock_mode' => false,
```

That's it. All four `fetchWS_*()` functions in `weather.js` automatically switch to real `fetch()` calls.

### What happens under the hood

Each function has two branches:

```js
function fetchWS_current() {
    /* -- LIVE ---------------------------------------------------- */
    if (!wsDB_config.mock_mode) {
        return fetch(`${wsDB_config.api_base}current?station=${wsDB_currentStation}`)
            .then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
            .catch(err => { console.error('[wsDB] fetchWS_current:', err); return null; });
    }
    /* -- MOCK ---------------------------------------------------- */
    return new Promise(resolve => { setTimeout(() => { resolve({ ... }); }, 300); });
}
```

The mock branch is **never executed** when `mock_mode` is false.

---

## 6. API Endpoint Contract

Your backend must implement these four GET endpoints. All return JSON.

### `GET /api/weather_station/current?station={key}`

Returns current conditions for the given station.

```json
{
  "station":         "Station Alpha",
  "location":        "West End, NC",
  "condition":       "Partly Cloudy",
  "temperature":     72.4,
  "temperatureC":    "22.4",
  "feelsLike":       70.1,
  "feelsLikeType":   "Wind Chill",
  "tempMin":         64,
  "tempMax":         78,
  "humidity":        68,
  "dewpoint":        60.2,
  "absHumidity":     "12.2",
  "windSpeed":       8.3,
  "windGust":        13.5,
  "windDir":         "SW",
  "windDeg":         225.0,
  "pressure":        30.05,
  "pressureTrend":   "Steady",
  "pressureRate":    "0.001",
  "rainRate":        0.0,
  "rainToday":       0.12,
  "rain24h":         "0.32",
  "rain7d":          "1.85",
  "rainMonth":       "2.14",
  "rainYear":        "36.7",
  "uvIndex":         0,
  "solarRadiation":  0,
  "sunrise":         "7:12 AM",
  "sunset":          "6:48 PM",
  "lightningCount":  0,
  "lightningDist":   null,
  "lightningLastTime": null,
  "tempHistory":     [71, 70, 68, 69, 72, 74, 73],
  "rainHistory":     [0.1, 0.0, 0.3, 0.0, 0.05, 0.0, 0.12],
  "observationTime": "2026-10-10T01:11:00"
}
```

**DB mapping (intel145_weather):**

| JSON field | Suggested DB column |
|---|---|
| `temperature` | `wx_current.temp_f` |
| `humidity` | `wx_current.humidity_pct` |
| `windSpeed` | `wx_current.wind_speed_mph` |
| `windDir` | `wx_current.wind_dir` |
| `windDeg` | `wx_current.wind_dir_deg` |
| `pressure` | `wx_current.pressure_inhg` |
| `rainRate` | `wx_current.rain_rate_inh` |
| `uvIndex` | `wx_current.uv_index` |
| `solarRadiation` | `wx_current.solar_wm2` |
| `lightningCount` | `wx_lightning.strike_count_1h` |
| `lightningDist` | `wx_lightning.nearest_mi` |

---

### `GET /api/weather_station/forecast?station={key}`

Returns a 5-element array of daily forecast objects.

```json
[
  {
    "day":       "Sun",
    "date":      "Oct 11",
    "icon":      "cloud-sun",
    "condition": "Partly Cloudy",
    "high":      76,
    "low":       58,
    "precip":    30
  },
  ...
]
```

**`icon` values recognized by the dashboard:**
`sun` · `cloud-sun` · `cloud-rain` · `cloud`

---

### `GET /api/weather_station/alerts?station={key}`

Returns an array of active alert objects. Empty array = no alerts.

```json
[
  {
    "type":      "WATCH",
    "severity":  "orange",
    "title":     "Severe Thunderstorm Watch",
    "office":    "NWS Raleigh",
    "effective": "Oct 10, 2026 12:00 AM",
    "expires":   "Oct 10, 2026 6:00 AM",
    "desc":      "Conditions favorable for severe thunderstorms..."
  }
]
```

**`type` values:** `WARNING` · `WATCH` · `ADVISORY` · `STATEMENT`  
**`severity` values:** `red` · `orange` · `yellow` · `blue`

---

### `GET /api/weather_station/history?metric={metric}&hours=24&station={key}`

Returns an array of 24 numeric values (hourly), oldest first.

```json
[71, 70, 69, 68, 67, 68, 70, 72, 74, 75, 76, 75, 74, 73, 72, 71, 70, 69, 68, 70, 71, 72, 73, 72]
```

**`metric` values:** `temperature` · `feels-like` · `wind` · `humidity` · `rain` · `lightning` · `uv-solar` · `pressure`

---

## 7. Naming Conventions

All identifiers follow consistent prefixes to avoid collisions with AdminLTE and Bootstrap.

### CSS classes — `ws-`

| Prefix | Usage |
|---|---|
| `ws-card-{metric}` | Card wrapper IDs |
| `ws-val-{metric}` | Value display elements |
| `ws-badge-{color}` | Severity/status badges |
| `ws-gradient-{name}` | Card header gradient classes |
| `ws-chart-{name}` | Chart canvas elements |

### JavaScript — grouped prefixes

| Prefix | Scope |
|---|---|
| `wsDB_` | Data layer variables and station config |
| `wsUI_` | DOM rendering and UI helper functions |
| `wsAlert_` | Alert detection and toast logic |
| `fetchWS_` | Data fetch functions (one per endpoint) |
| `wsChart_` | Chart.js instance variables |
| `wsPrefs_` | `localStorage` user preference keys |

### localStorage keys

| Key | Value |
|---|---|
| `wsPrefs_theme` | `"light"` or `"dark"` |

---

## 8. Cards Reference

| Card | Element ID | Header gradient | Metric IDs |
|---|---|---|---|
| Current Conditions | `ws-card-current` | `ws-gradient-blue` | `ws-val-condition`, `ws-val-station-name`, `ws-val-obs-time` |
| Temperature | `ws-card-temperature` | `ws-gradient-red` | `ws-val-temp-f`, `ws-val-temp-c`, `ws-val-temp-min`, `ws-val-temp-max` |
| Feels Like | `ws-card-feels-like` | `ws-gradient-amber` | `ws-val-feels`, `ws-val-feels-type`, `ws-val-feels-diff`, `ws-val-comfort` |
| Wind | `ws-card-wind` | `ws-gradient-teal` | `ws-val-wind-speed`, `ws-val-wind-dir`, `ws-val-wind-gust`, `ws-val-beaufort` |
| Humidity | `ws-card-humidity` | `ws-gradient-blue` | `ws-val-humidity`, `ws-val-dewpoint`, `ws-val-abs-humidity`, `ws-val-humidity-comfort` |
| Rainfall | `ws-card-rain` | `ws-gradient-bluegrey` | `ws-val-rain-rate`, `ws-val-rain-today`, `ws-val-rain-24h`, `ws-val-rain-7d`, `ws-val-rain-month`, `ws-val-rain-year` |
| Lightning | `ws-card-lightning` | `ws-gradient-yellow-purple` | `ws-val-lightning-count`, `ws-val-lightning-dist`, `ws-val-lightning-time`, `ws-val-lightning-risk` |
| UV & Solar | `ws-card-uv-solar` | `ws-gradient-orange` | `ws-val-uv`, `ws-val-uv-badge`, `ws-val-solar`, `ws-val-solar-bar`, `ws-val-solar-pct`, `ws-val-sunrise`, `ws-val-sunset` |
| Barometric Pressure | `ws-card-pressure` | `ws-gradient-purple` | `ws-val-pressure`, `ws-val-pressure-trend`, `ws-val-pressure-forecast` |
| 5-Day Forecast | `ws-card-forecast` | `ws-gradient-blue` | `ws-forecast-grid` (populated by JS) |
| Weather Alerts | `ws-card-alerts` | `ws-gradient-blue` | `ws-alerts-list`, `ws-alerts-count-badge` |

### Chart canvases

| ID | Type | Data source |
|---|---|---|
| `ws-chart-temp-spark` | Line sparkline | `data.tempHistory` (7 points) |
| `ws-chart-rain-bar` | Bar chart | `data.rainHistory` (7 days) |
| `ws-modal-chart` | Line (24h) | `fetchWS_history(metric)` |

---

## 9. JavaScript API

### Master refresh

```js
// Fetch all three endpoints in parallel and update all cards
await wsUI_refreshAll();
```

### Individual fetch functions

```js
const current  = await fetchWS_current();    // current conditions
const forecast = await fetchWS_forecast();   // 5-day forecast array
const alerts   = await fetchWS_alerts();     // active alerts array
const history  = await fetchWS_history('temperature'); // 24-pt array
```

### UI update functions (call after fetch)

```js
wsUI_updateCurrent(current);     // updates all metric cards
wsUI_updateForecast(forecast);   // populates forecast grid
wsUI_updateAlerts(alerts);       // renders alert list + badges
```

### Alert check

```js
wsAlert_check(current);
// Triggers lightning toast if lightningDist < 5 miles
```

### Auto-refresh control

```js
wsUI_autoRefresh();       // starts/resets 60-second timer
// Timer stored in wsAutoRefreshTimer — clearInterval() to stop
```

### Station switching

```js
wsDB_currentStation = 'beta';  // 'alpha' | 'beta' | 'gamma'
await wsUI_refreshAll();       // re-fetches with new station
```

### Hero icon

```js
wsUI_setHeroIcon('Partly Cloudy');
// Condition string determines SVG: sunny, rain, thunder, cloud, night
```

---

## 10. CSS Variables & Theming

Override these in your own CSS after loading `weather.css`:

```css
:root {
  --ws-primary:        #3468b0;   /* accent color */
  --ws-danger:         #ac1922;   /* warnings, toasts */
  --ws-warning:        #d6a632;   /* advisory color */
  --ws-success:        #01712b;   /* good/clear state */
  --ws-info:           #2f5ea3;   /* informational */
  --ws-navy:           #1a2332;   /* sidebar / navbar */
  --ws-navy-light:     #243044;
  --ws-navy-lighter:   #2d3b52;

  --ws-bg:             #f8f4f1;   /* main content background */
  --ws-card-bg:        #fffbf8;   /* card surface */
  --ws-text:           #14110e;   /* primary text */
  --ws-text-secondary: #746d68;   /* secondary text */
  --ws-text-muted:     #98918b;   /* labels / captions */
  --ws-border:         rgba(218,198,183,0.25);

  --ws-sidebar-w:      260px;     /* sidebar width */
  --ws-navbar-h:       52px;      /* navbar height */
}
```

### Gradient classes (card headers)

| Class | Colors |
|---|---|
| `ws-gradient-red` | `#e7615b → #d57702` |
| `ws-gradient-amber` | `#d6a632 → #ed974a` |
| `ws-gradient-teal` | `#26b89d → #5489d6` |
| `ws-gradient-blue` | `#3468b0 → #5489d6` |
| `ws-gradient-bluegrey` | `#5a544f → #746d68` |
| `ws-gradient-yellow-purple` | `#d6a632 → #9c6ac9` |
| `ws-gradient-orange` | `#d57702 → #f3c357` |
| `ws-gradient-purple` | `#6f3da0 → #9c6ac9` |

---

## 11. Dark Mode

Dark mode is toggled via the moon/sun button in the navbar.

**Preference persistence:**

```js
localStorage.setItem('wsPrefs_theme', 'dark');  // or 'light'
```

The page reads this key on load and applies `body.ws-dark` immediately — no flash.

**Dark mode CSS hook:**

```css
body.ws-dark {
  --ws-bg:      #0f1724;
  --ws-card-bg: #1a2332;
  --ws-text:    #e2ddd9;
  /* all tokens automatically cascade */
}
```

To extend dark mode for custom elements:

```css
body.ws-dark .my-custom-panel {
  background: var(--ws-card-bg);
  color: var(--ws-text);
}
```

---

## 12. Alerts System

### Flow

```
wsUI_refreshAll()
  └── fetchWS_alerts()
        └── wsUI_updateAlerts(alerts)
              ├── Renders alert items in #ws-alerts-list
              ├── Updates bell badge count (#ws-alert-badge)
              ├── Flashes sidebar Alerts nav item yellow
              └── If type=WARNING: adds .ws-alert-pulse (pulsing border)

wsAlert_check(currentData)
  └── If lightningDist < 5 miles → shows #ws-toast-lightning toast (8s)
```

### Alert severity → visual mapping

| `severity` | Border | Background | Badge class |
|---|---|---|---|
| `red` (WARNING) | `#ac1922` | red tint | `ws-badge-red` |
| `orange` (WATCH) | `#d57702` | orange tint | `ws-badge-orange` |
| `yellow` (ADVISORY) | `#d6a632` | yellow tint | `ws-badge-yellow` |
| `blue` (STATEMENT) | `#3468b0` | blue tint | `ws-badge-blue` |

### Pulsing border animation

Applied automatically when any alert has `type: "WARNING"`:

```css
.ws-alert-pulse {
  animation: ws-alert-pulse 2s ease-in-out infinite;
  border: 2px solid rgba(172,25,34,0.3);
}
```

---

## 13. Responsive Behavior

| Breakpoint | Layout change |
|---|---|
| `> 1200px` | Full 4-col primary row, 3-col secondary row |
| `900–1200px` | Primary row: 2×2 grid; secondary row: single column |
| `< 900px` | All cards single column |
| `< 768px` | Sidebar becomes off-canvas drawer (hamburger toggle); navbar title hidden |

**Mobile sidebar toggle:**

```js
// JS toggles .ws-mobile-open on #ws-sidebar
// CSS: left: -260px (hidden) → left: 0 (visible)
```

---

## 14. Print Stylesheet

The `@media print` block in `weather.css` hides navigation and renders a clean two-column card grid:

- Sidebar, navbar, refresh buttons, toast, and modal are hidden
- Cards set to `break-inside: avoid`
- 4-col grid collapses to 2-col

To print: use browser print dialog or `window.print()`.

---

## 15. Extending the Dashboard

### Adding a new metric card

1. **HTML** — Add your card markup to `weather.view.php` following the `ws-card` pattern:

```html
<div id="ws-card-soil" class="ws-card ws-clickable">
    <div class="ws-card-header ws-gradient-teal">
        <span><i data-lucide="sprout"></i> Soil Moisture</span>
        <button class="ws-card-refresh" data-refresh="soil"><i data-lucide="refresh-cw"></i></button>
    </div>
    <div class="ws-card-body ws-card-center">
        <div id="ws-val-soil" class="ws-metric-value">--%</div>
        <div class="ws-metric-label">Volumetric %</div>
    </div>
</div>
```

2. **JS** — Add a fetch function and update call in `weather.js`:

```js
function fetchWS_soil() {
    if (!wsDB_config.mock_mode) {
        return fetch(`${wsDB_config.api_base}soil?station=${wsDB_currentStation}`)
            .then(r => r.json());
    }
    return Promise.resolve({ soilMoisture: Math.round(wsUI_rand(20, 80)) });
}

function wsUI_updateSoil(data) {
    wsUI_setTxt('ws-val-soil', data.soilMoisture + '%');
}
```

3. **Wire into master refresh** — Add to `wsUI_refreshAll()`:

```js
async function wsUI_refreshAll() {
    const [current, forecast, alerts, soil] = await Promise.all([
        fetchWS_current(),
        fetchWS_forecast(),
        fetchWS_alerts(),
        fetchWS_soil()        // <-- add here
    ]);
    wsUI_updateCurrent(current);
    wsUI_updateForecast(forecast);
    wsUI_updateAlerts(alerts);
    wsUI_updateSoil(soil);   // <-- add here
}
```

4. **Add to history modal map** in `weather.js` `cardMetrics` object:

```js
'ws-card-soil': {
    title: 'Soil Moisture (%) — Last 24 Hours',
    color: '#26b89d',
    metric: 'soil'
}
```

### Adding a new station

In `weather.config.php`:

```php
'stations' => [
    // existing...
    'delta' => [
        'name'       => 'Station Delta',
        'location'   => 'Aberdeen, NC',
        'coords'     => '35.1327°N, 79.4300°W',
        'temp_base'  => 73,
        'humid_base' => 66,
    ],
],
```

Add the `<option>` in `weather.view.php` (auto-generated from the PHP loop — nothing more needed).

---

## 16. Troubleshooting

### Dashboard shows "Loading…" forever

- **Mock mode:** Check browser console for JS errors. Likely a Chart.js CDN load failure or a JS syntax error.
- **Live mode:** Open Network tab. Check that `/api/weather_station/current` returns 200 with valid JSON.

### Charts not rendering

Chart.js must load before `weather.js`. Verify the `<script src="chart.js">` tag appears before the `weather.js` `<script>` tag in `weather.view.php`.

### Compass needle not rotating

`ws-compass-needle` uses `transform-origin: 35px 35px`. If you move or resize the SVG, update the transform origin to match the new center.

### Pressure gauge needle stuck at left

The gauge maps `28–32 inHg → -90° to +90°`. If your live pressure value falls outside this range, clamp it server-side or adjust the needle formula in `wsUI_updateCurrent()`:

```js
const angle = ((data.pressure - 28) / (32 - 28)) * 180 - 90;
```

### Alert bell not showing count

Confirm your `/api/weather_station/alerts` response is a JSON **array** (even when empty: `[]`), not `null` or an object.

### Dark mode flash on page load

The `body.ws-dark` class is applied via inline JS in `weather.view.php` before the page paints. If you see a flash, ensure the JS block appears immediately after `<body>`, before any CSS framework classes are rendered.

### Sidebar not collapsing on mobile

The sidebar uses `left: calc(-1 * var(--ws-sidebar-w))` when hidden. If `--ws-sidebar-w` is overridden in your parent layout, the sidebar may not hide correctly. Set it back to `260px` or adjust your override.

---

## Changelog

| Version | Date | Notes |
|---|---|---|
| 1.0.0 | Oct 2026 | Initial release — 10 cards, mock mode, live-mode hooks, dark mode |

---

*DefApp Weather Dashboard · intel145_weather · rickweb.test/weatherdash*
