mirror of
https://github.com/ajnart/homarr.git
synced 2026-07-09 10:52:28 +02:00
Merge branch 'dev' into board-in-database
This commit is contained in:
@@ -41,7 +41,7 @@ export const StepOnboardingFinished = () => {
|
||||
<Divider />
|
||||
<NavLink
|
||||
component={Link}
|
||||
href="/b/default"
|
||||
href="/b"
|
||||
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
|
||||
className={classes.link}
|
||||
icon={<IconDashboard />}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Consola from 'consola';
|
||||
import fs from 'fs/promises';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { env } from 'process';
|
||||
import { v4 } from 'uuid';
|
||||
@@ -37,11 +39,8 @@ export async function middleware(req: NextRequest) {
|
||||
return HomarrResponse.next(deviceId);
|
||||
}
|
||||
|
||||
// is only called from when there were no users in the database in this session (Since the app started)
|
||||
cachedUserCount = await client.user.count.query();
|
||||
|
||||
// Do not redirect if there are users in the database
|
||||
if (cachedUserCount > 0) {
|
||||
if (!(await shouldRedirectToOnboard())) {
|
||||
return HomarrResponse.next(deviceId);
|
||||
}
|
||||
|
||||
@@ -63,3 +62,25 @@ const HomarrResponse = {
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
const shouldRedirectToOnboard = async (): Promise<boolean> => {
|
||||
const cacheAndGetUserCount = async () => {
|
||||
cachedUserCount = await client.user.count.query();
|
||||
return cachedUserCount === 0;
|
||||
};
|
||||
|
||||
if (!env.DATABASE_URL?.startsWith('file:')) {
|
||||
return await cacheAndGetUserCount();
|
||||
}
|
||||
|
||||
const fileUri = env.DATABASE_URL.substring(4);
|
||||
try {
|
||||
await fs.access(fileUri, fs.constants.W_OK);
|
||||
return await cacheAndGetUserCount();
|
||||
} catch {
|
||||
Consola.warn(
|
||||
`detected that the path ${fileUri} was not readable. Showing onboarding page for setup...`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,6 +59,11 @@ export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = a
|
||||
if (!board.allowGuests && !session?.user) {
|
||||
return {
|
||||
notFound: true,
|
||||
props: {
|
||||
primaryColor: board.primaryColor,
|
||||
secondaryColor: board.secondaryColor,
|
||||
primaryShade: board.primaryShade,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,12 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res
|
||||
};
|
||||
}
|
||||
|
||||
const caller = dockerRouter.createCaller({
|
||||
session: session,
|
||||
cookies: req.cookies,
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
[...boardNamespaces, 'layout/manage', 'tools/docker'],
|
||||
locale,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { and, eq, inArray } from 'drizzle-orm';
|
||||
import fs from 'fs';
|
||||
import { z } from 'zod';
|
||||
import { db } from '~/server/db';
|
||||
import { WidgetSort } from '~/server/db/items';
|
||||
import { getDefaultBoardAsync } from '~/server/db/queries/userSettings';
|
||||
import {
|
||||
appStatusCodes,
|
||||
|
||||
@@ -67,12 +67,6 @@ export const configRouter = createTRPCRouter({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (process.env.DISABLE_EDIT_MODE?.toLowerCase() === 'true') {
|
||||
throw new TRPCError({
|
||||
code: 'METHOD_NOT_SUPPORTED',
|
||||
message: 'Edit is not allowed, because edit mode is disabled',
|
||||
});
|
||||
}
|
||||
Consola.info(`Saving updated configuration of '${input.name}' config.`);
|
||||
|
||||
const previousConfig = getConfig(input.name);
|
||||
|
||||
@@ -62,22 +62,24 @@ export const dnsHoleRouter = createTRPCRouter({
|
||||
)
|
||||
);
|
||||
|
||||
const data = result.reduce(
|
||||
(prev: AdStatistics, curr) => ({
|
||||
domainsBeingBlocked: prev.domainsBeingBlocked + curr.domainsBeingBlocked,
|
||||
adsBlockedToday: prev.adsBlockedToday + curr.adsBlockedToday,
|
||||
dnsQueriesToday: prev.dnsQueriesToday + curr.dnsQueriesToday,
|
||||
status: [...prev.status, curr.status],
|
||||
adsBlockedTodayPercentage: 0,
|
||||
}),
|
||||
{
|
||||
domainsBeingBlocked: 0,
|
||||
adsBlockedToday: 0,
|
||||
adsBlockedTodayPercentage: 0,
|
||||
dnsQueriesToday: 0,
|
||||
status: [],
|
||||
}
|
||||
);
|
||||
const data = result
|
||||
.filter((x) => x !== null)
|
||||
.reduce(
|
||||
(prev: AdStatistics, curr) => ({
|
||||
domainsBeingBlocked: prev.domainsBeingBlocked + curr!.domainsBeingBlocked,
|
||||
adsBlockedToday: prev.adsBlockedToday + curr!.adsBlockedToday,
|
||||
dnsQueriesToday: prev.dnsQueriesToday + curr!.dnsQueriesToday,
|
||||
status: [...prev.status, curr!.status],
|
||||
adsBlockedTodayPercentage: 0,
|
||||
}),
|
||||
{
|
||||
domainsBeingBlocked: 0,
|
||||
adsBlockedToday: 0,
|
||||
adsBlockedTodayPercentage: 0,
|
||||
dnsQueriesToday: 0,
|
||||
status: [],
|
||||
}
|
||||
);
|
||||
|
||||
data.adsBlockedTodayPercentage = data.adsBlockedToday / data.dnsQueriesToday;
|
||||
if (Number.isNaN(data.adsBlockedTodayPercentage)) {
|
||||
@@ -131,7 +133,13 @@ const processPiHole = async (app: ConfigAppType, enable: boolean) => {
|
||||
|
||||
const collectPiHoleSummary = async (app: ConfigAppType) => {
|
||||
const piHole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey'));
|
||||
const summary = await piHole.getSummary();
|
||||
const summary = await piHole.getSummary().catch(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!summary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
domainsBeingBlocked: summary.domains_being_blocked,
|
||||
@@ -152,7 +160,14 @@ const collectAdGuardSummary = async (app: ConfigAppType) => {
|
||||
findAppProperty(app, 'password')
|
||||
);
|
||||
|
||||
const stats = await adGuard.getStats();
|
||||
const stats = await adGuard.getStats().catch(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = await adGuard.getStatus();
|
||||
const countFilteredDomains = await adGuard.getCountFilteringDomains();
|
||||
|
||||
|
||||
@@ -2,17 +2,21 @@ import { Jellyfin } from '@jellyfin/sdk';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
|
||||
import Consola from 'consola';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
import { ConfigAppType } from '~/types/app';
|
||||
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
|
||||
import { getConfig } from '~/tools/config/getConfig';
|
||||
import { PlexClient } from '~/tools/server/sdk/plex/plexClient';
|
||||
import { trimStringEnding } from '~/tools/shared/strings';
|
||||
import { GenericMediaServer } from '~/types/api/media-server/media-server';
|
||||
import { MediaServersResponseType } from '~/types/api/media-server/response';
|
||||
import { GenericCurrentlyPlaying, GenericSessionInfo } from '~/types/api/media-server/session-info';
|
||||
import { ConfigAppType } from '~/types/app';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
import { PlexClient } from '~/tools/server/sdk/plex/plexClient';
|
||||
|
||||
const jellyfin = new Jellyfin({
|
||||
clientInfo: {
|
||||
@@ -104,7 +108,7 @@ const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | un
|
||||
return {
|
||||
type: 'jellyfin',
|
||||
appId: app.id,
|
||||
serverAddress: app.url,
|
||||
serverAddress: trimStringEnding(app.url, ['/web/index.html#!/home.html', '/web', '/web/index.html', '/web/', '/web/index.html#']),
|
||||
version: infoApi.data.Version ?? undefined,
|
||||
sessions: sessions
|
||||
.filter((session) => session.NowPlayingItem)
|
||||
@@ -176,7 +180,7 @@ const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | un
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
serverAddress: app.url,
|
||||
serverAddress: trimStringEnding(app.url, ['/web', '/web/index.html', '/web/index.html#!', '/web/index.html#!/settings/web/general', '/web/']),
|
||||
sessions: [],
|
||||
type: 'plex',
|
||||
appId: app.id,
|
||||
|
||||
@@ -4,10 +4,10 @@ import { z } from 'zod';
|
||||
import { db } from '~/server/db';
|
||||
import { items, widgetOptions } from '~/server/db/schema';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const notebookRouter = createTRPCRouter({
|
||||
update: publicProcedure
|
||||
update: adminProcedure
|
||||
.input(z.object({ widgetId: z.string(), content: z.string(), boardName: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const item = await db.query.items.findFirst({
|
||||
|
||||
@@ -32,7 +32,7 @@ export const adGuardApiStatusResponseSchema = z.object({
|
||||
export const adGuardApiFilteringStatusSchema = z.object({
|
||||
filters: z.array(
|
||||
z.object({
|
||||
url: z.string().url(),
|
||||
url: z.string(),
|
||||
name: z.string(),
|
||||
last_updated: z.string().optional(),
|
||||
id: z.number().nonnegative(),
|
||||
|
||||
@@ -254,6 +254,7 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
|
||||
);
|
||||
case 'horizontal':
|
||||
case 'vertical':
|
||||
const flexDirection = widget.options.layout === 'vertical' ? 'column' : 'row';
|
||||
return (
|
||||
<Stack h="100%" spacing={0}>
|
||||
<Title size="h4" px="0.25rem">
|
||||
@@ -282,15 +283,16 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
|
||||
w="100%"
|
||||
>
|
||||
{widget.options.items.map((item: BookmarkItem, index) => (
|
||||
<>
|
||||
{index > 0 && (
|
||||
<Divider
|
||||
m="3px"
|
||||
orientation={widget.options.layout !== 'vertical' ? 'vertical' : 'horizontal'}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
key={index}
|
||||
style={{ display: 'flex', flex: '1', flexDirection: flexDirection }}
|
||||
>
|
||||
<Divider
|
||||
m="3px"
|
||||
orientation={widget.options.layout !== 'vertical' ? 'vertical' : 'horizontal'}
|
||||
color={index === 0 ? 'transparent' : undefined}
|
||||
/>
|
||||
<Card
|
||||
key={index}
|
||||
px="md"
|
||||
py="1px"
|
||||
component="a"
|
||||
@@ -307,7 +309,7 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
|
||||
>
|
||||
<BookmarkItemContent item={item} />
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -98,7 +98,6 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
||||
style={{ position: 'relative' }}
|
||||
date={month}
|
||||
maxLevel="month"
|
||||
minLevel="month"
|
||||
styles={{
|
||||
calendarHeader: {
|
||||
maxWidth: 'inherit',
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
@@ -71,7 +73,7 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
||||
const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery();
|
||||
const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation();
|
||||
const { width, ref } = useElementSize();
|
||||
const { t } = useTranslation('common');
|
||||
const { t } = useTranslation(['common', 'modules/dns-hole-controls']);
|
||||
|
||||
const { name: configName, config } = useConfigContext();
|
||||
|
||||
@@ -81,6 +83,20 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
||||
return <WidgetLoading />;
|
||||
}
|
||||
|
||||
if (data.status.length === 0) {
|
||||
return(
|
||||
<Center h="100%">
|
||||
<Stack align="center">
|
||||
<IconDeviceGamepad size={40} strokeWidth={1}/>
|
||||
<Title align="center" order={6}>{t('modules/dns-hole-controls:descriptor.errors.general.title')}</Title>
|
||||
<Text align="center">{t('modules/dns-hole-controls:descriptor.errors.general.text')}</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
type getDnsStatusAcc = {
|
||||
enabled: string[];
|
||||
disabled: string[];
|
||||
|
||||
@@ -145,8 +145,8 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
) : (
|
||||
<Text>{t('nonePending')}</Text>
|
||||
)}
|
||||
{sortedData.map((item) => (
|
||||
<Card radius="md" withBorder>
|
||||
{sortedData.map((item, index) => (
|
||||
<Card radius="md" withBorder key={index}>
|
||||
<Flex wrap="wrap" justify="space-between" gap="md">
|
||||
<Flex gap="md">
|
||||
<Image
|
||||
|
||||
@@ -100,7 +100,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
|
||||
<Group pos="absolute" bottom="15" right="15" mt="auto">
|
||||
<Avatar.Group>
|
||||
{data?.servers.map((server) => {
|
||||
{data?.servers.map((server, index) => {
|
||||
const app = config?.apps.find((x) => x.id === server.appId);
|
||||
|
||||
if (!app) {
|
||||
@@ -109,6 +109,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
|
||||
return (
|
||||
<AppAvatar
|
||||
key={index}
|
||||
iconUrl={app.appearance.iconUrl}
|
||||
// If success, the color is undefined, otherwise it's red but if isFetching is true, it's yellow
|
||||
color={server.success ? (isFetching ? 'yellow' : undefined) : 'red'}
|
||||
|
||||
@@ -30,8 +30,8 @@ const definition = defineWidget({
|
||||
},
|
||||
},
|
||||
gridstack: {
|
||||
minWidth: 3,
|
||||
minHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
maxWidth: 12,
|
||||
maxHeight: 12,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user