fix: instance detail crash + add play/stop/redeploy/remove actions
Fixes: 1. useFetch: add credentials:'same-origin' — cookie wasn't being sent with API calls, causing 401 → React error boundary → 'Etwas ist schiefgelaufen' 2. Instance detail Sheet now loads correctly (cookie sent with request) New instance actions (UI + API): - Redeploy: triggers Dokploy application.redeploy (rebuilds from latest commit) - Stop: triggers Dokploy application.stop (pauses container) - Start: triggers Dokploy application.start (resumes container) - Remove: existing delete (SlideCommit confirmation) Dokploy deploy fix: - deployApplication now calls markRunning before deploy - This ensures Dokploy creates a deployment job ActionsRow redesigned: - demo_active/active: [Redeploy] [Stop] [Demo +7d] [Remove] - grace: [Start] [Remove] - failed: [Retry] [Remove] - provisioning/deleting/deleted: no actions
This commit is contained in:
1 parent
590310ca95
commit
32a8b01845
65 files changed
-103
No files matched your search
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -1,103 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server'
|
|
||||||
import { db } from '@/lib/db'
|
|
||||||
import { storeFile, detectContentType } from '@/lib/storage'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { checkRateLimit } from '@/lib/rate-limit'
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
export const maxDuration = 30
|
|
||||||
|
|
||||||
// POST /api/aos/belege/upload
|
|
||||||
// Upload a receipt file with content-addressed storage (PRD §5.5 + §5.7 Belege).
|
|
||||||
// Accepts multipart/form-data with file + metadata.
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
try {
|
|
||||||
const rateLimited = checkRateLimit(req, 'public')
|
|
||||||
if (rateLimited) return rateLimited
|
|
||||||
|
|
||||||
const formData = await req.formData()
|
|
||||||
const file = formData.get('file') as File | null
|
|
||||||
const type = formData.get('type') as string | null
|
|
||||||
const vendor = formData.get('vendor') as string | null
|
|
||||||
const invoiceNumber = formData.get('invoiceNumber') as string | null
|
|
||||||
const amountCentsStr = formData.get('amountCents') as string | null
|
|
||||||
const bookingDateStr = formData.get('bookingDate') as string | null
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return NextResponse.json({ error: 'file required (multipart/form-data, field name: file)' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content
|
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
|
||||||
const buffer = Buffer.from(arrayBuffer)
|
|
||||||
|
|
||||||
// Detect content type from magic bytes
|
|
||||||
const detectedType = detectContentType(buffer)
|
|
||||||
const fileName = file.name
|
|
||||||
const fileType = file.type || detectedType
|
|
||||||
const fileSizeBytes = buffer.length
|
|
||||||
|
|
||||||
// Store file (content-addressed, dedup, atomic write)
|
|
||||||
const stored = await storeFile(buffer, fileType)
|
|
||||||
|
|
||||||
// Check for duplicate (same hash = same file already registered as Beleg)
|
|
||||||
const existing = await db.beleg.findUnique({ where: { fileHash: stored.hash } })
|
|
||||||
if (existing) {
|
|
||||||
return NextResponse.json({
|
|
||||||
beleg: existing,
|
|
||||||
duplicate: true,
|
|
||||||
message: 'File already registered (same SHA-256 hash)',
|
|
||||||
}, { status: 409 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Beleg record
|
|
||||||
const count = await db.beleg.count()
|
|
||||||
const number = `BEL-2026-${String(count + 1).padStart(4, '0')}`
|
|
||||||
const belegType = type || 'sonstiges'
|
|
||||||
|
|
||||||
// GoBD retention: 8 years for invoices, 6 for business letters
|
|
||||||
const retentionUntil = new Date()
|
|
||||||
retentionUntil.setFullYear(retentionUntil.getFullYear() + (belegType.includes('rechnung') ? 8 : 6))
|
|
||||||
|
|
||||||
const beleg = await db.beleg.create({
|
|
||||||
data: {
|
|
||||||
number,
|
|
||||||
type: belegType,
|
|
||||||
fileHash: stored.hash,
|
|
||||||
fileName,
|
|
||||||
fileType,
|
|
||||||
fileSizeBytes,
|
|
||||||
vendor: vendor ?? null,
|
|
||||||
invoiceNumber: invoiceNumber ?? null,
|
|
||||||
amountCents: amountCentsStr ? parseInt(amountCentsStr) : null,
|
|
||||||
bookingDate: bookingDateStr ? new Date(bookingDateStr) : null,
|
|
||||||
retentionUntil,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await db.auditLog.create({
|
|
||||||
data: {
|
|
||||||
actor: 'nora@nordlicht.digital',
|
|
||||||
action: 'beleg.upload',
|
|
||||||
entity: 'Beleg',
|
|
||||||
entityId: beleg.id,
|
|
||||||
after: JSON.stringify({ number, type: belegType, fileName, hash: stored.hash.slice(0, 16) + '…', sizeBytes: fileSizeBytes }),
|
|
||||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
beleg,
|
|
||||||
storage: {
|
|
||||||
hash: stored.hash,
|
|
||||||
sizeBytes: stored.size,
|
|
||||||
contentType: stored.contentType,
|
|
||||||
detectedType,
|
|
||||||
deduped: stored.size !== fileSizeBytes, // if size differs, existing file was returned
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
|
||||||
return NextResponse.json({ error: msg }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Reference in new issue
Block a user