From aa7cfa58ffd8517d9762079cf2595c0ee31c1663 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Sun, 31 Dec 2023 10:48:10 -0800
Subject: [PATCH 01/11] Better handle malformed docker labels (#2552)

---
 src/utils/config/service-helpers.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index f96f6509..1c5ee894 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -102,6 +102,16 @@ export async function servicesFromDocker() {
             }
           });
 
+          if (!constructedService.name || !constructedService.group) {
+            logger.error(
+              `Error constructing service using homepage labels for container '${containerName.replace(
+                /^\//,
+                "",
+              )}'. Ensure required labels are present.`,
+            );
+            return null;
+          }
+
           return constructedService;
         });
 

From 1c47d9d70ed61ff9a6000abbb726f51ab5be2508 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Sun, 31 Dec 2023 20:18:17 -0800
Subject: [PATCH 02/11] Fix: pass user/pass as strings with OMV proxy (#2555)

---
 src/widgets/openmediavault/proxy.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/widgets/openmediavault/proxy.js b/src/widgets/openmediavault/proxy.js
index 2152f305..e1f97a56 100644
--- a/src/widgets/openmediavault/proxy.js
+++ b/src/widgets/openmediavault/proxy.js
@@ -64,7 +64,7 @@ async function tryLogin(widget) {
   const resp = await rpc(url, {
     method: "login",
     service: "session",
-    params: { username, password },
+    params: { username: username.toString(), password: password.toString() },
   });
 
   if (resp.status !== 200) {

From 50c989e36ab06b66780da7923bc68bba45b091b9 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Sat, 6 Jan 2024 09:22:25 -0800
Subject: [PATCH 03/11] Fix: unique element key generation in quicklaunch and
 services (#2586)

---
 src/components/quicklaunch.jsx   | 2 +-
 src/components/services/list.jsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/quicklaunch.jsx b/src/components/quicklaunch.jsx
index a356fdee..7fb1460a 100644
--- a/src/components/quicklaunch.jsx
+++ b/src/components/quicklaunch.jsx
@@ -199,7 +199,7 @@ export default function QuickLaunch({
             {results.length > 0 && (
               <ul className="max-h-[60vh] overflow-y-auto m-2">
                 {results.map((r, i) => (
-                  <li key={r.container ?? r.app ?? `${r.name}-${r.href}`}>
+                  <li key={[r.name, r.container, r.app, r.href].filter((s) => s).join("-")}>
                     <button
                       type="button"
                       data-index={i}
diff --git a/src/components/services/list.jsx b/src/components/services/list.jsx
index 85436178..f3fd6e2a 100644
--- a/src/components/services/list.jsx
+++ b/src/components/services/list.jsx
@@ -14,7 +14,7 @@ export default function List({ group, services, layout, useEqualHeights }) {
     >
       {services.map((service) => (
         <Item
-          key={service.container ?? service.app ?? service.name}
+          key={[service.container, service.app, service.name].filter((s) => s).join("-")}
           service={service}
           group={group}
           useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}

From 1103df2b64d0d9134dca29e16aa79875094f9785 Mon Sep 17 00:00:00 2001
From: Metin Yazici <me@strboul.com>
Date: Sun, 7 Jan 2024 18:17:07 +0100
Subject: [PATCH 04/11] Feature: support multiple checks for healthchecks
 widget (#2580)

* Change healthchecks online/offline with the original up/down

* Add group statistics to healthcheck widget

* Update healthchecks docs

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
---
 docs/widgets/services/healthchecks.md  | 11 ++++---
 public/locales/en/common.json          |  4 +--
 src/utils/config/service-helpers.js    |  6 ++++
 src/widgets/healthchecks/component.jsx | 40 ++++++++++++++++++++++----
 src/widgets/healthchecks/widget.js     |  3 +-
 5 files changed, 51 insertions(+), 13 deletions(-)

diff --git a/docs/widgets/services/healthchecks.md b/docs/widgets/services/healthchecks.md
index ae8f1e26..b438e153 100644
--- a/docs/widgets/services/healthchecks.md
+++ b/docs/widgets/services/healthchecks.md
@@ -3,18 +3,21 @@ title: Health checks
 description: Health checks Widget Configuration
 ---
 
-To use the Health Checks widget, you first need to generate an API key. To do this, follow these steps:
+Specify a single check by including the `uuid` field or show the total 'up' and 'down' for all
+checks by leaving off the `uuid` field.
 
-1. Go to Settings in your check dashboard.
+To use the Health Checks widget, you first need to generate an API key.
+
+1. In your project, go to project Settings on the navigation bar.
 2. Click on API key (read-only) and then click _Create_.
 3. Copy the API key that is generated for you.
 
-Allowed fields: `["status", "last_ping"]`.
+Allowed fields: `["status", "last_ping"]` for single checks, `["up", "down"]` for total stats.
 
 ```yaml
 widget:
   type: healthchecks
   url: http://healthchecks.host.or.ip:port
   key: <YOUR_API_KEY>
-  uuid: <YOUR_CHECK_UUID>
+  uuid: <CHECK_UUID> # optional, if not included total statistics for all checks is shown
 ```
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index aa4ef1e5..bf39f1d9 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -491,9 +491,9 @@
     },
     "healthchecks": {
         "new": "New",
-        "up": "Online",
+        "up": "Up",
         "grace": "In Grace Period",
-        "down": "Offline",
+        "down": "Down",
         "paused": "Paused",
         "status": "Status",
         "last_ping": "Last Ping",
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 1c5ee894..d73c1b5a 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -397,6 +397,9 @@ export function cleanServiceGroups(groups) {
           // glances, customapi, iframe
           refreshInterval,
 
+          // healthchecks
+          uuid,
+
           // iframe
           allowFullscreen,
           allowPolicy,
@@ -536,6 +539,9 @@ export function cleanServiceGroups(groups) {
           if (previousDays) cleanedService.widget.previousDays = previousDays;
           if (showTime) cleanedService.widget.showTime = showTime;
         }
+        if (type === "healthchecks") {
+          if (uuid !== undefined) cleanedService.widget.uuid = uuid;
+        }
       }
 
       return cleanedService;
diff --git a/src/widgets/healthchecks/component.jsx b/src/widgets/healthchecks/component.jsx
index 12ec726e..bcb8d740 100644
--- a/src/widgets/healthchecks/component.jsx
+++ b/src/widgets/healthchecks/component.jsx
@@ -27,6 +27,23 @@ function formatDate(dateString) {
   return new Intl.DateTimeFormat(i18n.language, dateOptions).format(date);
 }
 
+function countStatus(data) {
+  let upCount = 0;
+  let downCount = 0;
+
+  if (data.checks) {
+    data.checks.forEach((check) => {
+      if (check.status === "up") {
+        upCount += 1;
+      } else if (check.status === "down") {
+        downCount += 1;
+      }
+    });
+  }
+
+  return { upCount, downCount };
+}
+
 export default function Component({ service }) {
   const { t } = useTranslation();
   const { widget } = service;
@@ -46,13 +63,26 @@ export default function Component({ service }) {
     );
   }
 
+  const hasUuid = widget?.uuid;
+
+  const { upCount, downCount } = countStatus(data);
+
   return (
     <Container service={service}>
-      <Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
-      <Block
-        label="healthchecks.last_ping"
-        value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
-      />
+      {hasUuid ? (
+        <>
+          <Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
+          <Block
+            label="healthchecks.last_ping"
+            value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
+          />
+        </>
+      ) : (
+        <>
+          <Block label="healthchecks.up" value={upCount} />
+          <Block label="healthchecks.down" value={downCount} />
+        </>
+      )}
     </Container>
   );
 }
diff --git a/src/widgets/healthchecks/widget.js b/src/widgets/healthchecks/widget.js
index 02ae9acf..50324dd5 100644
--- a/src/widgets/healthchecks/widget.js
+++ b/src/widgets/healthchecks/widget.js
@@ -1,13 +1,12 @@
 import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
 
 const widget = {
-  api: "{url}/api/v2/{endpoint}/{uuid}",
+  api: "{url}/api/v3/{endpoint}/{uuid}",
   proxyHandler: credentialedProxyHandler,
 
   mappings: {
     checks: {
       endpoint: "checks",
-      validate: ["status", "last_ping"],
     },
   },
 };

From 8f121d675c057ce455690201574641300c7a4984 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Wed, 10 Jan 2024 11:16:48 -0800
Subject: [PATCH 05/11] Fix custom API docker labels example

---
 docs/configs/docker.md | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/configs/docker.md b/docs/configs/docker.md
index 2eaf2683..4d3026db 100644
--- a/docs/configs/docker.md
+++ b/docs/configs/docker.md
@@ -164,10 +164,10 @@ labels:
   - homepage.description=Media server
   - homepage.widget.type=customapi
   - homepage.widget.url=http://argus.service/api/v1/service/summary/emby
-  - homepage.widget.field[0].label=Deployed Version
-  - homepage.widget.field[0].field.status=deployed_version
-  - homepage.widget.field[1].label=Latest Version
-  - homepage.widget.field[1].field.status=latest_version
+  - homepage.widget.mappings[0].label=Deployed Version
+  - homepage.widget.mappings[0].field.status=deployed_version
+  - homepage.widget.mappings[1].label=Latest Version
+  - homepage.widget.mappings[1].field.status=latest_version
 ```
 
 ## Docker Swarm

From 66a1368aa3529e110a4853a8fe3de1845b5fa08a Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Wed, 10 Jan 2024 14:24:38 -0800
Subject: [PATCH 06/11] Fix: sort ical events in monthly view (#2604)

---
 src/widgets/calendar/monthly.jsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/widgets/calendar/monthly.jsx b/src/widgets/calendar/monthly.jsx
index 8a208bc1..62f869ed 100644
--- a/src/widgets/calendar/monthly.jsx
+++ b/src/widgets/calendar/monthly.jsx
@@ -111,6 +111,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
   }
 
   const eventsArray = Object.keys(events).map((eventKey) => events[eventKey]);
+  eventsArray.sort((a, b) => a.date - b.date);
 
   return (
     <div className="w-full text-center">

From 9984e7894fa04fe0ddb4b49850fe4b4ba2cf99bb Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Wed, 10 Jan 2024 14:26:40 -0800
Subject: [PATCH 07/11] Fix lint error for service anchors

---
 src/components/services/item.jsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx
index 89319691..0d0c1b5b 100644
--- a/src/components/services/item.jsx
+++ b/src/components/services/item.jsx
@@ -49,6 +49,7 @@ export default function Item({ service, group, useEqualHeights }) {
                 target={service.target ?? settings.target ?? "_blank"}
                 rel="noreferrer"
                 className="flex-shrink-0 flex items-center justify-center w-12 service-icon"
+                aria-label={service.icon}
               >
                 <ResolvedIcon icon={service.icon} />
               </a>

From 674d7f2e01f5fbf345d4744c16072002a4937a4a Mon Sep 17 00:00:00 2001
From: Denis Papec <denis.papec@gmail.com>
Date: Sun, 14 Jan 2024 21:49:28 +0000
Subject: [PATCH 08/11] Fix for events repeating on mothly basis and old events
 that are shown as occuring today (#2624)

---
 src/widgets/calendar/integrations/ical.jsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/widgets/calendar/integrations/ical.jsx b/src/widgets/calendar/integrations/ical.jsx
index f5063331..c14aa347 100644
--- a/src/widgets/calendar/integrations/ical.jsx
+++ b/src/widgets/calendar/integrations/ical.jsx
@@ -55,8 +55,9 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
         }
       };
 
-      if (event?.recurrenceRule?.options) {
-        const rule = new RRule(event.recurrenceRule.options);
+      const recurrenceOptions = event?.recurrenceRule?.origOptions;
+      if (recurrenceOptions && Object.keys(recurrenceOptions).length !== 0) {
+        const rule = new RRule(recurrenceOptions);
         const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
 
         recurringEvents.forEach((date, i) => eventToAdd(date, i, "recurring"));

From 1f2081af5d7642f6eeec92210ded14e3b9e1bc0d Mon Sep 17 00:00:00 2001
From: Denis Papec <denis.papec@gmail.com>
Date: Mon, 15 Jan 2024 02:01:10 +0000
Subject: [PATCH 09/11] Add option to specify a timezone for events (#2623)

* Add option to specify a timezone for events

* Amend message, update docs
---
 docs/widgets/services/calendar.md          |  1 +
 src/widgets/calendar/agenda.jsx            | 10 ++++++----
 src/widgets/calendar/event.jsx             |  4 ++++
 src/widgets/calendar/integrations/ical.jsx | 12 ++++++++----
 src/widgets/calendar/monthly.jsx           | 10 ++++------
 5 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/docs/widgets/services/calendar.md b/docs/widgets/services/calendar.md
index f9ed6284..74751f6e 100644
--- a/docs/widgets/services/calendar.md
+++ b/docs/widgets/services/calendar.md
@@ -27,6 +27,7 @@ widget:
       url: https://domain.url/with/link/to.ics # URL with calendar events
       name: My Events # required - name for these calendar events
       color: zinc # optional - defaults to pre-defined color for the service (zinc for ical)
+      timezone: America/Los_Angeles # optional - force timezone for events (if it's the same - no change, if missing or different in ical - will be converted to this timezone)
       params: # optional - additional params for the service
         showName: true # optional - show name before event title in event line - defaults to false
 ```
diff --git a/src/widgets/calendar/agenda.jsx b/src/widgets/calendar/agenda.jsx
index 90359269..9313cb8e 100644
--- a/src/widgets/calendar/agenda.jsx
+++ b/src/widgets/calendar/agenda.jsx
@@ -2,7 +2,7 @@ import { DateTime } from "luxon";
 import classNames from "classnames";
 import { useTranslation } from "next-i18next";
 
-import Event from "./event";
+import Event, { compareDateTimezoneAware } from "./event";
 
 export default function Agenda({ service, colorVariants, events, showDate }) {
   const { widget } = service;
@@ -15,8 +15,10 @@ export default function Agenda({ service, colorVariants, events, showDate }) {
   const eventsArray = Object.keys(events)
     .filter(
       (eventKey) =>
-        showDate.minus({ days: widget?.previousDays ?? 0 }).startOf("day").ts <=
-        events[eventKey].date?.startOf("day").ts,
+        showDate
+          .setZone(events[eventKey].date.zoneName)
+          .minus({ days: widget?.previousDays ?? 0 })
+          .startOf("day").ts <= events[eventKey].date?.startOf("day").ts,
     )
     .map((eventKey) => events[eventKey])
     .sort((a, b) => a.date - b.date)
@@ -56,7 +58,7 @@ export default function Agenda({ service, colorVariants, events, showDate }) {
                 event={event}
                 colorVariants={colorVariants}
                 showDate={j === 0}
-                showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
+                showTime={widget?.showTime && compareDateTimezoneAware(showDate, event)}
               />
             ))}
           </div>
diff --git a/src/widgets/calendar/event.jsx b/src/widgets/calendar/event.jsx
index 5d3699d7..7d348285 100644
--- a/src/widgets/calendar/event.jsx
+++ b/src/widgets/calendar/event.jsx
@@ -39,3 +39,7 @@ export default function Event({ event, colorVariants, showDate = false, showTime
     </div>
   );
 }
+
+export function compareDateTimezoneAware(date, event) {
+  return date.setZone(event.date.zoneName).startOf("day").valueOf() === event.date.startOf("day").valueOf();
+}
diff --git a/src/widgets/calendar/integrations/ical.jsx b/src/widgets/calendar/integrations/ical.jsx
index c14aa347..4c4ec9ca 100644
--- a/src/widgets/calendar/integrations/ical.jsx
+++ b/src/widgets/calendar/integrations/ical.jsx
@@ -23,8 +23,9 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
       }
     }
 
-    const startDate = DateTime.fromISO(params.start);
-    const endDate = DateTime.fromISO(params.end);
+    const zone = config?.timezone || null;
+    const startDate = DateTime.fromISO(params.start, { zone });
+    const endDate = DateTime.fromISO(params.end, { zone });
 
     if (icalError || !parsedIcal || !startDate.isValid || !endDate.isValid) {
       return;
@@ -43,12 +44,15 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
         const duration = event.dtend.value - event.dtstart.value;
         const days = duration / (1000 * 60 * 60 * 24);
 
+        const now = DateTime.now().setZone(zone);
+        const eventDate = DateTime.fromJSDate(date, { zone });
+
         for (let j = 0; j < days; j += 1) {
           eventsToAdd[`${event?.uid?.value}${i}${j}${type}`] = {
             title,
-            date: DateTime.fromJSDate(date).plus({ days: j }),
+            date: eventDate.plus({ days: j }),
             color: config?.color ?? "zinc",
-            isCompleted: DateTime.fromJSDate(date) < DateTime.now(),
+            isCompleted: eventDate < now,
             additional: event.location?.value,
             type: "ical",
           };
diff --git a/src/widgets/calendar/monthly.jsx b/src/widgets/calendar/monthly.jsx
index 62f869ed..ddb9cd87 100644
--- a/src/widgets/calendar/monthly.jsx
+++ b/src/widgets/calendar/monthly.jsx
@@ -3,7 +3,7 @@ import { DateTime, Info } from "luxon";
 import classNames from "classnames";
 import { useTranslation } from "next-i18next";
 
-import Event from "./event";
+import Event, { compareDateTimezoneAware } from "./event";
 
 const cellStyle = "relative w-10 flex items-center justify-center flex-col";
 const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
@@ -12,9 +12,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
   const currentDate = DateTime.now();
 
   const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
-  const filteredEvents = events?.filter(
-    (event) => event.date?.startOf("day").toUnixInteger() === cellDate.toUnixInteger(),
-  );
+  const filteredEvents = events?.filter((event) => compareDateTimezoneAware(cellDate, event));
 
   const dayStyles = (displayDate) => {
     let style = "h-9 ";
@@ -173,7 +171,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
 
         <div className="flex flex-col">
           {eventsArray
-            ?.filter((event) => showDate.startOf("day").ts === event.date?.startOf("day").ts)
+            ?.filter((event) => compareDateTimezoneAware(showDate, event))
             .slice(0, widget?.maxEvents ?? 10)
             .map((event) => (
               <Event
@@ -181,7 +179,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
                 event={event}
                 colorVariants={colorVariants}
                 showDateColumn={widget?.showTime ?? false}
-                showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
+                showTime={widget?.showTime && compareDateTimezoneAware(showDate, event)}
               />
             ))}
         </div>

From d61d0eb88ff04d60ef768de03bc05dac0d4d0731 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 15 Jan 2024 06:30:46 -0800
Subject: [PATCH 10/11] Fix configured service weight = 0 (#2628)

---
 src/utils/config/service-helpers.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index d73c1b5a..b0854b08 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -38,7 +38,7 @@ export async function servicesFromConfig() {
   // add default weight to services based on their position in the configuration
   servicesArray.forEach((group, groupIndex) => {
     group.services.forEach((service, serviceIndex) => {
-      if (!service.weight) {
+      if (service.weight === undefined) {
         servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
       }
     });

From 1f905bc241daaa0f3f5420b8a54a25a1977e1a2e Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 16 Jan 2024 16:07:20 -0800
Subject: [PATCH 11/11] Fix: constrain usage bar to 0-100 (#2650)

---
 src/components/widgets/resources/usage-bar.jsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/widgets/resources/usage-bar.jsx b/src/components/widgets/resources/usage-bar.jsx
index 8a22339a..82278e9d 100644
--- a/src/components/widgets/resources/usage-bar.jsx
+++ b/src/components/widgets/resources/usage-bar.jsx
@@ -1,10 +1,11 @@
 export default function UsageBar({ percent, additionalClassNames = "" }) {
+  const normalized = Math.min(100, Math.max(0, percent));
   return (
     <div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}>
       <div
         className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
         style={{
-          width: `${percent}%`,
+          width: `${normalized}%`,
         }}
       />
     </div>