mirror of
https://github.com/karl0ss/homepage.git
synced 2025-05-03 05:53:40 +01:00
Merge remote-tracking branch 'origin/benphelpsMain' into LocalMain
This commit is contained in:
commit
b4094b316e
@ -164,10 +164,10 @@ labels:
|
|||||||
- homepage.description=Media server
|
- homepage.description=Media server
|
||||||
- homepage.widget.type=customapi
|
- homepage.widget.type=customapi
|
||||||
- homepage.widget.url=http://argus.service/api/v1/service/summary/emby
|
- homepage.widget.url=http://argus.service/api/v1/service/summary/emby
|
||||||
- homepage.widget.field[0].label=Deployed Version
|
- homepage.widget.mappings[0].label=Deployed Version
|
||||||
- homepage.widget.field[0].field.status=deployed_version
|
- homepage.widget.mappings[0].field.status=deployed_version
|
||||||
- homepage.widget.field[1].label=Latest Version
|
- homepage.widget.mappings[1].label=Latest Version
|
||||||
- homepage.widget.field[1].field.status=latest_version
|
- homepage.widget.mappings[1].field.status=latest_version
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Swarm
|
## Docker Swarm
|
||||||
|
@ -27,6 +27,7 @@ widget:
|
|||||||
url: https://domain.url/with/link/to.ics # URL with calendar events
|
url: https://domain.url/with/link/to.ics # URL with calendar events
|
||||||
name: My Events # required - name for these 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)
|
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
|
params: # optional - additional params for the service
|
||||||
showName: true # optional - show name before event title in event line - defaults to false
|
showName: true # optional - show name before event title in event line - defaults to false
|
||||||
```
|
```
|
||||||
|
@ -3,18 +3,21 @@ title: Health checks
|
|||||||
description: Health checks Widget Configuration
|
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_.
|
2. Click on API key (read-only) and then click _Create_.
|
||||||
3. Copy the API key that is generated for you.
|
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
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
type: healthchecks
|
type: healthchecks
|
||||||
url: http://healthchecks.host.or.ip:port
|
url: http://healthchecks.host.or.ip:port
|
||||||
key: <YOUR_API_KEY>
|
key: <YOUR_API_KEY>
|
||||||
uuid: <YOUR_CHECK_UUID>
|
uuid: <CHECK_UUID> # optional, if not included total statistics for all checks is shown
|
||||||
```
|
```
|
||||||
|
@ -491,9 +491,9 @@
|
|||||||
},
|
},
|
||||||
"healthchecks": {
|
"healthchecks": {
|
||||||
"new": "New",
|
"new": "New",
|
||||||
"up": "Online",
|
"up": "Up",
|
||||||
"grace": "In Grace Period",
|
"grace": "In Grace Period",
|
||||||
"down": "Offline",
|
"down": "Down",
|
||||||
"paused": "Paused",
|
"paused": "Paused",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"last_ping": "Last Ping",
|
"last_ping": "Last Ping",
|
||||||
|
@ -199,7 +199,7 @@ export default function QuickLaunch({
|
|||||||
{results.length > 0 && (
|
{results.length > 0 && (
|
||||||
<ul className="max-h-[60vh] overflow-y-auto m-2">
|
<ul className="max-h-[60vh] overflow-y-auto m-2">
|
||||||
{results.map((r, i) => (
|
{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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-index={i}
|
data-index={i}
|
||||||
|
@ -49,6 +49,7 @@ export default function Item({ service, group, useEqualHeights }) {
|
|||||||
target={service.target ?? settings.target ?? "_blank"}
|
target={service.target ?? settings.target ?? "_blank"}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex-shrink-0 flex items-center justify-center w-12 service-icon"
|
className="flex-shrink-0 flex items-center justify-center w-12 service-icon"
|
||||||
|
aria-label={service.icon}
|
||||||
>
|
>
|
||||||
<ResolvedIcon icon={service.icon} />
|
<ResolvedIcon icon={service.icon} />
|
||||||
</a>
|
</a>
|
||||||
|
@ -14,7 +14,7 @@ export default function List({ group, services, layout, useEqualHeights }) {
|
|||||||
>
|
>
|
||||||
{services.map((service) => (
|
{services.map((service) => (
|
||||||
<Item
|
<Item
|
||||||
key={service.container ?? service.app ?? service.name}
|
key={[service.container, service.app, service.name].filter((s) => s).join("-")}
|
||||||
service={service}
|
service={service}
|
||||||
group={group}
|
group={group}
|
||||||
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}
|
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
export default function UsageBar({ percent, additionalClassNames = "" }) {
|
export default function UsageBar({ percent, additionalClassNames = "" }) {
|
||||||
|
const normalized = Math.min(100, Math.max(0, percent));
|
||||||
return (
|
return (
|
||||||
<div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}>
|
<div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}>
|
||||||
<div
|
<div
|
||||||
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
|
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
|
||||||
style={{
|
style={{
|
||||||
width: `${percent}%`,
|
width: `${normalized}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,7 +38,7 @@ export async function servicesFromConfig() {
|
|||||||
// add default weight to services based on their position in the configuration
|
// add default weight to services based on their position in the configuration
|
||||||
servicesArray.forEach((group, groupIndex) => {
|
servicesArray.forEach((group, groupIndex) => {
|
||||||
group.services.forEach((service, serviceIndex) => {
|
group.services.forEach((service, serviceIndex) => {
|
||||||
if (!service.weight) {
|
if (service.weight === undefined) {
|
||||||
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
|
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -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;
|
return constructedService;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -387,6 +397,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
// glances, customapi, iframe
|
// glances, customapi, iframe
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
|
||||||
|
// healthchecks
|
||||||
|
uuid,
|
||||||
|
|
||||||
// iframe
|
// iframe
|
||||||
allowFullscreen,
|
allowFullscreen,
|
||||||
allowPolicy,
|
allowPolicy,
|
||||||
@ -526,6 +539,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (previousDays) cleanedService.widget.previousDays = previousDays;
|
if (previousDays) cleanedService.widget.previousDays = previousDays;
|
||||||
if (showTime) cleanedService.widget.showTime = showTime;
|
if (showTime) cleanedService.widget.showTime = showTime;
|
||||||
}
|
}
|
||||||
|
if (type === "healthchecks") {
|
||||||
|
if (uuid !== undefined) cleanedService.widget.uuid = uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
@ -2,7 +2,7 @@ import { DateTime } from "luxon";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import Event from "./event";
|
import Event, { compareDateTimezoneAware } from "./event";
|
||||||
|
|
||||||
export default function Agenda({ service, colorVariants, events, showDate }) {
|
export default function Agenda({ service, colorVariants, events, showDate }) {
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
@ -15,8 +15,10 @@ export default function Agenda({ service, colorVariants, events, showDate }) {
|
|||||||
const eventsArray = Object.keys(events)
|
const eventsArray = Object.keys(events)
|
||||||
.filter(
|
.filter(
|
||||||
(eventKey) =>
|
(eventKey) =>
|
||||||
showDate.minus({ days: widget?.previousDays ?? 0 }).startOf("day").ts <=
|
showDate
|
||||||
events[eventKey].date?.startOf("day").ts,
|
.setZone(events[eventKey].date.zoneName)
|
||||||
|
.minus({ days: widget?.previousDays ?? 0 })
|
||||||
|
.startOf("day").ts <= events[eventKey].date?.startOf("day").ts,
|
||||||
)
|
)
|
||||||
.map((eventKey) => events[eventKey])
|
.map((eventKey) => events[eventKey])
|
||||||
.sort((a, b) => a.date - b.date)
|
.sort((a, b) => a.date - b.date)
|
||||||
@ -56,7 +58,7 @@ export default function Agenda({ service, colorVariants, events, showDate }) {
|
|||||||
event={event}
|
event={event}
|
||||||
colorVariants={colorVariants}
|
colorVariants={colorVariants}
|
||||||
showDate={j === 0}
|
showDate={j === 0}
|
||||||
showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
|
showTime={widget?.showTime && compareDateTimezoneAware(showDate, event)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,3 +39,7 @@ export default function Event({ event, colorVariants, showDate = false, showTime
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function compareDateTimezoneAware(date, event) {
|
||||||
|
return date.setZone(event.date.zoneName).startOf("day").valueOf() === event.date.startOf("day").valueOf();
|
||||||
|
}
|
||||||
|
@ -23,8 +23,9 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = DateTime.fromISO(params.start);
|
const zone = config?.timezone || null;
|
||||||
const endDate = DateTime.fromISO(params.end);
|
const startDate = DateTime.fromISO(params.start, { zone });
|
||||||
|
const endDate = DateTime.fromISO(params.end, { zone });
|
||||||
|
|
||||||
if (icalError || !parsedIcal || !startDate.isValid || !endDate.isValid) {
|
if (icalError || !parsedIcal || !startDate.isValid || !endDate.isValid) {
|
||||||
return;
|
return;
|
||||||
@ -43,20 +44,24 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
|
|||||||
const duration = event.dtend.value - event.dtstart.value;
|
const duration = event.dtend.value - event.dtstart.value;
|
||||||
const days = duration / (1000 * 60 * 60 * 24);
|
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) {
|
for (let j = 0; j < days; j += 1) {
|
||||||
eventsToAdd[`${event?.uid?.value}${i}${j}${type}`] = {
|
eventsToAdd[`${event?.uid?.value}${i}${j}${type}`] = {
|
||||||
title,
|
title,
|
||||||
date: DateTime.fromJSDate(date).plus({ days: j }),
|
date: eventDate.plus({ days: j }),
|
||||||
color: config?.color ?? "zinc",
|
color: config?.color ?? "zinc",
|
||||||
isCompleted: DateTime.fromJSDate(date) < DateTime.now(),
|
isCompleted: eventDate < now,
|
||||||
additional: event.location?.value,
|
additional: event.location?.value,
|
||||||
type: "ical",
|
type: "ical",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event?.recurrenceRule?.options) {
|
const recurrenceOptions = event?.recurrenceRule?.origOptions;
|
||||||
const rule = new RRule(event.recurrenceRule.options);
|
if (recurrenceOptions && Object.keys(recurrenceOptions).length !== 0) {
|
||||||
|
const rule = new RRule(recurrenceOptions);
|
||||||
const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
|
const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
|
||||||
|
|
||||||
recurringEvents.forEach((date, i) => eventToAdd(date, i, "recurring"));
|
recurringEvents.forEach((date, i) => eventToAdd(date, i, "recurring"));
|
||||||
|
@ -3,7 +3,7 @@ import { DateTime, Info } from "luxon";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTranslation } from "next-i18next";
|
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 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";
|
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 currentDate = DateTime.now();
|
||||||
|
|
||||||
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
|
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
|
||||||
const filteredEvents = events?.filter(
|
const filteredEvents = events?.filter((event) => compareDateTimezoneAware(cellDate, event));
|
||||||
(event) => event.date?.startOf("day").toUnixInteger() === cellDate.toUnixInteger(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const dayStyles = (displayDate) => {
|
const dayStyles = (displayDate) => {
|
||||||
let style = "h-9 ";
|
let style = "h-9 ";
|
||||||
@ -111,6 +109,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
|
|||||||
}
|
}
|
||||||
|
|
||||||
const eventsArray = Object.keys(events).map((eventKey) => events[eventKey]);
|
const eventsArray = Object.keys(events).map((eventKey) => events[eventKey]);
|
||||||
|
eventsArray.sort((a, b) => a.date - b.date);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-center">
|
<div className="w-full text-center">
|
||||||
@ -172,7 +171,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
|
|||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{eventsArray
|
{eventsArray
|
||||||
?.filter((event) => showDate.startOf("day").ts === event.date?.startOf("day").ts)
|
?.filter((event) => compareDateTimezoneAware(showDate, event))
|
||||||
.slice(0, widget?.maxEvents ?? 10)
|
.slice(0, widget?.maxEvents ?? 10)
|
||||||
.map((event) => (
|
.map((event) => (
|
||||||
<Event
|
<Event
|
||||||
@ -180,7 +179,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
|
|||||||
event={event}
|
event={event}
|
||||||
colorVariants={colorVariants}
|
colorVariants={colorVariants}
|
||||||
showDateColumn={widget?.showTime ?? false}
|
showDateColumn={widget?.showTime ?? false}
|
||||||
showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
|
showTime={widget?.showTime && compareDateTimezoneAware(showDate, event)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,6 +27,23 @@ function formatDate(dateString) {
|
|||||||
return new Intl.DateTimeFormat(i18n.language, dateOptions).format(date);
|
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 }) {
|
export default function Component({ service }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
@ -46,13 +63,26 @@ export default function Component({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasUuid = widget?.uuid;
|
||||||
|
|
||||||
|
const { upCount, downCount } = countStatus(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
|
{hasUuid ? (
|
||||||
<Block
|
<>
|
||||||
label="healthchecks.last_ping"
|
<Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
|
||||||
value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
|
<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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/api/v2/{endpoint}/{uuid}",
|
api: "{url}/api/v3/{endpoint}/{uuid}",
|
||||||
proxyHandler: credentialedProxyHandler,
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
|
||||||
mappings: {
|
mappings: {
|
||||||
checks: {
|
checks: {
|
||||||
endpoint: "checks",
|
endpoint: "checks",
|
||||||
validate: ["status", "last_ping"],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -64,7 +64,7 @@ async function tryLogin(widget) {
|
|||||||
const resp = await rpc(url, {
|
const resp = await rpc(url, {
|
||||||
method: "login",
|
method: "login",
|
||||||
service: "session",
|
service: "session",
|
||||||
params: { username, password },
|
params: { username: username.toString(), password: password.toString() },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user