From 9fb6dde61bb07a15975b99acd02c91ed27241c4d Mon Sep 17 00:00:00 2001 From: Andreas Molnar <75847494+andreasmolnardev@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:15:19 +0200 Subject: [PATCH 1/8] Revise features section in README.md Updated features section to clarify principles and dashboard functionalities. --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b9263650..1e989d4c 100755 --- a/README.md +++ b/README.md @@ -12,15 +12,12 @@ This is my attempt to solving that. ## Features -- GUI Editing: Edit and manage links, search engines, your wallpapers in settings via the GUI instead of relying purely on a config. -- Built-in Authentication: dashwise has Auth built right into it - and even features -- SSO via OIDC: While tested with PocketId, it should work via OIDC - which can be configured in Pocketbase directly. -- Links: store your most important links for quick access and group them into Link Groups and Folders coming. Links can also be monitored with downtime being logged. +- Principles: Configure dashwise using its UI. No need to touch config files. Dashwise also has Authentication (including SSO) built in. +- **Dashboard**s show Glanceables and Widgets, for example Links (which can be grouped into Link groups and folders) - Glanceables: Customizable bits of one-line information next to the clock. - Widgets: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually. - News: Subscribe to RSS feeds to stay on top of everything. - Notfications: Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME -- Wallpapers: Upload them to dashwise directly, or even change the default one for new users by mounting one into the container - Spotlight-like Search: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings. - Integrations: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned From 8218c6197229cea6cdf26c04b0d51c2e6dc1b1ab Mon Sep 17 00:00:00 2001 From: Andreas Molnar <75847494+andreasmolnardev@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:16:15 +0200 Subject: [PATCH 2/8] Refactor features section in README.md Removed duplicate feature descriptions and fixed minor formatting issues. --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1e989d4c..257121eb 100755 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ This is my attempt to solving that. ## Features -- Principles: Configure dashwise using its UI. No need to touch config files. Dashwise also has Authentication (including SSO) built in. -- **Dashboard**s show Glanceables and Widgets, for example Links (which can be grouped into Link groups and folders) -- Glanceables: Customizable bits of one-line information next to the clock. -- Widgets: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually. -- News: Subscribe to RSS feeds to stay on top of everything. -- Notfications: Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME -- Spotlight-like Search: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings. -- Integrations: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned +- **Principles**: Configure dashwise using its UI. No need to touch config files. Dashwise also has Authentication (including SSO) built in. +- **Dashboards** show Glanceables and Widgets, for example Links (which can be grouped into Link groups and folders) +- **Glanceables**: Customizable bits of one-line information next to the clock. +- **Widgets**: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually. +- **News**: Subscribe to RSS feeds to stay on top of everything. +- **Notfications**: Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME +- ** Spotlight-like Search**: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings. +- **Integrations**: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned ## Installation For production depolyments, use the docker-compose.yaml (image is currently only built for arm, will change later). From 214be04262b4ac36ea87314caba3f2b33087eeb0 Mon Sep 17 00:00:00 2001 From: Andreas Molnar <75847494+andreasmolnardev@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:16:40 +0200 Subject: [PATCH 3/8] Fix formatting issue in Notifications section of README Corrected a typo in the README regarding notifications. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 257121eb..024f4d34 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This is my attempt to solving that. - **Glanceables**: Customizable bits of one-line information next to the clock. - **Widgets**: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually. - **News**: Subscribe to RSS feeds to stay on top of everything. -- **Notfications**: Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME +- **Notfications** : Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME - ** Spotlight-like Search**: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings. - **Integrations**: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned From 373aa2ac46f8cb1f9b11055951545a883c4b9967 Mon Sep 17 00:00:00 2001 From: Andreas Molnar <75847494+andreasmolnardev@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:17:32 +0200 Subject: [PATCH 4/8] Fix formatting in README Corrected a typo in the README regarding 'Spotlight-like Search'. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 024f4d34..650a4128 100755 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This is my attempt to solving that. - **Widgets**: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually. - **News**: Subscribe to RSS feeds to stay on top of everything. - **Notfications** : Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME -- ** Spotlight-like Search**: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings. +- **Spotlight-like Search**: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings. - **Integrations**: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned ## Installation From cd9b20e79a191c91ad13064dc1ea9f7bae8be8ab Mon Sep 17 00:00:00 2001 From: Andreas Molnar Date: Fri, 10 Jul 2026 06:53:48 +0000 Subject: [PATCH 5/8] wip: links widget --- .../settings/SmartFramesManager.tsx | 2 + apps/web/src/components/widgets/Widget.tsx | 136 ++++++++++++++++++ packages/assets/integrations/default.yaml | 9 ++ 3 files changed, 147 insertions(+) diff --git a/apps/web/src/components/settings/SmartFramesManager.tsx b/apps/web/src/components/settings/SmartFramesManager.tsx index 721e1dce..758b0dd9 100644 --- a/apps/web/src/components/settings/SmartFramesManager.tsx +++ b/apps/web/src/components/settings/SmartFramesManager.tsx @@ -41,6 +41,7 @@ const LOCAL_WIDGET_OPTIONS = [ { value: "progress", label: "Calendar progress" }, "countdown", "rss-feed", + "latest-links", "monitoring", ] as const; @@ -68,6 +69,7 @@ const LOCAL_WIDGET_SCHEMAS: Record> = { "calendar-upcoming": { maxEvents: 5 }, countdown: { date: "", date_format: "yyyy-MM-dd", label: "Countdown" }, "rss-feed": { feedId: "all", maxItems: 8, title: "Latest Articles" }, + "latest-links": { listId: "", maxItems: 8, title: "Latest Links" }, progress: { period: { type: "select", diff --git a/apps/web/src/components/widgets/Widget.tsx b/apps/web/src/components/widgets/Widget.tsx index c48acea6..bb4eeab7 100644 --- a/apps/web/src/components/widgets/Widget.tsx +++ b/apps/web/src/components/widgets/Widget.tsx @@ -17,6 +17,7 @@ import { } from "@dashwise/integrationskit/static-widgets/CalendarWidgets"; import CountdownWidget from "@dashwise/integrationskit/static-widgets/CountdownWidget"; import RssFeedWidget from "@dashwise/integrationskit/static-widgets/RssFeedWidget"; +import VerticalList from "@dashwise/integrationskit/templates/VerticalList"; import LinkView from "./LinkView"; import SearchBar from "./SearchBar"; import IframeTemplate from "@dashwise/integrationskit/templates/IFrame"; @@ -27,6 +28,8 @@ import useAuth from "@/context/useAuth"; import { getConsumerDataAction, getIntegrationCalendarEventsAction, + getLinksCollectionsAction, + getLinksItemsAction, getNewsFeedAction, } from '@/lib/apiClient'; @@ -91,6 +94,9 @@ export function renderWidget({ case "latest-rss-feed": return ; + case "latest-links": + return ; + case "countdown": return ; @@ -123,6 +129,136 @@ export function renderWidget({ } } +type LatestLinkItem = { + id: string; + url?: string; + title?: string; + iconUrl?: string; + description?: string; + collection?: string; + created?: string; + updated?: string; +}; + +function LatestLinksWidgetWrapper({ + className, + listId, + collectionId, + maxItems = 8, + title = "Latest Links", +}: { + className?: string; + listId?: string; + collectionId?: string; + maxItems?: number; + title?: string; +}) { + const { withAuth } = useAuth(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + const selectedListId = String(listId || collectionId || "").trim(); + + setLoading(true); + + const fetchItems = async () => { + const links = await withAuth(async (auth) => { + if (selectedListId) { + const listItems = await getLinksItemsAction(auth, selectedListId); + return Array.isArray(listItems) ? listItems : []; + } + + const collections = await getLinksCollectionsAction(auth); + const listIds = Array.isArray(collections) + ? collections.map((collection: any) => String(collection?.id ?? "").trim()).filter(Boolean) + : []; + const groupedItems = await Promise.all( + listIds.map(async (id) => { + try { + const listItems = await getLinksItemsAction(auth, id); + return Array.isArray(listItems) ? listItems : []; + } catch { + return []; + } + }), + ); + + return groupedItems.flat(); + }); + + return Array.isArray(links) ? links : []; + }; + + void fetchItems() + .then((links) => { + if (!cancelled) { + setItems(sortLatestLinks(links).slice(0, maxItems)); + } + }) + .catch((err) => { + console.error("Failed to fetch latest links", err); + if (!cancelled) setItems([]); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [collectionId, listId, maxItems, withAuth]); + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + return ( + ({ + title: item.title || "Untitled", + titleAction: item.url || undefined, + subtitle: [item.description || item.url, formatLatestLinkDate(item.created)].filter(Boolean) as string[], + thumbnail: item.iconUrl || undefined, + icon: "fa6-solid:link", + })), + raw: {}, + }} + /> + ); +} + +function sortLatestLinks(items: LatestLinkItem[]) { + return [...items].sort((left, right) => { + const leftTime = new Date(left.created || left.updated || 0).getTime(); + const rightTime = new Date(right.created || right.updated || 0).getTime(); + return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime); + }); +} + +function formatLatestLinkDate(value?: string) { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + return date.toLocaleDateString([], { month: "short", day: "numeric" }); +} + function resolveProgressPeriod(type: string, params?: Record) { const candidate = String(params?.period ?? type ?? "day").trim(); if (candidate === "year" || candidate === "month" || candidate === "day" || candidate === "week") { diff --git a/packages/assets/integrations/default.yaml b/packages/assets/integrations/default.yaml index eb995e74..20e6bec9 100644 --- a/packages/assets/integrations/default.yaml +++ b/packages/assets/integrations/default.yaml @@ -16,6 +16,15 @@ configuration: key: "link-view" template: "link-view" category: "Links" + - name: "Latest Links" + key: "latest-links" + template: "latest-links" + category: "Links" + data: + input: + listId: "" + maxItems: 8 + title: "Latest Links" - name: "Placeholder" key: "placeholder" template: "placeholder" From d587ae08ce7100ae3ec6e3913cfc280749eb3f9c Mon Sep 17 00:00:00 2001 From: Andreas Molnar Date: Fri, 10 Jul 2026 08:27:40 +0000 Subject: [PATCH 6/8] integrations: add tailscale widgets --- apps/backend/src/lib/data/integrations.ts | 3 +- packages/assets/defaults/integrations.json | 9 +- packages/assets/index.ts | 4 +- packages/assets/integrations/tailscale.yaml | 216 ++++++++++++++++++++ 4 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 packages/assets/integrations/tailscale.yaml diff --git a/apps/backend/src/lib/data/integrations.ts b/apps/backend/src/lib/data/integrations.ts index e1bbbe3a..8e3d0783 100644 --- a/apps/backend/src/lib/data/integrations.ts +++ b/apps/backend/src/lib/data/integrations.ts @@ -1,6 +1,6 @@ import { Buffer } from "buffer"; import { defaultIntegrationsManifest } from "@dashwise/assets"; -import { weatherIntegrationBlueprint } from "@dashwise/assets"; +import { tailscaleIntegrationBlueprint, weatherIntegrationBlueprint } from "@dashwise/assets"; import { ApiActionError } from "./auth"; import { getSuperuserPB } from "../pb/pocketbase"; import { decodeBase64Json, parseNullableJson, tryParseJson, tryParseYaml } from "../parseHelpers"; @@ -65,6 +65,7 @@ const builtinManifest = isPlainObject(defaultIntegrationsManifest) const builtinConfigsBySource: Record = { "/integrations/weather.yaml": weatherIntegrationBlueprint, + "/integrations/tailscale.yaml": tailscaleIntegrationBlueprint, }; let builtinSeedsPromise: Promise | null = null; diff --git a/packages/assets/defaults/integrations.json b/packages/assets/defaults/integrations.json index bdd5b186..2db6d92e 100644 --- a/packages/assets/defaults/integrations.json +++ b/packages/assets/defaults/integrations.json @@ -1,7 +1,10 @@ { - "weather": - { + "weather": { "source": "/integrations/weather.yaml", "defaultEnv": {} - } + }, + "tailscale": { + "source": "/integrations/tailscale.yaml", + "defaultEnv": {} + } } diff --git a/packages/assets/index.ts b/packages/assets/index.ts index a218b1af..ec1821b7 100644 --- a/packages/assets/index.ts +++ b/packages/assets/index.ts @@ -5,9 +5,11 @@ import integrationsDefaults from "./defaults/integrations.json"; import shortcutsDefaults from "./defaults/shortcuts.json"; import defaultIntegrations from "./integrations/default.yaml"; import weatherIntegration from "./integrations/weather.yaml"; +import tailscaleIntegration from "./integrations/tailscale.yaml"; export const defaultHomeConfig = homeDefaults; export const defaultIntegrationsManifest = integrationsDefaults; export const defaultShortcutsManifest = shortcutsDefaults; export const defaultIntegrationsBlueprint = defaultIntegrations; -export const weatherIntegrationBlueprint = weatherIntegration; \ No newline at end of file +export const weatherIntegrationBlueprint = weatherIntegration; +export const tailscaleIntegrationBlueprint = tailscaleIntegration; diff --git a/packages/assets/integrations/tailscale.yaml b/packages/assets/integrations/tailscale.yaml new file mode 100644 index 00000000..05f5a597 --- /dev/null +++ b/packages/assets/integrations/tailscale.yaml @@ -0,0 +1,216 @@ +# Dashwise Integration YAML +details: + name: Tailscale + description: "Tailnet device inventory and status widgets." + author: "Dashwise" + version: 1.0.0 + repository: "" + icon: "/icons/png/tailscale-light.png" + links: + website: "https://tailscale.com" + documentation: "https://tailscale.com/kb/1215/oauth-clients" + support: "https://tailscale.com/contact/support" + categories: + - Network + - Monitoring + +configuration: + environment_variables: + TAILNET: + required: true + description: "Tailnet name, usually your organization domain or tailnet ID." + API_KEY: + required: true + edit: "overwrite-only" + description: "Tailscale API access token with device read access." + URL: + required: false + default: "https://login.tailscale.com/admin/machines" + description: "Tailscale machines page URL used for widget and search actions." + CRITICAL_TAGS: + required: false + default: "tag:critical" + description: "Comma-separated tag names used to count critical offline devices." + + endpoints: + fetch-devices: + method: GET + url: "https://api.tailscale.com/api/v2/tailnet/${TAILNET}/devices?fields=all" + auth: "Bearer ${API_KEY}" + custom_headers: + Accept: "application/json" + response: + type: json + data_path: "devices" + response_mapping: + devices: + iterate: "devices" + mappingProperties: + id: "id" + node_id: "nodeId" + name: + operation: coalesce + fields: ["name", "hostname"] + fallback: "Unnamed device" + hostname: "hostname" + os: "os" + version: "clientVersion" + online: "online" + last_seen: "lastSeen" + addresses: "addresses" + primary_ip: + operation: coalesce + fields: ["addresses[0]", "addresses"] + fallback: "" + tags: "tags" + tags_label: + operation: join + value: "tags" + separator: ", " + fallback: "No tags" + authorized: "authorized" + update_available: "updateAvailable" + + computed: + devices: + operation: expand + on: "this.endpoints.fetch-devices.mappedResponse.devices as device" + key: "${id}" + fields: + status: + operation: if + condition: "online == true" + then: "Online" + else: "Offline" + action: "url:${URL}" + subtitle: + operation: join + value: + - "${hostname}" + - "${primary_ip}" + - "${tags_label}" + - "v${version}" + separator: " - " + total_count: + iterate_over: "this.endpoints.fetch-devices.mappedResponse.devices" + initial_value: 0 + operation: math + from: "computed.total_count" + add: 1 + online_count: + iterate_over: "this.endpoints.fetch-devices.mappedResponse.devices" + initial_value: 0 + filter: "online == true" + operation: math + from: "computed.online_count" + add: 1 + offline_count: + iterate_over: "this.endpoints.fetch-devices.mappedResponse.devices" + initial_value: 0 + filter: "online != true" + operation: math + from: "computed.offline_count" + add: 1 + critical_offline_count: + iterate_over: "this.endpoints.fetch-devices.mappedResponse.devices" + initial_value: 0 + filter: "online != true and tags contains '${CRITICAL_TAGS}'" + operation: math + from: "computed.critical_offline_count" + add: 1 + + widgets: + - name: "Tailscale Devices" + key: "tailscale-devices" + template: "vertical-list" + depends_on: + - "computed.devices" + data: + source: "computed.devices" + input: + TAILNET: "${TAILNET}" + API_KEY: "${API_KEY}" + URL: "${URL}" + CRITICAL_TAGS: "${CRITICAL_TAGS}" + properties: + header: + title: "Tailscale Devices" + icon: "integrations.tailscale.details.icon" + titleAction: "url:${URL}" + list: + iterate_over: "computed.devices" + user_customizations: + - allow_reorder + - allow_hide + prototype: + accent: + value: "status" + map: + Online: "#16a34a" + Offline: "#ef4444" + icon: + value: "status" + map: + Online: "faCircleCheck" + Offline: "faCircleXmark" + title: "${name}" + titleAction: "url:${action}" + subtitle: + type: "list" + value: '["Last seen: ${last_seen}", "${subtitle}"]' + - name: "Tailscale Status" + key: "tailscale-status" + template: "columns" + depends_on: + - "computed.online_count" + - "computed.critical_offline_count" + - "computed.offline_count" + data: + source: "computed.devices" + input: + TAILNET: "${TAILNET}" + API_KEY: "${API_KEY}" + URL: "${URL}" + CRITICAL_TAGS: "${CRITICAL_TAGS}" + properties: + header: + title: "Tailnet Health" + icon: "integrations.tailscale.details.icon" + titleAction: "url:${URL}" + columns: + - id: "online-devices" + label: "Online" + primary: "${computed.online_count}" + secondary: "of ${computed.total_count} devices" + icon: + file: "faCircleCheck" + - id: "critical-offline" + label: "Critical Offline" + primary: "${computed.critical_offline_count}" + secondary: "Tags: ${CRITICAL_TAGS}" + icon: + file: "faTriangleExclamation" + - id: "offline-devices" + label: "Offline" + primary: "${computed.offline_count}" + secondary: "devices offline" + icon: + file: "faCircleXmark" + + shortcuts: + - devices: + iterate_over: "computed.devices" + mappingProperties: + id: "${id}" + name: "${name}" + icon: "integrations.tailscale.details.icon" + secondaryInfo: "${status} - ${hostname} - ${primary_ip}" + type: "tailscaleDevice" + action: "url:${action}" + tags: + - "Tailscale" + - "Tailnet" + - "Device" + - "${name}" + - "${hostname}" + - "${tags_label}" From 4be1174ec4d045d9cbe11c1bbf1ecba399f4cf81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:57:57 +0000 Subject: [PATCH 7/8] Revert non-Tailscale builtin integration changes --- apps/backend/src/lib/data/integrations.ts | 3 +-- packages/assets/defaults/integrations.json | 9 +++------ packages/assets/index.ts | 4 +--- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/lib/data/integrations.ts b/apps/backend/src/lib/data/integrations.ts index 8e3d0783..e1bbbe3a 100644 --- a/apps/backend/src/lib/data/integrations.ts +++ b/apps/backend/src/lib/data/integrations.ts @@ -1,6 +1,6 @@ import { Buffer } from "buffer"; import { defaultIntegrationsManifest } from "@dashwise/assets"; -import { tailscaleIntegrationBlueprint, weatherIntegrationBlueprint } from "@dashwise/assets"; +import { weatherIntegrationBlueprint } from "@dashwise/assets"; import { ApiActionError } from "./auth"; import { getSuperuserPB } from "../pb/pocketbase"; import { decodeBase64Json, parseNullableJson, tryParseJson, tryParseYaml } from "../parseHelpers"; @@ -65,7 +65,6 @@ const builtinManifest = isPlainObject(defaultIntegrationsManifest) const builtinConfigsBySource: Record = { "/integrations/weather.yaml": weatherIntegrationBlueprint, - "/integrations/tailscale.yaml": tailscaleIntegrationBlueprint, }; let builtinSeedsPromise: Promise | null = null; diff --git a/packages/assets/defaults/integrations.json b/packages/assets/defaults/integrations.json index 2db6d92e..bdd5b186 100644 --- a/packages/assets/defaults/integrations.json +++ b/packages/assets/defaults/integrations.json @@ -1,10 +1,7 @@ { - "weather": { + "weather": + { "source": "/integrations/weather.yaml", "defaultEnv": {} - }, - "tailscale": { - "source": "/integrations/tailscale.yaml", - "defaultEnv": {} - } + } } diff --git a/packages/assets/index.ts b/packages/assets/index.ts index ec1821b7..a218b1af 100644 --- a/packages/assets/index.ts +++ b/packages/assets/index.ts @@ -5,11 +5,9 @@ import integrationsDefaults from "./defaults/integrations.json"; import shortcutsDefaults from "./defaults/shortcuts.json"; import defaultIntegrations from "./integrations/default.yaml"; import weatherIntegration from "./integrations/weather.yaml"; -import tailscaleIntegration from "./integrations/tailscale.yaml"; export const defaultHomeConfig = homeDefaults; export const defaultIntegrationsManifest = integrationsDefaults; export const defaultShortcutsManifest = shortcutsDefaults; export const defaultIntegrationsBlueprint = defaultIntegrations; -export const weatherIntegrationBlueprint = weatherIntegration; -export const tailscaleIntegrationBlueprint = tailscaleIntegration; +export const weatherIntegrationBlueprint = weatherIntegration; \ No newline at end of file From eaabe80b60c1752cc2a7a337feddb3990f3f7e25 Mon Sep 17 00:00:00 2001 From: Andreas Molnar Date: Fri, 10 Jul 2026 15:17:24 +0200 Subject: [PATCH 8/8] feat: add sunrise/sunset glanceables --- .../settings/pages/EditGlanceablesView.tsx | 1 + .../widgets/dashboard/GlanceableClock.tsx | 1 + packages/assets/integrations/weather.yaml | 18 ++++ packages/integrationskit/Glanceable.tsx | 83 +++++++++++++++---- 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/settings/pages/EditGlanceablesView.tsx b/apps/web/src/components/settings/pages/EditGlanceablesView.tsx index 058552f3..1c6f32da 100644 --- a/apps/web/src/components/settings/pages/EditGlanceablesView.tsx +++ b/apps/web/src/components/settings/pages/EditGlanceablesView.tsx @@ -12,6 +12,7 @@ import { } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; import { GlanceableSide } from "./utils"; import { useLocalization } from "@/context/LocalizationContext"; import useAuth from "@/context/useAuth"; diff --git a/apps/web/src/components/widgets/dashboard/GlanceableClock.tsx b/apps/web/src/components/widgets/dashboard/GlanceableClock.tsx index 3f007c97..e5d28dbc 100644 --- a/apps/web/src/components/widgets/dashboard/GlanceableClock.tsx +++ b/apps/web/src/components/widgets/dashboard/GlanceableClock.tsx @@ -228,6 +228,7 @@ function ResolvedGlanceable({ return ( - {resolved.icon && ( - - )} - {renderLocalizedText(resolved.text, formatters)} - - ); - } - - if (!glanceableJSON && type) { - return ; - } - const safeGlanceableJSON = useMemo( () => mergeGlanceableJSON(glanceableJSON ?? {}, params), [glanceableJSON, params], @@ -136,6 +126,56 @@ export default function Glanceable({ }; }, [integrationJSON, isPreview, resolvedRuntimeData, safeGlanceableJSON]); + + const multiColumnItems = Array.isArray(safeGlanceableJSON.columns) + ? safeGlanceableJSON.columns + .filter((entry): entry is MultiColumnGlanceableItem => !!entry && typeof entry === "object") + .map((entry) => { + const rawValue = typeof entry.value === "string" + ? resolveGlanceableText(entry.value, env, formatters) + : typeof entry.text === "string" + ? resolveGlanceableText(entry.text, env, formatters) + : ""; + + return { + icon: getGlanceableIconSource(entry.icon, env), + text: entry.format === "time" ? formatGlanceableTime(rawValue, formatters) : rawValue, + }; + }) + : null; + + if (multiColumnItems && multiColumnItems.length > 0) { + return ( + + {multiColumnItems.map((item, index) => ( + + {item.icon && ( + + )} + {item.text} + + ))} + + ); + } + + if (resolved) { + return ( + + {resolved.icon && ( + + )} + {renderLocalizedText(resolved.text, formatters)} + + ); + } + + if (!glanceableJSON && type) { + return ; + } + let text = ""; // support structured text operations (eg. stringadd) as well as plain strings @@ -394,6 +434,15 @@ function formatWeather(params?: Record, formatters?: TextFormatters return `${emoji} ${temperatureText}${location}`.trim(); } +function formatGlanceableTime(value: string, formatters?: TextFormatters) { + if (!value.trim()) return ""; + + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + + return formatTime(date, undefined, formatters); +} + function getWeatherEmoji(weatherCode: unknown, description: unknown) { const code = typeof weatherCode === "number" ? weatherCode : Number(weatherCode); if (Number.isFinite(code)) {