dont load directory treemap by default
Continuous Integration / backend-tests (push) Successful in 48s
Continuous Integration / frontend-check (push) Successful in 25s
Continuous Integration / e2e-tests (push) Failing after 7m49s

This commit is contained in:
2026-05-01 20:10:58 -04:00
parent c56973c254
commit 901b17f7cd
10 changed files with 296 additions and 98 deletions
+75 -1
View File
@@ -567,10 +567,84 @@ def get_system_analytics(db_session: Session = Depends(get_db)):
{"path": dup[0], "size": dup[1], "copies": dup[2], "saved": dup[3]}
for dup in duplicate_offenders
],
"directories": convert_tree_to_list(nested_dir_map, 10),
}
@router.get("/directories")
def get_directory_treemap(db_session: Session = Depends(get_db)):
"""Returns directory tree data for treemap visualization."""
# Directory aggregation - same as insights but only directories
directory_aggregation_sql = text("""
SELECT
RTRIM(file_path, REPLACE(file_path, '/', '')) as dir_path,
SUM(size) as byte_total,
MAX(mtime) as latest_mtime
FROM filesystem_state
WHERE is_ignored = 0
GROUP BY dir_path
""")
all_directories = db_session.execute(directory_aggregation_sql).fetchall()
# Hierarchical tree construction
nested_dir_map = {}
for path_str, size_val, mtime_val in all_directories:
if not path_str:
continue
path_segments = [p for p in path_str.split("/") if p]
current_node = nested_dir_map
accumulated_path = ""
for segment in path_segments:
if not accumulated_path:
accumulated_path = (
"/" + segment if path_str.startswith("/") else segment
)
else:
accumulated_path += "/" + segment
if segment not in current_node:
current_node[segment] = {
"size": 0,
"mtime": 0,
"children": {},
"fullPath": accumulated_path,
}
current_node[segment]["size"] += size_val or 0
current_node[segment]["mtime"] = max(
current_node[segment]["mtime"], mtime_val or 0
)
current_node = current_node[segment]["children"]
# Collapse unhelpful single-child roots
while len(nested_dir_map) == 1:
root_key = list(nested_dir_map.keys())[0]
if not nested_dir_map[root_key]["children"]:
break
nested_dir_map = nested_dir_map[root_key]["children"]
def convert_tree_to_list(tree_dict, max_depth, current_depth=0):
if current_depth >= max_depth:
return []
output_list = []
for key, value in tree_dict.items():
children_list = convert_tree_to_list(
value["children"], max_depth, current_depth + 1
)
output_list.append(
{
"path": key,
"size": value["size"],
"mtime": value["mtime"],
"fullPath": value["fullPath"],
"children": children_list,
}
)
output_list.sort(key=lambda x: x["size"], reverse=True)
return output_list[:15]
return convert_tree_to_list(nested_dir_map, 10)
@router.get("/detect")
def detect_unregistered_media(db_session: Session = Depends(get_db)):
"""Scans all configured hardware providers for newly inserted, unregistered media."""
+10 -2
View File
@@ -46,6 +46,7 @@ class DashboardStatsSchema(BaseModel):
ignored_data_size: int
unprotected_files_count: int
unprotected_data_size: int
discrepancies_count: int
media_distribution: Dict[str, int]
last_scan_time: Optional[datetime]
redundancy_ratio: float
@@ -253,7 +254,9 @@ def get_dashboard_stats(db_session: Session = Depends(get_db)):
SELECT fv.filesystem_state_id FROM file_versions fv
JOIN storage_media sm ON sm.id = fv.media_id
WHERE sm.status IN ('active', 'full')
) THEN size ELSE 0 END) as archived_size
) THEN size ELSE 0 END) as archived_size,
SUM(CASE WHEN is_deleted = 1 THEN 1 ELSE 0 END) as missing_count,
SUM(CASE WHEN is_deleted = 1 AND missing_acknowledged_at IS NULL AND is_ignored = 0 THEN 1 ELSE 0 END) as active_discrepancies_count
FROM filesystem_state
""")
@@ -265,10 +268,14 @@ def get_dashboard_stats(db_session: Session = Depends(get_db)):
hashed_count = res[6] or 0
eligible_count = res[7] or 0
archived_size = res[8] or 0
# missing_count = res[9] or 0
active_discrepancies_count = res[10] or 0
else:
total_count = total_size = ignored_count = ignored_size = unprotected_count = (
unprotected_size
) = hashed_count = eligible_count = archived_size = 0
) = hashed_count = eligible_count = archived_size = (
active_discrepancies_count
) = 0
media_counts = {
"LTO": db_session.query(models.StorageMedia)
@@ -310,6 +317,7 @@ def get_dashboard_stats(db_session: Session = Depends(get_db)):
ignored_data_size=ignored_size,
unprotected_files_count=unprotected_count,
unprotected_data_size=unprotected_size,
discrepancies_count=active_discrepancies_count,
media_distribution=media_counts,
last_scan_time=last_scan.completed_at if last_scan else None,
redundancy_ratio=round(redundancy_percentage, 1),
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+120 -60
View File
@@ -170,6 +170,10 @@ export type DashboardStatsSchema = {
* Unprotected Data Size
*/
unprotected_data_size: number;
/**
* Discrepancies Count
*/
discrepancies_count: number;
/**
* Media Distribution
*/
@@ -684,6 +688,10 @@ export type TreeNodeSchema = {
* Has Children
*/
has_children?: boolean;
/**
* Children
*/
children?: Array<TreeNodeSchema>;
};
/**
@@ -1354,66 +1362,6 @@ export type GetSystemTreeSystemTreeGetResponses = {
export type GetSystemTreeSystemTreeGetResponse = GetSystemTreeSystemTreeGetResponses[keyof GetSystemTreeSystemTreeGetResponses];
export type GetDiscrepanciesTreeGetData = {
body?: never;
path?: never;
query?: {
/**
* Path
*/
path?: string | null;
};
url: '/system/discrepancies/tree';
};
export type GetDiscrepanciesTreeGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetDiscrepanciesTreeGetError = GetDiscrepanciesTreeGetErrors[keyof GetDiscrepanciesTreeGetErrors];
export type GetDiscrepanciesTreeGetResponses = {
/**
* Successful Response
*/
200: Array<TreeNodeSchema>;
};
export type GetDiscrepanciesTreeGetResponse = GetDiscrepanciesTreeGetResponses[keyof GetDiscrepanciesTreeGetResponses];
export type BrowseDiscrepanciesGetData = {
body?: never;
path?: never;
query?: {
/**
* Path
*/
path?: string | null;
};
url: '/system/discrepancies/browse';
};
export type BrowseDiscrepanciesGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type BrowseDiscrepanciesGetError = BrowseDiscrepanciesGetErrors[keyof BrowseDiscrepanciesGetErrors];
export type BrowseDiscrepanciesGetResponses = {
/**
* Successful Response
*/
200: BrowseResponseSchema;
};
export type BrowseDiscrepanciesGetResponse = BrowseDiscrepanciesGetResponses[keyof BrowseDiscrepanciesGetResponses];
export type ListDiscrepanciesSystemDiscrepanciesGetData = {
body?: never;
path?: never;
@@ -1557,6 +1505,34 @@ export type DismissDiscrepancySystemDiscrepanciesFileIdDismissPostResponses = {
200: unknown;
};
export type UndoDismissDiscrepancySystemDiscrepanciesFileIdUndoDismissPostData = {
body?: never;
path: {
/**
* File Id
*/
file_id: number;
};
query?: never;
url: '/system/discrepancies/{file_id}/undo-dismiss';
};
export type UndoDismissDiscrepancySystemDiscrepanciesFileIdUndoDismissPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UndoDismissDiscrepancySystemDiscrepanciesFileIdUndoDismissPostError = UndoDismissDiscrepancySystemDiscrepanciesFileIdUndoDismissPostErrors[keyof UndoDismissDiscrepancySystemDiscrepanciesFileIdUndoDismissPostErrors];
export type UndoDismissDiscrepancySystemDiscrepanciesFileIdUndoDismissPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type DeleteFileRecordSystemDiscrepanciesFileIdDeleteData = {
body?: never;
path: {
@@ -1585,6 +1561,76 @@ export type DeleteFileRecordSystemDiscrepanciesFileIdDeleteResponses = {
200: unknown;
};
export type GetDiscrepanciesTreeSystemDiscrepanciesTreeGetData = {
body?: never;
path?: never;
query?: {
/**
* Path
*
* Root path to get tree for
*/
path?: string | null;
};
url: '/system/discrepancies/tree';
};
export type GetDiscrepanciesTreeSystemDiscrepanciesTreeGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetDiscrepanciesTreeSystemDiscrepanciesTreeGetError = GetDiscrepanciesTreeSystemDiscrepanciesTreeGetErrors[keyof GetDiscrepanciesTreeSystemDiscrepanciesTreeGetErrors];
export type GetDiscrepanciesTreeSystemDiscrepanciesTreeGetResponses = {
/**
* Response Get Discrepancies Tree System Discrepancies Tree Get
*
* Successful Response
*/
200: Array<TreeNodeSchema>;
};
export type GetDiscrepanciesTreeSystemDiscrepanciesTreeGetResponse = GetDiscrepanciesTreeSystemDiscrepanciesTreeGetResponses[keyof GetDiscrepanciesTreeSystemDiscrepanciesTreeGetResponses];
export type BrowseDiscrepanciesSystemDiscrepanciesBrowseGetData = {
body?: never;
path?: never;
query?: {
/**
* Path
*
* Directory path to browse
*/
path?: string | null;
};
url: '/system/discrepancies/browse';
};
export type BrowseDiscrepanciesSystemDiscrepanciesBrowseGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type BrowseDiscrepanciesSystemDiscrepanciesBrowseGetError = BrowseDiscrepanciesSystemDiscrepanciesBrowseGetErrors[keyof BrowseDiscrepanciesSystemDiscrepanciesBrowseGetErrors];
export type BrowseDiscrepanciesSystemDiscrepanciesBrowseGetResponses = {
/**
* Response Browse Discrepancies System Discrepancies Browse Get
*
* Successful Response
*/
200: {
[key: string]: unknown;
};
};
export type BrowseDiscrepanciesSystemDiscrepanciesBrowseGetResponse = BrowseDiscrepanciesSystemDiscrepanciesBrowseGetResponses[keyof BrowseDiscrepanciesSystemDiscrepanciesBrowseGetResponses];
export type ListStorageProvidersInventoryProvidersGetData = {
body?: never;
path?: never;
@@ -1788,6 +1834,20 @@ export type GetSystemAnalyticsInventoryInsightsGetResponses = {
200: unknown;
};
export type GetDirectoryTreemapInventoryDirectoriesGetData = {
body?: never;
path?: never;
query?: never;
url: '/inventory/directories';
};
export type GetDirectoryTreemapInventoryDirectoriesGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type DetectUnregisteredMediaInventoryDetectGetData = {
body?: never;
path?: never;
@@ -24,8 +24,8 @@
getArchiveTreeInventoryTreeGet,
browseSystemPathSystemBrowseGet,
browseArchiveIndexInventoryBrowseGet,
getDiscrepanciesTreeGet,
browseDiscrepanciesGet,
getDiscrepanciesTreeSystemDiscrepanciesTreeGet,
browseDiscrepanciesSystemDiscrepanciesBrowseGet,
} from "$lib/api";
let {
@@ -198,7 +198,7 @@
onMount(async () => {
if (mode === "discrepancies") {
try {
const response = await getDiscrepanciesTreeGet({ query: { path: "ROOT" } });
const response = await getDiscrepanciesTreeSystemDiscrepanciesTreeGet({ query: { path: "ROOT" } });
if (response.data && Array.isArray(response.data)) {
discrepancyRoot.children = response.data.map((d: any) => ({
name: d.name,
@@ -5,7 +5,7 @@
import type { TreeNode } from "$lib/types";
import { cn } from "$lib/utils";
import FileBrowserTreeItem from "./FileBrowserTreeItem.svelte";
import { getSystemTreeSystemTreeGet, getArchiveTreeInventoryTreeGet, getDiscrepanciesTreeGet } from "$lib/api";
import { getSystemTreeSystemTreeGet, getArchiveTreeInventoryTreeGet, getDiscrepanciesTreeSystemDiscrepanciesTreeGet } from "$lib/api";
let {
node,
@@ -62,7 +62,7 @@
try {
let response;
if (mode === "discrepancies") {
response = await getDiscrepanciesTreeGet({
response = await getDiscrepanciesTreeSystemDiscrepanciesTreeGet({
query: { path: node.path }
});
} else {
+8
View File
@@ -162,6 +162,14 @@
<p class="text-[10px] font-medium text-text-secondary uppercase opacity-40">Pending archival</p>
</div>
</div>
<div class="space-y-4">
<div class="flex flex-col gap-1.5">
<span class="text-xs text-text-secondary opacity-60 block">Missing files</span>
<h4 class="text-3xl font-bold text-orange-400 mono tabular-nums">{(stats.discrepancies_count || 0).toLocaleString()}</h4>
<p class="text-[10px] font-medium text-text-secondary uppercase opacity-40">Unresolved discrepancies</p>
</div>
</div>
</div>
<div class="mt-8 pt-6 border-t border-border-color/30 grid grid-cols-2 gap-6">
@@ -14,8 +14,8 @@
batchHardDeleteSystemDiscrepanciesBatchDeletePost,
addFileToRecoveryQueueRestoresQueueFileFileIdPost,
batchAddToRecoveryQueueRestoresQueueBatchPost,
browseDiscrepanciesGet,
getDiscrepanciesTreeGet,
browseDiscrepanciesSystemDiscrepanciesBrowseGet,
getDiscrepanciesTreeSystemDiscrepanciesTreeGet,
type DiscrepancySchema,
} from '$lib/api';
import { type FileItem } from '$lib/types';
@@ -47,9 +47,9 @@
async function loadFiles(path: string) {
try {
const response = await browseDiscrepanciesGet({ query: { path } });
if (response.data?.files) {
files = response.data.files.map((d: any) => {
const response = await browseDiscrepanciesSystemDiscrepanciesBrowseGet({ query: { path } });
if (response.data && (response.data as any).files) {
files = (response.data as any).files.map((d: any) => {
// Check if it's a directory (has "type" property) or a file (has "id")
if (d.type === 'directory') {
// It's a directory
+36 -2
View File
@@ -18,13 +18,16 @@
import StatCard from '$lib/components/ui/StatCard.svelte';
import ProgressBar from '$lib/components/ui/ProgressBar.svelte';
import Treemap from '$lib/components/Treemap.svelte';
import { getSystemAnalyticsInventoryInsightsGet } from '$lib/api';
import { getSystemAnalyticsInventoryInsightsGet, getDirectoryTreemapInventoryDirectoriesGet } from '$lib/api';
import { cn, formatSize } from '$lib/utils';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
let insights = $state<any>(null);
let loading = $state(true);
let dirTreemapLoaded = $state(false);
let dirTreemapLoading = $state(false);
let dirTreemapData = $state<any[]>([]);
async function loadInsights() {
loading = true;
@@ -38,6 +41,22 @@
}
}
async function loadDirTreemap() {
if (dirTreemapLoaded) return;
dirTreemapLoading = true;
try {
const response = await getDirectoryTreemapInventoryDirectoriesGet();
if (response.data) {
dirTreemapData = mapDirectoryTree(response.data as any[]);
dirTreemapLoaded = true;
}
} catch (error) {
toast.error("Failed to load directory treemap");
} finally {
dirTreemapLoading = false;
}
}
function mapDirectoryTree(nodes: any[]): any[] {
if (!nodes) return [];
return nodes.filter((n: any) => n.size > 0).map((n: any) => ({
@@ -150,14 +169,29 @@
<!-- DIRECTORY BREAKDOWN (Full Width) -->
<Card class="p-5 shadow-xl flex flex-col gap-8 lg:col-span-2">
<div class="flex items-center justify-between">
<SectionHeader title="Space by directory" icon={FolderTree} iconColor="text-emerald-500" />
{#if !dirTreemapLoaded}
<Button variant="outline" size="sm" onclick={loadDirTreemap} disabled={dirTreemapLoading}>
{dirTreemapLoading ? 'Loading...' : 'Load directory treemap'}
</Button>
{/if}
</div>
{#if dirTreemapLoaded}
<div class="flex-1 flex flex-col min-h-0 min-h-[500px]">
<Treemap
items={mapDirectoryTree(insights.directories)}
items={dirTreemapData}
onSelect={handleDirectorySelect}
/>
</div>
{:else}
<div class="flex-1 flex items-center justify-center min-h-[300px] border-2 border-dashed border-border-color rounded-xl">
<Button variant="outline" onclick={loadDirTreemap} disabled={dirTreemapLoading}>
{dirTreemapLoading ? 'Loading...' : 'Click to load directory treemap'}
</Button>
</div>
{/if}
</Card>
</div>
</div>