diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index aa5265ed..1d9c7137 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -94,6 +94,10 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} # https://github.com/docker/setup-qemu-action#about # platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 diff --git a/Dockerfile b/Dockerfile index b1193813..da7d290c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,10 @@ RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm i FROM node:current-alpine AS builder WORKDIR /app +ARG BUILDTIME +ARG VERSION +ARG REVISION + COPY --link --from=deps /app/node_modules ./node_modules/ COPY . . @@ -29,7 +33,7 @@ RUN < config/settings.yaml - npm run build + NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION npm run build EOF # Production image, copy all the files and run next diff --git a/package.json b/package.json index cf9c4131..239d68dc 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@headlessui/react": "^1.7.0", "@tailwindcss/forms": "^0.5.3", "classnames": "^2.3.1", + "compare-versions": "^5.0.1", "dockerode": "^3.3.4", "follow-redirects": "^1.15.2", "i18next": "^21.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2625a90..2c9e167b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ specifiers: '@tailwindcss/forms': ^0.5.3 autoprefixer: ^10.4.9 classnames: ^2.3.1 + compare-versions: ^5.0.1 dockerode: ^3.3.4 eslint: ^8.23.1 eslint-config-airbnb: ^19.0.4 @@ -45,6 +46,7 @@ dependencies: '@headlessui/react': 1.7.0_biqbaboplfbrettd7655fr4n2y '@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8 classnames: 2.3.1 + compare-versions: 5.0.1 dockerode: 3.3.4 follow-redirects: 1.15.2 i18next: 21.9.1 @@ -715,6 +717,10 @@ packages: delayed-stream: 1.0.0 dev: false + /compare-versions/5.0.1: + resolution: {integrity: sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ==} + dev: false + /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true diff --git a/public/locales/de/common.json b/public/locales/de/common.json index cff97b11..4500abf6 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -159,14 +159,14 @@ "seed": "Seed" }, "mastodon": { - "user_count": "Users", - "status_count": "Posts", - "domain_count": "Domains" + "user_count": "Nutzer", + "status_count": "Beiträge", + "domain_count": "Domänen" }, "strelaysrv": { - "numActiveSessions": "Sessions", - "numConnections": "Connections", - "dataRelayed": "Relayed", - "transferRate": "Rate" + "numActiveSessions": "Sitzungen", + "numConnections": "Verbindungen", + "dataRelayed": "Weitergeleitet", + "transferRate": "Bewerten" } } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 47ffd142..d5679090 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -35,7 +35,7 @@ "rutorrent": { "active": "Activo", "upload": "Subida", - "download": "Descarga" + "download": "Bajada" }, "sonarr": { "wanted": "Más deseado", @@ -69,7 +69,7 @@ }, "speedtest": { "upload": "Subida", - "download": "Descarga", + "download": "Bajada", "ping": "Ping" }, "portainer": { @@ -88,7 +88,7 @@ "total": "Total" }, "weather": { - "current": "Localización Actual", + "current": "Ubicación actual", "allow": "Clic para permitir", "updating": "Actualizando", "wait": "Espere, por favor" @@ -128,7 +128,7 @@ "numberOfFailQueries": "Consultas fallidas" }, "transmission": { - "download": "Descarga", + "download": "Bajada", "upload": "Subida", "leech": "Compañeros", "seed": "Semillas" @@ -153,7 +153,7 @@ "latency": "Latencia" }, "qbittorrent": { - "download": "Descarga", + "download": "Bajada", "upload": "Subida", "leech": "Compañeros", "seed": "Semillas" @@ -166,7 +166,7 @@ "strelaysrv": { "numActiveSessions": "Sesiones", "numConnections": "Conexiones", - "dataRelayed": "Transmitido", + "dataRelayed": "Retransmitido", "transferRate": "Velocidad" } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 03d9c169..5372d899 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -38,18 +38,18 @@ "download": "Réception" }, "sonarr": { - "wanted": "Demandé", - "queued": "En queue", + "wanted": "Demande", + "queued": "En attente", "series": "Séries" }, "radarr": { - "wanted": "Demandé", - "queued": "En queue", + "wanted": "Demande", + "queued": "En attente", "movies": "Films" }, "readarr": { - "wanted": "Demandé", - "queued": "En Queue", + "wanted": "Demande", + "queued": "Attente", "books": "Livres" }, "ombi": { @@ -132,11 +132,11 @@ "messages": "Msg" }, "prowlarr": { - "enableIndexers": "Indexeurs", + "enableIndexers": "Indexeur", "numberOfGrabs": "Capture", - "numberOfQueries": "Demandes", - "numberOfFailGrabs": "Capture échouée", - "numberOfFailQueries": "Demande échouée" + "numberOfQueries": "Demande", + "numberOfFailGrabs": "Capt. échouée", + "numberOfFailQueries": "Dem. échouée" }, "transmission": { "download": "Réception", @@ -176,8 +176,8 @@ }, "strelaysrv": { "numActiveSessions": "Sessions", - "numConnections": "Connections", - "dataRelayed": "Relayed", - "transferRate": "Rate" + "numConnections": "Cnx", + "dataRelayed": "Relayé", + "transferRate": "Débit" } } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index 415eca6d..fd9226df 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -11,7 +11,7 @@ "total": "Total", "free": "Livre", "used": "Usado", - "load": "Load" + "load": "Carregar" }, "docker": { "rx": "Rx", @@ -23,7 +23,7 @@ "emby": { "playing": "A reproduzir", "transcoding": "Transcodificação", - "bitrate": "Bitrate", + "bitrate": "Taxa de bits", "no_active": "Sem streams ativas" }, "tautulli": { @@ -34,8 +34,8 @@ }, "rutorrent": { "active": "Ativo", - "upload": "Envio", - "download": "ReceçãoDownload" + "upload": "Enviando", + "download": "Baixando" }, "sonarr": { "wanted": "Desejada", @@ -48,7 +48,7 @@ "movies": "Filmes" }, "readarr": { - "wanted": "Wanted", + "wanted": "Desejados", "queued": "Em fila", "books": "Livros" }, @@ -65,7 +65,7 @@ "pihole": { "queries": "Consultas", "blocked": "Bloqueado", - "gravity": "Gravity" + "gravity": "Gravidade" }, "speedtest": { "upload": "Envio", @@ -78,7 +78,7 @@ "total": "Total" }, "traefik": { - "routers": "Routers", + "routers": "Roteadores", "services": "Serviços", "middleware": "Middleware" }, @@ -110,21 +110,21 @@ "available": "Disponível" }, "sabnzbd": { - "rate": "Rate", + "rate": "Taxa", "queue": "Fila", "timeleft": "Tempo restante" }, "nzbget": { - "rate": "Rate", + "rate": "Taxa", "remaining": "Restante", - "downloaded": "Downloaded" + "downloaded": "Baixado" }, "coinmarketcap": { "configure": "Configurar uma ou mais moedas", - "1hour": "1 Hour", - "1day": "1 Day", - "7days": "7 Days", - "30days": "30 Days" + "1hour": "1 Hora", + "1day": "1 Dia", + "7days": "7 Dias", + "30days": "30 Dias" }, "gotify": { "apps": "Aplicações", @@ -132,52 +132,52 @@ "messages": "Mensagens" }, "prowlarr": { - "enableIndexers": "Indexers", - "numberOfGrabs": "Grabs", - "numberOfQueries": "Queries", + "enableIndexers": "Indexadores", + "numberOfGrabs": "Agarrados", + "numberOfQueries": "Consultas", "numberOfFailGrabs": "Falhados", "numberOfFailQueries": "Pesquisas falhadas" }, "transmission": { - "download": "Download", - "upload": "Envio", - "leech": "Leech", - "seed": "Seed" + "download": "Baixando", + "upload": "Enviando", + "leech": "Sanguessugas", + "seed": "Semeadores" }, "jackett": { - "configured": "Configured", - "errored": "Errored" + "configured": "Configurado", + "errored": "Errado" }, "bazarr": { - "missingEpisodes": "Missing Episodes", - "missingMovies": "Missing Movies" + "missingEpisodes": "Episódios Faltantes", + "missingMovies": "Filmes Faltantes" }, "lidarr": { - "queued": "Queued", - "wanted": "Wanted", - "albums": "Albums" + "queued": "Enfileirado", + "wanted": "Desejado", + "albums": "Álbuns" }, "adguard": { - "queries": "Queries", - "blocked": "Blocked", - "filtered": "Filtered", - "latency": "Latency" + "queries": "Consultas", + "blocked": "Bloqueado", + "filtered": "Filtrado", + "latency": "Latência" }, "qbittorrent": { - "download": "Download", - "upload": "Upload", - "leech": "Leech", - "seed": "Seed" + "download": "Baixando", + "upload": "Enviando", + "leech": "Sanguessugas", + "seed": "Semeadores" }, "mastodon": { - "user_count": "Users", - "status_count": "Posts", - "domain_count": "Domains" + "user_count": "Usuários", + "status_count": "Postagens", + "domain_count": "Domínios" }, "strelaysrv": { - "numActiveSessions": "Sessions", - "numConnections": "Connections", - "dataRelayed": "Relayed", - "transferRate": "Rate" + "numActiveSessions": "Sessões", + "numConnections": "Conexões", + "dataRelayed": "Retransmitido", + "transferRate": "Taxa" } } diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index c87992dd..f43aff8a 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -138,34 +138,34 @@ "prowlarr": { "enableIndexers": "Indexerare", "numberOfGrabs": "Hämtningar", - "numberOfQueries": "Queries", + "numberOfQueries": "Hämtningar", "numberOfFailGrabs": "Misslyckade hämtningar", - "numberOfFailQueries": "Fail Queries" + "numberOfFailQueries": "Misslyckade hämtningar" }, "jackett": { "configured": "Konfigurerade", "errored": "Felaktiga" }, "adguard": { - "queries": "Queries", - "blocked": "Blocked", - "filtered": "Filtered", - "latency": "Latency" + "queries": "Förfrågningar", + "blocked": "Blockerad", + "filtered": "Filtrerad", + "latency": "Svarstid" }, "qbittorrent": { - "download": "Download", - "upload": "Upload", + "download": "Nedladdning", + "upload": "Uppladdning", "leech": "Leech", "seed": "Seed" }, "mastodon": { - "user_count": "Users", + "user_count": "Användare", "status_count": "Posts", "domain_count": "Domains" }, "strelaysrv": { - "numActiveSessions": "Sessions", - "numConnections": "Connections", + "numActiveSessions": "Sessioner", + "numConnections": "Anslutningar", "dataRelayed": "Relayed", "transferRate": "Rate" } diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index 966755d7..a4a4c8f8 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -130,7 +130,7 @@ "transmission": { "download": "下载", "upload": "上传", - "leech": "吸血", + "leech": "下载中", "seed": "做种" }, "jackett": { @@ -155,11 +155,11 @@ "qbittorrent": { "download": "下载", "upload": "上传", - "leech": "吸血", + "leech": "下载中", "seed": "做种" }, "mastodon": { - "user_count": "Users", + "user_count": "用户", "status_count": "Posts", "domain_count": "Domains" }, diff --git a/src/components/version.jsx b/src/components/version.jsx new file mode 100644 index 00000000..187ad327 --- /dev/null +++ b/src/components/version.jsx @@ -0,0 +1,47 @@ +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { compareVersions } from "compare-versions"; +import { MdNewReleases } from "react-icons/md"; + +export default function Version() { + const { t, i18n } = useTranslation(); + + const buildTime = process.env.NEXT_PUBLIC_BUILDTIME ?? new Date().toISOString(); + const revision = process.env.NEXT_PUBLIC_REVISION ?? "dev"; + const version = process.env.NEXT_PUBLIC_VERSION ?? "dev"; + + const { data: releaseData } = useSWR("https://api.github.com/repos/benphelps/homepage/releases"); + + // use Intl.DateTimeFormat to format the date + const formatDate = (date) => { + const options = { + year: "numeric", + month: "short", + day: "numeric", + }; + return new Intl.DateTimeFormat(i18n.language, options).format(new Date(date)); + }; + + const latestRelease = releaseData?.[0]; + + return ( +
+ + {version} ({revision.substring(0, 7)}, {formatDate(buildTime)}) + + {version === "main" || version === "dev" + ? null + : releaseData && + compareVersions(latestRelease.tag_name, version) > 0 && ( + + {t("Update Available")} + + )} +
+ ); +} diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index c7b3ed22..3ac2f3b6 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -1,4 +1,4 @@ -import logger from "utils/logger"; +import createLogger from "utils/logger"; import genericProxyHandler from "utils/proxies/generic"; import credentialedProxyHandler from "utils/proxies/credentialed"; import rutorrentProxyHandler from "utils/proxies/rutorrent"; @@ -7,6 +7,8 @@ import npmProxyHandler from "utils/proxies/npm"; import transmissionProxyHandler from "utils/proxies/transmission"; import qbittorrentProxyHandler from "utils/proxies/qbittorrent"; +const logger = createLogger('servicesProxy'); + function asJson(data) { if (data?.length > 0) { const json = JSON.parse(data.toString()); diff --git a/src/pages/index.jsx b/src/pages/index.jsx index db2bba85..c083d337 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -10,6 +10,7 @@ import ServicesGroup from "components/services/group"; import BookmarksGroup from "components/bookmarks/group"; import Widget from "components/widget"; import Revalidate from "components/revalidate"; +import createLogger from "utils/logger"; import { getSettings } from "utils/config"; import { ColorContext } from "utils/color-context"; import { ThemeContext } from "utils/theme-context"; @@ -23,10 +24,16 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), { ssr: false, }); +const Version = dynamic(() => import("components/version"), { + ssr: false, +}); + const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"]; export function getStaticProps() { + let logger; try { + logger = createLogger("index"); const { providers, ...settings } = getSettings(); return { @@ -35,6 +42,9 @@ export function getStaticProps() { }, }; } catch (e) { + if (logger) { + logger.error(e); + } return { props: { initialSettings: {}, @@ -153,11 +163,15 @@ function Home({ initialSettings }) { )} -
+
{!settings?.color && } {!settings?.theme && }
+ +
+ +
); diff --git a/src/utils/logger.js b/src/utils/logger.js index a4d0f1fe..5bc0ed37 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,80 +1,88 @@ +/* eslint-disable no-console */ import { join } from "path"; +import { format as utilFormat } from "node:util" import winston from "winston"; -const configPath = join(process.cwd(), "config"); +let winstonLogger; -function messageFormatter(logInfo) { - if (logInfo.stack) { - return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.stack}`; +function init() { + const configPath = join(process.cwd(), "config"); + + function combineMessageAndSplat() { + return { + // eslint-disable-next-line no-unused-vars + transform: (info, opts) => { + // combine message and args if any + // eslint-disable-next-line no-param-reassign + info.message = utilFormat(info.message, ...info[Symbol.for('splat')] || []); + return info; + } + } } - return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.message}`; -}; -const consoleFormat = winston.format.combine( - winston.format.errors({ stack: true }), - winston.format.splat(), - winston.format.timestamp(), - winston.format.colorize(), - winston.format.printf(messageFormatter) -); + function messageFormatter(logInfo) { + if (logInfo.label) { + if (logInfo.stack) { + return `[${logInfo.timestamp}] ${logInfo.level}: <${logInfo.label}> ${logInfo.stack}`; + } + return `[${logInfo.timestamp}] ${logInfo.level}: <${logInfo.label}> ${logInfo.message}`; + } -const fileFormat = winston.format.combine( - winston.format.errors({ stack: true }), - winston.format.splat(), - winston.format.timestamp(), - winston.format.printf(messageFormatter) -); + if (logInfo.stack) { + return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.stack}`; + } + return `[${logInfo.timestamp}] ${logInfo.level}: ${logInfo.message}`; + }; -const logger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - transports: [ - new winston.transports.Console({ - format: consoleFormat, - handleExceptions: true, - handleRejections: true - }), + winstonLogger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.errors({ stack: true}), + combineMessageAndSplat(), + winston.format.timestamp(), + winston.format.colorize(), + winston.format.printf(messageFormatter) + ), + handleExceptions: true, + handleRejections: true + }), - new winston.transports.File({ - format: fileFormat, - filename: `${configPath}/logs/homepage.log`, - handleExceptions: true, - handleRejections: true - }), - ] -}); + new winston.transports.File({ + format: winston.format.combine( + winston.format.errors({ stack: true}), + combineMessageAndSplat(), + winston.format.timestamp(), + winston.format.printf(messageFormatter) + ), + filename: `${configPath}/logs/homepage.log`, + handleExceptions: true, + handleRejections: true + }), + ] + }); -function debug(message, ...args) { - logger.debug(message, ...args); + // patch the console log mechanism to use our logger + const consoleMethods = ['log', 'debug', 'info', 'warn', 'error'] + consoleMethods.forEach(method => { + // workaround for https://github.com/winstonjs/winston/issues/1591 + switch (method) { + case 'log': + console[method] = winstonLogger.info.bind(winstonLogger); + break; + default: + console[method] = winstonLogger[method].bind(winstonLogger); + break; + } + }) } -function verbose(message, ...args) { - logger.verbose(message, ...args); -} +export default function createLogger(label) { + if (!winstonLogger) { + init(); + } -function info(message, ...args) { - logger.info(message, ...args); -} - -function warn(message, ...args) { - logger.warn(message, ...args); -} - -function error(message, ...args) { - logger.error(message, ...args); -} - -function crit(message, ...args) { - logger.crit(message, ...args); -} - -const thisModule = { - debug, - verbose, - info, - warn, - error, - crit -}; - -export default thisModule; \ No newline at end of file + return winstonLogger.child({ label }); +} \ No newline at end of file diff --git a/src/utils/proxies/generic.js b/src/utils/proxies/generic.js index 7bd136de..98265059 100644 --- a/src/utils/proxies/generic.js +++ b/src/utils/proxies/generic.js @@ -1,7 +1,9 @@ import getServiceWidget from "utils/service-helpers"; import { formatApiCall } from "utils/api-helpers"; import { httpProxy } from "utils/http"; -import logger from "utils/logger"; +import createLogger from "utils/logger"; + +const logger = createLogger('genericProxyHandler'); export default async function genericProxyHandler(req, res, maps) { const { group, service, endpoint } = req.query;