Add local vs R2 destination picker for admin asset uploads; v2.0.39
Publish Container / docker (push) Successful in 14m12s

Upload dropdown lets you choose R2 folder (uploads/images/guides/finished)
or local server disk. Asset table shows location column for each file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-18 12:20:21 -04:00
1 parent 4e6449f3a7
commit c79589b311
3 files changed
+35 -8

No files matched your search

+26 -1
View File
@@ -1553,6 +1553,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [assetTagEdits, setAssetTagEdits] = useState<Record<string, string>>({})
const [opsStatus, setOpsStatus] = useState<OpsStatus | null>(null)
const [assetUploadPending, setAssetUploadPending] = useState(false)
const [assetUploadDest, setAssetUploadDest] = useState<string>('r2:uploads')
const [endSeriesOpen, setEndSeriesOpen] = useState(false)
const [endSeriesNewTitle, setEndSeriesNewTitle] = useState('')
@@ -2196,10 +2197,13 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
reader.readAsDataURL(file)
})
const [storage, folder] = assetUploadDest === 'local'
? ['local', 'uploads']
: ['r2', assetUploadDest.replace('r2:', '')]
const res = await fetch('/api/admin-assets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, dataUrl }),
body: JSON.stringify({ filename: file.name, dataUrl, storage, folder }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
@@ -5781,6 +5785,19 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
<p>Upload, tag, and reuse hosted images from this domain.</p>
</div>
<div className="admin-actions admin-actions--maintenance">
<select
value={assetUploadDest}
onChange={e => setAssetUploadDest(e.target.value)}
disabled={assetUploadPending}
className="admin-select"
style={{ marginRight: '0.5rem' }}
>
<option value="r2:uploads">R2 – uploads/</option>
<option value="r2:images">R2 – images/</option>
<option value="r2:guides">R2 – guides/</option>
<option value="r2:finished">R2 – finished/</option>
<option value="local">Local (server disk)</option>
</select>
<label className="btn-admin-reset" style={{ cursor: 'pointer' }}>
{assetUploadPending ? 'Uploading…' : 'Upload Image or PDF'}
<input type="file" accept="image/*,.pdf" style={{ display: 'none' }} onChange={handleAssetUpload} disabled={assetUploadPending} />
@@ -5794,6 +5811,7 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
<tr>
<th>Preview</th>
<th>Filename</th>
<th>Location</th>
<th>Size</th>
<th>Updated</th>
<th>Tags</th>
@@ -5809,6 +5827,13 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
: <span>PDF/File</span>}
</td>
<td>{asset.filename}</td>
<td style={{ whiteSpace: 'nowrap', fontSize: '0.8em', opacity: 0.75 }}>
{(asset as { folder?: string }).folder === 'local'
? 'Local'
: (asset as { folder?: string }).folder
? `R2: ${(asset as { folder?: string }).folder}/`
: 'R2'}
</td>
<td>{(asset.sizeBytes / 1024).toFixed(1)} KB</td>
<td>{formatDate(asset.updatedAt)}</td>
<td>