feat: enhance dev management page with status updates for structure, loose files, and ghost directories

This commit is contained in:
seaznCode 2026-01-20 20:49:11 +01:00
parent 2a05dfa853
commit fe0a5d079b
2 changed files with 96 additions and 30 deletions

View File

@ -32,6 +32,7 @@ export type GhostDirectory = {
export type FixResult = {
userId: number;
contractCategory?: string;
created?: number;
moved: number;
skipped: number;
@ -40,6 +41,7 @@ export type FixResult = {
export type FixMeta = {
processedUsers?: number;
createdTotal?: number;
movedTotal?: number;
errorCount?: number;
};

View File

@ -63,6 +63,11 @@ export default function DevManagementPage() {
const [structureActionResults, setStructureActionResults] = React.useState<FixResult[]>([])
const [looseActionMeta, setLooseActionMeta] = React.useState<{ processedUsers?: number; movedTotal?: number; errorCount?: number } | null>(null)
const [looseActionResults, setLooseActionResults] = React.useState<FixResult[]>([])
const [structureStatus, setStructureStatus] = React.useState('')
const [looseStatus, setLooseStatus] = React.useState('')
const [ghostStatus, setGhostStatus] = React.useState('')
const formatNow = () => new Date().toLocaleString()
const runImport = async () => {
setError('')
@ -93,6 +98,7 @@ export default function DevManagementPage() {
const loadStructureIssues = async () => {
setExoscaleError('')
setExoscaleLoading(true)
setStructureStatus('Refreshing list...')
try {
const res = await listFolderStructureIssues()
if (!res.ok) {
@ -101,6 +107,7 @@ export default function DevManagementPage() {
}
setStructureUsers(res.data || [])
setStructureMeta(res.meta || null)
setStructureStatus(`Last refresh: ${formatNow()}`)
} finally {
setExoscaleLoading(false)
}
@ -109,6 +116,7 @@ export default function DevManagementPage() {
const loadLooseFiles = async () => {
setExoscaleError('')
setExoscaleLoading(true)
setLooseStatus('Refreshing list...')
try {
const res = await listLooseFiles()
if (!res.ok) {
@ -117,6 +125,7 @@ export default function DevManagementPage() {
}
setLooseUsers(res.data || [])
setLooseMeta(res.meta || null)
setLooseStatus(`Last refresh: ${formatNow()}`)
} finally {
setExoscaleLoading(false)
}
@ -125,6 +134,7 @@ export default function DevManagementPage() {
const loadGhostDirectories = async () => {
setExoscaleError('')
setExoscaleLoading(true)
setGhostStatus('Refreshing list...')
try {
const res = await listGhostDirectories()
if (!res.ok) {
@ -133,6 +143,7 @@ export default function DevManagementPage() {
}
setGhostDirs(res.data || [])
setGhostMeta(res.meta || null)
setGhostStatus(`Last refresh: ${formatNow()}`)
} finally {
setExoscaleLoading(false)
}
@ -140,24 +151,40 @@ export default function DevManagementPage() {
const runCreateStructure = async (userId?: number) => {
setExoscaleError('')
if (userId) setFixingUserId(userId)
else setFixingAll(true)
if (userId) {
setFixingUserId(userId)
setStructureStatus(`Creating folders for #${userId}...`)
} else {
setFixingAll(true)
setStructureStatus('Creating folders for each user...')
setStructureActionResults([])
setStructureActionMeta({ processedUsers: 0, createdTotal: 0, errorCount: 0 })
}
try {
const res = await createFolderStructure(userId)
const targets = userId ? [{ userId }] : structureUsers.map(u => ({ userId: u.userId }))
for (const target of targets) {
const res = await createFolderStructure(target.userId)
if (!res.ok) {
setExoscaleError(res.message || 'Failed to create folder structure.')
return
break
}
setStructureActionMeta(res.meta || null)
setStructureActionResults(res.data || [])
const batch = res.data || []
setStructureActionResults(prev => [...prev, ...batch])
setStructureActionMeta(prev => ({
processedUsers: (prev?.processedUsers || 0) + (res.meta?.processedUsers || 0),
createdTotal: (prev?.createdTotal || 0) + (res.meta?.createdTotal || 0),
errorCount: (prev?.errorCount || 0) + (res.meta?.errorCount || 0)
}))
setFixResults(prev => {
const next = { ...prev }
for (const item of res.data || []) {
for (const item of batch) {
next[item.userId] = item
}
return next
})
setStructureStatus(`Last create: ${formatNow()} (processed #${target.userId})`)
}
await loadStructureIssues()
} finally {
setFixingUserId(null)
@ -167,24 +194,40 @@ export default function DevManagementPage() {
const runMoveLooseFiles = async (userId?: number) => {
setExoscaleError('')
if (userId) setFixingUserId(userId)
else setFixingAll(true)
if (userId) {
setFixingUserId(userId)
setLooseStatus(`Moving loose files for #${userId}...`)
} else {
setFixingAll(true)
setLooseStatus('Moving loose files for each user...')
setLooseActionResults([])
setLooseActionMeta({ processedUsers: 0, movedTotal: 0, errorCount: 0 })
}
try {
const res = await moveLooseFilesToContract(userId)
const targets = userId ? [{ userId }] : looseUsers.map(u => ({ userId: u.userId }))
for (const target of targets) {
const res = await moveLooseFilesToContract(target.userId)
if (!res.ok) {
setExoscaleError(res.message || 'Failed to move loose files.')
return
break
}
setLooseActionMeta(res.meta || null)
setLooseActionResults(res.data || [])
const batch = res.data || []
setLooseActionResults(prev => [...prev, ...batch])
setLooseActionMeta(prev => ({
processedUsers: (prev?.processedUsers || 0) + (res.meta?.processedUsers || 0),
movedTotal: (prev?.movedTotal || 0) + (res.meta?.movedTotal || 0),
errorCount: (prev?.errorCount || 0) + (res.meta?.errorCount || 0)
}))
setFixResults(prev => {
const next = { ...prev }
for (const item of res.data || []) {
for (const item of batch) {
next[item.userId] = item
}
return next
})
setLooseStatus(`Last move: ${formatNow()} (processed #${target.userId})`)
}
await loadLooseFiles()
} finally {
setFixingUserId(null)
@ -192,6 +235,12 @@ export default function DevManagementPage() {
}
}
React.useEffect(() => {
if (activeTab === 'structure') loadStructureIssues()
if (activeTab === 'loose') loadLooseFiles()
if (activeTab === 'ghost') loadGhostDirectories()
}, [activeTab])
const onImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
@ -374,6 +423,9 @@ export default function DevManagementPage() {
<p className="text-sm text-gray-600">
Ensures both contract and gdpr folders exist for each user.
</p>
{structureStatus && (
<div className="text-xs text-slate-500">{structureStatus}</div>
)}
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<button
@ -461,6 +513,9 @@ export default function DevManagementPage() {
{structureActionResults.map(item => (
<div key={item.userId} className="flex flex-wrap gap-2">
<span className="font-semibold text-blue-900">#{item.userId}</span>
{item.contractCategory && (
<span className="text-gray-500">{item.contractCategory}</span>
)}
<span>Created: {item.created ?? 0}</span>
{item.errors && item.errors.length > 0 && (
<span className="text-red-700">Errors: {item.errors.length}</span>
@ -482,6 +537,9 @@ export default function DevManagementPage() {
<p className="text-sm text-gray-600">
Shows files directly under the user folder that are not in contract or gdpr.
</p>
{looseStatus && (
<div className="text-xs text-slate-500">{looseStatus}</div>
)}
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<button
@ -575,6 +633,9 @@ export default function DevManagementPage() {
{looseActionResults.map(item => (
<div key={item.userId} className="flex flex-wrap gap-2">
<span className="font-semibold text-blue-900">#{item.userId}</span>
{item.contractCategory && (
<span className="text-gray-500">{item.contractCategory}</span>
)}
<span>Moved: {item.moved}</span>
<span>Skipped: {item.skipped}</span>
{item.errors && item.errors.length > 0 && (
@ -597,6 +658,9 @@ export default function DevManagementPage() {
<p className="text-sm text-gray-600">
Exoscale directories that do not have a matching user in the database.
</p>
{ghostStatus && (
<div className="text-xs text-slate-500">{ghostStatus}</div>
)}
</div>
<button
onClick={loadGhostDirectories}