Skip to content

feat: add generic custom data source widget#610

Open
RC1140 wants to merge 1 commit intoimmichFrame:mainfrom
RC1140:feat/custom-data-source-widget
Open

feat: add generic custom data source widget#610
RC1140 wants to merge 1 commit intoimmichFrame:mainfrom
RC1140:feat/custom-data-source-widget

Conversation

@RC1140
Copy link
Copy Markdown

@RC1140 RC1140 commented Mar 23, 2026

Summary

Adds a configurable widget that fetches structured data from user-specified URLs and displays it on the photo frame. This enables displaying arbitrary external data (fitness stats, Home Assistant sensors, weather from custom sources, etc.) alongside photos.

  • Supports multiple data sources, each with independent refresh intervals and caching
  • URLs stay server-side (proxied via /api/CustomWidget) — only display config is exposed to the client
  • Follows existing widget patterns (weather/calendar/appointments) for consistency

Configuration

General:
  ShowCustomWidget: true
  CustomWidgetPosition: 'top-left'  # top-left, top-right, bottom-left, bottom-right
  CustomWidgetSources:
    - Url: 'http://my-service:3001/api/stats'
      RefreshMinutes: 10
    - Url: 'http://another-service/data'
      RefreshMinutes: 30

Expected response format from data sources

{
  "title": "My Stats",
  "items": [
    { "label": "Metric A", "value": "42", "secondary": "optional detail" },
    { "label": "Metric B", "value": "100" }
  ]
}

Changes

Backend (C# / ASP.NET Core):

  • CustomWidgetSourceConfig model for per-source URL + refresh interval
  • CustomWidgetData / CustomWidgetItem response models
  • ICustomWidgetService + CustomWidgetService with per-source caching
  • CustomWidgetController — proxies data (keeps URLs server-side)
  • Settings: ShowCustomWidget, CustomWidgetSources, CustomWidgetPosition
  • ClientSettingsDto exposes display config only

Frontend (Svelte 5):

  • New custom-widget.svelte component with configurable corner positioning
  • Integrated into home-page.svelte with conditional rendering
  • Regenerated oazapfts API client with new endpoint types

Test plan

  • Verify widget renders with valid data source URL
  • Verify widget hidden when ShowCustomWidget: false
  • Verify multiple data sources display correctly
  • Verify positioning works for all four corners
  • Verify graceful handling when data source is unreachable

Summary by CodeRabbit

  • New Features
    • Custom widget component now available on the home page for displaying external data from configured sources
    • Users can configure widget visibility, position, data source URLs, and refresh intervals
    • Automatic data refresh based on configured intervals per source

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 124a34d5-b27e-4acf-9f19-fd9911c5cd49

📥 Commits

Reviewing files that changed from the base of the PR and between 50b48a5 and 8066eee.

📒 Files selected for processing (15)
  • ImmichFrame.Core/Interfaces/ICustomWidgetService.cs
  • ImmichFrame.Core/Interfaces/IServerSettings.cs
  • ImmichFrame.Core/Models/CustomWidgetData.cs
  • ImmichFrame.Core/Models/CustomWidgetSourceConfig.cs
  • ImmichFrame.Core/Services/CustomWidgetService.cs
  • ImmichFrame.WebApi/Controllers/CustomWidgetController.cs
  • ImmichFrame.WebApi/Models/ClientSettingsDto.cs
  • ImmichFrame.WebApi/Models/ServerSettings.cs
  • ImmichFrame.WebApi/Program.cs
  • docker/Settings.example.json
  • docker/Settings.example.yml
  • immichFrame.Web/src/lib/components/elements/custom-widget.svelte
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte
  • immichFrame.Web/src/lib/immichFrameApi.ts
  • openApi/swagger.json
✅ Files skipped from review due to trivial changes (8)
  • ImmichFrame.Core/Interfaces/ICustomWidgetService.cs
  • docker/Settings.example.yml
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte
  • docker/Settings.example.json
  • ImmichFrame.WebApi/Program.cs
  • ImmichFrame.Core/Models/CustomWidgetSourceConfig.cs
  • openApi/swagger.json
  • ImmichFrame.Core/Models/CustomWidgetData.cs
🚧 Files skipped from review as they are similar to previous changes (5)
  • ImmichFrame.Core/Interfaces/IServerSettings.cs
  • ImmichFrame.WebApi/Controllers/CustomWidgetController.cs
  • immichFrame.Web/src/lib/components/elements/custom-widget.svelte
  • immichFrame.Web/src/lib/immichFrameApi.ts
  • ImmichFrame.Core/Services/CustomWidgetService.cs

📝 Walkthrough

Walkthrough

This PR adds a custom widget feature that enables users to display data fetched from external HTTP endpoints. It includes service-layer caching, API endpoints, configuration management, and a frontend Svelte component for rendering the widgets on the home page.

Changes

Cohort / File(s) Summary
Core Models and Interfaces
ImmichFrame.Core/Interfaces/ICustomWidgetService.cs, ImmichFrame.Core/Interfaces/IServerSettings.cs, ImmichFrame.Core/Models/CustomWidgetData.cs, ImmichFrame.Core/Models/CustomWidgetSourceConfig.cs
New service interface, data models, and configuration properties. IGeneralSettings extended with ShowCustomWidget, CustomWidgetSources, and CustomWidgetPosition fields.
Service Implementation
ImmichFrame.Core/Services/CustomWidgetService.cs
Implements custom widget data fetching with in-memory caching, HTTP client integration, JSON deserialization, and error logging. Cache uses URL-keyed entries with configurable TTL per source.
API Layer
ImmichFrame.WebApi/Controllers/CustomWidgetController.cs, ImmichFrame.WebApi/Models/ClientSettingsDto.cs, ImmichFrame.WebApi/Models/ServerSettings.cs
New authenticated API endpoint, extended DTOs with custom widget properties, and general settings configuration model updates.
Dependency Injection & API Spec
ImmichFrame.WebApi/Program.cs, openApi/swagger.json
Registered CustomWidgetService singleton; updated OpenAPI specification with new endpoint and schema definitions.
Configuration Examples
docker/Settings.example.json, docker/Settings.example.yml
Added example configuration showing ShowCustomWidget, CustomWidgetPosition, and CustomWidgetSources with sample URL and refresh interval.
Web Frontend
immichFrame.Web/src/lib/components/elements/custom-widget.svelte, immichFrame.Web/src/lib/components/home-page/home-page.svelte, immichFrame.Web/src/lib/immichFrameApi.ts
New Svelte component for rendering widgets with 60-second refresh interval; conditionally integrated into home page; API client extended with new types and getCustomWidgetData() function.

Sequence Diagram

sequenceDiagram
    participant FrontEnd as Frontend Component
    participant API as CustomWidget<br/>Controller
    participant Service as CustomWidget<br/>Service
    participant Cache as Memory Cache
    participant External as External<br/>HTTP Source

    FrontEnd->>API: GET /api/CustomWidget<br/>(clientIdentifier)
    API->>Service: GetCustomWidgetData()
    
    loop For each CustomWidgetSource
        Service->>Cache: Check cache<br/>(key: customwidget_{url})
        alt Cache Hit
            Cache-->>Service: Return CustomWidgetData
        else Cache Miss
            Service->>External: GET {source.Url}
            External-->>Service: JSON Response
            Service->>Service: Deserialize JSON<br/>→ CustomWidgetData
            Service->>Cache: Store with TTL<br/>(RefreshMinutes)
        end
        
        alt Success
            Service->>Service: Add to results
        else Error
            Service->>Service: Log error,<br/>continue
        end
    end
    
    Service-->>API: List<CustomWidgetData>
    API-->>FrontEnd: HTTP 200 + Data
    FrontEnd->>FrontEnd: Update widgetData<br/>Schedule next refresh
    FrontEnd->>FrontEnd: Re-render widgets
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Labels

enhancement

Suggested Reviewers

  • JW-CH

Poem

🐰 A widget hops into view,
Fetching data fresh and new,
Cached and timed with sixty-second flair,
Custom sources everywhere!
The frame displays with graceful care.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add generic custom data source widget' directly and accurately summarizes the main change—adding a new configurable widget that fetches JSON data from user-specified sources and displays it on the photo frame.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@RC1140 RC1140 closed this Mar 23, 2026
Add a configurable widget that fetches structured data from user-specified
URLs and displays it on the photo frame. Supports multiple data sources,
each with independent cache TTL via CustomWidgetSources config.

Backend:
- CustomWidgetSourceConfig model for per-source URL + refresh interval
- CustomWidgetData/CustomWidgetItem response models
- ICustomWidgetService interface + CustomWidgetService with per-source caching
- CustomWidgetController proxying data (keeps URLs server-side)
- Settings: ShowCustomWidget, CustomWidgetSources, CustomWidgetPosition
- ClientSettingsDto exposes ShowCustomWidget and CustomWidgetPosition

Frontend:
- custom-widget.svelte component with configurable corner positioning
- Integrated into home-page.svelte with conditional render
- Regenerated oazapfts API client with new types
@RC1140 RC1140 reopened this Mar 23, 2026
@JW-CH
Copy link
Copy Markdown
Collaborator

JW-CH commented Mar 23, 2026

Do you have any example sources by chance to test?

@JW-CH JW-CH self-requested a review March 23, 2026 10:54
@RC1140
Copy link
Copy Markdown
Author

RC1140 commented Mar 23, 2026

@JW-CH Sorry I built that out myself will add some screenshots in a bit.

This is what it looks like when rendered using the webview
image

As mentioned the API endpoint I hit to get my strength data is a custom service and not really anything public, but returning the data in the structure below works for me atleast

 {
    "title": "IRON 3",
    "items": [
      { "label": "Last Workout", "value": "Full Body + Accessory", "secondary": "3h ago" },
      { "label": "Squat", "value": "30 kg x 5x5" },
      { "label": "Bench", "value": "10 kg x 5x5" },
      { "label": "Deadlift", "value": "30 kg x 1x5" },
      { "label": "OHP", "value": "10 kg x 5x5" },
      { "label": "Row", "value": "15 kg x 5x5" },
      { "label": "Week Volume", "value": "1,735 kg" },
      { "label": "Streak", "value": "4 weeks" },
      { "label": "Fitness (CTL)", "value": "21" },
      { "label": "Fatigue (ATL)", "value": "9" },
      { "label": "Resting HR", "value": "51 bpm" },
      { "label": "HRV", "value": "44" },
      { "label": "Rides", "value": "1 (60min)" }
    ]
  }

The idea here is that you have a title and then any number of rows returned which then gets rendered. You can add multiple sources if your data is not centralized

@RC1140 RC1140 force-pushed the feat/custom-data-source-widget branch from 50b48a5 to 8066eee Compare March 23, 2026 12:26
@RC1140
Copy link
Copy Markdown
Author

RC1140 commented Mar 23, 2026

Dont know how much extra work this would be but you can test it with a small python server like this

python3 -c "
import json
from http.server import HTTPServer, BaseHTTPRequestHandler

data = {'title':'IRON 3','items':[{'label':'Last Workout','value':'Full Body + Accessory','secondary':'3h ago'},{'label':'Squat','value':'30 kg x 5x5'},{'label':'Bench','value':'10 kg x 5x5'},{'label':'Deadlift','value':'30 kg x 1x5'},{'label':'OHP','value':'10 kg x 5x5'},{'label':'Row','value':'15 kg x 5x5'},{'label':'Week Volume','value':'1,735 kg'},{'label':'Streak','value':'4 weeks'},{'label':'Fitness (CTL)','value':'21'},{'label':'Fatigue (ATL)','value':'9'},{'label':'Resting HR','value':'51 bpm'},{'label':'HRV','value':'44'},{'label':'Rides','value':'1 (60min)'}]}

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        body = json.dumps(data).encode()
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', len(body))
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *a): pass

HTTPServer(('', 8080), H).serve_forever()
"

Paste that in a console and hit localhost:8080

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants