Compare commits

..

9 Commits

16 changed files with 72 additions and 41 deletions

View File

@@ -98,6 +98,8 @@ When the Kubernetes cluster connection has been properly configured, this servic
If you are using multiple instances of homepage, an `instance` annotation can be specified to limit services to a specific instance. If no instance is provided, the service will be visible on all instances. If you are using multiple instances of homepage, an `instance` annotation can be specified to limit services to a specific instance. If no instance is provided, the service will be visible on all instances.
If you have a single service that needs to be shown on multiple specific instances of homepage (but not on all of them), the service can be annotated by multiple `instance.name` annotations, where `name` can be the names of your specific multiple homepage instances. For example, a service that is annotated with `gethomepage.dev/instance.public: ""` and `gethomepage.dev/instance.internal: ""` will be shown on `public` and `internal` homepage instances.
### Traefik IngressRoute support ### Traefik IngressRoute support
Homepage can also read ingresses defined using the Traefik IngressRoute custom resource definition. Due to the complex nature of Traefik routing rules, it is required for the `gethomepage.dev/href` annotation to be set: Homepage can also read ingresses defined using the Traefik IngressRoute custom resource definition. Due to the complex nature of Traefik routing rules, it is required for the `gethomepage.dev/href` annotation to be set:

View File

@@ -140,7 +140,7 @@
"connectionStatusPendingDisconnect": "Desconexión pendiente", "connectionStatusPendingDisconnect": "Desconexión pendiente",
"connectionStatusDisconnecting": "Desconectando", "connectionStatusDisconnecting": "Desconectando",
"connectionStatusDisconnected": "Desconectado", "connectionStatusDisconnected": "Desconectado",
"connectionStatusConnected": "Connected", "connectionStatusConnected": "Conectado",
"uptime": "Tiempo activo", "uptime": "Tiempo activo",
"maxDown": "Descarga máxima", "maxDown": "Descarga máxima",
"maxUp": "Subida máxima", "maxUp": "Subida máxima",
@@ -279,9 +279,9 @@
}, },
"netalertx": { "netalertx": {
"total": "Total", "total": "Total",
"connected": "Connected", "connected": "Conectado",
"new_devices": "New Devices", "new_devices": "Nuevos dispositivos",
"down_alerts": "Down Alerts" "down_alerts": "Alertas de caída"
}, },
"pihole": { "pihole": {
"queries": "Consultas", "queries": "Consultas",
@@ -544,7 +544,7 @@
"hdhomerun": { "hdhomerun": {
"channels": "Canales", "channels": "Canales",
"hd": "Alta definición", "hd": "Alta definición",
"tunerCount": "Tuners", "tunerCount": "Sintonizadores",
"channelNumber": "Canal", "channelNumber": "Canal",
"channelNetwork": "Red", "channelNetwork": "Red",
"signalStrength": "Intensidad", "signalStrength": "Intensidad",
@@ -827,7 +827,7 @@
}, },
"romm": { "romm": {
"platforms": "Plataformas", "platforms": "Plataformas",
"totalRoms": "Total ROMs" "totalRoms": "ROMs totales"
}, },
"netdata": { "netdata": {
"warnings": "Advertencias", "warnings": "Advertencias",
@@ -835,38 +835,38 @@
}, },
"plantit": { "plantit": {
"events": "Eventos", "events": "Eventos",
"plants": "Plants", "plants": "Plantas",
"photos": "Fotos", "photos": "Fotos",
"species": "Species" "species": "Especies"
}, },
"gitea": { "gitea": {
"notifications": "Notificaciones", "notifications": "Notificaciones",
"issues": "Números", "issues": "Números",
"pulls": "Pull Requests" "pulls": "Solicitudes de cambios"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Escenas",
"scenesPlayed": "Scenes Played", "scenesPlayed": "Escenas reproducidas",
"playCount": "Total Plays", "playCount": "Reproducciones totales",
"playDuration": "Time Watched", "playDuration": "Tiempo visto",
"sceneSize": "Scenes Size", "sceneSize": "Tamaño de las escenas",
"sceneDuration": "Scenes Duration", "sceneDuration": "Duración de las escenas",
"images": "Imágenes", "images": "Imágenes",
"imageSize": "Tamaño de imagen", "imageSize": "Tamaño de imagen",
"galleries": "Galerías", "galleries": "Galerías",
"performers": "Performers", "performers": "Intérpretes",
"studios": "Studios", "studios": "Estudios",
"movies": "Películas", "movies": "Películas",
"tags": "Etiquetas", "tags": "Etiquetas",
"oCount": "O Count" "oCount": "O cuenta"
}, },
"tandoor": { "tandoor": {
"users": "Usuarios", "users": "Usuarios",
"recipes": "Recetas", "recipes": "Recetas",
"keywords": "Keywords" "keywords": "Palabras clave"
}, },
"homebox": { "homebox": {
"items": "Items", "items": "Objetos",
"totalWithWarranty": "Con Garantía", "totalWithWarranty": "Con Garantía",
"locations": "Ubicaciones", "locations": "Ubicaciones",
"labels": "Etiquetas", "labels": "Etiquetas",
@@ -875,10 +875,10 @@
}, },
"crowdsec": { "crowdsec": {
"alerts": "Alertas", "alerts": "Alertas",
"bans": "Bans" "bans": "Baneos"
}, },
"wgeasy": { "wgeasy": {
"connected": "Connected", "connected": "Conectado",
"enabled": "Activado", "enabled": "Activado",
"disabled": "Desactivado", "disabled": "Desactivado",
"total": "Total" "total": "Total"

View File

@@ -884,9 +884,9 @@
"total": "Total" "total": "Total"
}, },
"swagdashboard": { "swagdashboard": {
"proxied": "Proxied", "proxied": "Par proxy",
"auth": "With Auth", "auth": "Avec authentification",
"outdated": "Outdated", "outdated": "Obsolète",
"banned": "Banned" "banned": "Banni"
} }
} }

View File

@@ -1,9 +1,11 @@
import cachedFetch from "utils/proxy/cached-fetch"; import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config"; import { getSettings } from "utils/config/config";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
export default async function handler(req, res) { export default async function handler(req, res) {
const { latitude, longitude, units, provider, cache, lang } = req.query; const { latitude, longitude, units, provider, cache, lang, index } = req.query;
let { apiKey } = req.query; const privateWidgetOptions = await getPrivateWidgetOptions("openweathermap", index);
let { apiKey } = privateWidgetOptions;
if (!apiKey && !provider) { if (!apiKey && !provider) {
return res.status(400).json({ error: "Missing API key or provider" }); return res.status(400).json({ error: "Missing API key or provider" });

View File

@@ -1,9 +1,11 @@
import cachedFetch from "utils/proxy/cached-fetch"; import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config"; import { getSettings } from "utils/config/config";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
export default async function handler(req, res) { export default async function handler(req, res) {
const { latitude, longitude, provider, cache, lang } = req.query; const { latitude, longitude, provider, cache, lang, index } = req.query;
let { apiKey } = req.query; const privateWidgetOptions = await getPrivateWidgetOptions("weatherapi", index);
let { apiKey } = privateWidgetOptions;
if (!apiKey && !provider) { if (!apiKey && !provider) {
return res.status(400).json({ error: "Missing API key or provider" }); return res.status(400).json({ error: "Missing API key or provider" });

View File

@@ -254,7 +254,8 @@ export async function servicesFromKubernetes() {
ingress.metadata.annotations && ingress.metadata.annotations &&
ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" &&
(!ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] || (!ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] ||
ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] === instanceName), ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] === instanceName ||
`${ANNOTATION_BASE}/instance.${instanceName}` in ingress.metadata.annotations),
) )
.map((ingress) => { .map((ingress) => {
let constructedService = { let constructedService = {

View File

@@ -32,7 +32,7 @@ export async function cleanWidgetGroups(widgets) {
const optionKeys = Object.keys(sanitizedOptions); const optionKeys = Object.keys(sanitizedOptions);
// delete private options from the sanitized options // delete private options from the sanitized options
["username", "password", "key"].forEach((pO) => { ["username", "password", "key", "apiKey"].forEach((pO) => {
if (optionKeys.includes(pO)) { if (optionKeys.includes(pO)) {
delete sanitizedOptions[pO]; delete sanitizedOptions[pO];
} }
@@ -57,7 +57,7 @@ export async function getPrivateWidgetOptions(type, widgetIndex) {
const widgets = await widgetsFromConfig(); const widgets = await widgetsFromConfig();
const privateOptions = widgets.map((widget) => { const privateOptions = widgets.map((widget) => {
const { index, url, username, password, key } = widget.options; const { index, url, username, password, key, apiKey } = widget.options;
return { return {
type: widget.type, type: widget.type,
@@ -67,6 +67,7 @@ export async function getPrivateWidgetOptions(type, widgetIndex) {
username, username,
password, password,
key, key,
apiKey,
}, },
}; };
}); });

View File

@@ -31,6 +31,10 @@ export async function sendJsonRpcRequest(url, method, params, username, password
if (status === 200) { if (status === 200) {
const json = JSON.parse(data.toString()); const json = JSON.parse(data.toString());
if (json.id === null) {
json.id = 1;
}
// in order to get access to the underlying error object in the JSON response // in order to get access to the underlying error object in the JSON response
// you must set `result` equal to undefined // you must set `result` equal to undefined
if (json.error && json.result === null) { if (json.error && json.result === null) {

View File

@@ -8,7 +8,7 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: resultData, error: resultError } = useWidgetAPI(widget, "result"); const { data: resultData, error: resultError } = useWidgetAPI(widget, "upstreams");
if (resultError) { if (resultError) {
return <Container service={service} error={resultError} />; return <Container service={service} error={resultError} />;

View File

@@ -1,8 +1,14 @@
import genericProxyHandler from "utils/proxy/handlers/generic"; import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = { const widget = {
api: "{url}/reverse_proxy/upstreams", api: "{url}/{endpoint}",
proxyHandler: genericProxyHandler, proxyHandler: genericProxyHandler,
mappings: {
upstreams: {
endpoint: "reverse_proxy/upstreams",
},
},
}; };
export default widget; export default widget;

View File

@@ -65,7 +65,7 @@ export default function Component({ service }) {
return ( return (
<Container service={service}> <Container service={service}>
<div className={classNames(service.description ? "-top-10" : "-top-8", "absolute right-1")}> <div className={classNames(service.description ? "-top-10" : "-top-8", "absolute right-1 z-20")}>
<Dropdown options={dateRangeOptions} value={dateRange} setValue={setDateRange} /> <Dropdown options={dateRangeOptions} value={dateRange} setValue={setDateRange} />
</div> </div>

View File

@@ -9,7 +9,7 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, { const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, "info", {
refreshInterval: 5000, refreshInterval: 5000,
}); });

View File

@@ -2,6 +2,12 @@ import omadaProxyHandler from "./proxy";
const widget = { const widget = {
proxyHandler: omadaProxyHandler, proxyHandler: omadaProxyHandler,
mappings: {
info: {
endpoint: "api/info",
},
},
}; };
export default widget; export default widget;

View File

@@ -77,7 +77,7 @@ async function fetchSystem(url) {
const systemResponse = JSON.parse(data.toString())[1]; const systemResponse = JSON.parse(data.toString())[1];
const response = { const response = {
uptime: systemResponse.uptime, uptime: systemResponse.uptime,
cpuLoad: systemResponse.load[1], cpuLoad: (systemResponse.load[1] / 65536.0).toFixed(2),
}; };
return [200, contentType, response]; return [200, contentType, response];
} }

View File

@@ -28,7 +28,7 @@ export default function Component({ service }) {
const enabled = infoData.filter((item) => item.enabled).length; const enabled = infoData.filter((item) => item.enabled).length;
const disabled = infoData.length - enabled; const disabled = infoData.length - enabled;
const connectionThreshold = widget.threshold ?? 2 * 60 * 1000; const connectionThreshold = (widget.threshold ?? 2) * 60 * 1000;
const currentTime = new Date(); const currentTime = new Date();
const connected = infoData.filter( const connected = infoData.filter(
(item) => currentTime - new Date(item.latestHandshakeAt) < connectionThreshold, (item) => currentTime - new Date(item.latestHandshakeAt) < connectionThreshold,

View File

@@ -21,14 +21,21 @@ async function login(widget, service) {
}); });
try { try {
const connectSidCookie = responseHeaders["set-cookie"] let connectSidCookie = responseHeaders["set-cookie"];
if (!connectSidCookie) {
const sid = cache.get(`${sessionSIDCacheKey}.${service}`);
if (sid) {
return sid;
}
}
connectSidCookie = connectSidCookie
.find((cookie) => cookie.startsWith("connect.sid=")) .find((cookie) => cookie.startsWith("connect.sid="))
.split(";")[0] .split(";")[0]
.replace("connect.sid=", ""); .replace("connect.sid=", "");
cache.put(`${sessionSIDCacheKey}.${service}`, connectSidCookie); cache.put(`${sessionSIDCacheKey}.${service}`, connectSidCookie);
return connectSidCookie; return connectSidCookie;
} catch (e) { } catch (e) {
logger.error(`Error logging into wg-easy`); logger.error(`Error logging into wg-easy, error: ${e}`);
cache.del(`${sessionSIDCacheKey}.${service}`); cache.del(`${sessionSIDCacheKey}.${service}`);
return null; return null;
} }