diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js
index edeb6a86..e5c822f8 100644
--- a/app/src/components/Archive.js
+++ b/app/src/components/Archive.js
@@ -10,9 +10,14 @@ import {
GridRow,
Image,
Input,
+ PlaceholderHeader,
+ PlaceholderLine,
+ StatisticLabel,
+ StatisticValue,
TableCell,
TextArea,
} from "semantic-ui-react";
+import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import {
APIButton,
BackButton,
@@ -23,6 +28,7 @@ import {
FileIcon,
findPosterPath,
humanFileSize,
+ humanNumber,
InfoHeader,
isoDatetimeToAgoPopup,
mimetypeColor,
@@ -48,12 +54,12 @@ import {
import {CollectionTagModal} from "./collections/CollectionTagModal";
import {Link, Route, Routes, useLocation, useNavigate, useParams} from "react-router";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
-import {useArchive, useDomain, useDomains, useOneQuery, useSearchArchives, useSearchOrder} from "../hooks/customHooks";
+import {useArchive, useDomain, useDomains, useOneQuery, useSearchArchives, useSearchDomain, useSearchOrder} from "../hooks/customHooks";
import {FileCards, FileRowTagIcon, FilesView} from "./Files";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import _ from "lodash";
import {Media, ThemeContext} from "../contexts/contexts";
-import {Button, Card, darkTheme, Header, Loader, Popup, Segment, Tab} from "./Theme";
+import {Button, Card, darkTheme, Header, Loader, Placeholder, Popup, Segment, Statistic, Tab} from "./Theme";
import {taggedImageLabel, TagsSelector} from "../Tags";
import {toast} from "react-semantic-toasts-2";
import {API_ARCHIVE_UPLOAD_URI, Downloaders} from "./Vars";
@@ -384,6 +390,32 @@ const DOMAIN_ROUTES = {
searchParam: 'domain'
};
+function DomainStatistics({statistics}) {
+ if (!statistics) {
+ return <>>
+ }
+
+ return
+
+
+ {statistics.archive_count}
+ Archives
+
+
+ {humanFileSize(statistics.size, true)}
+ Total Size
+
+
+ {humanFileSize(statistics.largest_archive, true)}
+ Largest Archive
+
+
+ {humanNumber(statistics.archive_tags)}
+ Archive Tags
+
+
+}
+
export function DomainsPage() {
useTitle('Archive Domains');
@@ -565,6 +597,10 @@ export function DomainEditPage() {
return <>
+
+
+
+
+
+ {domain && domain.statistics && }
>;
}
@@ -666,7 +704,16 @@ function ArchiveSettingsPage() {
function ArchivesPage() {
const [selectedArchives, setSelectedArchives] = useState([]);
- useTitle('Archives');
+ const {domain, domains} = useSearchDomain();
+
+ // Find the domain object from the domains list to get the ID for the edit link
+ const domainObj = domain && domains ? domains.find(d => d.domain === domain) : null;
+
+ let title = 'Archives';
+ if (domainObj && domainObj.domain) {
+ title = `${domainObj.domain} Archives`;
+ }
+ useTitle(title);
const {
archives,
@@ -763,7 +810,29 @@ function ArchivesPage() {
placeholder='Search Archives...'
/>;
+ // Domain header with edit link when filtering by domain
+ let header;
+ if (domainObj && domainObj.domain) {
+ const editLink = `/archive/domain/${domainObj.id}/edit`;
+ header = <>
+
+ {domainObj.domain}
+
+
+
+
+ >;
+ } else if (domain) {
+ // Domain filter is set but domain object not loaded yet
+ header =
+
+
+
+ ;
+ }
+
return <>
+ {header}
diff --git a/wrolpi/collections/lib.py b/wrolpi/collections/lib.py
index ca1756f0..ace3dc4a 100644
--- a/wrolpi/collections/lib.py
+++ b/wrolpi/collections/lib.py
@@ -25,6 +25,7 @@ logger = logger.getChild(__name__)
__all__ = [
'get_collections',
'get_collection_with_stats',
+ 'get_domain_statistics',
'update_collection',
'refresh_collection',
'tag_collection',
@@ -162,21 +163,11 @@ def get_collection_with_stats(session: Session, collection_id: int) -> dict:
# This can be extended for different collection types
if collection.kind == 'domain':
# Add archive statistics for domain collections
- # Local imports to avoid circular import: collections -> archive -> collections
- from modules.archive import Archive
- from wrolpi.files.models import FileGroup
-
- stats_query = session.query(
- func.count(Archive.id).label('archive_count'),
- func.sum(FileGroup.size).label('size')
- ).outerjoin(
- FileGroup, FileGroup.id == Archive.file_group_id
- ).filter(
- Archive.collection_id == collection_id
- ).one()
-
- data['archive_count'] = stats_query.archive_count or 0
- data['size'] = stats_query.size or 0
+ # Use the detailed statistics function (similar to Channel.get_statistics())
+ statistics = get_domain_statistics(session, collection_id)
+ data['archive_count'] = statistics['archive_count']
+ data['size'] = statistics['size']
+ data['statistics'] = statistics
data['domain'] = data['name'] # Alias for backward compatibility
elif collection.kind == 'channel':
@@ -618,3 +609,48 @@ def search_collections(
collections = query.all()
return [collection.__json__() for collection in collections]
+
+
+def get_domain_statistics(session: Session, collection_id: int) -> Dict:
+ """
+ Get detailed statistics for a domain collection.
+
+ Similar to Channel.get_statistics(), this returns comprehensive stats about
+ a domain collection's archives.
+
+ Args:
+ session: Database session
+ collection_id: The domain collection ID
+
+ Returns:
+ Dict with:
+ - archive_count: Number of archives in the domain
+ - size: Total size of all archive files
+ - largest_archive: Size of the largest archive
+ - archive_tags: Count of archives that have at least one tag
+ """
+ # Local imports to avoid circular import: collections -> archive -> collections
+ from modules.archive.models import Archive
+ from wrolpi.files.models import FileGroup
+ from wrolpi.tags import TagFile
+
+ # Use a single query with aggregate functions similar to Channel.get_statistics()
+ stats_query = session.query(
+ func.count(Archive.id).label('archive_count'),
+ func.coalesce(func.sum(FileGroup.size), 0).label('size'),
+ func.coalesce(func.max(FileGroup.size), 0).label('largest_archive'),
+ func.count(Archive.id).filter(TagFile.file_group_id.isnot(None)).label('archive_tags')
+ ).outerjoin(
+ FileGroup, FileGroup.id == Archive.file_group_id
+ ).outerjoin(
+ TagFile, FileGroup.id == TagFile.file_group_id
+ ).filter(
+ Archive.collection_id == collection_id
+ ).one()
+
+ return {
+ 'archive_count': stats_query.archive_count or 0,
+ 'size': int(stats_query.size or 0),
+ 'largest_archive': int(stats_query.largest_archive or 0),
+ 'archive_tags': stats_query.archive_tags or 0,
+ }