https://github.com/tipsy/trondheim-dashboard
https://github.com/tipsy/trondheim-dashboard
Last synced: 5 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/tipsy/trondheim-dashboard
- Owner: tipsy
- Created: 2025-10-26T17:58:09.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-11-23T11:34:04.000Z (8 months ago)
- Last Synced: 2025-11-23T12:19:27.128Z (8 months ago)
- Language: JavaScript
- Size: 1.93 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Trondheim Dashboard
A modern, client-side web dashboard built with vanilla Web Components and Lit for Trondheim residents. It aggregates real-time local information including bus departures, weather forecasts, trash collection schedules, electricity prices, police logs, news, and events โ all in a customizable, themeable interface.
**Live demo:** https://trondheim-dashboard.com
This repository is intentionally lightweight โ just static HTML, CSS and JavaScript with no build process required โ so it can be served from any static host.
## โจ Features
- **๐ Location-based widgets** - Enter any address in Trondheim/Trรธndelag to get personalized information
- **๐ Real-time bus departures** - Live ATB/Entur data with nearest stops and platform selection
- **๐ค๏ธ Weather forecasts** - Current conditions, hourly forecast, and 5-day outlook from MET Norway
- **โก Electricity prices** - Current and upcoming hourly prices by Norwegian price area
- **๐๏ธ Trash collection** - Personalized waste pickup schedule from TRV
- **๐ฎ Police log** - Latest incidents from Politiet (Trรธndelag district)
- **๐ฐ News** - Top stories from NRK Trรธndelag
- **๐ญ Events** - Upcoming events in Trondheim with date filtering
- **๐ URL state** - Shareable links with address and theme parameters
- **โป๏ธ Auto-refresh** - Dashboard reloads every 5 minutes to get latest data and app updates
- **๐ฑ Responsive** - Optimized layouts for desktop (grid with scrollable widgets) and mobile/tablet (stacked)
## ๐ Quick Start
1. **Clone the repository:**
```bash
git clone https://github.com/your-user/trondheim-dashboard.git
cd trondheim-dashboard
```
2. **Serve with any static server:**
```bash
# Python 3 built-in server
python -m http.server 8000
# Node (http-server)
npx http-server
# Or simply open index.html in a browser
```
3. **Open in browser:** Navigate to `http://localhost:8000`
No build process, no dependencies to install โ just serve and go!
## ๐ Project Structure
```
trondheim-dashboard/
โโโ index.html # Main entry point
โโโ manifest.json # PWA manifest
โโโ robots.txt # Search engine crawler rules
โโโ sitemap.xml # XML sitemap for SEO
โโโ CNAME # GitHub Pages custom domain
โโโ README.md # This file
โโโ SEO_GUIDE.md # SEO documentation and guidelines
โโโ img/
โ โโโ trondheimsrosa.svg # Dashboard favicon (Trondheim rose)
โ โโโ example-background.jpg
โโโ og/
โ โโโ screenshot.png # Open Graph / social media preview image
โโโ styles/
โ โโโ main.css # Global styles and layout
โ โโโ themes/ # Themes
โโโ img/
โ โโโ example-background.jpg
โโโ js/
โโโ components/ # Web Components (Lit-based)
โ โโโ dashboard.js # Main dashboard orchestrator
โ โโโ address/ # Address input with autocomplete
โ โ โโโ address-input.js
โ โ โโโ address-suggestion-item.js
โ โโโ bus/ # Bus departure widget
โ โ โโโ bus-widget.js
โ โ โโโ bus-row.js
โ โโโ weather/ # Weather widgets (4 components)
โ โ โโโ weather-right-now.js # Current + next 4 hours
โ โ โโโ weather-today.js # 5-day forecast
โ โ โโโ weather-current.js
โ โ โโโ weather-detail.js
โ โ โโโ weather-hour.js
โ โโโ energy/ # Electricity price widget
โ โ โโโ energy-widget.js
โ โโโ trash/ # Trash collection widget
โ โ โโโ trash-widget.js
โ โ โโโ trash-row.js
โ โโโ police/ # Police log widget
โ โ โโโ police-widget.js
โ โโโ nrk/ # News widget
โ โ โโโ nrk-widget.js
โ โโโ events/ # Events widget
โ โ โโโ events-widget.js
โ โโโ config/ # Configuration components
โ โ โโโ theme-selector.js
โ โโโ common/ # Reusable UI components
โ โโโ base-widget.js # Base class for all widgets
โ โโโ widget-list.js
โ โโโ widget-row.js
โ โโโ heading-2.js
โ โโโ input-field.js
โ โโโ custom-select.js
โ โโโ loading-spinner.js
โ โโโ error-message.js
โ โโโ buttons/
โ โโโ base-button.js
โ โโโ icon-button.js
โ โโโ primary-button.js
โ โโโ secondary-button.js
โโโ utils/ # API clients and helpers
โโโ api-base.js # Fetch wrapper with caching
โโโ cache-client.js # localStorage-based cache
โโโ cache-config.js # Cache TTL configuration
โโโ icon-library.js # Weather icon mapping
โโโ date-formatter.js # Norwegian date formatting
โโโ shared-styles.js # Lit shared CSS
โโโ event-helpers.js # Custom event utilities
โโโ api/ # External API clients
โโโ api-base.js # Base class for all APIs
โโโ bus-api.js # Entur GraphQL client
โโโ weather-api.js # MET Norway client
โโโ energy-api.js # Electricity price client
โโโ trash-api.js # TRV wasteplan client
โโโ geocoding-api.js # Nominatim geocoding client
โโโ police-api.js # Politiet API client
โโโ nrk-rss-api.js # NRK RSS parser
โโโ events-api.js # TrdEvents GraphQL client
```
## ๐๏ธ Architecture
### Technology Stack
- **Lit 3** - Lightweight reactive Web Components library (loaded via CDN)
- **Material Design Icons** - Icon font (loaded via CDN)
- **Vanilla CSS** - No preprocessors, CSS variables for theming
- **ES Modules** - Native browser modules, no bundler needed
- **Custom Elements v1** - Web Components standard with Shadow DOM
### Component Architecture
All widgets extend `BaseWidget`, which provides:
- Standard layout (header with title/icon, scrollable content area)
- Loading state management with spinner
- Error state management with error messages
- Placeholder state for widgets awaiting user input
- Scroll detection for header border effects
- Responsive behavior (fixed height on desktop, natural height on mobile)
```javascript
// Typical widget structure
class MyWidget extends BaseWidget {
constructor() {
super();
this.title = "Widget Title";
this.icon = "mdi-icon-name";
}
async connectedCallback() {
super.connectedCallback();
await this.loadData();
}
renderContent() {
// Widget-specific rendering
}
}
```
### State Management
- **Component-level state** - Each widget manages its own data using Lit's reactive properties
- **URL parameters** - Address and theme persisted in URL for sharing
- **localStorage** - Theme preference and API cache storage
- **Event-driven updates** - Components communicate via custom events:
- `location-updated` - Fired when address changes
- `theme-changed` - Fired when theme changes
### Caching Strategy
Smart caching with configurable TTLs (defined in `cache-config.js`):
| Data Type | TTL | Rationale |
|-----------|-----|-----------|
| Weather | 4 minutes | Slightly below 5-min dashboard refresh |
| Bus stops (metadata) | 6 hours | Stop locations change infrequently |
| Bus departures | 0 (no cache) | Real-time data |
| Energy prices | 1 hour | Updated hourly |
| Trash schedule | 24 hours | Daily granularity |
| News (NRK RSS) | 4 minutes | Fresh news on each dashboard cycle |
| Police log | 4 minutes | Recent incidents |
| Events | 24 hours | Relatively static |
| Geocoding | 1 hour | Respect rate limits |
## ๐ External Integrations
### 1. Entur Journey Planner (Bus Data)
- **Endpoint:** `https://api.entur.io/journey-planner/v3/graphql`
- **Type:** GraphQL API
- **Purpose:** Real-time bus departures, stop searches, and platform information
- **Headers:** `ET-Client-Name` for API identification
- **Used by:** `bus-api.js` โ `bus-widget.js`
### 2. MET Norway Weather API
- **Endpoint:** `https://api.met.no/weatherapi/locationforecast/2.0/complete`
- **Type:** REST API (JSON)
- **Purpose:** Weather forecasts and current conditions
- **Headers:** `User-Agent` required
- **Used by:** `weather-api.js` โ `weather-right-now.js`, `weather-today.js`
### 3. Sunrise-Sunset API
- **Endpoint:** `https://api.sunrise-sunset.org/json`
- **Type:** REST API (JSON)
- **Purpose:** Sunrise/sunset times and day length
- **Used by:** `weather-api.js` โ `weather-today.js`
### 4. TRV Wasteplan API
- **Endpoints:**
- Search: `https://trv.no/wp-json/wasteplan/v2/adress`
- Calendar: `https://trv.no/wp-json/wasteplan/v2/calendar/{id}`
- **Type:** WordPress REST API
- **Purpose:** Trash collection schedules for Trondheim addresses
- **Cache:** 24 hours
- **Used by:** `trash-api.js` โ `trash-widget.js`
### 5. HvaKosterStrommen (Electricity Prices)
- **Endpoint:** `https://www.hvakosterstrommen.no/api/v1/prices/{year}/{month}-{day}_{area}.json`
- **Type:** REST API (JSON)
- **Purpose:** Hourly electricity prices by Norwegian price area (NO1-NO5)
- **Cache:** 1 hour
- **Used by:** `energy-api.js` โ `energy-widget.js`
### 6. OpenStreetMap Nominatim (Geocoding)
- **Endpoints:**
- Search: `https://nominatim.openstreetmap.org/search`
- Reverse: `https://nominatim.openstreetmap.org/reverse`
- **Type:** REST API (JSON)
- **Purpose:** Address autocomplete and geocoding
- **Headers:** Custom `User-Agent`
- **Features:** Filters to Norway, prioritizes Trรธndelag, requires house numbers
- **Used by:** `geocoding-api.js` โ `address-input.js`
### 7. Politiet API (Police Log)
- **Endpoint:** `https://api.politiet.no/politiloggen/v1/messages`
- **Type:** REST API (JSON)
- **Purpose:** Latest police incidents in Trรธndelag
- **Notes:** May require CORS proxy
- **Cache:** 4 minutes
- **Used by:** `police-api.js` โ `police-widget.js`
### 8. NRK RSS Feed (News)
- **Endpoint:** `https://www.nrk.no/trondelag/siste.rss`
- **Type:** RSS/XML
- **Purpose:** Top 10 news stories from NRK Trรธndelag
- **Parsing:** XML parsed in-browser to JSON
- **Cache:** 4 minutes
- **Used by:** `nrk-rss-api.js` โ `nrk-widget.js`
### 9. TrdEvents GraphQL (Events)
- **Endpoint:** `https://trdevents-224613.web.app/graphQL`
- **Type:** GraphQL API
- **Purpose:** Upcoming events in Trondheim
- **Cache:** 24 hours
- **Used by:** `events-api.js` โ `events-widget.js`
## ๐จ Theming
The dashboard uses CSS variables for theming. Each theme file defines:
```css
[data-theme="theme-name"] {
/* Core colors */
--background-color: ...;
--card-background: ...;
--text-color: ...;
--text-light: ...;
--heading-color: ...;
/* Interactive elements */
--primary-color: ...;
--secondary-color: ...;
--button-text: ...;
/* Borders and dividers */
--border-color: ...;
/* Optional: background image */
--background-image: url(...);
/* Typography */
--font-family: ...;
}
```
**Available themes:**
- `midnight-blue` (default) - Deep blue with cyan accents
- `sakura` - Pink peach with cherry blossom highlights
- `solarized` - Classic Solarized color palette
- `monokai` - Developer-friendly dark theme
- `cat` - Playful cat-themed design
- `dark` - Clean dark theme
- `light` - Classic light theme
## ๐ URL Parameters
Share configured dashboards via URL:
```
https://trondheim-dashboard.com/?address=Nidarosdomen%2C%20Trondheim&theme=midnight-blue
```
**Parameters:**
- `address` - Address string (URL-encoded)
- `theme` - Theme name (lowercase, hyphenated)
## ๐ฑ Responsive Design
### Desktop (โฅ1025px)
- Fixed viewport height (no page scroll)
- 4-column grid layout
- Widgets have fixed height with internal scrolling
- Scroll indicators on widget headers
### Tablet (768-1024px)
- 2-column grid
- Natural widget heights
- Page scrolls naturally
### Mobile (<768px)
- Single column stack
- Full-width widgets
- Touch-optimized spacing
## ๐๏ธ Layout Widget
The Layout Widget allows you to customize the dashboard layout in real-time by reorganizing widgets and adjusting column widths.
### Features
- **Drag-and-drop reorganization** - Reorder widgets between columns by dragging
- **Swap widgets** - Drag a widget onto another widget to swap their positions
- **Column width adjustment** - Use sliders to resize columns (percentage-based)
- **Toggle column visibility** - Disable columns to focus on fewer widgets
- **Toggle widget visibility** - Hide individual widgets without removing them
- **Reset layout** - Restore the default dashboard layout
- **Persistent storage** - All layout changes are automatically saved to localStorage
### How to Use
1. **Open the Layout Editor** - Click the "Layout" widget (usually in the top-right)
2. **Drag widgets** - Click and drag any widget handle (โฎโฎ symbol) to reorder
3. **Swap widgets** - Drag a widget onto another widget to exchange their positions
4. **Adjust column width** - Use the slider beneath each column header to resize (Min 15%, Max 100%)
5. **Toggle columns** - Click the eye icon in the column header to hide/show the column
6. **Toggle widgets** - Click the eye icon next to a widget to hide/show it individually
7. **Reset layout** - Click the "Reset" button to restore the default layout
8. **Close editor** - Click "Close" to exit the layout editor
### Layout Data Structure
The layout is stored in localStorage under the key `dashboard-layout` and has this structure:
```javascript
{
columns: [
{
enabled: true,
width: 25, // Percentage of container width
previousWidth: null, // Width before disabling
widgets: [
"weather-right-now",
"weather-today"
]
},
// ... more columns
],
hiddenWidgets: {
"bus-widget": true, // Widget ID -> visibility state
"events-widget": false
}
}
```
### Default Layout
The dashboard ships with a 4-column default layout:
| Column 1 | Column 2 | Column 3 | Column 4 |
|----------|----------|----------|----------|
| Weather (Now) | Energy Prices | Bus Departures | Police Log |
| Weather (Today) | News | Trash Collection | Events |
This can be reset at any time using the "Reset" button.
### Technical Details
#### Architecture: Separation of Layout State and DOM
The Layout Widget achieves efficient widget reorganization by **completely separating layout state from Lit rendering**:
1. **Layout Widget (Editor Only)** - `layout-widget.js` manages only the layout configuration UI:
- Renders drag-drop interface showing widget names and column settings
- Updates layout state in localStorage
- Fires `layout-changed` events when layout changes
- **Does NOT render actual widgets**
2. **Widgets (Already in DOM)** - All 8 widgets are pre-rendered once in `dashboard.js`:
```javascript
// ... etc - all widgets exist in the DOM with display:none
```
3. **Dashboard Orchestrator** - `dashboard.js` handles layout application:
- Listens for `layout-changed` events
- Calls `applyLayoutToStyles()` to rearrange widgets
- Uses native DOM APIs to move widgets between column containers
#### How Widgets Move Without Re-rendering
When you drag a widget in the layout editor, here's what happens:
```
User drags widget
โ
Layout widget updates state & fires layout-changed event
โ
Dashboard.handleLayoutChanged() โ applyLayoutToStyles()
โ
DOM manipulation (NOT Lit re-render):
1. Move widget DOM nodes using appendChild()
2. Update CSS flex properties on columns
3. Toggle display: none/'' for visibility
โ
Widget remains in place visually, no Lit update, state preserved
```
**Key code from `dashboard.js`:**
```javascript
applyLayoutToStyles() {
// Move widget DOM nodes (not Lit rendering!)
col.widgets.slice(0, 2).forEach((widgetId) => {
const widget = grid.querySelector(`#${widgetId}`);
// Native DOM API - no Lit involved
container.appendChild(widget); // Move to new column
widget.style.display = ''; // Show/hide
});
}
```
#### Performance Benefits
- **No re-initialization** - Widgets keep their loaded data, scroll positions, and internal state
- **Fast DOM moves** - `appendChild()` is a fast native operation (just updates DOM pointers)
- **No data refetch** - When a widget moves columns, its cached API data remains valid
- **Instant visual feedback** - Layout changes are visible immediately without widget lifecycle events
#### Constraints
- Each column can hold a maximum of 2 widgets
- Disabled columns cannot receive drops
- Column width minimum is 15%, respecting layout constraints
#### Persistence & Events
- Changes automatically sync to localStorage under key `dashboard-layout`
- Fires `layout-changed` custom event so dashboard can react
- Uses native HTML5 Drag & Drop API with fallback to instance-level `_dragging` state for cross-target reliability
## ๐ ๏ธ Development
### Adding a New Widget
1. **Create widget component** in `js/components/your-widget/`
```javascript
import { BaseWidget } from "../common/base-widget.js";
class YourWidget extends BaseWidget {
constructor() {
super();
this.title = "Your Widget";
this.icon = "mdi-your-icon";
}
renderContent() {
return html`
Your content
`;
}
}
customElements.define("your-widget", YourWidget);
```
2. **Import in dashboard.js**
```javascript
import "./your-widget/your-widget.js";
```
3. **Add to template** in `dashboard.js`
```html
```
4. **Update grid layout** in dashboard.js styles if needed
### Adding a New API Integration
2. **Create API client** in `js/utils/api/your-api.js`
```javascript
import { APIBase } from "./api-base.js";
export class YourAPI {
static async getData() {
return APIBase.cachedFetch(
"https://api.example.com/data",
{ ttl: 60 * 60 * 1000 } // 1 hour
);
}
}
```
2. **Use in widget** via async methods
3. **Configure caching** in `cache-config.js`
### Creating a New Theme
1. **Create theme file** in `styles/themes/your-theme.css`
2. **Import in main.css**
```css
@import url('themes/your-theme.css');
```
3. **Add to theme selector** in `theme-selector.js`
```javascript
{ value: "your-theme", label: "Your Theme" }
```
## ๐ Known Issues & Limitations
- Some APIs may require CORS proxies in certain hosting environments
- Nominatim has usage limits - consider self-hosting for high traffic
- MET Norway API requires User-Agent header
- Dashboard auto-refreshes every 5 minutes (hard reload)
## ๐ License
MIT License - feel free to use, modify, and distribute.
## ๐ SEO & Discoverability
This project includes comprehensive SEO optimizations to improve search engine visibility and social media sharing:
- **Meta Tags**: Optimized title, description, and keywords in Norwegian for local search
- **Open Graph**: Rich previews for Facebook, LinkedIn, and other social platforms
- **Twitter Cards**: Enhanced sharing on Twitter/X with large image cards
- **Structured Data**: Schema.org JSON-LD markup for better search engine understanding
- **Semantic HTML**: Proper HTML5 structure with ARIA labels
- **robots.txt**: Allows all search engine crawlers
- **sitemap.xml**: XML sitemap for search engines
- **PWA Manifest**: Progressive Web App support (manifest.json)
- **Canonical URLs**: Prevents duplicate content issues
For detailed SEO documentation and maintenance guidelines, see [SEO_GUIDE.md](./SEO_GUIDE.md).
## ๐ค Contributing
Contributions welcome! Please feel free to submit pull requests or open issues.
## ๐ Acknowledgments
- Weather data from [MET Norway](https://api.met.no/)
- Bus data from [Entur](https://entur.org/)
- Icons from [Material Design Icons](https://materialdesignicons.com/)
- Powered by [Lit](https://lit.dev/)
- Notes: GraphQL endpoint; results are cached (24h) by default.
- External assets and helpers
- Material Design Icons (CDN): `https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css` (used by `js/utils/icon-library.js`).
- Google Fonts (in `styles/main.css`): Quicksand and Fira Code via `fonts.googleapis.com`.
- CORS proxy used optionally: `https://corsproxy.io/?` (applied from `js/utils/api-base.js` when `useCorsProxy` is true).
- External site links (used for deep links from components)
- Politiet event page: `https://www.politiet.no/politiloggen/hendelse/#/{threadId}/` (linked from police rows)
- TrdEvents site: `https://trdevents.no/event/{slug}` (linked from event rows)
Caching & CORS proxy
This project uses a small, centralized client-side cache and an optional CORS proxy. The cache and proxy are used by `js/utils/api-base.js` and the various `*api.js` utilities.
New central cache configuration
- TTL values are now controlled from a single file: `js/utils/cache-config.js`.
- The file exposes a small `CacheConfig` class with named static constants (for example `CacheConfig.WEATHER_TTL`, `CacheConfig.NRK_TTL`, `CacheConfig.BUS_STOPS_TTL`, etc.).
- Update those constants in `js/utils/cache-config.js` to tune caching globally โ no need to search individual API files.
How caching works (CacheClient)
- Storage: All cached entries are stored in `localStorage` using keys prefixed with `trondheim-cache-`.
- Keying: Cache keys are derived from the request URL using a simple 32-bit hash converted to base36 (see `CacheClient.getCacheKey(url)`).
- Payload: Each entry stores `{ timestamp, url, data }` so the app can report age and size.
- API: `CacheClient.get(url, ttl)`, `set(url, data)`, `getAge(url)`, and `isStale(url, ttl)` โ the repo now prefers the object-style calls: `CacheClient.get({ key, ttl })` and `CacheClient.set({ key, data })`.
- TTL semantics (implementation):
- If `ttl` is the number `0`, caching is disabled for that request (no cache read, no cache write).
- If `ttl` is a number > 0, the cache entry is considered valid only if age <= ttl; otherwise `CacheClient.get` returns `null`.
- If `ttl` is `null`, `CacheClient.get` will return any cached entry regardless of age (this is useful if you want to show cached data even when stale). Note: the library does not automatically revalidate stale entries in the background โ see below.
Which constants are used by the code
- The code references named constants in `CacheConfig` for each integration. Examples (names only โ change values in `js/utils/cache-config.js`):
- `CacheConfig.WEATHER_TTL` โ weather forecast freshness
- `CacheConfig.SUN_TTL` โ sunrise/sunset data
- `CacheConfig.TRASH_TTL` โ TRV wasteplan search & calendar
- `CacheConfig.ENERGY_TTL` โ electricity prices
- `CacheConfig.EVENTS_TTL` โ TrdEvents data
- `CacheConfig.BUS_STOPS_TTL` โ nearest bus stops (stop/quay metadata)
- `CacheConfig.BUS_DEPARTURES_TTL` โ departures (typically set to `0` โ no cache)
- `CacheConfig.NRK_TTL` โ NRK RSS feed
- `CacheConfig.POLICE_TTL` โ Politiet feed
- `CacheConfig.GEOCODING_TTL` โ Nominatim geocoding
- If a particular API call does not pass a `ttl` explicitly it will default to the `fetchJSON` default (which is `0` = no cache).
How the higher-level API uses the cache (APIBase.fetchJSON / fetchGraphQL)
- Function signatures (high level):
- `fetchJSON(apiName, urlOrConfig, options = {}, timeout = 10000, cacheTTL = 0, useCorsProxy = false)`
- `fetchGraphQL(apiName, urlOrConfig, queryOrNothing, variables = {}, headers = {}, timeout = 10000, cacheTTL = 0)`
- Implemented behavior (accurate):
- `cacheTTL === 0`: No cache is used โ the request is fetched live and the result is not cached.
- `cacheTTL === null`: The code will read and return any cached entry regardless of age. The current implementation does NOT automatically revalidate the cache in the background โ it simply returns the cached value. If you want "stale-while-revalidate" behavior (return cached immediately, then refresh cache in background) you'll need to implement a small background refresh in `APIBase.fetchJSON`.
- `cacheTTL` is a positive number: the cache is read and the entry is returned only if it is within the TTL. If no valid (non-expired) entry exists, the network request is performed and the fresh response is cached.
Examples from the codebase (what to look for)
- `js/utils/trash-api.js` โ uses `TrashAPI.CACHE_DURATION` (configured via `CacheConfig.TRASH_TTL`)
- `js/utils/energy-api.js` โ uses `EnergyAPI.CACHE_DURATION` (configured via `CacheConfig.ENERGY_TTL`)
- `js/utils/events-api.js` โ uses `CacheConfig.EVENTS_TTL`
- `js/utils/nrk-rss-api.js` โ uses `CacheConfig.NRK_TTL` (short TTL, RSS is parsed client-side)
- `js/utils/bus-api.js` โ stop metadata uses `CacheConfig.BUS_STOPS_TTL`; real-time departures use no cache (`ttl === 0`)
Background refresh (optional enhancement)
- The README used to describe automatic background refresh behavior; the current implementation does not perform background revalidation. If you want that UX, a recommended approach is:
1. When `CacheClient.get({ key, ttl })` returns a value but `CacheClient.isStale({ key, ttl })` is true, return the cached value immediately.
2. Start a non-blocking `fetchJSONDirect(...)` for that key and `CacheClient.set({ key, data })` when it resolves (optionally emit an event so the UI can refresh).
- I can implement this `stale-while-revalidate` behavior in `js/utils/api-base.js` if you'd like.
CORS proxy behavior
- The optional CORS proxy is applied by `APIBase.fetchJSON` when `useCorsProxy` is truthy: the URL is prefixed with `https://corsproxy.io/?` and the original URL is encoded as a parameter.
- The proxy is used in code where cross-origin restrictions or missing CORS headers are likely (for example, MET Norway and Politiet calls in this repo set `useCorsProxy = true`).
- Notes and trade-offs:
- Proxies add an extra network hop and may add rate limits or latency. They may also alter response caching semantics.
- If you control a backend for the project, the recommended production approach is to proxy requests server-side (so you can add caching, API keys, and logging) instead of relying on a public CORS proxy.
Development
- No build step required: the app uses ES modules and runs in modern browsers. Serve with a static server (see Quick start).
- Cache & debugging:
- Clear or inspect cache during development: open your browser DevTools โ Application โ Local Storage and remove keys prefixed with `trondheim-cache-`.
- Programmatic inspection: use `CacheClient.getAge(url)` and `CacheClient.isStale(url, ttl)` in the console as needed.
- To change TTLs, edit `js/utils/cache-config.js` and reload the page.
- Testing & linting:
- There are no automated tests included. For quick checks, run the app locally and open DevTools to inspect console and network requests.
- Suggested next steps: add unit tests for pure utilities (`js/utils/`) with a lightweight runner (Vitest) and add an end-to-end smoke test (Playwright) that verifies widgets render and handle cached/mocked responses.
- Disabling the public CORS proxy: edit `js/utils/api-base.js` or pass `useCorsProxy = false` from the calling API util to avoid `https://corsproxy.io/?`.
Security and privacy
- This project runs entirely client-side. No data is sent to the project owner (except to the third-party APIs the app contacts).
- Be mindful of exposing API keys in client-side code. This project does not include any private API keys by default.
Contributing
- Improvements and bug fixes are welcome. Please open a GitHub issue or a pull request.
- When contributing code, prefer small, focused PRs and include a short description and screenshots if the change affects the UI.
License
- The project is provided for personal and educational use. If you plan to reuse code commercially, please confirm license terms with the author.
Credits
- Bus data: Entur
- Weather: MET Norway / YR
- Trash schedules: TRV (Trondheim Renholdsverk)
- Electricity prices: hvakosterstrommen.no
- Geocoding: OpenStreetMap Nominatim
- Animated backgrounds: bubbly-bg by @tipsy