This commit is contained in:
seb
2026-10-13 17:49:52 +02:00
parent 345d6f5752
commit bc583a180f
3 changed files with 930 additions and 299 deletions

View File

@@ -19,7 +19,6 @@ import ViewModuleIcon from '@mui/icons-material/ViewModule';
import CloseIcon from '@mui/icons-material/Close';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import RefreshIcon from '@mui/icons-material/Refresh';
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
@@ -29,17 +28,21 @@ import HistoryIcon from '@mui/icons-material/History';
import GridViewIcon from '@mui/icons-material/GridView';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import LayersIcon from '@mui/icons-material/Layers';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import orderAutomat, {
CHUNK_SIZE,
MAX_LRU_CHUNKS,
isOrderPaid,
isOrderDelivered,
subscribeChunkStats,
} from '../automats/orderAutomat';
import { formatPrice } from '../utils/price';
const PANEL_WIDTH = 620;
const PANEL_HEIGHT = 480;
const PANEL_WIDTH = 680;
const PANEL_HEIGHT = 520;
const pulseAnimationA = keyframes`
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(25, 118, 210, 0); }
@@ -69,29 +72,38 @@ function orderGrossTotal(lines) {
class ChunkCacheMonitor extends PureComponent {
constructor(props) {
super(props);
const initialStats = orderAutomat.getChunkStats?.() || {
chunkSize: CHUNK_SIZE,
maxLruChunks: MAX_LRU_CHUNKS,
lruChunksCount: 0,
total: 0,
chunkCount: 0,
loadedChunks: [],
inflightChunks: [],
pendingChunks: [],
loadedItemsCount: 0,
status: 'idle',
queryEpoch: 0,
filter: '',
onlyUnpaid: false,
onlyUndelivered: false,
negativeSeekMonths: 0,
items: null,
activeFilterKey: '',
activeFilterLabel: 'Standard',
filterCaches: [],
lruEntries: [],
events: [],
};
this.state = {
open: false,
position: null,
tab: 'grid', // 'grid' | 'log'
tab: 'grid', // 'grid' | 'lru' | 'log'
inspectFilterKey: null, // null means follow activeFilterKey
selectedChunk: null,
pulseCounter: 0,
stats: orderAutomat.getChunkStats?.() || {
chunkSize: CHUNK_SIZE,
total: 0,
chunkCount: 0,
loadedChunks: [],
inflightChunks: [],
pendingChunks: [],
loadedItemsCount: 0,
status: 'idle',
queryEpoch: 0,
filter: '',
onlyUnpaid: false,
onlyUndelivered: false,
negativeSeekMonths: 0,
items: null,
events: [],
},
stats: initialStats,
};
this.panelRef = createRef();
@@ -105,6 +117,8 @@ class ChunkCacheMonitor extends PureComponent {
this.handleLoadChunk = this.handleLoadChunk.bind(this);
this.handleEvictChunk = this.handleEvictChunk.bind(this);
this.handleClearCache = this.handleClearCache.bind(this);
this.handleClearFilter = this.handleClearFilter.bind(this);
this.handleApplyFilter = this.handleApplyFilter.bind(this);
this.handlePrefetchAll = this.handlePrefetchAll.bind(this);
this.handleReload = this.handleReload.bind(this);
}
@@ -152,7 +166,7 @@ class ChunkCacheMonitor extends PureComponent {
}
handlePointerDown(event) {
if (event.target.closest('button') || event.target.closest('.MuiTab-root')) return;
if (event.target.closest('button') || event.target.closest('.MuiTab-root') || event.target.closest('.MuiChip-root')) return;
const rect = this.panelRef.current?.getBoundingClientRect();
if (!rect) return;
event.preventDefault();
@@ -188,14 +202,16 @@ class ChunkCacheMonitor extends PureComponent {
}));
}
handleLoadChunk(chunkIndex, e) {
handleLoadChunk(chunkIndex, filterKey, e) {
e?.stopPropagation();
void orderAutomat.actions.requireChunk(chunkIndex, chunkIndex, { immediate: true });
const targetFilter = filterKey || this.getInspectedFilterKey();
void orderAutomat.actions.loadChunk(chunkIndex, orderAutomat.state.queryEpoch, targetFilter);
}
handleEvictChunk(chunkIndex, e) {
handleEvictChunk(chunkIndex, filterKey, e) {
e?.stopPropagation();
orderAutomat.evictChunk?.(chunkIndex);
const targetFilter = filterKey || this.getInspectedFilterKey();
orderAutomat.evictChunk?.(chunkIndex, targetFilter);
if (this.state.selectedChunk === chunkIndex) {
this.setState({ selectedChunk: null });
}
@@ -206,34 +222,145 @@ class ChunkCacheMonitor extends PureComponent {
this.setState({ selectedChunk: null });
}
handlePrefetchAll() {
orderAutomat.prefetchAllChunks?.();
handleClearFilter(filterKey, e) {
e?.stopPropagation();
orderAutomat.clearFilterCache?.(filterKey);
if (this.state.inspectFilterKey === filterKey) {
this.setState({ inspectFilterKey: null, selectedChunk: null });
}
}
handleApplyFilter(filterKey) {
const entry = this.state.stats.filterCaches?.find((f) => f.key === filterKey);
if (!entry) return;
const p = entry.params || {};
orderAutomat.actions.resetFilters();
if (p.filter) orderAutomat.actions.filter(p.filter);
if (p.onlyUnpaid) orderAutomat.actions.setOnlyUnpaid(true);
if (p.onlyUndelivered) orderAutomat.actions.setOnlyUndelivered(true);
if (p.negativeSeekMonths) orderAutomat.actions.addNegativeSeek(p.negativeSeekMonths);
this.setState({ inspectFilterKey: null, selectedChunk: null });
}
handlePrefetchAll(filterKey) {
const targetFilter = filterKey || this.getInspectedFilterKey();
orderAutomat.prefetchAllChunks?.(targetFilter);
}
handleReload() {
void orderAutomat.actions.reload();
void orderAutomat.actions.reload(false);
this.setState({ selectedChunk: null });
}
getInspectedFilterKey() {
return this.state.inspectFilterKey || this.state.stats.activeFilterKey;
}
getInspectedFilterEntry() {
const key = this.getInspectedFilterKey();
const entry = this.state.stats.filterCaches?.find((f) => f.key === key);
if (entry) return entry;
return {
key,
label: this.state.stats.activeFilterLabel || 'Aktiver Filter',
total: this.state.stats.total,
loadedChunksCount: this.state.stats.loadedChunks.length,
loadedChunks: this.state.stats.loadedChunks,
chunkCount: this.state.stats.chunkCount,
loadedItemsCount: this.state.stats.loadedItemsCount,
isActive: true,
items: this.state.stats.items,
status: this.state.stats.status,
};
}
renderFilterSelector() {
const { stats, inspectFilterKey } = this.state;
const filterCaches = stats.filterCaches || [];
const activeKey = stats.activeFilterKey;
const currentInspected = this.getInspectedFilterKey();
return (
<Box
sx={{
px: 1.5,
py: 0.75,
bgcolor: 'background.paper',
borderBottom: '1px solid',
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
gap: 1,
overflowX: 'auto',
'&::-webkit-scrollbar': { height: 4 },
'&::-webkit-scrollbar-thumb': { bgcolor: 'divider', borderRadius: 2 },
}}
>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', whiteSpace: 'nowrap' }}>
Filter-Caches ({filterCaches.length}):
</Typography>
{filterCaches.map((entry) => {
const isInspected = entry.key === currentInspected;
const isActive = entry.key === activeKey;
return (
<Chip
key={entry.key}
size="small"
variant={isInspected ? 'filled' : 'outlined'}
color={isActive ? 'primary' : isInspected ? 'secondary' : 'default'}
label={`${entry.label} (${entry.loadedChunksCount} Chunks)`}
onClick={() => this.setState({ inspectFilterKey: entry.key, selectedChunk: null })}
onDelete={!isActive ? (e) => this.handleClearFilter(entry.key, e) : undefined}
sx={{
fontWeight: isInspected ? 700 : 500,
fontSize: '0.72rem',
height: 26,
cursor: 'pointer',
whiteSpace: 'nowrap',
flexShrink: 0,
borderWidth: isInspected ? 2 : 1,
}}
/>
);
})}
</Box>
);
}
renderChunkGrid() {
const { stats, selectedChunk } = this.state;
const { total, chunkCount, loadedChunks, inflightChunks, pendingChunks, items } = stats;
const loadedSet = new Set(loadedChunks);
const inflightSet = new Set(inflightChunks);
const pendingSet = new Set(pendingChunks);
const currentFilter = this.getInspectedFilterEntry();
const isActiveFilter = currentFilter.key === stats.activeFilterKey;
const { total, chunkCount, loadedChunks, items } = currentFilter;
const loadedSet = new Set(loadedChunks || []);
const inflightSet = new Set(isActiveFilter ? stats.inflightChunks : []);
const pendingSet = new Set(isActiveFilter ? stats.pendingChunks : []);
// LRU rank lookup Map: lruKey -> rank (1..50)
const lruRankMap = new Map();
if (Array.isArray(stats.lruEntries)) {
for (const entry of stats.lruEntries) {
if (entry.filterKey === currentFilter.key) {
lruRankMap.set(entry.chunkIndex, entry.lruRank);
}
}
}
// If total is 0 and no chunks loaded, show uninitialized state
if (chunkCount === 0 && (!items || items.length === 0)) {
return (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
Automat ist noch nicht initialisiert (keine Daten geladen).
Keine Daten für diesen Filter geladen ({currentFilter.label}).
</Typography>
<Button
size="small"
variant="contained"
startIcon={<CloudDownloadIcon />}
onClick={() => this.handleLoadChunk(0)}
onClick={() => this.handleLoadChunk(0, currentFilter.key)}
>
Ersten Chunk (#0) laden
</Button>
@@ -249,11 +376,11 @@ class ChunkCacheMonitor extends PureComponent {
const isInflight = inflightSet.has(c);
const isPending = pendingSet.has(c);
const isSelected = selectedChunk === c;
const lruRank = lruRankMap.get(c);
const offsetStart = c * CHUNK_SIZE;
const offsetEnd = Math.min((c + 1) * CHUNK_SIZE - 1, Math.max(0, total - 1));
// Calculate loaded items inside this chunk
let itemsInChunk = 0;
if (Array.isArray(items)) {
for (let i = offsetStart; i <= offsetEnd; i++) {
@@ -266,20 +393,16 @@ class ChunkCacheMonitor extends PureComponent {
let borderStyle = '1px solid';
let borderColor = 'divider';
let bgColor = 'background.paper';
let statusLabel = 'Nicht geladen';
if (isLoaded) {
statusLabel = `Geladen (${itemsInChunk}/${expectedInChunk})`;
statusColor = 'success.main';
borderColor = 'success.main';
bgColor = 'rgba(46, 125, 50, 0.08)';
} else if (isInflight) {
statusLabel = 'Lädt über Netzwerk...';
statusColor = 'info.main';
borderColor = 'info.main';
bgColor = 'rgba(2, 136, 209, 0.08)';
} else if (isPending) {
statusLabel = 'Vorgemerkt (Debounce)';
statusColor = 'warning.main';
borderStyle = '1px dashed';
borderColor = 'warning.main';
@@ -289,7 +412,7 @@ class ChunkCacheMonitor extends PureComponent {
blocks.push(
<Box
key={c}
onClick={() => (isLoaded ? this.handleSelectChunk(c) : this.handleLoadChunk(c))}
onClick={() => (isLoaded ? this.handleSelectChunk(c) : this.handleLoadChunk(c, currentFilter.key))}
sx={{
position: 'relative',
p: 1,
@@ -309,7 +432,7 @@ class ChunkCacheMonitor extends PureComponent {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
minHeight: 74,
minHeight: 82,
}}
>
{/* Top Row: Chunk Number & Status Indicator */}
@@ -318,27 +441,46 @@ class ChunkCacheMonitor extends PureComponent {
#{c}
</Typography>
{isLoaded ? (
<Tooltip title="Im Cache">
<CheckCircleOutlinedIcon fontSize="small" color="success" sx={{ fontSize: 18 }} />
</Tooltip>
) : isInflight ? (
<Tooltip title="Netzwerk-Request läuft">
<CircularProgress size={14} color="info" />
</Tooltip>
) : isPending ? (
<Tooltip title="Wartet auf Debounce">
<HourglassEmptyIcon fontSize="small" color="warning" sx={{ fontSize: 16 }} />
</Tooltip>
) : (
<Tooltip title="Klicken zum Laden">
<CloudDownloadIcon fontSize="small" color="action" sx={{ fontSize: 16, opacity: 0.6 }} />
</Tooltip>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{isLoaded && lruRank != null && (
<Tooltip title={lruRank === 1 ? 'Ältester Chunk im RAM: Wird bei Überschreiten von 50 Chunks als nächstes verworfen!' : `LRU-Position #${lruRank} von ${stats.lruChunksCount}`}>
<Chip
size="small"
label={`LRU #${lruRank}`}
color={lruRank === 1 ? 'warning' : 'default'}
sx={{
height: 18,
fontSize: '0.62rem',
fontWeight: 700,
px: 0.25,
'& .MuiChip-label': { px: 0.5 },
}}
/>
</Tooltip>
)}
{isLoaded ? (
<Tooltip title="Im RAM">
<CheckCircleOutlinedIcon fontSize="small" color="success" sx={{ fontSize: 16 }} />
</Tooltip>
) : isInflight ? (
<Tooltip title="Netzwerk-Request läuft">
<CircularProgress size={14} color="info" />
</Tooltip>
) : isPending ? (
<Tooltip title="Wartet auf Debounce">
<HourglassEmptyIcon fontSize="small" color="warning" sx={{ fontSize: 16 }} />
</Tooltip>
) : (
<Tooltip title="Klicken zum Laden">
<CloudDownloadIcon fontSize="small" color="action" sx={{ fontSize: 16, opacity: 0.6 }} />
</Tooltip>
)}
</Box>
</Box>
{/* Middle: Item range index */}
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums' }}>
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', fontSize: '0.72rem' }}>
{offsetStart}–{offsetEnd}
</Typography>
@@ -350,18 +492,18 @@ class ChunkCacheMonitor extends PureComponent {
fontWeight: 600,
color: statusColor,
fontVariantNumeric: 'tabular-nums',
fontSize: '0.7rem',
fontSize: '0.68rem',
}}
>
{isLoaded ? `${itemsInChunk} Aufträge` : isInflight ? 'Laden…' : isPending ? 'Wartet…' : 'Klick: Laden'}
{isLoaded ? `${itemsInChunk}/${expectedInChunk}` : isInflight ? 'Laden…' : isPending ? 'Wartet…' : 'Klick: Laden'}
</Typography>
{isLoaded && (
<Tooltip title="Diesen Chunk aus Cache verwerfen (Evict)">
<Tooltip title="Chunk aus RAM verwerfen (Evict)">
<IconButton
size="small"
aria-label={`Chunk ${c} verwerfen`}
onClick={(e) => this.handleEvictChunk(c, e)}
onClick={(e) => this.handleEvictChunk(c, currentFilter.key, e)}
sx={{ p: 0.25, ml: 0.5 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 14 }} color="action" />
@@ -374,26 +516,70 @@ class ChunkCacheMonitor extends PureComponent {
}
return (
<Box
sx={{
p: 1.5,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(105px, 1fr))',
gap: 1.25,
overflowY: 'auto',
flex: 1,
}}
>
{blocks}
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
{/* Inactive filter inspector banner */}
{!isActiveFilter && (
<Box
sx={{
px: 1.5,
py: 0.6,
bgcolor: 'action.selected',
borderBottom: '1px solid',
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
Inspektion: {currentFilter.label}
</Typography>
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="outlined"
color="primary"
startIcon={<PlayArrowIcon sx={{ fontSize: 14 }} />}
onClick={() => this.handleApplyFilter(currentFilter.key)}
sx={{ py: 0.1, px: 1, fontSize: '0.72rem', textTransform: 'none', height: 24 }}
>
In UI anwenden
</Button>
<Button
size="small"
variant="text"
color="inherit"
onClick={() => this.setState({ inspectFilterKey: null, selectedChunk: null })}
sx={{ py: 0.1, px: 1, fontSize: '0.72rem', textTransform: 'none', height: 24 }}
>
Zum aktiven Filter
</Button>
</Stack>
</Box>
)}
<Box
sx={{
p: 1.5,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(115px, 1fr))',
gap: 1.25,
overflowY: 'auto',
flex: 1,
}}
>
{blocks}
</Box>
</Box>
);
}
renderChunkDetail() {
const { stats, selectedChunk } = this.state;
const { selectedChunk } = this.state;
if (selectedChunk === null) return null;
const { items, total } = stats;
const currentFilter = this.getInspectedFilterEntry();
const { items, total } = currentFilter;
const offsetStart = selectedChunk * CHUNK_SIZE;
const offsetEnd = Math.min((selectedChunk + 1) * CHUNK_SIZE - 1, Math.max(0, total - 1));
@@ -432,11 +618,11 @@ class ChunkCacheMonitor extends PureComponent {
<ArrowBackIcon fontSize="small" />
</IconButton>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
Chunk #{selectedChunk} Inspection
Chunk #{selectedChunk} Detail
</Typography>
<Chip
size="small"
label={`${chunkOrders.length} Einträge (Index ${offsetStart}–${offsetEnd})`}
label={`${chunkOrders.length} geladene Aufträge (Index ${offsetStart}–${offsetEnd})`}
color="primary"
variant="outlined"
/>
@@ -447,16 +633,16 @@ class ChunkCacheMonitor extends PureComponent {
size="small"
color="warning"
variant="outlined"
onClick={() => this.handleEvictChunk(selectedChunk)}
onClick={() => this.handleEvictChunk(selectedChunk, currentFilter.key)}
sx={{ textTransform: 'none', py: 0.25, fontSize: '0.75rem' }}
>
Evict (Aus Cache)
Aus Cache verwerfen
</Button>
<Button
size="small"
color="primary"
variant="contained"
onClick={() => this.handleLoadChunk(selectedChunk)}
onClick={() => this.handleLoadChunk(selectedChunk, currentFilter.key)}
sx={{ textTransform: 'none', py: 0.25, fontSize: '0.75rem' }}
>
Neu laden
@@ -534,6 +720,97 @@ class ChunkCacheMonitor extends PureComponent {
);
}
renderLruQueue() {
const { stats } = this.state;
const lruEntries = stats.lruEntries || [];
if (lruEntries.length === 0) {
return (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
Der globale LRU-Cache ist aktuell leer (keine Chunks im RAM).
</Typography>
</Box>
);
}
return (
<Box sx={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ px: 2, py: 1, bgcolor: 'action.hover', borderBottom: '1px solid', borderColor: 'divider' }}>
<Typography variant="caption" color="text.secondary">
Geordnet nach Alter (LRU). Position #1 ist der älteste Chunk und wird als nächstes automatisch verworfen, sobald das Gesamtlimit von {stats.maxLruChunks || 50} Chunks überschritten wird.
</Typography>
</Box>
<Box sx={{ flex: 1, overflowY: 'auto', p: 1.5 }}>
<Stack spacing={1}>
{lruEntries.map((entry) => {
const isOldest = entry.lruRank === 1;
const isNewest = entry.lruRank === lruEntries.length;
const offsetStart = entry.chunkIndex * CHUNK_SIZE;
const offsetEnd = (entry.chunkIndex + 1) * CHUNK_SIZE - 1;
return (
<Paper
key={entry.lruKey}
variant="outlined"
sx={{
p: 1.25,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
borderColor: isOldest ? 'warning.main' : isNewest ? 'primary.main' : 'divider',
bgcolor: isOldest ? 'rgba(237, 108, 2, 0.04)' : 'background.paper',
}}
>
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0, flex: 1 }}>
<Chip
size="small"
label={`#${entry.lruRank}`}
color={isOldest ? 'warning' : isNewest ? 'primary' : 'default'}
sx={{ fontWeight: 700, width: 44 }}
/>
{isOldest && (
<Tooltip title="Wird bei neuem Chunk als nächstes verworfen">
<WarningAmberIcon fontSize="small" color="warning" />
</Tooltip>
)}
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{entry.filterLabel}
</Typography>
<Typography variant="caption" color="text.secondary">
Chunk #{entry.chunkIndex} (Index {offsetStart}..{offsetEnd}) · {entry.orderCount} Aufträge geladen
</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums' }}>
{new Date(entry.accessedAt).toLocaleTimeString()}
</Typography>
<Tooltip title="Diesen Chunk manuell aus dem Cache entfernen">
<IconButton
size="small"
color="warning"
onClick={(e) => this.handleEvictChunk(entry.chunkIndex, entry.filterKey, e)}
>
<DeleteOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
</Paper>
);
})}
</Stack>
</Box>
</Box>
);
}
renderEventLog() {
const { stats } = this.state;
const events = stats.events || [];
@@ -555,9 +832,10 @@ class ChunkCacheMonitor extends PureComponent {
const timeStr = new Date(entry.time).toLocaleTimeString();
let chipColor = 'default';
if (entry.type === 'loaded' || entry.type === 'hit') chipColor = 'success';
if (entry.type === 'start' || entry.type === 'prefetch') chipColor = 'info';
if (entry.type === 'filter_hit') chipColor = 'secondary';
if (entry.type === 'filter_miss' || entry.type === 'start') chipColor = 'info';
if (entry.type === 'pending' || entry.type === 'debounce_fire') chipColor = 'warning';
if (entry.type === 'error') chipColor = 'error';
if (entry.type === 'lru_evict' || entry.type === 'evict' || entry.type === 'error') chipColor = 'error';
return (
<Box
@@ -596,22 +874,23 @@ class ChunkCacheMonitor extends PureComponent {
render() {
const { open, position, tab, stats, pulseCounter } = this.state;
const { total, chunkCount, loadedChunks, inflightChunks, pendingChunks, loadedItemsCount, status } = stats;
const { lruChunksCount, maxLruChunks, inflightChunks } = stats;
const loadedCount = loadedChunks.length;
const expectedChunks = Math.max(chunkCount, 1);
const progressPercent = expectedChunks > 0 ? Math.min(100, Math.round((loadedCount / expectedChunks) * 100)) : 0;
const maxLru = maxLruChunks || MAX_LRU_CHUNKS;
const lruCount = lruChunksCount || 0;
const lruPercent = Math.min(100, Math.round((lruCount / maxLru) * 100));
const hasInflight = inflightChunks.length > 0;
const filterCachesCount = stats.filterCaches?.length || 1;
return (
<>
{!open ? (
<Tooltip title="Chunk-Cache-Monitor (Alt+Shift+C)" placement="right">
<Tooltip title={`Chunk-Cache-Monitor (Alt+Shift+C) — ${lruCount}/${maxLru} Chunks im RAM`} placement="right">
<Badge
badgeContent={loadedCount}
color="success"
badgeContent={`${lruCount}/${maxLru}`}
color={lruCount >= maxLru ? 'warning' : 'success'}
overlap="circular"
max={99}
max={999}
sx={{
position: 'fixed',
left: { xs: 27, sm: 171 },
@@ -681,20 +960,26 @@ class ChunkCacheMonitor extends PureComponent {
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexGrow: 1 }}>
Chunk-Cache-Monitor
</Typography>
<Chip size="small" color="primary" variant="outlined" label="orderAutomat" />
<Chip
size="small"
color={hasInflight ? 'info' : 'default'}
label={`${loadedCount} / ${expectedChunks} Chunks`}
color={lruCount >= maxLru ? 'warning' : 'success'}
label={`LRU: ${lruCount}/${maxLru} Chunks`}
/>
<Chip
size="small"
variant="outlined"
label={`${filterCachesCount} Filter`}
/>
<Tooltip title="Prefetch All">
<IconButton size="small" aria-label="Alle Chunks vorladen" onClick={this.handlePrefetchAll}>
<Tooltip title="Prefetch All Chunks">
<IconButton size="small" aria-label="Alle Chunks vorladen" onClick={() => this.handlePrefetchAll()}>
<PlaylistAddCheckIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Cache leeren">
<Tooltip title="Gesamten Cache leeren">
<IconButton size="small" aria-label="Cache leeren" onClick={this.handleClearCache}>
<LayersClearIcon fontSize="small" />
</IconButton>
@@ -717,15 +1002,16 @@ class ChunkCacheMonitor extends PureComponent {
<Box sx={{ px: 2, py: 1, bgcolor: 'action.hover', borderBottom: '1px solid', borderColor: 'divider' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.75 }}>
<Typography variant="caption" sx={{ fontWeight: 600 }}>
Cache-Füllstand: {progressPercent}% ({loadedCount} von {expectedChunks} Chunks)
LRU-Speicher: {lruPercent}% ({lruCount} von {maxLru} Chunks belegt)
</Typography>
<Typography variant="caption" color="text.secondary">
{loadedItemsCount} von {total} Aufträgen im RAM · Chunk-Größe {CHUNK_SIZE}
Aktiver Filter: {stats.activeFilterLabel}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progressPercent}
value={lruPercent}
color={lruCount >= maxLru ? 'warning' : 'primary'}
sx={{
height: 6,
borderRadius: 3,
@@ -733,63 +1019,11 @@ class ChunkCacheMonitor extends PureComponent {
'& .MuiLinearProgress-bar': { borderRadius: 3 },
}}
/>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 1, flexWrap: 'wrap', gap: 0.5 }}>
<Chip size="small" label={`Status: ${status}`} sx={{ height: 20, fontSize: '0.7rem' }} />
{hasInflight && (
<Chip
size="small"
color="info"
icon={<CircularProgress size={12} color="inherit" />}
label={`${inflightChunks.length} lädt`}
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
{pendingChunks.length > 0 && (
<Chip
size="small"
color="warning"
label={`${pendingChunks.length} vorgemerkt`}
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
{stats.filter && (
<Chip
size="small"
variant="outlined"
label={`Filter: "${stats.filter}"`}
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
{stats.onlyUnpaid && (
<Chip
size="small"
color="warning"
variant="outlined"
label="Nur unbezahlt"
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
{stats.onlyUndelivered && (
<Chip
size="small"
color="primary"
variant="outlined"
label="Nur ungeliefert"
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
{stats.negativeSeekMonths !== 0 && (
<Chip
size="small"
variant="outlined"
label={`Seek: ${stats.negativeSeekMonths}M`}
sx={{ height: 20, fontSize: '0.7rem' }}
/>
)}
</Stack>
</Box>
{/* Filter Caches Switcher Bar */}
{this.renderFilterSelector()}
{/* View Tabs */}
<Box sx={{ borderBottom: '1px solid', borderColor: 'divider', px: 1 }}>
<Tabs
@@ -798,6 +1032,12 @@ class ChunkCacheMonitor extends PureComponent {
sx={{ minHeight: 36, '& .MuiTab-root': { minHeight: 36, py: 0.5, fontSize: '0.75rem' } }}
>
<Tab icon={<GridViewIcon fontSize="small" />} iconPosition="start" label="Chunk-Map" value="grid" />
<Tab
icon={<LayersIcon fontSize="small" />}
iconPosition="start"
label={`LRU-Queue (${lruCount}/${maxLru})`}
value="lru"
/>
<Tab
icon={<HistoryIcon fontSize="small" />}
iconPosition="start"
@@ -809,8 +1049,10 @@ class ChunkCacheMonitor extends PureComponent {
{/* Body */}
<Box sx={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{tab === 'grid' ? this.renderChunkGrid() : this.renderEventLog()}
{tab === 'grid' && this.renderChunkGrid()}
{tab === 'grid' && this.renderChunkDetail()}
{tab === 'lru' && this.renderLruQueue()}
{tab === 'log' && this.renderEventLog()}
</Box>
</Paper>
)}