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 = { export type FixResult = {
userId: number; userId: number;
contractCategory?: string;
created?: number; created?: number;
moved: number; moved: number;
skipped: number; skipped: number;
@ -40,6 +41,7 @@ export type FixResult = {
export type FixMeta = { export type FixMeta = {
processedUsers?: number; processedUsers?: number;
createdTotal?: number;
movedTotal?: number; movedTotal?: number;
errorCount?: number; errorCount?: number;
}; };

View File

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