{"id":42703020,"url":"https://github.com/tipsy/trondheim-dashboard","last_synced_at":"2026-01-29T14:24:07.454Z","repository":{"id":320936927,"uuid":"1083785529","full_name":"tipsy/trondheim-dashboard","owner":"tipsy","description":null,"archived":false,"fork":false,"pushed_at":"2025-11-23T11:34:04.000Z","size":2022,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-23T12:19:27.128Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tipsy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-26T17:58:09.000Z","updated_at":"2025-11-23T11:34:07.000Z","dependencies_parsed_at":null,"dependency_job_id":"1bf28808-6460-47b4-9e40-9665b48a29a4","html_url":"https://github.com/tipsy/trondheim-dashboard","commit_stats":null,"previous_names":["tipsy/trondheim-dashboard"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tipsy/trondheim-dashboard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Ftrondheim-dashboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Ftrondheim-dashboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Ftrondheim-dashboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Ftrondheim-dashboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tipsy","download_url":"https://codeload.github.com/tipsy/trondheim-dashboard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Ftrondheim-dashboard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28879422,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-29T10:31:27.438Z","status":"ssl_error","status_checked_at":"2026-01-29T10:31:01.017Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-29T14:24:06.302Z","updated_at":"2026-01-29T14:24:07.435Z","avatar_url":"https://github.com/tipsy.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Trondheim Dashboard\n\nA 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.\n\n**Live demo:** https://trondheim-dashboard.com\n\nThis repository is intentionally lightweight — just static HTML, CSS and JavaScript with no build process required — so it can be served from any static host.\n\n## ✨ Features\n\n- **📍 Location-based widgets** - Enter any address in Trondheim/Trøndelag to get personalized information\n- **🚌 Real-time bus departures** - Live ATB/Entur data with nearest stops and platform selection\n- **🌤️ Weather forecasts** - Current conditions, hourly forecast, and 5-day outlook from MET Norway\n- **⚡ Electricity prices** - Current and upcoming hourly prices by Norwegian price area\n- **🗑️ Trash collection** - Personalized waste pickup schedule from TRV\n- **👮 Police log** - Latest incidents from Politiet (Trøndelag district)\n- **📰 News** - Top stories from NRK Trøndelag\n- **🎭 Events** - Upcoming events in Trondheim with date filtering\n- **🔗 URL state** - Shareable links with address and theme parameters\n- **♻️ Auto-refresh** - Dashboard reloads every 5 minutes to get latest data and app updates\n- **📱 Responsive** - Optimized layouts for desktop (grid with scrollable widgets) and mobile/tablet (stacked)\n\n## 🚀 Quick Start\n\n1. **Clone the repository:**\n   ```bash\n   git clone https://github.com/your-user/trondheim-dashboard.git\n   cd trondheim-dashboard\n   ```\n\n2. **Serve with any static server:**\n   ```bash\n   # Python 3 built-in server\n   python -m http.server 8000\n\n   # Node (http-server)\n   npx http-server\n\n   # Or simply open index.html in a browser\n   ```\n\n3. **Open in browser:** Navigate to `http://localhost:8000`\n\nNo build process, no dependencies to install — just serve and go!\n\n## 📁 Project Structure\n\n```\ntrondheim-dashboard/\n├── index.html              # Main entry point\n├── manifest.json           # PWA manifest\n├── robots.txt              # Search engine crawler rules\n├── sitemap.xml             # XML sitemap for SEO\n├── CNAME                   # GitHub Pages custom domain\n├── README.md               # This file\n├── SEO_GUIDE.md            # SEO documentation and guidelines\n├── img/\n│   ├── trondheimsrosa.svg  # Dashboard favicon (Trondheim rose)\n│   └── example-background.jpg\n├── og/\n│   └── screenshot.png      # Open Graph / social media preview image\n├── styles/\n│   ├── main.css            # Global styles and layout\n│   └── themes/             # Themes\n├── img/\n│   └── example-background.jpg\n└── js/\n    ├── components/         # Web Components (Lit-based)\n    │   ├── dashboard.js    # Main dashboard orchestrator\n    │   ├── address/        # Address input with autocomplete\n    │   │   ├── address-input.js\n    │   │   └── address-suggestion-item.js\n    │   ├── bus/            # Bus departure widget\n    │   │   ├── bus-widget.js\n    │   │   └── bus-row.js\n    │   ├── weather/        # Weather widgets (4 components)\n    │   │   ├── weather-right-now.js    # Current + next 4 hours\n    │   │   ├── weather-today.js        # 5-day forecast\n    │   │   ├── weather-current.js\n    │   │   ├── weather-detail.js\n    │   │   └── weather-hour.js\n    │   ├── energy/         # Electricity price widget\n    │   │   └── energy-widget.js\n    │   ├── trash/          # Trash collection widget\n    │   │   ├── trash-widget.js\n    │   │   └── trash-row.js\n    │   ├── police/         # Police log widget\n    │   │   └── police-widget.js\n    │   ├── nrk/            # News widget\n    │   │   └── nrk-widget.js\n    │   ├── events/         # Events widget\n    │   │   └── events-widget.js\n    │   ├── config/         # Configuration components\n    │   │   └── theme-selector.js\n    │   └── common/         # Reusable UI components\n    │       ├── base-widget.js          # Base class for all widgets\n    │       ├── widget-list.js\n    │       ├── widget-row.js\n    │       ├── heading-2.js\n    │       ├── input-field.js\n    │       ├── custom-select.js\n    │       ├── loading-spinner.js\n    │       ├── error-message.js\n    │       └── buttons/\n    │           ├── base-button.js\n    │           ├── icon-button.js\n    │           ├── primary-button.js\n    │           └── secondary-button.js\n    └── utils/              # API clients and helpers\n        ├── api-base.js              # Fetch wrapper with caching\n        ├── cache-client.js          # localStorage-based cache\n        ├── cache-config.js          # Cache TTL configuration\n        ├── icon-library.js          # Weather icon mapping\n        ├── date-formatter.js        # Norwegian date formatting\n        ├── shared-styles.js         # Lit shared CSS\n        ├── event-helpers.js         # Custom event utilities\n        └── api/                     # External API clients\n            ├── api-base.js          # Base class for all APIs\n            ├── bus-api.js           # Entur GraphQL client\n            ├── weather-api.js       # MET Norway client\n            ├── energy-api.js        # Electricity price client\n            ├── trash-api.js         # TRV wasteplan client\n            ├── geocoding-api.js     # Nominatim geocoding client\n            ├── police-api.js        # Politiet API client\n            ├── nrk-rss-api.js      # NRK RSS parser\n            └── events-api.js        # TrdEvents GraphQL client\n```\n\n## 🏗️ Architecture\n\n### Technology Stack\n\n- **Lit 3** - Lightweight reactive Web Components library (loaded via CDN)\n- **Material Design Icons** - Icon font (loaded via CDN)\n- **Vanilla CSS** - No preprocessors, CSS variables for theming\n- **ES Modules** - Native browser modules, no bundler needed\n- **Custom Elements v1** - Web Components standard with Shadow DOM\n\n### Component Architecture\n\nAll widgets extend `BaseWidget`, which provides:\n- Standard layout (header with title/icon, scrollable content area)\n- Loading state management with spinner\n- Error state management with error messages\n- Placeholder state for widgets awaiting user input\n- Scroll detection for header border effects\n- Responsive behavior (fixed height on desktop, natural height on mobile)\n\n```javascript\n// Typical widget structure\nclass MyWidget extends BaseWidget {\n  constructor() {\n    super();\n    this.title = \"Widget Title\";\n    this.icon = \"mdi-icon-name\";\n  }\n\n  async connectedCallback() {\n    super.connectedCallback();\n    await this.loadData();\n  }\n\n  renderContent() {\n    // Widget-specific rendering\n  }\n}\n```\n\n### State Management\n\n- **Component-level state** - Each widget manages its own data using Lit's reactive properties\n- **URL parameters** - Address and theme persisted in URL for sharing\n- **localStorage** - Theme preference and API cache storage\n- **Event-driven updates** - Components communicate via custom events:\n  - `location-updated` - Fired when address changes\n  - `theme-changed` - Fired when theme changes\n\n### Caching Strategy\n\nSmart caching with configurable TTLs (defined in `cache-config.js`):\n\n| Data Type | TTL | Rationale |\n|-----------|-----|-----------|\n| Weather | 4 minutes | Slightly below 5-min dashboard refresh |\n| Bus stops (metadata) | 6 hours | Stop locations change infrequently |\n| Bus departures | 0 (no cache) | Real-time data |\n| Energy prices | 1 hour | Updated hourly |\n| Trash schedule | 24 hours | Daily granularity |\n| News (NRK RSS) | 4 minutes | Fresh news on each dashboard cycle |\n| Police log | 4 minutes | Recent incidents |\n| Events | 24 hours | Relatively static |\n| Geocoding | 1 hour | Respect rate limits |\n\n## 🔌 External Integrations\n\n### 1. Entur Journey Planner (Bus Data)\n- **Endpoint:** `https://api.entur.io/journey-planner/v3/graphql`\n- **Type:** GraphQL API\n- **Purpose:** Real-time bus departures, stop searches, and platform information\n- **Headers:** `ET-Client-Name` for API identification\n- **Used by:** `bus-api.js` → `bus-widget.js`\n\n### 2. MET Norway Weather API\n- **Endpoint:** `https://api.met.no/weatherapi/locationforecast/2.0/complete`\n- **Type:** REST API (JSON)\n- **Purpose:** Weather forecasts and current conditions\n- **Headers:** `User-Agent` required\n- **Used by:** `weather-api.js` → `weather-right-now.js`, `weather-today.js`\n\n### 3. Sunrise-Sunset API\n- **Endpoint:** `https://api.sunrise-sunset.org/json`\n- **Type:** REST API (JSON)\n- **Purpose:** Sunrise/sunset times and day length\n- **Used by:** `weather-api.js` → `weather-today.js`\n\n### 4. TRV Wasteplan API\n- **Endpoints:**\n  - Search: `https://trv.no/wp-json/wasteplan/v2/adress`\n  - Calendar: `https://trv.no/wp-json/wasteplan/v2/calendar/{id}`\n- **Type:** WordPress REST API\n- **Purpose:** Trash collection schedules for Trondheim addresses\n- **Cache:** 24 hours\n- **Used by:** `trash-api.js` → `trash-widget.js`\n\n### 5. HvaKosterStrommen (Electricity Prices)\n- **Endpoint:** `https://www.hvakosterstrommen.no/api/v1/prices/{year}/{month}-{day}_{area}.json`\n- **Type:** REST API (JSON)\n- **Purpose:** Hourly electricity prices by Norwegian price area (NO1-NO5)\n- **Cache:** 1 hour\n- **Used by:** `energy-api.js` → `energy-widget.js`\n\n### 6. OpenStreetMap Nominatim (Geocoding)\n- **Endpoints:**\n  - Search: `https://nominatim.openstreetmap.org/search`\n  - Reverse: `https://nominatim.openstreetmap.org/reverse`\n- **Type:** REST API (JSON)\n- **Purpose:** Address autocomplete and geocoding\n- **Headers:** Custom `User-Agent`\n- **Features:** Filters to Norway, prioritizes Trøndelag, requires house numbers\n- **Used by:** `geocoding-api.js` → `address-input.js`\n\n### 7. Politiet API (Police Log)\n- **Endpoint:** `https://api.politiet.no/politiloggen/v1/messages`\n- **Type:** REST API (JSON)\n- **Purpose:** Latest police incidents in Trøndelag\n- **Notes:** May require CORS proxy\n- **Cache:** 4 minutes\n- **Used by:** `police-api.js` → `police-widget.js`\n\n### 8. NRK RSS Feed (News)\n- **Endpoint:** `https://www.nrk.no/trondelag/siste.rss`\n- **Type:** RSS/XML\n- **Purpose:** Top 10 news stories from NRK Trøndelag\n- **Parsing:** XML parsed in-browser to JSON\n- **Cache:** 4 minutes\n- **Used by:** `nrk-rss-api.js` → `nrk-widget.js`\n\n### 9. TrdEvents GraphQL (Events)\n- **Endpoint:** `https://trdevents-224613.web.app/graphQL`\n- **Type:** GraphQL API\n- **Purpose:** Upcoming events in Trondheim\n- **Cache:** 24 hours\n- **Used by:** `events-api.js` → `events-widget.js`\n\n## 🎨 Theming\n\nThe dashboard uses CSS variables for theming. Each theme file defines:\n\n```css\n[data-theme=\"theme-name\"] {\n  /* Core colors */\n  --background-color: ...;\n  --card-background: ...;\n  --text-color: ...;\n  --text-light: ...;\n  --heading-color: ...;\n  \n  /* Interactive elements */\n  --primary-color: ...;\n  --secondary-color: ...;\n  --button-text: ...;\n  \n  /* Borders and dividers */\n  --border-color: ...;\n  \n  /* Optional: background image */\n  --background-image: url(...);\n  \n  /* Typography */\n  --font-family: ...;\n}\n```\n\n**Available themes:**\n- `midnight-blue` (default) - Deep blue with cyan accents\n- `sakura` - Pink peach with cherry blossom highlights\n- `solarized` - Classic Solarized color palette\n- `monokai` - Developer-friendly dark theme\n- `cat` - Playful cat-themed design\n- `dark` - Clean dark theme\n- `light` - Classic light theme\n\n## 🌐 URL Parameters\n\nShare configured dashboards via URL:\n\n```\nhttps://trondheim-dashboard.com/?address=Nidarosdomen%2C%20Trondheim\u0026theme=midnight-blue\n```\n\n**Parameters:**\n- `address` - Address string (URL-encoded)\n- `theme` - Theme name (lowercase, hyphenated)\n\n## 📱 Responsive Design\n\n### Desktop (≥1025px)\n- Fixed viewport height (no page scroll)\n- 4-column grid layout\n- Widgets have fixed height with internal scrolling\n- Scroll indicators on widget headers\n\n### Tablet (768-1024px)\n- 2-column grid\n- Natural widget heights\n- Page scrolls naturally\n\n### Mobile (\u003c768px)\n- Single column stack\n- Full-width widgets\n- Touch-optimized spacing\n\n## 🎛️ Layout Widget\n\nThe Layout Widget allows you to customize the dashboard layout in real-time by reorganizing widgets and adjusting column widths.\n\n### Features\n\n- **Drag-and-drop reorganization** - Reorder widgets between columns by dragging\n- **Swap widgets** - Drag a widget onto another widget to swap their positions\n- **Column width adjustment** - Use sliders to resize columns (percentage-based)\n- **Toggle column visibility** - Disable columns to focus on fewer widgets\n- **Toggle widget visibility** - Hide individual widgets without removing them\n- **Reset layout** - Restore the default dashboard layout\n- **Persistent storage** - All layout changes are automatically saved to localStorage\n\n### How to Use\n\n1. **Open the Layout Editor** - Click the \"Layout\" widget (usually in the top-right)\n2. **Drag widgets** - Click and drag any widget handle (⋮⋮ symbol) to reorder\n3. **Swap widgets** - Drag a widget onto another widget to exchange their positions\n4. **Adjust column width** - Use the slider beneath each column header to resize (Min 15%, Max 100%)\n5. **Toggle columns** - Click the eye icon in the column header to hide/show the column\n6. **Toggle widgets** - Click the eye icon next to a widget to hide/show it individually\n7. **Reset layout** - Click the \"Reset\" button to restore the default layout\n8. **Close editor** - Click \"Close\" to exit the layout editor\n\n### Layout Data Structure\n\nThe layout is stored in localStorage under the key `dashboard-layout` and has this structure:\n\n```javascript\n{\n  columns: [\n    {\n      enabled: true,\n      width: 25,           // Percentage of container width\n      previousWidth: null, // Width before disabling\n      widgets: [\n        \"weather-right-now\",\n        \"weather-today\"\n      ]\n    },\n    // ... more columns\n  ],\n  hiddenWidgets: {\n    \"bus-widget\": true,    // Widget ID -\u003e visibility state\n    \"events-widget\": false\n  }\n}\n```\n\n### Default Layout\n\nThe dashboard ships with a 4-column default layout:\n\n| Column 1 | Column 2 | Column 3 | Column 4 |\n|----------|----------|----------|----------|\n| Weather (Now) | Energy Prices | Bus Departures | Police Log |\n| Weather (Today) | News | Trash Collection | Events |\n\nThis can be reset at any time using the \"Reset\" button.\n\n### Technical Details\n\n#### Architecture: Separation of Layout State and DOM\n\nThe Layout Widget achieves efficient widget reorganization by **completely separating layout state from Lit rendering**:\n\n1. **Layout Widget (Editor Only)** - `layout-widget.js` manages only the layout configuration UI:\n   - Renders drag-drop interface showing widget names and column settings\n   - Updates layout state in localStorage\n   - Fires `layout-changed` events when layout changes\n   - **Does NOT render actual widgets**\n\n2. **Widgets (Already in DOM)** - All 8 widgets are pre-rendered once in `dashboard.js`:\n   ```javascript\n   \u003cbus-widget id=\"bus-widget\" style=\"display:none\"\u003e\u003c/bus-widget\u003e\n   \u003cevents-widget id=\"events-widget\" style=\"display:none\"\u003e\u003c/events-widget\u003e\n   // ... etc - all widgets exist in the DOM with display:none\n   ```\n\n3. **Dashboard Orchestrator** - `dashboard.js` handles layout application:\n   - Listens for `layout-changed` events\n   - Calls `applyLayoutToStyles()` to rearrange widgets\n   - Uses native DOM APIs to move widgets between column containers\n\n#### How Widgets Move Without Re-rendering\n\nWhen you drag a widget in the layout editor, here's what happens:\n\n```\nUser drags widget\n         ↓\nLayout widget updates state \u0026 fires layout-changed event\n         ↓\nDashboard.handleLayoutChanged() → applyLayoutToStyles()\n         ↓\nDOM manipulation (NOT Lit re-render):\n  1. Move widget DOM nodes using appendChild()\n  2. Update CSS flex properties on columns\n  3. Toggle display: none/'' for visibility\n         ↓\nWidget remains in place visually, no Lit update, state preserved\n```\n\n**Key code from `dashboard.js`:**\n```javascript\napplyLayoutToStyles() {\n  // Move widget DOM nodes (not Lit rendering!)\n  col.widgets.slice(0, 2).forEach((widgetId) =\u003e {\n    const widget = grid.querySelector(`#${widgetId}`);\n    // Native DOM API - no Lit involved\n    container.appendChild(widget);  // Move to new column\n    widget.style.display = '';      // Show/hide\n  });\n}\n```\n\n#### Performance Benefits\n\n- **No re-initialization** - Widgets keep their loaded data, scroll positions, and internal state\n- **Fast DOM moves** - `appendChild()` is a fast native operation (just updates DOM pointers)\n- **No data refetch** - When a widget moves columns, its cached API data remains valid\n- **Instant visual feedback** - Layout changes are visible immediately without widget lifecycle events\n\n#### Constraints\n\n- Each column can hold a maximum of 2 widgets\n- Disabled columns cannot receive drops\n- Column width minimum is 15%, respecting layout constraints\n\n#### Persistence \u0026 Events\n\n- Changes automatically sync to localStorage under key `dashboard-layout`\n- Fires `layout-changed` custom event so dashboard can react\n- Uses native HTML5 Drag \u0026 Drop API with fallback to instance-level `_dragging` state for cross-target reliability\n\n## 🛠️ Development\n\n### Adding a New Widget\n\n1. **Create widget component** in `js/components/your-widget/`\n   ```javascript\n   import { BaseWidget } from \"../common/base-widget.js\";\n   \n   class YourWidget extends BaseWidget {\n     constructor() {\n       super();\n       this.title = \"Your Widget\";\n       this.icon = \"mdi-your-icon\";\n     }\n     \n     renderContent() {\n       return html`\u003cp\u003eYour content\u003c/p\u003e`;\n     }\n   }\n   \n   customElements.define(\"your-widget\", YourWidget);\n   ```\n\n2. **Import in dashboard.js**\n   ```javascript\n   import \"./your-widget/your-widget.js\";\n   ```\n\n3. **Add to template** in `dashboard.js`\n   ```html\n   \u003cyour-widget id=\"your-widget\"\u003e\u003c/your-widget\u003e\n   ```\n\n4. **Update grid layout** in dashboard.js styles if needed\n\n### Adding a New API Integration\n\n2. **Create API client** in `js/utils/api/your-api.js`\n   ```javascript\n   import { APIBase } from \"./api-base.js\";\n   \n   export class YourAPI {\n     static async getData() {\n       return APIBase.cachedFetch(\n         \"https://api.example.com/data\",\n         { ttl: 60 * 60 * 1000 } // 1 hour\n       );\n     }\n   }\n   ```\n\n2. **Use in widget** via async methods\n3. **Configure caching** in `cache-config.js`\n\n### Creating a New Theme\n\n1. **Create theme file** in `styles/themes/your-theme.css`\n2. **Import in main.css**\n   ```css\n   @import url('themes/your-theme.css');\n   ```\n3. **Add to theme selector** in `theme-selector.js`\n   ```javascript\n   { value: \"your-theme\", label: \"Your Theme\" }\n   ```\n\n## 🐛 Known Issues \u0026 Limitations\n\n- Some APIs may require CORS proxies in certain hosting environments\n- Nominatim has usage limits - consider self-hosting for high traffic\n- MET Norway API requires User-Agent header\n- Dashboard auto-refreshes every 5 minutes (hard reload)\n\n## 📄 License\n\nMIT License - feel free to use, modify, and distribute.\n\n## 🔍 SEO \u0026 Discoverability\n\nThis project includes comprehensive SEO optimizations to improve search engine visibility and social media sharing:\n\n- **Meta Tags**: Optimized title, description, and keywords in Norwegian for local search\n- **Open Graph**: Rich previews for Facebook, LinkedIn, and other social platforms\n- **Twitter Cards**: Enhanced sharing on Twitter/X with large image cards\n- **Structured Data**: Schema.org JSON-LD markup for better search engine understanding\n- **Semantic HTML**: Proper HTML5 structure with ARIA labels\n- **robots.txt**: Allows all search engine crawlers\n- **sitemap.xml**: XML sitemap for search engines\n- **PWA Manifest**: Progressive Web App support (manifest.json)\n- **Canonical URLs**: Prevents duplicate content issues\n\nFor detailed SEO documentation and maintenance guidelines, see [SEO_GUIDE.md](./SEO_GUIDE.md).\n\n## 🤝 Contributing\n\nContributions welcome! Please feel free to submit pull requests or open issues.\n\n## 🙏 Acknowledgments\n\n- Weather data from [MET Norway](https://api.met.no/)\n- Bus data from [Entur](https://entur.org/)\n- Icons from [Material Design Icons](https://materialdesignicons.com/)\n- Powered by [Lit](https://lit.dev/)\n  - Notes: GraphQL endpoint; results are cached (24h) by default.\n\n- External assets and helpers\n  - 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`).\n  - Google Fonts (in `styles/main.css`): Quicksand and Fira Code via `fonts.googleapis.com`.\n  - CORS proxy used optionally: `https://corsproxy.io/?` (applied from `js/utils/api-base.js` when `useCorsProxy` is true).\n\n- External site links (used for deep links from components)\n  - Politiet event page: `https://www.politiet.no/politiloggen/hendelse/#/{threadId}/` (linked from police rows)\n  - TrdEvents site: `https://trdevents.no/event/{slug}` (linked from event rows)\n\nCaching \u0026 CORS proxy\nThis 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.\n\nNew central cache configuration\n- TTL values are now controlled from a single file: `js/utils/cache-config.js`.\n- The file exposes a small `CacheConfig` class with named static constants (for example `CacheConfig.WEATHER_TTL`, `CacheConfig.NRK_TTL`, `CacheConfig.BUS_STOPS_TTL`, etc.).\n- Update those constants in `js/utils/cache-config.js` to tune caching globally — no need to search individual API files.\n\nHow caching works (CacheClient)\n- Storage: All cached entries are stored in `localStorage` using keys prefixed with `trondheim-cache-`.\n- Keying: Cache keys are derived from the request URL using a simple 32-bit hash converted to base36 (see `CacheClient.getCacheKey(url)`).\n- Payload: Each entry stores `{ timestamp, url, data }` so the app can report age and size.\n- 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 })`.\n- TTL semantics (implementation):\n  - If `ttl` is the number `0`, caching is disabled for that request (no cache read, no cache write).\n  - If `ttl` is a number \u003e 0, the cache entry is considered valid only if age \u003c= ttl; otherwise `CacheClient.get` returns `null`.\n  - 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.\n\nWhich constants are used by the code\n- The code references named constants in `CacheConfig` for each integration. Examples (names only — change values in `js/utils/cache-config.js`):\n  - `CacheConfig.WEATHER_TTL` — weather forecast freshness\n  - `CacheConfig.SUN_TTL` — sunrise/sunset data\n  - `CacheConfig.TRASH_TTL` — TRV wasteplan search \u0026 calendar\n  - `CacheConfig.ENERGY_TTL` — electricity prices\n  - `CacheConfig.EVENTS_TTL` — TrdEvents data\n  - `CacheConfig.BUS_STOPS_TTL` — nearest bus stops (stop/quay metadata)\n  - `CacheConfig.BUS_DEPARTURES_TTL` — departures (typically set to `0` — no cache)\n  - `CacheConfig.NRK_TTL` — NRK RSS feed\n  - `CacheConfig.POLICE_TTL` — Politiet feed\n  - `CacheConfig.GEOCODING_TTL` — Nominatim geocoding\n\n- If a particular API call does not pass a `ttl` explicitly it will default to the `fetchJSON` default (which is `0` = no cache).\n\nHow the higher-level API uses the cache (APIBase.fetchJSON / fetchGraphQL)\n- Function signatures (high level):\n  - `fetchJSON(apiName, urlOrConfig, options = {}, timeout = 10000, cacheTTL = 0, useCorsProxy = false)`\n  - `fetchGraphQL(apiName, urlOrConfig, queryOrNothing, variables = {}, headers = {}, timeout = 10000, cacheTTL = 0)`\n- Implemented behavior (accurate):\n  - `cacheTTL === 0`: No cache is used — the request is fetched live and the result is not cached.\n  - `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`.\n  - `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.\n\nExamples from the codebase (what to look for)\n- `js/utils/trash-api.js` — uses `TrashAPI.CACHE_DURATION` (configured via `CacheConfig.TRASH_TTL`)\n- `js/utils/energy-api.js` — uses `EnergyAPI.CACHE_DURATION` (configured via `CacheConfig.ENERGY_TTL`)\n- `js/utils/events-api.js` — uses `CacheConfig.EVENTS_TTL`\n- `js/utils/nrk-rss-api.js` — uses `CacheConfig.NRK_TTL` (short TTL, RSS is parsed client-side)\n- `js/utils/bus-api.js` — stop metadata uses `CacheConfig.BUS_STOPS_TTL`; real-time departures use no cache (`ttl === 0`)\n\nBackground refresh (optional enhancement)\n- 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:\n  1. When `CacheClient.get({ key, ttl })` returns a value but `CacheClient.isStale({ key, ttl })` is true, return the cached value immediately.\n  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).\n- I can implement this `stale-while-revalidate` behavior in `js/utils/api-base.js` if you'd like.\n\nCORS proxy behavior\n- 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.\n- 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`).\n- Notes and trade-offs:\n  - Proxies add an extra network hop and may add rate limits or latency. They may also alter response caching semantics.\n  - 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.\n\nDevelopment\n- No build step required: the app uses ES modules and runs in modern browsers. Serve with a static server (see Quick start).\n- Cache \u0026 debugging:\n  - Clear or inspect cache during development: open your browser DevTools → Application → Local Storage and remove keys prefixed with `trondheim-cache-`.\n  - Programmatic inspection: use `CacheClient.getAge(url)` and `CacheClient.isStale(url, ttl)` in the console as needed.\n  - To change TTLs, edit `js/utils/cache-config.js` and reload the page.\n- Testing \u0026 linting:\n  - There are no automated tests included. For quick checks, run the app locally and open DevTools to inspect console and network requests.\n  - 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.\n- 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/?`.\n\nSecurity and privacy\n- This project runs entirely client-side. No data is sent to the project owner (except to the third-party APIs the app contacts).\n- Be mindful of exposing API keys in client-side code. This project does not include any private API keys by default.\n\nContributing\n- Improvements and bug fixes are welcome. Please open a GitHub issue or a pull request.\n- When contributing code, prefer small, focused PRs and include a short description and screenshots if the change affects the UI.\n\nLicense\n- The project is provided for personal and educational use. If you plan to reuse code commercially, please confirm license terms with the author.\n\nCredits\n- Bus data: Entur\n- Weather: MET Norway / YR\n- Trash schedules: TRV (Trondheim Renholdsverk)\n- Electricity prices: hvakosterstrommen.no\n- Geocoding: OpenStreetMap Nominatim\n- Animated backgrounds: bubbly-bg by @tipsy\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftipsy%2Ftrondheim-dashboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftipsy%2Ftrondheim-dashboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftipsy%2Ftrondheim-dashboard/lists"}