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