fix: singing bowls ambient + Next.js 16 async params/cookies

Ambient:
- Replace oscillator drone/melody with singing bowl synthesis
- Each bowl: instant attack + exponential decay (7–12s) = real physical sound
- Two detuned copies per strike for natural shimmer beat
- A-minor pentatonic (A3 C4 D4 E4 G4 A4 C5 E5), weighted toward warm lows
- 30% chance of soft harmonic fifth companion tone per strike
- Random gaps 3–8s between strikes so it breathes naturally
- Two long hall reverb tails (2.4s / 4.8s) for warmth and space
- Graceful 2.5s fade-out on stop

Next.js 16 compatibility (breaking changes from v14→v16):
- Dynamic route params now Promise<{...}> → await params in all handlers
- cookies() now returns Promise → isAdmin() made async, await cookies()
- Files: [...path], memories/[id], media/[id], auth, upload all updated

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-02-16 02:44:45 +01:00
parent 00abbfda51
commit 313b5ff7fd
7 changed files with 113 additions and 166 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ function getExpectedToken() {
}
export async function GET() {
const cookieStore = cookies()
const cookieStore = await cookies()
const token = cookieStore.get('admin_auth')?.value
return NextResponse.json({ authed: token === getExpectedToken() })
}
+3 -2
View File
@@ -25,9 +25,10 @@ const MIME: Record<string, string> = {
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
{ params }: { params: Promise<{ path: string[] }> }
) {
const filePath = path.join(DATA_DIR, 'uploads', ...params.path)
const { path: segments } = await params
const filePath = path.join(DATA_DIR, 'uploads', ...segments)
// Prevent path traversal
const uploadsBase = path.join(DATA_DIR, 'uploads')
+8 -7
View File
@@ -6,8 +6,9 @@ import { unlink } from 'fs/promises'
import { existsSync } from 'fs'
import path from 'path'
function isAdmin() {
const token = cookies().get('admin_auth')?.value
async function isAdmin() {
const cookieStore = await cookies()
const token = cookieStore.get('admin_auth')?.value
const expected = createHash('sha256')
.update(process.env.ADMIN_PASSWORD || 'change-me')
.digest('hex')
@@ -18,14 +19,15 @@ const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data')
export async function DELETE(
_req: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
if (!isAdmin()) {
if (!await isAdmin()) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const db = getDb()
const media = db.prepare('SELECT * FROM media WHERE id = ?').get(params.id) as
const media = db.prepare('SELECT * FROM media WHERE id = ?').get(id) as
| { filename: string }
| undefined
@@ -33,12 +35,11 @@ export async function DELETE(
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
// Delete file from disk
const filePath = path.join(DATA_DIR, 'uploads', media.filename)
if (existsSync(filePath)) {
await unlink(filePath)
}
db.prepare('DELETE FROM media WHERE id = ?').run(params.id)
db.prepare('DELETE FROM media WHERE id = ?').run(id)
return NextResponse.json({ success: true })
}
+12 -9
View File
@@ -3,8 +3,9 @@ import { getDb } from '@/lib/db'
import { cookies } from 'next/headers'
import { createHash } from 'crypto'
function isAdmin() {
const token = cookies().get('admin_auth')?.value
async function isAdmin() {
const cookieStore = await cookies()
const token = cookieStore.get('admin_auth')?.value
const expected = createHash('sha256')
.update(process.env.ADMIN_PASSWORD || 'change-me')
.digest('hex')
@@ -13,30 +14,32 @@ function isAdmin() {
export async function PUT(
req: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
if (!isAdmin()) {
if (!await isAdmin()) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const { title, content } = await req.json()
const db = getDb()
db.prepare(
"UPDATE memories SET title = ?, content = ?, updated_at = datetime('now') WHERE id = ?"
).run(title, content, params.id)
const memory = db.prepare('SELECT * FROM memories WHERE id = ?').get(params.id)
).run(title, content, id)
const memory = db.prepare('SELECT * FROM memories WHERE id = ?').get(id)
return NextResponse.json(memory)
}
export async function DELETE(
_req: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
if (!isAdmin()) {
if (!await isAdmin()) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const db = getDb()
db.prepare('DELETE FROM memories WHERE id = ?').run(params.id)
db.prepare('DELETE FROM memories WHERE id = ?').run(id)
return NextResponse.json({ success: true })
}
-10
View File
@@ -1,15 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db'
import { cookies } from 'next/headers'
import { createHash } from 'crypto'
function isAdmin() {
const token = cookies().get('admin_auth')?.value
const expected = createHash('sha256')
.update(process.env.ADMIN_PASSWORD || 'change-me')
.digest('hex')
return token === expected
}
export async function GET() {
const db = getDb()
+4 -5
View File
@@ -8,8 +8,9 @@ import { getDb } from '@/lib/db'
export const runtime = 'nodejs'
export const maxDuration = 60
function isAdmin() {
const token = cookies().get('admin_auth')?.value
async function isAdmin() {
const cookieStore = await cookies()
const token = cookieStore.get('admin_auth')?.value
const expected = createHash('sha256')
.update(process.env.ADMIN_PASSWORD || 'change-me')
.digest('hex')
@@ -46,7 +47,7 @@ const FOLDER_TO_TYPE: Record<string, 'photo' | 'video' | 'music'> = {
}
export async function POST(req: NextRequest) {
if (!isAdmin()) {
if (!await isAdmin()) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
@@ -58,11 +59,9 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Keine Datei' }, { status: 400 })
}
// Try to detect type from mime or extension
let mimeType = file.type?.toLowerCase() || ''
const ext = path.extname(file.name).toLowerCase()
// iOS HEIC fallback
if (!mimeType && (ext === '.heic' || ext === '.heif')) {
mimeType = 'image/heic'
}