Skip to content

Commit 7d6615d

Browse files
authored
Merge pull request #77 from rbouteiller/fix/setup-device-reliability
Fix/setup device reliability
2 parents f0cb175 + 1c6f155 commit 7d6615d

58 files changed

Lines changed: 3053 additions & 977 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ FORCE_WIKIPEDIA_RESERVOIR=false
3434
# Set to "false" to disable authentication (mono-user mode)
3535
AUTH_ENABLED=true
3636
BETTER_AUTH_SECRET=your_better_auth_secret_32_characters_min
37-
# Admin email - users signing up with this email will automatically become admin
38-
ADMIN_EMAIL=
37+
# The first account created during setup becomes admin automatically.
3938

4039
# TRMNL Live Proxy (Optional)
4140
# By default the read-only TRMNL endpoints (/api/models, /api/palettes,

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ENV NODE_ENV=production
4040
ENV NEXT_TELEMETRY_DISABLED=1
4141
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
4242
ENV CHROME_EXECUTABLE_PATH=/headless-shell/headless-shell
43+
ENV XDG_CACHE_HOME=/home/nextjs/.cache
4344

4445
# Copy Node.js binary from build stage
4546
COPY --from=base /usr/local/bin/node /usr/local/bin/node
@@ -53,6 +54,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
5354
RUN groupadd -g 1001 nodejs \
5455
&& useradd -u 1001 -g nodejs -s /bin/false nextjs
5556

57+
RUN mkdir -p /var/cache/fontconfig /home/nextjs/.cache/fontconfig /home/nextjs/.fontconfig \
58+
&& chown -R nextjs:nodejs /var/cache/fontconfig /home/nextjs
59+
5660
# Copy built application
5761
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
5862
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,12 @@ Create `.env.local` (for `pnpm dev`) or `.env` (for Docker Compose) with the key
115115
| `BETTER_AUTH_SECRET` | Required when `AUTH_ENABLED=true`. Generate with `openssl rand -base64 32`. |
116116
| `BETTER_AUTH_URL` | Public URL of your deployment (defaults to `http://localhost:3000`). |
117117
| `AUTH_ENABLED` | Set to `false` to disable authentication (mono-user mode). |
118-
| `ADMIN_EMAIL` | Email that receives admin role on first sign-up. |
119118
| `REACT_RENDERER` | `takumi` (default), `satori`, or `browser`. See below. |
120119
| `ENABLE_EXTERNAL_CATALOG` | Allow fetching the community / TRMNL recipe catalog. |
121120

121+
When authentication is enabled, visit `/setup` after configuring the database.
122+
The first account created during setup is assigned the admin role automatically.
123+
122124
### Renderer Options
123125
- **`takumi`** (default): fast Rust-backed Satori-compatible renderer.
124126
- **`satori`**: original Vercel Satori renderer.

app/(app)/client-page.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
TableHeader,
1616
TableRow,
1717
} from "@/components/ui/table";
18+
import {
19+
DEFAULT_IMAGE_HEIGHT,
20+
DEFAULT_IMAGE_WIDTH,
21+
} from "@/lib/recipes/constants";
1822
import { normalizeGrayscale } from "@/lib/trmnl/grayscale";
1923
import { DEFAULT_MODEL_NAME } from "@/lib/trmnl/types";
2024
import type { Device, SystemLog } from "@/lib/types";
@@ -48,8 +52,16 @@ export default function DashboardClientPage({
4852
: null;
4953

5054
const isPortrait = lastUpdatedDevice?.screen_orientation === "portrait";
55+
// Orientation-adjusted device resolution, used for BOTH the rendered image
56+
// and the frame aspect ratio so the preview isn't cropped by object-cover.
57+
const previewWidth = isPortrait
58+
? lastUpdatedDevice?.screen_height || DEFAULT_IMAGE_HEIGHT
59+
: lastUpdatedDevice?.screen_width || DEFAULT_IMAGE_WIDTH;
60+
const previewHeight = isPortrait
61+
? lastUpdatedDevice?.screen_width || DEFAULT_IMAGE_WIDTH
62+
: lastUpdatedDevice?.screen_height || DEFAULT_IMAGE_HEIGHT;
5163
const latestScreenSrc = lastUpdatedDevice
52-
? buildLatestScreenSrc(lastUpdatedDevice)
64+
? buildLatestScreenSrc(lastUpdatedDevice, previewWidth, previewHeight)
5365
: "";
5466

5567
return (
@@ -87,7 +99,11 @@ export default function DashboardClientPage({
8799
isPortrait ? "max-w-[260px]" : "max-w-[520px]",
88100
)}
89101
>
90-
<DeviceFrame size="lg" portrait={isPortrait}>
102+
<DeviceFrame
103+
size="lg"
104+
portrait={isPortrait}
105+
screenAspectRatio={`${previewWidth} / ${previewHeight}`}
106+
>
91107
<Image
92108
src={latestScreenSrc}
93109
alt={`${lastUpdatedDevice.name} screen`}
@@ -233,8 +249,14 @@ export default function DashboardClientPage({
233249
);
234250
}
235251

236-
function buildLatestScreenSrc(device: Device): string {
252+
function buildLatestScreenSrc(
253+
device: Device,
254+
width: number,
255+
height: number,
256+
): string {
237257
const params = new URLSearchParams({
258+
width: String(width),
259+
height: String(height),
238260
grayscale: String(normalizeGrayscale(device.grayscale)),
239261
model: device.model?.trim() || DEFAULT_MODEL_NAME,
240262
});

app/(app)/device/[friendly_id]/client-page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,21 @@ export default function DeviceClientPage({
396396
});
397397
};
398398

399+
const handleRemoveTimeRange = (index: number) => {
400+
if (!editedDevice.refresh_schedule) return;
401+
402+
setEditedDevice({
403+
...editedDevice,
404+
refresh_schedule: {
405+
default_refresh_rate:
406+
editedDevice.refresh_schedule.default_refresh_rate,
407+
time_ranges: editedDevice.refresh_schedule.time_ranges.filter(
408+
(_, currentIndex) => currentIndex !== index,
409+
),
410+
},
411+
});
412+
};
413+
399414
useEffect(() => {
400415
if (editedDevice.playlist_id) {
401416
const playlistScreens = playlistItems
@@ -490,6 +505,7 @@ export default function DeviceClientPage({
490505
onRegenerateApiKey={handleRegenerateApiKey}
491506
onRegenerateFriendlyId={handleRegenerateFriendlyId}
492507
onAddTimeRange={handleAddTimeRange}
508+
onRemoveTimeRange={handleRemoveTimeRange}
493509
onSubmit={handleSubmit}
494510
onCancel={handleCancel}
495511
/>

app/(app)/device/[friendly_id]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { notFound } from "next/navigation";
22
import { Suspense } from "react";
3-
import { fetchRecipes } from "@/app/actions/mixup";
43
import { Skeleton } from "@/components/ui/skeleton";
54
import { getInitData } from "@/lib/getInitData";
5+
import { listAllRecipes } from "@/lib/recipes/catalog";
66
import { listModels, listPalettes } from "@/lib/trmnl/registry";
77
import { getDeviceStatus } from "@/utils/helpers";
88
import DeviceClientPage from "./client-page";
@@ -44,7 +44,7 @@ const DeviceData = async ({ friendlyId }: { friendlyId: string }) => {
4444
trmnlPalettes,
4545
] = await Promise.all([
4646
getInitData(),
47-
fetchRecipes(),
47+
listAllRecipes(),
4848
listModels(),
4949
listPalettes(),
5050
]);
@@ -62,7 +62,7 @@ const DeviceData = async ({ friendlyId }: { friendlyId: string }) => {
6262
status: getDeviceStatus(device),
6363
};
6464

65-
// Convert recipes to availableScreens array
65+
// Convert renderable recipe catalog rows to screen dropdown options.
6666
const availableScreens = recipes.map((recipe) => ({
6767
id: recipe.slug,
6868
title: recipe.name,

app/(app)/page.tsx

Lines changed: 2 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { connection } from "next/server";
22
import { Suspense } from "react";
33
import { PageTemplate } from "@/components/common/page-template";
44
import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton";
5-
import { DbInitializer } from "@/components/dashboard/db-initializer";
5+
import { DatabaseSetupPanel } from "@/components/setup/database-setup-panel";
66
import { Badge } from "@/components/ui/badge";
77
import { getInitData } from "@/lib/getInitData";
88
import DashboardClientPage from "./client-page";
@@ -15,100 +15,7 @@ const DashboardData = async () => {
1515
if (!dbStatus.ready) {
1616
return (
1717
<>
18-
<div className="mt-4 rounded-lg border bg-card shadow-sm">
19-
{dbStatus.error === "ERROR_ENV_VAR_DATABASE_URL_NOT_SET" && (
20-
<div className="p-6 border-b">
21-
<h3 className="font-bold text-2xl mb-2 tracking-tight">
22-
Database not configured
23-
</h3>
24-
<p className="mb-4 text-muted-foreground">
25-
The{" "}
26-
<span className="font-mono text-foreground bg-muted px-1.5 py-0.5 rounded text-sm">
27-
DATABASE_URL
28-
</span>{" "}
29-
environment variable is not set. Add it to your{" "}
30-
<span className="font-mono text-foreground bg-muted px-1.5 py-0.5 rounded text-sm">
31-
.env
32-
</span>{" "}
33-
file to continue.
34-
</p>
35-
36-
<div className="mt-6 space-y-4">
37-
<p className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
38-
How to fix
39-
</p>
40-
41-
<div className="pl-4 border-l-2 border-primary/40">
42-
<p className="font-medium mb-1">
43-
Option 1 — Vercel + Supabase integration
44-
</p>
45-
<p className="text-sm text-muted-foreground">
46-
Open{" "}
47-
<a
48-
href="https://app.supabase.com/project/_/settings/integrations"
49-
className="text-primary hover:underline font-medium"
50-
>
51-
Supabase Dashboard Integrations
52-
</a>
53-
, verify your Vercel connection, toggle
54-
&ldquo;Development&rdquo; on, then &ldquo;Manage&rdquo;
55-
&rarr; &ldquo;Resync environment variables&rdquo;. Locally,
56-
run{" "}
57-
<span className="font-mono text-foreground bg-muted px-1.5 py-0.5 rounded text-xs">
58-
vercel link
59-
</span>{" "}
60-
and{" "}
61-
<span className="font-mono text-foreground bg-muted px-1.5 py-0.5 rounded text-xs">
62-
vercel env pull
63-
</span>
64-
.
65-
</p>
66-
</div>
67-
68-
<div className="pl-4 border-l-2 border-primary/40">
69-
<p className="font-medium mb-1">
70-
Option 2 — Add credentials manually
71-
</p>
72-
<p className="text-sm text-muted-foreground">
73-
Grab credentials from{" "}
74-
<a
75-
href="https://app.supabase.com/project/_/settings/api?showConnect=true"
76-
className="text-primary hover:underline font-medium"
77-
>
78-
Supabase API Settings
79-
</a>{" "}
80-
(under &ldquo;App Frameworks&rdquo;), save to{" "}
81-
<span className="font-mono text-foreground bg-muted px-1.5 py-0.5 rounded text-xs">
82-
.env
83-
</span>
84-
, then run the SQL below in the{" "}
85-
<a
86-
href="https://app.supabase.com/project/_/sql/new"
87-
className="text-primary hover:underline font-medium"
88-
>
89-
Supabase SQL Editor
90-
</a>
91-
.
92-
</p>
93-
</div>
94-
</div>
95-
</div>
96-
)}
97-
{dbStatus.error?.includes("Missing required tables") && (
98-
<div className="p-6 border-b">
99-
<h3 className="font-bold text-2xl mb-2 tracking-tight">
100-
Database schema incomplete
101-
</h3>
102-
<p className="text-muted-foreground">
103-
Missing tables:{" "}
104-
<span className="font-mono text-foreground">
105-
{dbStatus.error?.replace("Missing required tables: ", "")}
106-
</span>
107-
</p>
108-
</div>
109-
)}
110-
<DbInitializer databaseConfigured={dbStatus.databaseConfigured} />
111-
</div>
18+
<DatabaseSetupPanel dbStatus={dbStatus} />
11219
<DashboardSkeleton className="filter blur-[1px] pointer-events-none mt-6" />
11320
</>
11421
);

app/(app)/recipes/screens/album/album.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {
44
DEFAULT_IMAGE_WIDTH,
55
} from "@/lib/recipes/constants";
66
import type { RecipeDefinition } from "@/lib/recipes/types";
7+
import {
8+
createScreenProfile,
9+
type ScreenProfile,
10+
} from "@/lib/trmnl/screen-profile";
711
import { PreSatori } from "@/utils/pre-satori";
812

913
const DEFAULT_IMAGE_URL = "https://byos-nextjs.vercel.app/album/london.png";
@@ -21,33 +25,40 @@ export const dataSchema = paramsSchema;
2125
interface AlbumProps {
2226
width?: number;
2327
height?: number;
28+
screen?: ScreenProfile;
2429
params?: {
2530
imageUrl?: string;
2631
};
2732
}
2833

2934
export default async function Album({
30-
width = DEFAULT_IMAGE_WIDTH,
31-
height = DEFAULT_IMAGE_HEIGHT,
35+
width: renderWidth = DEFAULT_IMAGE_WIDTH,
36+
height: renderHeight = DEFAULT_IMAGE_HEIGHT,
37+
screen,
3238
params,
3339
}: AlbumProps) {
40+
const screenProfile =
41+
screen ?? createScreenProfile({ width: renderWidth, height: renderHeight });
3442
const imageUrl = params?.imageUrl || DEFAULT_IMAGE_URL;
3543

3644
return (
37-
<PreSatori width={width} height={height}>
45+
<PreSatori
46+
width={screenProfile.logicalWidth}
47+
height={screenProfile.logicalHeight}
48+
>
3849
<div className="w-full h-full bg-black flex flex-col items-center justify-center relative">
3950
<picture className="w-full h-full absolute inset-0">
4051
<source srcSet={imageUrl} type="image/png" />
4152
<img
4253
src={imageUrl}
4354
alt="Album"
44-
width={width}
45-
height={height}
55+
width={screenProfile.logicalWidth}
56+
height={screenProfile.logicalHeight}
4657
className="w-full h-full object-cover"
4758
style={{ imageRendering: "pixelated" }}
4859
/>
4960
</picture>
50-
<div className="text-[60px] text-white absolute top-0 right-0 p-4 flex flex-col items-end leading-none">
61+
<div className="text-6xl lg:text-7xl 2xl:text-8xl text-white absolute top-0 right-0 p-4 lg:p-8 2xl:p-10 flex flex-col items-end leading-none">
5162
<span className="">
5263
{new Date().toLocaleTimeString("en-GB", {
5364
timeZone: "Europe/London",
@@ -87,7 +98,7 @@ export const definition: RecipeDefinition<typeof paramsSchema> = {
8798
},
8899
paramsSchema,
89100
dataSchema,
90-
Component: ({ width, height, params }) => (
91-
<Album width={width} height={height} params={params} />
101+
Component: ({ width, height, screen, params }) => (
102+
<Album width={width} height={height} screen={screen} params={params} />
92103
),
93104
};

0 commit comments

Comments
 (0)