mirror of
https://github.com/gethomepage/homepage.git
synced 2026-01-07 16:02:10 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b1304e22 | ||
|
|
ee729a7e6a | ||
|
|
bc7937db71 | ||
|
|
0e1aeaf54c | ||
|
|
2e8717247d | ||
|
|
d17a17bd3c | ||
|
|
0afc1b96f1 | ||
|
|
5fbc6702bc | ||
|
|
75455a23e2 | ||
|
|
2aed46671f | ||
|
|
88934ec39a | ||
|
|
21c0c687cd | ||
|
|
6b90d3ef28 |
14
README.md
14
README.md
@@ -45,15 +45,17 @@
|
|||||||
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
- Container status (Running / Stopped) & statistics (CPU, Memory, Network)
|
||||||
- Automatic service discovery (via labels)
|
- Automatic service discovery (via labels)
|
||||||
- Service Integration
|
- Service Integration
|
||||||
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli, Plex and more
|
||||||
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
|
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent and more
|
||||||
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentik, Proxmox
|
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentik, Proxmox and more
|
||||||
- Information Providers
|
- Information Providers
|
||||||
- Coin Market Cap, Mastodon
|
- Coin Market Cap, Mastodon and more
|
||||||
- Information & Utility Widgets
|
- Information & Utility Widgets
|
||||||
- System Stats (Disk, CPU, Memory)
|
- System Stats (Disk, CPU, Memory)
|
||||||
- Weather via [OpenWeatherMap](https://openweathermap.org/) or [Open-Meteo](https://open-meteo.com/)
|
- Weather via [OpenWeatherMap](https://openweathermap.org/) or [Open-Meteo](https://open-meteo.com/)
|
||||||
- Search Bar
|
- Web Search Bar
|
||||||
|
- UniFi Console, Glances and more
|
||||||
|
- Instant "Quick-launch" search
|
||||||
- Customizable
|
- Customizable
|
||||||
- 21 theme colors with light and dark mode support
|
- 21 theme colors with light and dark mode support
|
||||||
- Background image support
|
- Background image support
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
|
|
||||||
If you have any questions, suggestions, or general issues, please start a discussion on the [Discussions](https://github.com/benphelps/homepage/discussions) page.
|
If you have any questions, suggestions, or general issues, please start a discussion on the [Discussions](https://github.com/benphelps/homepage/discussions) page.
|
||||||
|
|
||||||
If you have a more specific issue, please open an issue on the [Issues](https://github.com/benphelps/homepage/issues) page.
|
For bug reports, please open an issue on the [Issues](https://github.com/benphelps/homepage/issues) page.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|||||||
@@ -394,14 +394,14 @@
|
|||||||
"numberOfLeases": "Alquileres"
|
"numberOfLeases": "Alquileres"
|
||||||
},
|
},
|
||||||
"xteve": {
|
"xteve": {
|
||||||
"streams_all": "All Streams",
|
"streams_all": "Todas las corrientes",
|
||||||
"streams_active": "Active Streams",
|
"streams_active": "Corrientes activas",
|
||||||
"streams_xepg": "XEPG Channels"
|
"streams_xepg": "Canales XEPG"
|
||||||
},
|
},
|
||||||
"opnsense": {
|
"opnsense": {
|
||||||
"cpu": "CPU Load",
|
"cpu": "Carga de la CPU",
|
||||||
"memory": "Active Memory",
|
"memory": "Memoria activa",
|
||||||
"wanUpload": "WAN Upload",
|
"wanUpload": "Carga WAN",
|
||||||
"wanDownload": "WAN Download"
|
"wanDownload": "Descargar WAN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,9 +399,9 @@
|
|||||||
"streams_xepg": "Canal XEPG"
|
"streams_xepg": "Canal XEPG"
|
||||||
},
|
},
|
||||||
"opnsense": {
|
"opnsense": {
|
||||||
"cpu": "CPU Load",
|
"cpu": "Charge CPU",
|
||||||
"memory": "Active Memory",
|
"memory": "Mém. Utilisée",
|
||||||
"wanUpload": "WAN Upload",
|
"wanUpload": "WAN Envoi",
|
||||||
"wanDownload": "WAN Download"
|
"wanDownload": "WAN Récep."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import useSWR from "swr";
|
|||||||
import { compareVersions } from "compare-versions";
|
import { compareVersions } from "compare-versions";
|
||||||
import { MdNewReleases } from "react-icons/md";
|
import { MdNewReleases } from "react-icons/md";
|
||||||
|
|
||||||
import cachedFetch from "utils/proxy/cached-fetch";
|
|
||||||
|
|
||||||
export default function Version() {
|
export default function Version() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
@@ -12,9 +10,7 @@ export default function Version() {
|
|||||||
const revision = process.env.NEXT_PUBLIC_REVISION?.length ? process.env.NEXT_PUBLIC_REVISION : "dev";
|
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 version = process.env.NEXT_PUBLIC_VERSION?.length ? process.env.NEXT_PUBLIC_VERSION : "dev";
|
||||||
|
|
||||||
const cachedFetcher = (resource) => cachedFetch(resource, 5);
|
const { data: releaseData } = useSWR("/api/releases");
|
||||||
|
|
||||||
const { data: releaseData } = useSWR("https://api.github.com/repos/benphelps/homepage/releases", cachedFetcher);
|
|
||||||
|
|
||||||
// use Intl.DateTimeFormat to format the date
|
// use Intl.DateTimeFormat to format the date
|
||||||
const formatDate = (date) => {
|
const formatDate = (date) => {
|
||||||
@@ -48,7 +44,7 @@ export default function Version() {
|
|||||||
</span>
|
</span>
|
||||||
{version === "main" || version === "dev" || version === "nightly"
|
{version === "main" || version === "dev" || version === "nightly"
|
||||||
? null
|
? null
|
||||||
: releaseData &&
|
: releaseData && latestRelease &&
|
||||||
compareVersions(latestRelease.tag_name, version) > 0 && (
|
compareVersions(latestRelease.tag_name, version) > 0 && (
|
||||||
<a
|
<a
|
||||||
href={latestRelease.html_url}
|
href={latestRelease.html_url}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function Cpu({ expanded }) {
|
|||||||
<div className="pr-1">{t("resources.load")}</div>
|
<div className="pr-1">{t("resources.load")}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<UsageBar percent={100} />
|
<UsageBar percent={0} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function Disk({ options, expanded }) {
|
|||||||
<div className="pr-1">{t("resources.total")}</div>
|
<div className="pr-1">{t("resources.total")}</div>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<UsageBar percent={100} />
|
<UsageBar percent={0} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function Memory({ expanded }) {
|
|||||||
<div className="pr-1">{t("resources.total")}</div>
|
<div className="pr-1">{t("resources.total")}</div>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<UsageBar percent={100} />
|
<UsageBar percent={0} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
6
src/pages/api/releases.js
Normal file
6
src/pages/api/releases.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import cachedFetch from "utils/proxy/cached-fetch";
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const releasesURL = "https://api.github.com/repos/benphelps/homepage/releases";
|
||||||
|
return res.send(await cachedFetch(releasesURL, 5));
|
||||||
|
}
|
||||||
@@ -50,9 +50,12 @@ export async function servicesResponse() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||||
|
if (discoveredServices?.length === 0) {
|
||||||
|
console.debug("No containers were found with homepage labels.");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||||
if (e) console.error(e);
|
if (e) console.error(e.toString());
|
||||||
discoveredServices = [];
|
discoveredServices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +63,7 @@ export async function servicesResponse() {
|
|||||||
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
configuredServices = cleanServiceGroups(await servicesFromConfig());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load services.yaml, please check for errors");
|
console.error("Failed to load services.yaml, please check for errors");
|
||||||
if (e) console.error(e);
|
if (e) console.error(e.toString());
|
||||||
configuredServices = [];
|
configuredServices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ export async function servicesResponse() {
|
|||||||
initialSettings = await getSettings();
|
initialSettings = await getSettings();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load settings.yaml, please check for errors");
|
console.error("Failed to load settings.yaml, please check for errors");
|
||||||
if (e) console.error(e);
|
if (e) console.error(e.toString());
|
||||||
initialSettings = {};
|
initialSettings = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,36 +44,41 @@ export async function servicesFromDocker() {
|
|||||||
|
|
||||||
const serviceServers = await Promise.all(
|
const serviceServers = await Promise.all(
|
||||||
Object.keys(servers).map(async (serverName) => {
|
Object.keys(servers).map(async (serverName) => {
|
||||||
const docker = new Docker(getDockerArguments(serverName).conn);
|
try {
|
||||||
const containers = await docker.listContainers({
|
const docker = new Docker(getDockerArguments(serverName).conn);
|
||||||
all: true,
|
const containers = await docker.listContainers({
|
||||||
});
|
all: true,
|
||||||
|
|
||||||
// bad docker connections can result in a <Buffer ...> object?
|
|
||||||
// in any case, this ensures the result is the expected array
|
|
||||||
if (!Array.isArray(containers)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const discovered = containers.map((container) => {
|
|
||||||
let constructedService = null;
|
|
||||||
|
|
||||||
Object.keys(container.Labels).forEach((label) => {
|
|
||||||
if (label.startsWith("homepage.")) {
|
|
||||||
if (!constructedService) {
|
|
||||||
constructedService = {
|
|
||||||
container: container.Names[0].replace(/^\//, ""),
|
|
||||||
server: serverName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return constructedService;
|
// bad docker connections can result in a <Buffer ...> object?
|
||||||
});
|
// in any case, this ensures the result is the expected array
|
||||||
|
if (!Array.isArray(containers)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
|
const discovered = containers.map((container) => {
|
||||||
|
let constructedService = null;
|
||||||
|
|
||||||
|
Object.keys(container.Labels).forEach((label) => {
|
||||||
|
if (label.startsWith("homepage.")) {
|
||||||
|
if (!constructedService) {
|
||||||
|
constructedService = {
|
||||||
|
container: container.Names[0].replace(/^\//, ""),
|
||||||
|
server: serverName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return constructedService;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
|
||||||
|
} catch (e) {
|
||||||
|
// a server failed, but others may succeed
|
||||||
|
return { server: serverName, services: [] };
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
|
|
||||||
if (widget) {
|
if (widget) {
|
||||||
|
|
||||||
const {url} = widget;
|
const { url } = widget;
|
||||||
|
|
||||||
const controllerInfoURL = `${widget.url}/api/info`;
|
const controllerInfoURL = `${url}/api/info`;
|
||||||
|
|
||||||
let [status, contentType, data] = await httpProxy(controllerInfoURL, {
|
let [status, contentType, data] = await httpProxy(controllerInfoURL, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -77,13 +77,13 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
|
|
||||||
switch (controllerVersionMajor) {
|
switch (controllerVersionMajor) {
|
||||||
case 3:
|
case 3:
|
||||||
loginUrl = `${widget.url}/api/user/login?ajax`;
|
loginUrl = `${url}/api/user/login?ajax`;
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
loginUrl = `${widget.url}/api/v2/login`;
|
loginUrl = `${url}/api/v2/login`;
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
loginUrl = `${widget.url}/${cId}/api/v2/login`;
|
loginUrl = `${url}/${cId}/api/v2/login`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -105,7 +105,7 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
|
|
||||||
switch (controllerVersionMajor) {
|
switch (controllerVersionMajor) {
|
||||||
case 3:
|
case 3:
|
||||||
sitesUrl = `${widget.url}/web/v1/controller?ajax=&token=${token}`;
|
sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
|
||||||
body = {
|
body = {
|
||||||
"method": "getUserSites",
|
"method": "getUserSites",
|
||||||
"params": {
|
"params": {
|
||||||
@@ -115,10 +115,10 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
method = "POST";
|
method = "POST";
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
sitesUrl = `${widget.url}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
sitesUrl = `${url}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
sitesUrl = `${widget.url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -133,7 +133,7 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
|
|
||||||
const sitesResponseData = JSON.parse(data);
|
const sitesResponseData = JSON.parse(data);
|
||||||
|
|
||||||
if (sitesResponseData.errorCode > 0) {
|
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||||
logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||||
return res.status(status).json({error: {message: "Error getting sites list", url, data: sitesResponseData}});
|
return res.status(status).json({error: {message: "Error getting sites list", url, data: sitesResponseData}});
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
sitesResponseData.result.data.find(s => s.name === widget.site);
|
sitesResponseData.result.data.find(s => s.name === widget.site);
|
||||||
|
|
||||||
if (!site) {
|
if (!site) {
|
||||||
return res.status(status).json({error: {message: `Site ${widget.site} is not found`, url, data}});
|
return res.status(status).json({error: {message: `Site ${widget.site} is not found`, url: sitesUrl, data}});
|
||||||
}
|
}
|
||||||
|
|
||||||
let siteResponseData;
|
let siteResponseData;
|
||||||
@@ -156,7 +156,7 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
|
|
||||||
if (controllerVersionMajor === 3) {
|
if (controllerVersionMajor === 3) {
|
||||||
// Omada v3 controller requires switching site
|
// Omada v3 controller requires switching site
|
||||||
const switchUrl = `${widget.url}/web/v1/controller?ajax=&token=${token}`;
|
const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
|
||||||
method = "POST";
|
method = "POST";
|
||||||
body = {
|
body = {
|
||||||
method: "switchSite",
|
method: "switchSite",
|
||||||
@@ -181,7 +181,7 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
return res.status(status).json({error: {message: "Error switching site", url: switchUrl, data}});
|
return res.status(status).json({error: {message: "Error switching site", url: switchUrl, data}});
|
||||||
}
|
}
|
||||||
|
|
||||||
const statsUrl = `${widget.url}/web/v1/controller?getGlobalStat=&token=${token}`;
|
const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
|
||||||
[status, contentType, data] = await httpProxy(statsUrl, {
|
[status, contentType, data] = await httpProxy(statsUrl, {
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ export default async function pyloadProxyHandler(req, res) {
|
|||||||
|
|
||||||
if (data?.error || status !== 200) {
|
if (data?.error || status !== 200) {
|
||||||
try {
|
try {
|
||||||
return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(data).toString()}});
|
return res.status(status).send({error: {message: "HTTP error communicating with Pyload API", data: Buffer.from(data).toString()}});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data}});
|
return res.status(status).send({error: {message: "HTTP error communicating with Pyload API", data}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export default async function pyloadProxyHandler(req, res) {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
return res.status(500).send({error: {message: `Error communicating with Plex API: ${e.toString()}`}});
|
return res.status(500).send({error: {message: `Error communicating with Pyload API: ${e.toString()}`}});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).json({ error: 'Invalid proxy service type' });
|
return res.status(400).json({ error: 'Invalid proxy service type' });
|
||||||
|
|||||||
Reference in New Issue
Block a user