Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c61af74d | ||
|
|
e6f2ff970b | ||
|
|
af1695687a | ||
|
|
f0d7cf3ce6 | ||
|
|
36e77e1fe3 | ||
|
|
35dd7ec230 | ||
|
|
15cc1d98c5 | ||
|
|
9803ef70c6 | ||
|
|
ccc27142ef | ||
|
|
9a20982406 | ||
|
|
67a69a5878 |
No files matched your search
@@ -66,6 +66,10 @@ Homepage is highly customizable, with support for custom themes, custom CSS & JS
|
||||
|
||||
For configuration options, examples and more, [please check out the homepage documentation](http://gethomepage.dev).
|
||||
|
||||
## Security Notice 🔒
|
||||
|
||||
Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system) and Homepage currently does not (and is not planned to) include any authentication layer itself. Thus, we recommend homepage be deployed behind a reverse proxy including authentication, SSL etc, and / or behind a VPN.
|
||||
|
||||
## With Docker
|
||||
|
||||
Using docker compose:
|
||||
|
||||
@@ -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 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -402,7 +402,7 @@ quicklaunch:
|
||||
suggestionUrl: https://ac.ecosia.org/autocomplete?type=list&q=
|
||||
```
|
||||
|
||||
## Homepage Version
|
||||
## Homepage Version & Update Checking
|
||||
|
||||
By default the release version is displayed at the bottom of the page. To hide this, use the `hideVersion` setting, like so:
|
||||
|
||||
@@ -410,6 +410,12 @@ By default the release version is displayed at the bottom of the page. To hide t
|
||||
hideVersion: true
|
||||
```
|
||||
|
||||
You can disable checking for new versions from GitHub (enabled by default) with:
|
||||
|
||||
```yaml
|
||||
disableUpdateCheck: true
|
||||
```
|
||||
|
||||
## Log Path
|
||||
|
||||
By default the homepage logfile is written to the a `logs` subdirectory of the `config` folder. In order to customize this path, you can set the `logpath` setting. A `logs` folder will be created in that location where the logfile will be written.
|
||||
|
||||
@@ -7,6 +7,10 @@ description: Docs intro
|
||||
You have a few options for deploying homepage, depending on your needs. We offer docker images for a majority of platforms. You can also install and run homepage from source if Docker is not your thing. It can even be installed on Kubernetes with Helm.
|
||||
</p>
|
||||
|
||||
!!! danger
|
||||
|
||||
Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system) and Homepage currently does not (and is not planned to) include any authentication layer itself. Thus, we recommend homepage be deployed behind a reverse proxy including authentication, SSL etc, and / or behind a VPN.
|
||||
|
||||
<br>
|
||||
|
||||
<div class="grid cards" style="margin: 0 auto;" markdown>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
"connectionStatusPendingDisconnect": "Desconexión pendiente",
|
||||
"connectionStatusDisconnecting": "Desconectando",
|
||||
"connectionStatusDisconnected": "Desconectado",
|
||||
"connectionStatusConnected": "Connected",
|
||||
"connectionStatusConnected": "Conectado",
|
||||
"uptime": "Tiempo activo",
|
||||
"maxDown": "Descarga máxima",
|
||||
"maxUp": "Subida máxima",
|
||||
@@ -279,9 +279,9 @@
|
||||
},
|
||||
"netalertx": {
|
||||
"total": "Total",
|
||||
"connected": "Connected",
|
||||
"new_devices": "New Devices",
|
||||
"down_alerts": "Down Alerts"
|
||||
"connected": "Conectado",
|
||||
"new_devices": "Nuevos dispositivos",
|
||||
"down_alerts": "Alertas de caída"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "Consultas",
|
||||
@@ -544,7 +544,7 @@
|
||||
"hdhomerun": {
|
||||
"channels": "Canales",
|
||||
"hd": "Alta definición",
|
||||
"tunerCount": "Tuners",
|
||||
"tunerCount": "Sintonizadores",
|
||||
"channelNumber": "Canal",
|
||||
"channelNetwork": "Red",
|
||||
"signalStrength": "Intensidad",
|
||||
@@ -827,7 +827,7 @@
|
||||
},
|
||||
"romm": {
|
||||
"platforms": "Plataformas",
|
||||
"totalRoms": "Total ROMs"
|
||||
"totalRoms": "ROMs totales"
|
||||
},
|
||||
"netdata": {
|
||||
"warnings": "Advertencias",
|
||||
@@ -835,38 +835,38 @@
|
||||
},
|
||||
"plantit": {
|
||||
"events": "Eventos",
|
||||
"plants": "Plants",
|
||||
"plants": "Plantas",
|
||||
"photos": "Fotos",
|
||||
"species": "Species"
|
||||
"species": "Especies"
|
||||
},
|
||||
"gitea": {
|
||||
"notifications": "Notificaciones",
|
||||
"issues": "Números",
|
||||
"pulls": "Pull Requests"
|
||||
"pulls": "Solicitudes de cambios"
|
||||
},
|
||||
"stash": {
|
||||
"scenes": "Scenes",
|
||||
"scenesPlayed": "Scenes Played",
|
||||
"playCount": "Total Plays",
|
||||
"playDuration": "Time Watched",
|
||||
"sceneSize": "Scenes Size",
|
||||
"sceneDuration": "Scenes Duration",
|
||||
"scenes": "Escenas",
|
||||
"scenesPlayed": "Escenas reproducidas",
|
||||
"playCount": "Reproducciones totales",
|
||||
"playDuration": "Tiempo visto",
|
||||
"sceneSize": "Tamaño de las escenas",
|
||||
"sceneDuration": "Duración de las escenas",
|
||||
"images": "Imágenes",
|
||||
"imageSize": "Tamaño de imagen",
|
||||
"galleries": "Galerías",
|
||||
"performers": "Performers",
|
||||
"studios": "Studios",
|
||||
"performers": "Intérpretes",
|
||||
"studios": "Estudios",
|
||||
"movies": "Películas",
|
||||
"tags": "Etiquetas",
|
||||
"oCount": "O Count"
|
||||
"oCount": "O cuenta"
|
||||
},
|
||||
"tandoor": {
|
||||
"users": "Usuarios",
|
||||
"recipes": "Recetas",
|
||||
"keywords": "Keywords"
|
||||
"keywords": "Palabras clave"
|
||||
},
|
||||
"homebox": {
|
||||
"items": "Items",
|
||||
"items": "Objetos",
|
||||
"totalWithWarranty": "Con Garantía",
|
||||
"locations": "Ubicaciones",
|
||||
"labels": "Etiquetas",
|
||||
@@ -875,10 +875,10 @@
|
||||
},
|
||||
"crowdsec": {
|
||||
"alerts": "Alertas",
|
||||
"bans": "Bans"
|
||||
"bans": "Baneos"
|
||||
},
|
||||
"wgeasy": {
|
||||
"connected": "Connected",
|
||||
"connected": "Conectado",
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"total": "Total"
|
||||
|
||||
@@ -884,9 +884,9 @@
|
||||
"total": "Total"
|
||||
},
|
||||
"swagdashboard": {
|
||||
"proxied": "Proxied",
|
||||
"auth": "With Auth",
|
||||
"outdated": "Outdated",
|
||||
"banned": "Banned"
|
||||
"proxied": "Par proxy",
|
||||
"auth": "Avec authentification",
|
||||
"outdated": "Obsolète",
|
||||
"banned": "Banni"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import useSWR from "swr";
|
||||
import { compareVersions } from "compare-versions";
|
||||
import { MdNewReleases } from "react-icons/md";
|
||||
|
||||
export default function Version() {
|
||||
export default function Version({ disableUpdateCheck = false }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const buildTime = process.env.NEXT_PUBLIC_BUILDTIME?.length
|
||||
@@ -12,7 +12,7 @@ export default function Version() {
|
||||
const revision = process.env.NEXT_PUBLIC_REVISION?.length ? process.env.NEXT_PUBLIC_REVISION : "dev";
|
||||
const version = process.env.NEXT_PUBLIC_VERSION?.length ? process.env.NEXT_PUBLIC_VERSION : "dev";
|
||||
|
||||
const { data: releaseData } = useSWR("/api/releases");
|
||||
const { data: releaseData } = useSWR(disableUpdateCheck ? null : "/api/releases");
|
||||
|
||||
// use Intl.DateTimeFormat to format the date
|
||||
const formatDate = (date) => {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import cachedFetch from "utils/proxy/cached-fetch";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("releases");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const releasesURL = "https://api.github.com/repos/gethomepage/homepage/releases";
|
||||
return res.send(await cachedFetch(releasesURL, 5));
|
||||
try {
|
||||
return res.send(await cachedFetch(releasesURL, 5));
|
||||
} catch (e) {
|
||||
logger.error(`Error checking GitHub releases: ${e}`);
|
||||
return res.send([]);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import cachedFetch from "utils/proxy/cached-fetch";
|
||||
import { getSettings } from "utils/config/config";
|
||||
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { latitude, longitude, units, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
const { latitude, longitude, units, provider, cache, lang, index } = req.query;
|
||||
const privateWidgetOptions = await getPrivateWidgetOptions("openweathermap", index);
|
||||
let { apiKey } = privateWidgetOptions;
|
||||
|
||||
if (!apiKey && !provider) {
|
||||
return res.status(400).json({ error: "Missing API key or provider" });
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import cachedFetch from "utils/proxy/cached-fetch";
|
||||
import { getSettings } from "utils/config/config";
|
||||
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { latitude, longitude, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
const { latitude, longitude, provider, cache, lang, index } = req.query;
|
||||
const privateWidgetOptions = await getPrivateWidgetOptions("weatherapi", index);
|
||||
let { apiKey } = privateWidgetOptions;
|
||||
|
||||
if (!apiKey && !provider) {
|
||||
return res.status(400).json({ error: "Missing API key or provider" });
|
||||
|
||||
+1
-1
@@ -445,7 +445,7 @@ function Home({ initialSettings }) {
|
||||
</div>
|
||||
|
||||
<div id="version" className="flex mt-4 w-full justify-end">
|
||||
{!settings.hideVersion && <Version />}
|
||||
{!settings.hideVersion && <Version disableUpdateCheck={settings.disableUpdateCheck} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -254,7 +254,8 @@ export async function servicesFromKubernetes() {
|
||||
ingress.metadata.annotations &&
|
||||
ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" &&
|
||||
(!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) => {
|
||||
let constructedService = {
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function cleanWidgetGroups(widgets) {
|
||||
const optionKeys = Object.keys(sanitizedOptions);
|
||||
|
||||
// delete private options from the sanitized options
|
||||
["username", "password", "key"].forEach((pO) => {
|
||||
["username", "password", "key", "apiKey"].forEach((pO) => {
|
||||
if (optionKeys.includes(pO)) {
|
||||
delete sanitizedOptions[pO];
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export async function getPrivateWidgetOptions(type, widgetIndex) {
|
||||
const widgets = await widgetsFromConfig();
|
||||
|
||||
const privateOptions = widgets.map((widget) => {
|
||||
const { index, url, username, password, key } = widget.options;
|
||||
const { index, url, username, password, key, apiKey } = widget.options;
|
||||
|
||||
return {
|
||||
type: widget.type,
|
||||
@@ -67,6 +67,7 @@ export async function getPrivateWidgetOptions(type, widgetIndex) {
|
||||
username,
|
||||
password,
|
||||
key,
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,6 +31,10 @@ export async function sendJsonRpcRequest(url, method, params, username, password
|
||||
if (status === 200) {
|
||||
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
|
||||
// you must set `result` equal to undefined
|
||||
if (json.error && json.result === null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
const { data: resultData, error: resultError } = useWidgetAPI(widget, "result");
|
||||
const { data: resultData, error: resultError } = useWidgetAPI(widget, "upstreams");
|
||||
|
||||
if (resultError) {
|
||||
return <Container service={service} error={resultError} />;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/reverse_proxy/upstreams",
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
upstreams: {
|
||||
endpoint: "reverse_proxy/upstreams",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -65,7 +65,7 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Component({ service }) {
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, {
|
||||
const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, "info", {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ import omadaProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
proxyHandler: omadaProxyHandler,
|
||||
|
||||
mappings: {
|
||||
info: {
|
||||
endpoint: "api/info",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -77,7 +77,7 @@ async function fetchSystem(url) {
|
||||
const systemResponse = JSON.parse(data.toString())[1];
|
||||
const response = {
|
||||
uptime: systemResponse.uptime,
|
||||
cpuLoad: systemResponse.load[1],
|
||||
cpuLoad: (systemResponse.load[1] / 65536.0).toFixed(2),
|
||||
};
|
||||
return [200, contentType, response];
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Component({ service }) {
|
||||
|
||||
const enabled = infoData.filter((item) => item.enabled).length;
|
||||
const disabled = infoData.length - enabled;
|
||||
const connectionThreshold = widget.threshold ?? 2 * 60 * 1000;
|
||||
const connectionThreshold = (widget.threshold ?? 2) * 60 * 1000;
|
||||
const currentTime = new Date();
|
||||
const connected = infoData.filter(
|
||||
(item) => currentTime - new Date(item.latestHandshakeAt) < connectionThreshold,
|
||||
|
||||
@@ -21,14 +21,21 @@ async function login(widget, service) {
|
||||
});
|
||||
|
||||
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="))
|
||||
.split(";")[0]
|
||||
.replace("connect.sid=", "");
|
||||
cache.put(`${sessionSIDCacheKey}.${service}`, connectSidCookie);
|
||||
return connectSidCookie;
|
||||
} catch (e) {
|
||||
logger.error(`Error logging into wg-easy`);
|
||||
logger.error(`Error logging into wg-easy, error: ${e}`);
|
||||
cache.del(`${sessionSIDCacheKey}.${service}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user