diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md
index e0502488..c2c80ddb 100644
--- a/docs/widgets/services/index.md
+++ b/docs/widgets/services/index.md
@@ -55,6 +55,7 @@ You can also find a list of all available service widgets in the sidebar navigat
- [Komga](komga.md)
- [Kopia](kopia.md)
- [Lidarr](lidarr.md)
+- [Linkwarden](linkwarden.md)
- [Mastodon](mastodon.md)
- [Mealie](mealie.md)
- [Medusa](medusa.md)
diff --git a/docs/widgets/services/linkwarden.md b/docs/widgets/services/linkwarden.md
new file mode 100644
index 00000000..3e731f25
--- /dev/null
+++ b/docs/widgets/services/linkwarden.md
@@ -0,0 +1,62 @@
+---
+title: Linkwarden
+description: Linkwarden Widget Configuration
+---
+
+Learn more about [Linkwarden](https://linkwarden.app/).
+
+Allowed fields: `["links", "collections", "tags"]`.
+
+```yaml
+widget:
+ type: linkwarden
+ url: http://linkwarden.host.or.ip
+ key: myApiKeyHere # On your Linkwarden install, go to Settings > Access Tokens. Generate a token.
+```
+
+Use `mode` to show the stats found in the fields and/or the recent bookmarks.
+
+Examples:
+`mode: ["stats", "recent"]` or `mode: ["stats"]` or `mode: ["recent"]`
+
+```yaml
+widget:
+ type: linkwarden
+ url: http://linkwarden.host.or.ip
+ key: myApiKeyHere
+ mode: ["stats", "recent"]
+```
+
+Use `params` to set which collections and/or tags to display links from.
+
+Examples:
+
+```yaml
+params:
+ collectionIds: ["8", "13", "6"] # ID's of collections
+```
+
+or
+
+```yaml
+params:
+ tagIds: ["84", "66", "88", "69"] # ID's of tags
+```
+
+or
+
+```yaml
+params:
+ collectionIds: ["8", "13", "6"] # ID's of collections
+ tagIds: ["84", "66", "88", "69"] # ID's of tags
+```
+
+```yaml
+widget:
+ type: linkwarden
+ url: http://linkwarden.host.or.ip
+ key: myApiKeyHere
+ params:
+ collectionIds: ["8", "13", "6"] # ID's of collections
+ tagIds: ["84", "66", "88", "69"] # ID's of tags
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 4d6524b3..d48cc35e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -80,6 +80,7 @@ nav:
- widgets/services/komga.md
- widgets/services/kopia.md
- widgets/services/lidarr.md
+ - widgets/services/linkwarden.md
- widgets/services/mastodon.md
- widgets/services/mealie.md
- widgets/services/medusa.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 28c54895..05c5cedc 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1,909 +1,914 @@
{
- "common": {
- "bytes": "{{value, bytes}}",
- "bits": "{{value, bytes(bits: true)}}",
- "bbytes": "{{value, bytes(binary: true)}}",
- "bbits": "{{value, bytes(bits: true; binary: true)}}",
- "byterate": "{{value, rate(bits: false)}}",
- "bibyterate": "{{value, rate(bits: false; binary: true)}}",
- "bitrate": "{{value, rate(bits: true)}}",
- "bibitrate": "{{value, rate(bits: true; binary: true)}}",
- "percent": "{{value, percent}}",
- "number": "{{value, number}}",
- "ms": "{{value, number}}",
- "date": "{{value, date}}",
- "relativeDate": "{{value, relativeDate}}",
- "uptime": "{{value, uptime}}",
- "months": "mo",
- "days": "d",
- "hours": "h",
- "minutes": "m",
- "seconds": "s"
- },
- "widget": {
- "missing_type": "Missing Widget Type: {{type}}",
- "api_error": "API Error",
- "information": "Information",
- "status": "Status",
- "url": "URL",
- "raw_error": "Raw Error",
- "response_data": "Response Data"
- },
- "weather": {
- "current": "Current Location",
- "allow": "Click to allow",
- "updating": "Updating",
- "wait": "Please wait"
- },
- "search": {
- "placeholder": "Search…"
- },
- "resources": {
- "cpu": "CPU",
- "mem": "MEM",
- "total": "Total",
- "free": "Free",
- "used": "Used",
- "load": "Load",
- "temp": "TEMP",
- "max": "Max",
- "uptime": "UP"
- },
- "unifi": {
- "users": "Users",
- "uptime": "Uptime",
- "days": "Days",
- "wan": "WAN",
- "lan": "LAN",
- "wlan": "WLAN",
- "devices": "Devices",
- "lan_devices": "LAN Devices",
- "wlan_devices": "WLAN Devices",
- "lan_users": "LAN Users",
- "wlan_users": "WLAN Users",
- "up": "UP",
- "down": "DOWN",
- "wait": "Please wait",
- "empty_data": "Subsystem status unknown"
- },
- "docker": {
- "rx": "RX",
- "tx": "TX",
- "mem": "MEM",
- "cpu": "CPU",
- "running": "Running",
- "offline": "Offline",
- "error": "Error",
- "unknown": "Unknown",
- "healthy": "Healthy",
- "starting": "Starting",
- "unhealthy": "Unhealthy",
- "not_found": "Not Found",
- "exited": "Exited",
- "partial": "Partial"
- },
- "ping": {
- "error": "Error",
- "ping": "Ping",
- "down": "Down",
- "up": "Up",
- "not_available": "Not Available"
- },
- "siteMonitor": {
- "http_status": "HTTP status",
- "error": "Error",
- "response": "Response",
- "down": "Down",
- "up": "Up",
- "not_available": "Not Available"
- },
- "emby": {
- "playing": "Playing",
- "transcoding": "Transcoding",
- "bitrate": "Bitrate",
- "no_active": "No Active Streams",
- "movies": "Movies",
- "series": "Series",
- "episodes": "Episodes",
- "songs": "Songs"
- },
- "esphome": {
- "offline": "Offline",
- "offline_alt": "Offline",
- "online": "Online",
- "total": "Total",
- "unknown": "Unknown"
- },
- "evcc": {
- "pv_power": "Production",
- "battery_soc": "Battery",
- "grid_power": "Grid",
- "home_power": "Consumption",
- "charge_power": "Charger",
- "watt_hour": "Wh"
- },
- "flood": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
- },
- "freshrss": {
- "subscriptions": "Subscriptions",
- "unread": "Unread"
- },
- "fritzbox": {
- "connectionStatus": "Status",
- "connectionStatusUnconfigured": "Unconfigured",
- "connectionStatusConnecting": "Connecting",
- "connectionStatusAuthenticating": "Authenticating",
- "connectionStatusPendingDisconnect": "Pending Disconnect",
- "connectionStatusDisconnecting": "Disconnecting",
- "connectionStatusDisconnected": "Disconnected",
- "connectionStatusConnected": "Connected",
- "uptime": "Uptime",
- "maxDown": "Max. Down",
- "maxUp": "Max. Up",
- "down": "Down",
- "up": "Up",
- "received": "Received",
- "sent": "Sent",
- "externalIPAddress": "Ext. IP"
- },
- "caddy": {
- "upstreams": "Upstreams",
- "requests": "Current requests",
- "requests_failed": "Failed requests"
- },
- "changedetectionio": {
- "totalObserved": "Total Observed",
- "diffsDetected": "Diffs Detected"
- },
- "channelsdvrserver": {
- "shows": "Shows",
- "recordings": "Recordings",
- "scheduled": "Scheduled",
- "passes": "Passes"
- },
- "tautulli": {
- "playing": "Playing",
- "transcoding": "Transcoding",
- "bitrate": "Bitrate",
- "no_active": "No Active Streams",
- "plex_connection_error": "Check Plex Connection"
- },
- "omada": {
- "connectedAp": "Connected APs",
- "activeUser": "Active devices",
- "alerts": "Alerts",
- "connectedGateway": "Connected gateways",
- "connectedSwitches": "Connected switches"
- },
- "nzbget": {
- "rate": "Rate",
- "remaining": "Remaining",
- "downloaded": "Downloaded"
- },
- "plex": {
- "streams": "Active Streams",
- "albums": "Albums",
- "movies": "Movies",
- "tv": "TV Shows"
- },
- "sabnzbd": {
- "rate": "Rate",
- "queue": "Queue",
- "timeleft": "Time Left"
- },
- "rutorrent": {
- "active": "Active",
- "upload": "Upload",
- "download": "Download"
- },
- "transmission": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
- },
- "qbittorrent": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
- },
- "qnap": {
- "cpuUsage": "CPU Usage",
- "memUsage": "MEM Usage",
- "systemTempC": "System Temp",
- "poolUsage": "Pool Usage",
- "volumeUsage": "Volume Usage",
- "invalid": "Invalid"
- },
- "deluge": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
- },
- "downloadstation": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
- },
- "sonarr": {
- "wanted": "Wanted",
- "queued": "Queued",
- "series": "Series",
- "queue": "Queue",
- "unknown": "Unknown"
- },
- "radarr": {
- "wanted": "Wanted",
- "missing": "Missing",
- "queued": "Queued",
- "movies": "Movies",
- "queue": "Queue",
- "unknown": "Unknown"
- },
- "lidarr": {
- "wanted": "Wanted",
- "queued": "Queued",
- "artists": "Artists"
- },
- "readarr": {
- "wanted": "Wanted",
- "queued": "Queued",
- "books": "Books"
- },
- "bazarr": {
- "missingEpisodes": "Missing Episodes",
- "missingMovies": "Missing Movies"
- },
- "ombi": {
- "pending": "Pending",
- "approved": "Approved",
- "available": "Available"
- },
- "jellyseerr": {
- "pending": "Pending",
- "approved": "Approved",
- "available": "Available"
- },
- "overseerr": {
- "pending": "Pending",
- "processing": "Processing",
- "approved": "Approved",
- "available": "Available"
- },
- "netalertx": {
- "total": "Total",
- "connected": "Connected",
- "new_devices": "New Devices",
- "down_alerts": "Down Alerts"
- },
- "pihole": {
- "queries": "Queries",
- "blocked": "Blocked",
- "blocked_percent": "Blocked %",
- "gravity": "Gravity"
- },
- "adguard": {
- "queries": "Queries",
- "blocked": "Blocked",
- "filtered": "Filtered",
- "latency": "Latency"
- },
- "speedtest": {
- "upload": "Upload",
- "download": "Download",
- "ping": "Ping"
- },
- "portainer": {
- "running": "Running",
- "stopped": "Stopped",
- "total": "Total"
- },
- "tailscale": {
- "address": "Address",
- "expires": "Expires",
- "never": "Never",
- "last_seen": "Last Seen",
- "now": "Now",
- "years": "{{number}}y",
- "weeks": "{{number}}w",
- "days": "{{number}}d",
- "hours": "{{number}}h",
- "minutes": "{{number}}m",
- "seconds": "{{number}}s",
- "ago": "{{value}} Ago"
- },
- "tdarr": {
- "queue": "Queue",
- "processed": "Processed",
- "errored": "Errored",
- "saved": "Saved"
- },
- "traefik": {
- "routers": "Routers",
- "services": "Services",
- "middleware": "Middleware"
- },
- "navidrome": {
- "nothing_streaming": "No Active Streams",
- "please_wait": "Please Wait"
- },
- "npm": {
- "enabled": "Enabled",
- "disabled": "Disabled",
- "total": "Total"
- },
- "coinmarketcap": {
- "configure": "Configure one or more crypto currencies to track",
- "1hour": "1 Hour",
- "1day": "1 Day",
- "7days": "7 Days",
- "30days": "30 Days"
- },
- "gotify": {
- "apps": "Applications",
- "clients": "Clients",
- "messages": "Messages"
- },
- "prowlarr": {
- "enableIndexers": "Indexers",
- "numberOfGrabs": "Grabs",
- "numberOfQueries": "Queries",
- "numberOfFailGrabs": "Fail Grabs",
- "numberOfFailQueries": "Fail Queries"
- },
- "jackett": {
- "configured": "Configured",
- "errored": "Errored"
- },
- "strelaysrv": {
- "numActiveSessions": "Sessions",
- "numConnections": "Connections",
- "dataRelayed": "Relayed",
- "transferRate": "Rate"
- },
- "mastodon": {
- "user_count": "Users",
- "status_count": "Posts",
- "domain_count": "Domains"
- },
- "medusa": {
- "wanted": "Wanted",
- "queued": "Queued",
- "series": "Series"
- },
- "minecraft": {
- "players": "Players",
- "version": "Version",
- "status": "Status",
- "up": "Online",
- "down": "Offline"
- },
- "miniflux": {
- "read": "Read",
- "unread": "Unread"
- },
- "authentik": {
- "users": "Users",
- "loginsLast24H": "Logins (24h)",
- "failedLoginsLast24H": "Failed Logins (24h)"
- },
- "proxmox": {
- "mem": "MEM",
- "cpu": "CPU",
- "lxc": "LXC",
- "vms": "VMs"
- },
- "glances": {
- "cpu": "CPU",
- "load": "Load",
- "wait": "Please wait",
- "temp": "TEMP",
- "_temp": "Temp",
- "warn": "Warn",
- "uptime": "UP",
- "total": "Total",
- "free": "Free",
- "used": "Used",
- "days": "d",
- "hours": "h",
- "crit": "Crit",
- "read": "Read",
- "write": "Write",
- "gpu": "GPU",
- "mem": "Mem",
- "swap": "Swap"
- },
- "quicklaunch": {
- "bookmark": "Bookmark",
- "service": "Service",
- "search": "Search",
- "custom": "Custom",
- "visit": "Visit",
- "url": "URL",
- "searchsuggestion": "Suggestion"
- },
- "wmo": {
- "0-day": "Sunny",
- "0-night": "Clear",
- "1-day": "Mainly Sunny",
- "1-night": "Mainly Clear",
- "2-day": "Partly Cloudy",
- "2-night": "Partly Cloudy",
- "3-day": "Cloudy",
- "3-night": "Cloudy",
- "45-day": "Foggy",
- "45-night": "Foggy",
- "48-day": "Foggy",
- "48-night": "Foggy",
- "51-day": "Light Drizzle",
- "51-night": "Light Drizzle",
- "53-day": "Drizzle",
- "53-night": "Drizzle",
- "55-day": "Heavy Drizzle",
- "55-night": "Heavy Drizzle",
- "56-day": "Light Freezing Drizzle",
- "56-night": "Light Freezing Drizzle",
- "57-day": "Freezing Drizzle",
- "57-night": "Freezing Drizzle",
- "61-day": "Light Rain",
- "61-night": "Light Rain",
- "63-day": "Rain",
- "63-night": "Rain",
- "65-day": "Heavy Rain",
- "65-night": "Heavy Rain",
- "66-day": "Freezing Rain",
- "66-night": "Freezing Rain",
- "67-day": "Freezing Rain",
- "67-night": "Freezing Rain",
- "71-day": "Light Snow",
- "71-night": "Light Snow",
- "73-day": "Snow",
- "73-night": "Snow",
- "75-day": "Heavy Snow",
- "75-night": "Heavy Snow",
- "77-day": "Snow Grains",
- "77-night": "Snow Grains",
- "80-day": "Light Showers",
- "80-night": "Light Showers",
- "81-day": "Showers",
- "81-night": "Showers",
- "82-day": "Heavy Showers",
- "82-night": "Heavy Showers",
- "85-day": "Snow Showers",
- "85-night": "Snow Showers",
- "86-day": "Snow Showers",
- "86-night": "Snow Showers",
- "95-day": "Thunderstorm",
- "95-night": "Thunderstorm",
- "96-day": "Thunderstorm With Hail",
- "96-night": "Thunderstorm With Hail",
- "99-day": "Thunderstorm With Hail",
- "99-night": "Thunderstorm With Hail"
- },
- "homebridge": {
- "available_update": "System",
- "updates": "Updates",
- "update_available": "Update Available",
- "up_to_date": "Up to Date",
- "child_bridges": "Child Bridges",
- "child_bridges_status": "{{ok}}/{{total}}",
- "up": "Up",
- "pending": "Pending",
- "down": "Down"
- },
- "healthchecks": {
- "new": "New",
- "up": "Up",
- "grace": "In Grace Period",
- "down": "Down",
- "paused": "Paused",
- "status": "Status",
- "last_ping": "Last Ping",
- "never": "No pings yet"
- },
- "watchtower": {
- "containers_scanned": "Scanned",
- "containers_updated": "Updated",
- "containers_failed": "Failed"
- },
- "autobrr": {
- "approvedPushes": "Approved",
- "rejectedPushes": "Rejected",
- "filters": "Filters",
- "indexers": "Indexers"
- },
- "tubearchivist": {
- "downloads": "Queue",
- "videos": "Videos",
- "channels": "Channels",
- "playlists": "Playlists"
- },
- "truenas": {
- "load": "System Load",
- "uptime": "Uptime",
- "alerts": "Alerts"
- },
- "pyload": {
- "speed": "Speed",
- "active": "Active",
- "queue": "Queue",
- "total": "Total"
- },
- "gluetun": {
- "public_ip": "Public IP",
- "region": "Region",
- "country": "Country"
- },
- "hdhomerun": {
- "channels": "Channels",
- "hd": "HD",
- "tunerCount": "Tuners",
- "channelNumber": "Channel",
- "channelNetwork": "Network",
- "signalStrength": "Strength",
- "signalQuality": "Quality",
- "symbolQuality": "Quality",
- "networkRate": "Bitrate",
- "clientIP": "Client"
- },
- "scrutiny": {
- "passed": "Passed",
- "failed": "Failed",
- "unknown": "Unknown"
- },
- "paperlessngx": {
- "inbox": "Inbox",
- "total": "Total"
- },
- "peanut": {
- "battery_charge": "Battery Charge",
- "ups_load": "UPS Load",
- "ups_status": "UPS Status",
- "online": "Online",
- "on_battery": "On Battery",
- "low_battery": "Low Battery"
- },
- "nextdns": {
- "wait": "Please Wait",
- "no_devices": "No Device Data Received"
- },
- "mikrotik": {
- "cpuLoad": "CPU Load",
- "memoryUsed": "Memory Used",
- "uptime": "Uptime",
- "numberOfLeases": "Leases"
- },
- "xteve": {
- "streams_all": "All Streams",
- "streams_active": "Active Streams",
- "streams_xepg": "XEPG Channels"
- },
- "opendtu": {
- "yieldDay": "Today",
- "absolutePower": "Power",
- "relativePower": "Power %",
- "limit": "Limit"
- },
- "opnsense": {
- "cpu": "CPU Load",
- "memory": "Active Memory",
- "wanUpload": "WAN Upload",
- "wanDownload": "WAN Download"
- },
- "moonraker": {
- "printer_state": "Printer State",
- "print_status": "Print Status",
- "print_progress": "Progress",
- "layers": "Layers"
- },
- "octoprint": {
- "printer_state": "Status",
- "temp_tool": "Tool temp",
- "temp_bed": "Bed temp",
- "job_completion": "Completion"
- },
- "cloudflared": {
- "origin_ip": "Origin IP",
- "status": "Status"
- },
- "pfsense": {
- "load": "Load Avg",
- "memory": "Mem Usage",
- "wanStatus": "WAN Status",
- "up": "Up",
- "down": "Down",
- "temp": "Temp",
- "disk": "Disk Usage",
- "wanIP": "WAN IP"
- },
- "proxmoxbackupserver": {
- "datastore_usage": "Datastore",
- "failed_tasks_24h": "Failed Tasks 24h",
- "cpu_usage": "CPU",
- "memory_usage": "Memory"
- },
- "immich": {
- "users": "Users",
- "photos": "Photos",
- "videos": "Videos",
- "storage": "Storage"
- },
- "uptimekuma": {
- "up": "Sites Up",
- "down": "Sites Down",
- "uptime": "Uptime",
- "incident": "Incident",
- "m": "m"
- },
- "atsumeru": {
- "series": "Series",
- "archives": "Archives",
- "chapters": "Chapters",
- "categories": "Categories"
- },
- "komga": {
- "libraries": "Libraries",
- "series": "Series",
- "books": "Books"
- },
- "diskstation": {
- "days": "Days",
- "uptime": "Uptime",
- "volumeAvailable": "Available"
- },
- "mylar": {
- "series": "Series",
- "issues": "Issues",
- "wanted": "Wanted"
- },
- "photoprism": {
- "albums": "Albums",
- "photos": "Photos",
- "videos": "Videos",
- "people": "People"
- },
- "fileflows": {
- "queue": "Queue",
- "processing": "Processing",
- "processed": "Processed",
- "time": "Time"
- },
- "grafana": {
- "dashboards": "Dashboards",
- "datasources": "Data Sources",
- "totalalerts": "Total Alerts",
- "alertstriggered": "Alerts Triggered"
- },
- "nextcloud": {
- "cpuload": "Cpu Load",
- "memoryusage": "Memory Usage",
- "freespace": "Free Space",
- "activeusers": "Active Users",
- "numfiles": "Files",
- "numshares": "Shared Items"
- },
- "kopia": {
- "status": "Status",
- "size": "Size",
- "lastrun": "Last Run",
- "nextrun": "Next Run",
- "failed": "Failed"
- },
- "unmanic": {
- "active_workers": "Active Workers",
- "total_workers": "Total Workers",
- "records_total": "Queue Length"
- },
- "pterodactyl": {
- "servers": "Servers",
- "nodes": "Nodes"
- },
- "prometheus": {
- "targets_up": "Targets Up",
- "targets_down": "Targets Down",
- "targets_total": "Total Targets"
- },
- "gatus": {
- "up": "Sites Up",
- "down": "Sites Down",
- "uptime": "Uptime"
- },
- "ghostfolio": {
- "gross_percent_today": "Today",
- "gross_percent_1y": "One year",
- "gross_percent_max": "All time"
- },
- "audiobookshelf": {
- "podcasts": "Podcasts",
- "books": "Books",
- "podcastsDuration": "Duration",
- "booksDuration": "Duration"
- },
- "homeassistant": {
- "people_home": "People Home",
- "lights_on": "Lights On",
- "switches_on": "Switches On"
- },
- "whatsupdocker": {
- "monitoring": "Monitoring",
- "updates": "Updates"
- },
- "calibreweb": {
- "books": "Books",
- "authors": "Authors",
- "categories": "Categories",
- "series": "Series"
- },
- "jdownloader": {
- "downloadCount": "Queue",
- "downloadBytesRemaining": "Remaining",
- "downloadTotalBytes": "Size",
- "downloadSpeed": "Speed"
- },
- "kavita": {
- "seriesCount": "Series",
- "totalFiles": "Files"
- },
- "azuredevops": {
- "result": "Result",
- "status": "Status",
- "buildId": "Build ID",
- "succeeded": "Succeeded",
- "notStarted": "Not Started",
- "failed": "Failed",
- "canceled": "Canceled",
- "inProgress": "In Progress",
- "totalPrs": "Total PRs",
- "myPrs": "My PRs",
- "approved": "Approved"
- },
- "gamedig": {
- "status": "Status",
- "online": "Online",
- "offline": "Offline",
- "name": "Name",
- "map": "Map",
- "currentPlayers": "Current players",
- "players": "Players",
- "maxPlayers": "Max players",
- "bots": "Bots",
- "ping": "Ping"
- },
- "urbackup": {
- "ok" : "Ok",
- "errored": "Errors",
- "noRecent": "Out of Date",
- "totalUsed": "Used Storage"
- },
- "mealie": {
- "recipes": "Recipes",
- "users": "Users",
- "categories": "Categories",
- "tags": "Tags"
- },
- "openmediavault": {
- "downloading": "Downloading",
- "total": "Total",
- "running": "Running",
- "stopped": "Stopped",
- "passed": "Passed",
- "failed": "Failed"
- },
- "openwrt": {
- "uptime": "Uptime",
- "cpuLoad": "CPU Load Avg (5m)",
- "up": "Up",
- "down": "Down",
- "bytesTx": "Transmitted",
- "bytesRx": "Received"
- },
- "uptimerobot": {
- "status": "Status",
- "uptime": "Uptime",
- "lastDown": "Last Downtime",
- "downDuration": "Downtime Duration",
- "sitesUp": "Sites Up",
- "sitesDown": "Sites Down",
- "paused": "Paused",
- "notyetchecked": "Not Yet Checked",
- "up": "Up",
- "seemsdown": "Seems Down",
- "down": "Down",
- "unknown": "Unknown"
- },
- "calendar": {
- "inCinemas": "In cinemas",
- "physicalRelease": "Physical release",
- "digitalRelease": "Digital release",
- "noEventsToday": "No events for today!",
- "noEventsFound": "No events found"
- },
- "romm": {
- "platforms": "Platforms",
- "totalRoms": "Total ROMs"
- },
- "netdata": {
- "warnings": "Warnings",
- "criticals": "Criticals"
- },
- "plantit": {
- "events": "Events",
- "plants": "Plants",
- "photos": "Photos",
- "species": "Species"
- },
- "gitea": {
- "notifications": "Notifications",
- "issues": "Issues",
- "pulls": "Pull Requests"
- },
- "stash": {
- "scenes": "Scenes",
- "scenesPlayed": "Scenes Played",
- "playCount": "Total Plays",
- "playDuration": "Time Watched",
- "sceneSize": "Scenes Size",
- "sceneDuration": "Scenes Duration",
- "images": "Images",
- "imageSize": "Images Size",
- "galleries": "Galleries",
- "performers": "Performers",
- "studios": "Studios",
- "movies": "Movies",
- "tags": "Tags",
- "oCount": "O Count"
- },
- "tandoor": {
- "users": "Users",
- "recipes": "Recipes",
- "keywords": "Keywords"
- },
- "homebox": {
- "items": "Items",
- "totalWithWarranty": "With Warranty",
- "locations": "Locations",
- "labels": "Labels",
- "users": "Users",
- "totalValue": "Total Value"
- },
- "crowdsec": {
- "alerts": "Alerts",
- "bans": "Bans"
- },
- "wgeasy": {
- "connected": "Connected",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "total": "Total"
- },
- "swagdashboard": {
- "proxied": "Proxied",
- "auth": "With Auth",
- "outdated": "Outdated",
- "banned": "Banned"
- },
- "myspeed": {
- "ping": "Ping",
- "download": "Download",
- "upload": "Upload"
- },
- "stocks": {
- "stocks": "Stocks",
- "loading": "Loading",
- "open": "Open - US Market",
- "closed": "Closed - US Market",
- "invalidConfiguration": "Invalid Configuration"
- },
- "frigate": {
- "cameras": "Cameras",
- "uptime": "Uptime",
- "version": "Version"
- }
+ "common": {
+ "bytes": "{{value, bytes}}",
+ "bits": "{{value, bytes(bits: true)}}",
+ "bbytes": "{{value, bytes(binary: true)}}",
+ "bbits": "{{value, bytes(bits: true; binary: true)}}",
+ "byterate": "{{value, rate(bits: false)}}",
+ "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+ "bitrate": "{{value, rate(bits: true)}}",
+ "bibitrate": "{{value, rate(bits: true; binary: true)}}",
+ "percent": "{{value, percent}}",
+ "number": "{{value, number}}",
+ "ms": "{{value, number}}",
+ "date": "{{value, date}}",
+ "relativeDate": "{{value, relativeDate}}",
+ "uptime": "{{value, uptime}}",
+ "months": "mo",
+ "days": "d",
+ "hours": "h",
+ "minutes": "m",
+ "seconds": "s"
+ },
+ "widget": {
+ "missing_type": "Missing Widget Type: {{type}}",
+ "api_error": "API Error",
+ "information": "Information",
+ "status": "Status",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
+ },
+ "weather": {
+ "current": "Current Location",
+ "allow": "Click to allow",
+ "updating": "Updating",
+ "wait": "Please wait"
+ },
+ "search": {
+ "placeholder": "Search…"
+ },
+ "resources": {
+ "cpu": "CPU",
+ "mem": "MEM",
+ "total": "Total",
+ "free": "Free",
+ "used": "Used",
+ "load": "Load",
+ "temp": "TEMP",
+ "max": "Max",
+ "uptime": "UP"
+ },
+ "unifi": {
+ "users": "Users",
+ "uptime": "Uptime",
+ "days": "Days",
+ "wan": "WAN",
+ "lan": "LAN",
+ "wlan": "WLAN",
+ "devices": "Devices",
+ "lan_devices": "LAN Devices",
+ "wlan_devices": "WLAN Devices",
+ "lan_users": "LAN Users",
+ "wlan_users": "WLAN Users",
+ "up": "UP",
+ "down": "DOWN",
+ "wait": "Please wait",
+ "empty_data": "Subsystem status unknown"
+ },
+ "docker": {
+ "rx": "RX",
+ "tx": "TX",
+ "mem": "MEM",
+ "cpu": "CPU",
+ "running": "Running",
+ "offline": "Offline",
+ "error": "Error",
+ "unknown": "Unknown",
+ "healthy": "Healthy",
+ "starting": "Starting",
+ "unhealthy": "Unhealthy",
+ "not_found": "Not Found",
+ "exited": "Exited",
+ "partial": "Partial"
+ },
+ "ping": {
+ "error": "Error",
+ "ping": "Ping",
+ "down": "Down",
+ "up": "Up",
+ "not_available": "Not Available"
+ },
+ "siteMonitor": {
+ "http_status": "HTTP status",
+ "error": "Error",
+ "response": "Response",
+ "down": "Down",
+ "up": "Up",
+ "not_available": "Not Available"
+ },
+ "emby": {
+ "playing": "Playing",
+ "transcoding": "Transcoding",
+ "bitrate": "Bitrate",
+ "no_active": "No Active Streams",
+ "movies": "Movies",
+ "series": "Series",
+ "episodes": "Episodes",
+ "songs": "Songs"
+ },
+ "esphome": {
+ "offline": "Offline",
+ "offline_alt": "Offline",
+ "online": "Online",
+ "total": "Total",
+ "unknown": "Unknown"
+ },
+ "evcc": {
+ "pv_power": "Production",
+ "battery_soc": "Battery",
+ "grid_power": "Grid",
+ "home_power": "Consumption",
+ "charge_power": "Charger",
+ "watt_hour": "Wh"
+ },
+ "flood": {
+ "download": "Download",
+ "upload": "Upload",
+ "leech": "Leech",
+ "seed": "Seed"
+ },
+ "freshrss": {
+ "subscriptions": "Subscriptions",
+ "unread": "Unread"
+ },
+ "fritzbox": {
+ "connectionStatus": "Status",
+ "connectionStatusUnconfigured": "Unconfigured",
+ "connectionStatusConnecting": "Connecting",
+ "connectionStatusAuthenticating": "Authenticating",
+ "connectionStatusPendingDisconnect": "Pending Disconnect",
+ "connectionStatusDisconnecting": "Disconnecting",
+ "connectionStatusDisconnected": "Disconnected",
+ "connectionStatusConnected": "Connected",
+ "uptime": "Uptime",
+ "maxDown": "Max. Down",
+ "maxUp": "Max. Up",
+ "down": "Down",
+ "up": "Up",
+ "received": "Received",
+ "sent": "Sent",
+ "externalIPAddress": "Ext. IP"
+ },
+ "caddy": {
+ "upstreams": "Upstreams",
+ "requests": "Current requests",
+ "requests_failed": "Failed requests"
+ },
+ "changedetectionio": {
+ "totalObserved": "Total Observed",
+ "diffsDetected": "Diffs Detected"
+ },
+ "channelsdvrserver": {
+ "shows": "Shows",
+ "recordings": "Recordings",
+ "scheduled": "Scheduled",
+ "passes": "Passes"
+ },
+ "tautulli": {
+ "playing": "Playing",
+ "transcoding": "Transcoding",
+ "bitrate": "Bitrate",
+ "no_active": "No Active Streams",
+ "plex_connection_error": "Check Plex Connection"
+ },
+ "omada": {
+ "connectedAp": "Connected APs",
+ "activeUser": "Active devices",
+ "alerts": "Alerts",
+ "connectedGateway": "Connected gateways",
+ "connectedSwitches": "Connected switches"
+ },
+ "nzbget": {
+ "rate": "Rate",
+ "remaining": "Remaining",
+ "downloaded": "Downloaded"
+ },
+ "plex": {
+ "streams": "Active Streams",
+ "albums": "Albums",
+ "movies": "Movies",
+ "tv": "TV Shows"
+ },
+ "sabnzbd": {
+ "rate": "Rate",
+ "queue": "Queue",
+ "timeleft": "Time Left"
+ },
+ "rutorrent": {
+ "active": "Active",
+ "upload": "Upload",
+ "download": "Download"
+ },
+ "transmission": {
+ "download": "Download",
+ "upload": "Upload",
+ "leech": "Leech",
+ "seed": "Seed"
+ },
+ "qbittorrent": {
+ "download": "Download",
+ "upload": "Upload",
+ "leech": "Leech",
+ "seed": "Seed"
+ },
+ "qnap": {
+ "cpuUsage": "CPU Usage",
+ "memUsage": "MEM Usage",
+ "systemTempC": "System Temp",
+ "poolUsage": "Pool Usage",
+ "volumeUsage": "Volume Usage",
+ "invalid": "Invalid"
+ },
+ "deluge": {
+ "download": "Download",
+ "upload": "Upload",
+ "leech": "Leech",
+ "seed": "Seed"
+ },
+ "downloadstation": {
+ "download": "Download",
+ "upload": "Upload",
+ "leech": "Leech",
+ "seed": "Seed"
+ },
+ "sonarr": {
+ "wanted": "Wanted",
+ "queued": "Queued",
+ "series": "Series",
+ "queue": "Queue",
+ "unknown": "Unknown"
+ },
+ "radarr": {
+ "wanted": "Wanted",
+ "missing": "Missing",
+ "queued": "Queued",
+ "movies": "Movies",
+ "queue": "Queue",
+ "unknown": "Unknown"
+ },
+ "lidarr": {
+ "wanted": "Wanted",
+ "queued": "Queued",
+ "artists": "Artists"
+ },
+ "readarr": {
+ "wanted": "Wanted",
+ "queued": "Queued",
+ "books": "Books"
+ },
+ "bazarr": {
+ "missingEpisodes": "Missing Episodes",
+ "missingMovies": "Missing Movies"
+ },
+ "ombi": {
+ "pending": "Pending",
+ "approved": "Approved",
+ "available": "Available"
+ },
+ "jellyseerr": {
+ "pending": "Pending",
+ "approved": "Approved",
+ "available": "Available"
+ },
+ "overseerr": {
+ "pending": "Pending",
+ "processing": "Processing",
+ "approved": "Approved",
+ "available": "Available"
+ },
+ "netalertx": {
+ "total": "Total",
+ "connected": "Connected",
+ "new_devices": "New Devices",
+ "down_alerts": "Down Alerts"
+ },
+ "pihole": {
+ "queries": "Queries",
+ "blocked": "Blocked",
+ "blocked_percent": "Blocked %",
+ "gravity": "Gravity"
+ },
+ "adguard": {
+ "queries": "Queries",
+ "blocked": "Blocked",
+ "filtered": "Filtered",
+ "latency": "Latency"
+ },
+ "speedtest": {
+ "upload": "Upload",
+ "download": "Download",
+ "ping": "Ping"
+ },
+ "portainer": {
+ "running": "Running",
+ "stopped": "Stopped",
+ "total": "Total"
+ },
+ "tailscale": {
+ "address": "Address",
+ "expires": "Expires",
+ "never": "Never",
+ "last_seen": "Last Seen",
+ "now": "Now",
+ "years": "{{number}}y",
+ "weeks": "{{number}}w",
+ "days": "{{number}}d",
+ "hours": "{{number}}h",
+ "minutes": "{{number}}m",
+ "seconds": "{{number}}s",
+ "ago": "{{value}} Ago"
+ },
+ "tdarr": {
+ "queue": "Queue",
+ "processed": "Processed",
+ "errored": "Errored",
+ "saved": "Saved"
+ },
+ "traefik": {
+ "routers": "Routers",
+ "services": "Services",
+ "middleware": "Middleware"
+ },
+ "navidrome": {
+ "nothing_streaming": "No Active Streams",
+ "please_wait": "Please Wait"
+ },
+ "npm": {
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "total": "Total"
+ },
+ "coinmarketcap": {
+ "configure": "Configure one or more crypto currencies to track",
+ "1hour": "1 Hour",
+ "1day": "1 Day",
+ "7days": "7 Days",
+ "30days": "30 Days"
+ },
+ "gotify": {
+ "apps": "Applications",
+ "clients": "Clients",
+ "messages": "Messages"
+ },
+ "prowlarr": {
+ "enableIndexers": "Indexers",
+ "numberOfGrabs": "Grabs",
+ "numberOfQueries": "Queries",
+ "numberOfFailGrabs": "Fail Grabs",
+ "numberOfFailQueries": "Fail Queries"
+ },
+ "jackett": {
+ "configured": "Configured",
+ "errored": "Errored"
+ },
+ "strelaysrv": {
+ "numActiveSessions": "Sessions",
+ "numConnections": "Connections",
+ "dataRelayed": "Relayed",
+ "transferRate": "Rate"
+ },
+ "mastodon": {
+ "user_count": "Users",
+ "status_count": "Posts",
+ "domain_count": "Domains"
+ },
+ "medusa": {
+ "wanted": "Wanted",
+ "queued": "Queued",
+ "series": "Series"
+ },
+ "minecraft": {
+ "players": "Players",
+ "version": "Version",
+ "status": "Status",
+ "up": "Online",
+ "down": "Offline"
+ },
+ "miniflux": {
+ "read": "Read",
+ "unread": "Unread"
+ },
+ "authentik": {
+ "users": "Users",
+ "loginsLast24H": "Logins (24h)",
+ "failedLoginsLast24H": "Failed Logins (24h)"
+ },
+ "proxmox": {
+ "mem": "MEM",
+ "cpu": "CPU",
+ "lxc": "LXC",
+ "vms": "VMs"
+ },
+ "glances": {
+ "cpu": "CPU",
+ "load": "Load",
+ "wait": "Please wait",
+ "temp": "TEMP",
+ "_temp": "Temp",
+ "warn": "Warn",
+ "uptime": "UP",
+ "total": "Total",
+ "free": "Free",
+ "used": "Used",
+ "days": "d",
+ "hours": "h",
+ "crit": "Crit",
+ "read": "Read",
+ "write": "Write",
+ "gpu": "GPU",
+ "mem": "Mem",
+ "swap": "Swap"
+ },
+ "quicklaunch": {
+ "bookmark": "Bookmark",
+ "service": "Service",
+ "search": "Search",
+ "custom": "Custom",
+ "visit": "Visit",
+ "url": "URL",
+ "searchsuggestion": "Suggestion"
+ },
+ "wmo": {
+ "0-day": "Sunny",
+ "0-night": "Clear",
+ "1-day": "Mainly Sunny",
+ "1-night": "Mainly Clear",
+ "2-day": "Partly Cloudy",
+ "2-night": "Partly Cloudy",
+ "3-day": "Cloudy",
+ "3-night": "Cloudy",
+ "45-day": "Foggy",
+ "45-night": "Foggy",
+ "48-day": "Foggy",
+ "48-night": "Foggy",
+ "51-day": "Light Drizzle",
+ "51-night": "Light Drizzle",
+ "53-day": "Drizzle",
+ "53-night": "Drizzle",
+ "55-day": "Heavy Drizzle",
+ "55-night": "Heavy Drizzle",
+ "56-day": "Light Freezing Drizzle",
+ "56-night": "Light Freezing Drizzle",
+ "57-day": "Freezing Drizzle",
+ "57-night": "Freezing Drizzle",
+ "61-day": "Light Rain",
+ "61-night": "Light Rain",
+ "63-day": "Rain",
+ "63-night": "Rain",
+ "65-day": "Heavy Rain",
+ "65-night": "Heavy Rain",
+ "66-day": "Freezing Rain",
+ "66-night": "Freezing Rain",
+ "67-day": "Freezing Rain",
+ "67-night": "Freezing Rain",
+ "71-day": "Light Snow",
+ "71-night": "Light Snow",
+ "73-day": "Snow",
+ "73-night": "Snow",
+ "75-day": "Heavy Snow",
+ "75-night": "Heavy Snow",
+ "77-day": "Snow Grains",
+ "77-night": "Snow Grains",
+ "80-day": "Light Showers",
+ "80-night": "Light Showers",
+ "81-day": "Showers",
+ "81-night": "Showers",
+ "82-day": "Heavy Showers",
+ "82-night": "Heavy Showers",
+ "85-day": "Snow Showers",
+ "85-night": "Snow Showers",
+ "86-day": "Snow Showers",
+ "86-night": "Snow Showers",
+ "95-day": "Thunderstorm",
+ "95-night": "Thunderstorm",
+ "96-day": "Thunderstorm With Hail",
+ "96-night": "Thunderstorm With Hail",
+ "99-day": "Thunderstorm With Hail",
+ "99-night": "Thunderstorm With Hail"
+ },
+ "homebridge": {
+ "available_update": "System",
+ "updates": "Updates",
+ "update_available": "Update Available",
+ "up_to_date": "Up to Date",
+ "child_bridges": "Child Bridges",
+ "child_bridges_status": "{{ok}}/{{total}}",
+ "up": "Up",
+ "pending": "Pending",
+ "down": "Down"
+ },
+ "healthchecks": {
+ "new": "New",
+ "up": "Up",
+ "grace": "In Grace Period",
+ "down": "Down",
+ "paused": "Paused",
+ "status": "Status",
+ "last_ping": "Last Ping",
+ "never": "No pings yet"
+ },
+ "watchtower": {
+ "containers_scanned": "Scanned",
+ "containers_updated": "Updated",
+ "containers_failed": "Failed"
+ },
+ "autobrr": {
+ "approvedPushes": "Approved",
+ "rejectedPushes": "Rejected",
+ "filters": "Filters",
+ "indexers": "Indexers"
+ },
+ "tubearchivist": {
+ "downloads": "Queue",
+ "videos": "Videos",
+ "channels": "Channels",
+ "playlists": "Playlists"
+ },
+ "truenas": {
+ "load": "System Load",
+ "uptime": "Uptime",
+ "alerts": "Alerts"
+ },
+ "pyload": {
+ "speed": "Speed",
+ "active": "Active",
+ "queue": "Queue",
+ "total": "Total"
+ },
+ "gluetun": {
+ "public_ip": "Public IP",
+ "region": "Region",
+ "country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD",
+ "tunerCount": "Tuners",
+ "channelNumber": "Channel",
+ "channelNetwork": "Network",
+ "signalStrength": "Strength",
+ "signalQuality": "Quality",
+ "symbolQuality": "Quality",
+ "networkRate": "Bitrate",
+ "clientIP": "Client"
+ },
+ "scrutiny": {
+ "passed": "Passed",
+ "failed": "Failed",
+ "unknown": "Unknown"
+ },
+ "paperlessngx": {
+ "inbox": "Inbox",
+ "total": "Total"
+ },
+ "peanut": {
+ "battery_charge": "Battery Charge",
+ "ups_load": "UPS Load",
+ "ups_status": "UPS Status",
+ "online": "Online",
+ "on_battery": "On Battery",
+ "low_battery": "Low Battery"
+ },
+ "nextdns": {
+ "wait": "Please Wait",
+ "no_devices": "No Device Data Received"
+ },
+ "mikrotik": {
+ "cpuLoad": "CPU Load",
+ "memoryUsed": "Memory Used",
+ "uptime": "Uptime",
+ "numberOfLeases": "Leases"
+ },
+ "xteve": {
+ "streams_all": "All Streams",
+ "streams_active": "Active Streams",
+ "streams_xepg": "XEPG Channels"
+ },
+ "opendtu": {
+ "yieldDay": "Today",
+ "absolutePower": "Power",
+ "relativePower": "Power %",
+ "limit": "Limit"
+ },
+ "opnsense": {
+ "cpu": "CPU Load",
+ "memory": "Active Memory",
+ "wanUpload": "WAN Upload",
+ "wanDownload": "WAN Download"
+ },
+ "moonraker": {
+ "printer_state": "Printer State",
+ "print_status": "Print Status",
+ "print_progress": "Progress",
+ "layers": "Layers"
+ },
+ "octoprint": {
+ "printer_state": "Status",
+ "temp_tool": "Tool temp",
+ "temp_bed": "Bed temp",
+ "job_completion": "Completion"
+ },
+ "cloudflared": {
+ "origin_ip": "Origin IP",
+ "status": "Status"
+ },
+ "pfsense": {
+ "load": "Load Avg",
+ "memory": "Mem Usage",
+ "wanStatus": "WAN Status",
+ "up": "Up",
+ "down": "Down",
+ "temp": "Temp",
+ "disk": "Disk Usage",
+ "wanIP": "WAN IP"
+ },
+ "proxmoxbackupserver": {
+ "datastore_usage": "Datastore",
+ "failed_tasks_24h": "Failed Tasks 24h",
+ "cpu_usage": "CPU",
+ "memory_usage": "Memory"
+ },
+ "immich": {
+ "users": "Users",
+ "photos": "Photos",
+ "videos": "Videos",
+ "storage": "Storage"
+ },
+ "uptimekuma": {
+ "up": "Sites Up",
+ "down": "Sites Down",
+ "uptime": "Uptime",
+ "incident": "Incident",
+ "m": "m"
+ },
+ "atsumeru": {
+ "series": "Series",
+ "archives": "Archives",
+ "chapters": "Chapters",
+ "categories": "Categories"
+ },
+ "komga": {
+ "libraries": "Libraries",
+ "series": "Series",
+ "books": "Books"
+ },
+ "diskstation": {
+ "days": "Days",
+ "uptime": "Uptime",
+ "volumeAvailable": "Available"
+ },
+ "mylar": {
+ "series": "Series",
+ "issues": "Issues",
+ "wanted": "Wanted"
+ },
+ "photoprism": {
+ "albums": "Albums",
+ "photos": "Photos",
+ "videos": "Videos",
+ "people": "People"
+ },
+ "fileflows": {
+ "queue": "Queue",
+ "processing": "Processing",
+ "processed": "Processed",
+ "time": "Time"
+ },
+ "grafana": {
+ "dashboards": "Dashboards",
+ "datasources": "Data Sources",
+ "totalalerts": "Total Alerts",
+ "alertstriggered": "Alerts Triggered"
+ },
+ "nextcloud": {
+ "cpuload": "Cpu Load",
+ "memoryusage": "Memory Usage",
+ "freespace": "Free Space",
+ "activeusers": "Active Users",
+ "numfiles": "Files",
+ "numshares": "Shared Items"
+ },
+ "kopia": {
+ "status": "Status",
+ "size": "Size",
+ "lastrun": "Last Run",
+ "nextrun": "Next Run",
+ "failed": "Failed"
+ },
+ "unmanic": {
+ "active_workers": "Active Workers",
+ "total_workers": "Total Workers",
+ "records_total": "Queue Length"
+ },
+ "pterodactyl": {
+ "servers": "Servers",
+ "nodes": "Nodes"
+ },
+ "prometheus": {
+ "targets_up": "Targets Up",
+ "targets_down": "Targets Down",
+ "targets_total": "Total Targets"
+ },
+ "gatus": {
+ "up": "Sites Up",
+ "down": "Sites Down",
+ "uptime": "Uptime"
+ },
+ "ghostfolio": {
+ "gross_percent_today": "Today",
+ "gross_percent_1y": "One year",
+ "gross_percent_max": "All time"
+ },
+ "audiobookshelf": {
+ "podcasts": "Podcasts",
+ "books": "Books",
+ "podcastsDuration": "Duration",
+ "booksDuration": "Duration"
+ },
+ "homeassistant": {
+ "people_home": "People Home",
+ "lights_on": "Lights On",
+ "switches_on": "Switches On"
+ },
+ "whatsupdocker": {
+ "monitoring": "Monitoring",
+ "updates": "Updates"
+ },
+ "calibreweb": {
+ "books": "Books",
+ "authors": "Authors",
+ "categories": "Categories",
+ "series": "Series"
+ },
+ "jdownloader": {
+ "downloadCount": "Queue",
+ "downloadBytesRemaining": "Remaining",
+ "downloadTotalBytes": "Size",
+ "downloadSpeed": "Speed"
+ },
+ "kavita": {
+ "seriesCount": "Series",
+ "totalFiles": "Files"
+ },
+ "azuredevops": {
+ "result": "Result",
+ "status": "Status",
+ "buildId": "Build ID",
+ "succeeded": "Succeeded",
+ "notStarted": "Not Started",
+ "failed": "Failed",
+ "canceled": "Canceled",
+ "inProgress": "In Progress",
+ "totalPrs": "Total PRs",
+ "myPrs": "My PRs",
+ "approved": "Approved"
+ },
+ "gamedig": {
+ "status": "Status",
+ "online": "Online",
+ "offline": "Offline",
+ "name": "Name",
+ "map": "Map",
+ "currentPlayers": "Current players",
+ "players": "Players",
+ "maxPlayers": "Max players",
+ "bots": "Bots",
+ "ping": "Ping"
+ },
+ "urbackup": {
+ "ok": "Ok",
+ "errored": "Errors",
+ "noRecent": "Out of Date",
+ "totalUsed": "Used Storage"
+ },
+ "mealie": {
+ "recipes": "Recipes",
+ "users": "Users",
+ "categories": "Categories",
+ "tags": "Tags"
+ },
+ "openmediavault": {
+ "downloading": "Downloading",
+ "total": "Total",
+ "running": "Running",
+ "stopped": "Stopped",
+ "passed": "Passed",
+ "failed": "Failed"
+ },
+ "openwrt": {
+ "uptime": "Uptime",
+ "cpuLoad": "CPU Load Avg (5m)",
+ "up": "Up",
+ "down": "Down",
+ "bytesTx": "Transmitted",
+ "bytesRx": "Received"
+ },
+ "uptimerobot": {
+ "status": "Status",
+ "uptime": "Uptime",
+ "lastDown": "Last Downtime",
+ "downDuration": "Downtime Duration",
+ "sitesUp": "Sites Up",
+ "sitesDown": "Sites Down",
+ "paused": "Paused",
+ "notyetchecked": "Not Yet Checked",
+ "up": "Up",
+ "seemsdown": "Seems Down",
+ "down": "Down",
+ "unknown": "Unknown"
+ },
+ "calendar": {
+ "inCinemas": "In cinemas",
+ "physicalRelease": "Physical release",
+ "digitalRelease": "Digital release",
+ "noEventsToday": "No events for today!",
+ "noEventsFound": "No events found"
+ },
+ "romm": {
+ "platforms": "Platforms",
+ "totalRoms": "Total ROMs"
+ },
+ "netdata": {
+ "warnings": "Warnings",
+ "criticals": "Criticals"
+ },
+ "plantit": {
+ "events": "Events",
+ "plants": "Plants",
+ "photos": "Photos",
+ "species": "Species"
+ },
+ "gitea": {
+ "notifications": "Notifications",
+ "issues": "Issues",
+ "pulls": "Pull Requests"
+ },
+ "stash": {
+ "scenes": "Scenes",
+ "scenesPlayed": "Scenes Played",
+ "playCount": "Total Plays",
+ "playDuration": "Time Watched",
+ "sceneSize": "Scenes Size",
+ "sceneDuration": "Scenes Duration",
+ "images": "Images",
+ "imageSize": "Images Size",
+ "galleries": "Galleries",
+ "performers": "Performers",
+ "studios": "Studios",
+ "movies": "Movies",
+ "tags": "Tags",
+ "oCount": "O Count"
+ },
+ "tandoor": {
+ "users": "Users",
+ "recipes": "Recipes",
+ "keywords": "Keywords"
+ },
+ "homebox": {
+ "items": "Items",
+ "totalWithWarranty": "With Warranty",
+ "locations": "Locations",
+ "labels": "Labels",
+ "users": "Users",
+ "totalValue": "Total Value"
+ },
+ "crowdsec": {
+ "alerts": "Alerts",
+ "bans": "Bans"
+ },
+ "wgeasy": {
+ "connected": "Connected",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "total": "Total"
+ },
+ "swagdashboard": {
+ "proxied": "Proxied",
+ "auth": "With Auth",
+ "outdated": "Outdated",
+ "banned": "Banned"
+ },
+ "myspeed": {
+ "ping": "Ping",
+ "download": "Download",
+ "upload": "Upload"
+ },
+ "stocks": {
+ "stocks": "Stocks",
+ "loading": "Loading",
+ "open": "Open - US Market",
+ "closed": "Closed - US Market",
+ "invalidConfiguration": "Invalid Configuration"
+ },
+ "frigate": {
+ "cameras": "Cameras",
+ "uptime": "Uptime",
+ "version": "Version"
+ },
+ "linkwarden": {
+ "links": "Links",
+ "collections": "Collections",
+ "tags": "Tags"
+ }
}
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 03b4507e..a80c1ac2 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -115,7 +115,10 @@ export async function servicesFromDocker() {
return constructedService;
});
- return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
+ return {
+ server: serverName,
+ services: discovered.filter((filteredService) => filteredService),
+ };
} catch (e) {
logger.error("Error getting services from Docker server '%s': %s", serverName, e);
@@ -438,6 +441,11 @@ export function cleanServiceGroups(groups) {
namespace,
podSelector,
+ // linkwarden
+ mode,
+ params,
+ url,
+
// mjpeg
fit,
stream,
@@ -575,6 +583,11 @@ export function cleanServiceGroups(groups) {
if (pointsLimit) cleanedService.widget.pointsLimit = pointsLimit;
if (diskUnits) cleanedService.widget.diskUnits = diskUnits;
}
+ if (type === "linkwarden") {
+ if (mode) cleanedService.widget.mode = mode;
+ if (params) cleanedService.widget.params = params;
+ if (url) cleanedService.widget.url = url;
+ }
if (type === "mjpeg") {
if (stream) cleanedService.widget.stream = stream;
if (fit) cleanedService.widget.fit = fit;
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 341f5211..15920242 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -54,6 +54,7 @@ const components = {
komga: dynamic(() => import("./komga/component")),
kopia: dynamic(() => import("./kopia/component")),
lidarr: dynamic(() => import("./lidarr/component")),
+ linkwarden: dynamic(() => import("./linkwarden/component")),
mastodon: dynamic(() => import("./mastodon/component")),
mealie: dynamic(() => import("./mealie/component")),
medusa: dynamic(() => import("./medusa/component")),
diff --git a/src/widgets/linkwarden/component.jsx b/src/widgets/linkwarden/component.jsx
new file mode 100644
index 00000000..5c3f9a89
--- /dev/null
+++ b/src/widgets/linkwarden/component.jsx
@@ -0,0 +1,258 @@
+import React, { useState, useEffect, useMemo, useCallback } from "react";
+
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+ const { widget } = service;
+
+ // Assign icons. Assign recent/collections/tags to query by id(s)
+ const bookmarkTypes = useMemo(
+ () => ({
+ recent: { ids: widget.mode.includes("recent") ? ["0"] : [] }, // "0" Is a made-up number used to allow looping in processBookmarks()
+ collection: {
+ ids: widget.params?.collectionIds ? widget.params.collectionIds : [],
+ },
+ tag: { ids: widget.params?.tagIds ? widget.params.tagIds : [] },
+ }),
+ [widget],
+ );
+
+ // State to hold Stats
+ const [stats, setStats] = useState({
+ totalLinks: null,
+ collections: { list: null, total: null },
+ tags: { list: null, total: null },
+ });
+
+ // State to hold Recent/Collection/Tag Bookmarks
+ const [bookmarks, setBookmarks] = useState({
+ recent: { icon: "📚️", data: {} },
+ collection: { icon: "📁", data: {} },
+ tag: { icon: "🏷️", data: {} },
+ });
+
+ const [fetchingMore, setFetchingMore] = useState({
+ recent: {},
+ collection: {},
+ tag: {},
+ });
+ const [error, setError] = useState(null);
+
+ const { data: collectionsStatsData, error: collectionsStatsError } = useWidgetAPI(widget, "collections"); // Fetch Collection Stats
+ const { data: tagsStatsData, error: tagsStatsError } = useWidgetAPI(widget, "tags"); // Fetch Tag Stats
+
+ // Effect to update Stats when collectionsStatsData or tagsStatsData changes
+ useEffect(() => {
+ if (collectionsStatsData?.response && tagsStatsData?.response) {
+ /* eslint-disable no-underscore-dangle */
+ setStats({
+ totalLinks: collectionsStatsData.response.reduce((sum, collection) => sum + (collection._count?.links || 0), 0),
+ collections: {
+ list: collectionsStatsData.response,
+ total: collectionsStatsData.response.length,
+ },
+ tags: {
+ list: tagsStatsData.response,
+ total: tagsStatsData.response.length,
+ },
+ });
+ /* eslint-enable no-underscore-dangle */
+ }
+ }, [collectionsStatsData, tagsStatsData]);
+
+ // Reusable function to fetch bookmarks based on ids.recent/ids.collection/ids.tag and type
+ const fetchBookmarks = useCallback(async (ids, type, cursor, currentWidget) => {
+ try {
+ const promises = ids.map(async (id) => {
+ const baseQuery = { sort: 0, cursor: cursor || "" };
+ const query = type === "recent" ? baseQuery : { ...baseQuery, [`${type}Id`]: id };
+
+ const queryParams = new URLSearchParams({
+ group: currentWidget.service_group,
+ service: currentWidget.service_name,
+ endpoint: "links",
+ query: JSON.stringify(query),
+ });
+
+ const response = await fetch(`/api/services/proxy?${queryParams}`);
+ if (!response.ok) {
+ if (response.status === 401) {
+ setError("Unauthorized access. Please log in again.");
+ } else {
+ setError(`HTTP error! Status: ${response.status}`);
+ }
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+ return { id, bookmarks: await response.json() };
+ });
+
+ return await Promise.all(promises);
+ } catch (fetchError) {
+ setError("An error occurred while fetching bookmarks.");
+ return [];
+ }
+ }, []);
+
+ const processBookmarks = useCallback(
+ async (ids, type, cursor, currentWidget, currentStats, updateBookmarks, append = false) => {
+ try {
+ const fetchedBookmarks = await fetchBookmarks(ids, type, cursor, currentWidget);
+ updateBookmarks((prev) => {
+ const newBookmarks = fetchedBookmarks.map((item) => ({
+ id: item.id,
+ title:
+ type === "recent"
+ ? "Recent Bookmarks"
+ : currentStats[`${type}s`]?.list?.find((statItem) => statItem.id.toString() === item.id)?.name ||
+ item.id,
+ url: `${currentWidget.url}${type === "recent" ? "/links/" : `/${type}s/${item.id}/`}`,
+ /* eslint-disable no-underscore-dangle */
+ total:
+ type === "recent"
+ ? currentStats.totalLinks
+ : currentStats[`${type}s`]?.list?.find((foundStatItem) => foundStatItem.id.toString() === item.id)
+ ?._count?.links || 0,
+ /* eslint-enable no-underscore-dangle */
+ cursor: item.bookmarks.response.length
+ ? item.bookmarks.response[item.bookmarks.response.length - 1]?.id
+ : "end",
+ bookmarks: append
+ ? [...(prev[type].data[item.id]?.bookmarks || []), ...item.bookmarks.response]
+ : item.bookmarks.response,
+ }));
+
+ return {
+ ...prev,
+ [type]: {
+ ...prev[type],
+ data: {
+ ...prev[type].data,
+ ...Object.fromEntries(newBookmarks.map((bookmark) => [bookmark.id, bookmark])),
+ },
+ },
+ };
+ });
+ } catch (processError) {
+ const errorMessage = `Error setting ${type} bookmarks: ${processError.message}`;
+ setError(errorMessage);
+ }
+ },
+ [fetchBookmarks],
+ );
+
+ // Effect to fetch and update Recent/Collection/Tag Bookmarks
+ useEffect(() => {
+ if (error) return; // Stop fetching if there's an error
+
+ const fetchAndProcessBookmarks = async () => {
+ try {
+ const bookmarkFetchPromises = Object.entries(bookmarkTypes)
+ .filter(([type, { ids }]) => ids.length > 0 && Object.keys(bookmarks[type].data).length === 0)
+ .map(async ([type, { ids }]) => {
+ // Process bookmarks for each type
+ await processBookmarks(ids, type, null, widget, stats, setBookmarks);
+ });
+
+ await Promise.all(bookmarkFetchPromises);
+ } catch (fetchError) {
+ setError(`Error processing bookmarks: ${fetchError.message}`);
+ }
+ };
+
+ fetchAndProcessBookmarks();
+ }, [bookmarkTypes, processBookmarks, widget, stats, bookmarks, error]);
+
+ const handleScroll = async (event, id, type, cursor) => {
+ const { scrollTop, scrollHeight, clientHeight } = event.target;
+ if (scrollHeight - scrollTop <= clientHeight + 1 && cursor !== "end") {
+ if (!fetchingMore[type][id]) {
+ setFetchingMore((prev) => ({
+ ...prev,
+ [type]: { ...prev[type], [id]: true },
+ }));
+ try {
+ await processBookmarks([id], type, cursor, widget, stats, setBookmarks, true);
+ } finally {
+ setFetchingMore((prev) => ({
+ ...prev,
+ [type]: { ...prev[type], [id]: false },
+ }));
+ }
+ }
+ }
+ };
+
+ // Handle errors
+ if (error) {
+ return