diff --git a/Dockerfile b/Dockerfile index a37a48c8..9f5a49cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ RUN apk upgrade --no-cache && \ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +RUN mkdir -p /app/data/settings /app/data/admin && chown -R nextjs:nodejs /app/data USER nextjs EXPOSE 3000 ENV PORT=3000 diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 1c96b622..7b230da3 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -6,6 +6,46 @@ import { sessionCookieName } from '@/lib/auth/session-cookie'; import { saveUserSettings, loadUserSettings, deleteUserSettings } from '@/lib/settings-sync'; import { configManager } from '@/lib/admin/config-manager'; +function classifyError(error: unknown): { message: string; status: number } { + const code = (error as NodeJS.ErrnoException).code; + const msg = error instanceof Error ? error.message : 'Unknown error'; + + switch (code) { + case 'EACCES': + case 'EPERM': + return { + message: 'Write permission denied on settings data directory. Check filesystem permissions for the SETTINGS_DATA_DIR (or data/settings/).', + status: 500, + }; + case 'EROFS': + return { + message: 'Filesystem is read-only. Settings cannot be saved. Ensure the data directory is on a writable volume.', + status: 500, + }; + case 'ENOSPC': + return { + message: 'No disk space available to save settings.', + status: 507, + }; + case 'ENOENT': + return { + message: 'Settings data directory does not exist and could not be created. Check SETTINGS_DATA_DIR configuration.', + status: 500, + }; + default: + if (msg.includes('SESSION_SECRET')) { + return { + message: 'Server configuration error: SESSION_SECRET is not set.', + status: 500, + }; + } + return { + message: `Internal server error: ${msg}`, + status: 500, + }; + } +} + function isEnabled(): boolean { return process.env.SETTINGS_SYNC_ENABLED === 'true' && !!process.env.SESSION_SECRET; } @@ -59,7 +99,8 @@ export async function GET(request: NextRequest) { const message = error instanceof Error ? error.message : 'Unknown error'; const code = (error as NodeJS.ErrnoException).code; logger.error('Settings load error', { error: message, code }); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + const classified = classifyError(error); + return NextResponse.json({ error: classified.message }, { status: classified.status }); } } @@ -99,7 +140,8 @@ export async function POST(request: NextRequest) { const message = error instanceof Error ? error.message : 'Unknown error'; const code = (error as NodeJS.ErrnoException).code; logger.error('Settings save error', { error: message, code }); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + const classified = classifyError(error); + return NextResponse.json({ error: classified.message }, { status: classified.status }); } } @@ -121,7 +163,10 @@ export async function DELETE(request: NextRequest) { await deleteUserSettings(username, serverUrl); return NextResponse.json({ ok: true }); } catch (error) { - logger.error('Settings delete error', { error: error instanceof Error ? error.message : 'Unknown error' }); - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + const message = error instanceof Error ? error.message : 'Unknown error'; + const code = (error as NodeJS.ErrnoException).code; + logger.error('Settings delete error', { error: message, code }); + const classified = classifyError(error); + return NextResponse.json({ error: classified.message }, { status: classified.status }); } } diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 3916b654..f89643dc 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -510,7 +510,8 @@ export const useSettingsStore = create()( }, }); if (!res.ok) { - syncLog('Settings fetch failed (status', res.status + ')'); + const body = await res.json().catch(() => ({})); + syncLog('Settings fetch failed:', body.error || `status ${res.status}`); return false; } const { settings } = await res.json(); @@ -647,11 +648,13 @@ if (typeof window !== 'undefined') { syncWarn('Settings sync endpoint returned 404, disabling sync'); syncEnabled = false; } else if (res.status >= 500 && retries > 0) { - syncWarn('Settings sync got server error, retrying...'); + const body = await res.json().catch(() => ({})); + syncWarn('Settings sync got server error:', body.error || `status ${res.status}`, '- retrying...'); await new Promise((r) => setTimeout(r, 2000)); return syncToServer(retries - 1); } else if (!res.ok) { - syncError('Settings sync failed with status', res.status); + const body = await res.json().catch(() => ({})); + syncError('Settings sync failed:', body.error || `status ${res.status}`); } else { syncLog('Settings synced to server successfully'); }