(function() {
'use strict';
// window.app is initialized in init.js. Reference shape (read-only docs):
// directories[], groupingFolders[], transmittalFolders[], files[],
// filteredFiles[], selectedFiles:Set, sourceMode ('local'|'http'),
// isScanning, scanProgress,
// columnFilters {trackingNumber,title,revisions}, columnFilterASTs {...},
// groupingFilter, transmittalFilter,
// enabledFolderTypes:Set('issued','received'),
// sortField ('trackingNumber'), sortDirection ('asc'|'desc'),
// selectedGroupingFolders:Set, selectedTransmittalFolders:Set,
// collapsedDateGroups:Set, collapsedGroupingFolders:Set,
// selectAllGroupingFolders:bool, selectAllTransmittals:bool,
// availableModifiers:Set, selectedModifiers:Set, showSelectedOnly:bool
// Parse search terms from filter string
function parseSearchTerms(filter) {
if (!filter || !filter.trim()) return [];
return filter.trim().toLowerCase().split(/\s+/);
}
// Check if text matches all search terms (AND logic)
function matchesSearchTerms(text, terms) {
if (!terms || terms.length === 0) return true;
return terms.every(term => text.includes(term));
}
// Initialize application
function initApp() {
// Detect source mode from protocol
window.app.sourceMode = (location.protocol === 'file:') ? 'local' : 'http';
if (window.app.sourceMode === 'local') {
// Check File System Access API support (local mode only)
if (!('showDirectoryPicker' in window)) {
showUnsupportedBrowserMessage();
return;
}
}
// Set up event listeners
window.app.modules.events.setupEventListeners();
// Set up file link handlers (event delegation)
window.app.modules.table.setupFileLinkHandlers();
// Apply source-mode-specific UI adjustments
applySourceModeUI();
// Restore filter/sort state from URL query string
window.app.modules.urlState.restore();
// Initialize UI
updateUI();
// Show initial sort indicator
window.app.modules.table.updateSortIndicators();
if (window.app.sourceMode === 'http') {
// Auto-connect to the server in HTTP mode
autoConnectHttpSource();
} else {
// Show empty state if no directories (local mode)
if (window.app.directories.length === 0) {
showEmptyState();
}
}
}
// Apply UI differences based on source mode
function applySourceModeUI() {
// "Add Local Directory" button is always visible in both modes —
// in HTTP mode the user can augment the online archive with local directories.
}
// Auto-connect to the HTTP server
// Derives the base URL from the current page's location
async function autoConnectHttpSource() {
var href = window.location.href;
// Strip query string and fragment
href = href.split('?')[0].split('#')[0];
// Strip the filename to get the directory
var lastSlash = href.lastIndexOf('/');
var baseUrl = (lastSlash >= 0) ? href.substring(0, lastSlash + 1) : href + '/';
// Multi-project mode is opt-in via ?projects= in the URL.
// ?projects= absent → not multi-project; scan whatever the URL
// points to (single-project or in-archive
// mode). The server's project list, if any,
// stays out of view.
// ?projects= empty → multi-project; include every project the
// server says the user can access.
// ?projects=A,B → multi-project; include only the listed
// projects (intersected with server access).
// The archive never sees projects beyond this scope — the visibility
// dropdown only narrows what's already in availableProjects.
var urlParams = new URLSearchParams(location.search);
var projectsParamPresent = urlParams.has('projects');
if (projectsParamPresent) {
window.app.isMultiProject = true;
// Fetch the server's ACL-filtered project list so we can drop any
// listed names the user doesn't actually have access to (and so
// the empty-projects= "include everything" mode has a list to use).
var serverNames = null;
try {
var resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' } });
if (resp.ok) {
var serverProjects = await resp.json();
if (Array.isArray(serverProjects) && serverProjects.length > 0
&& serverProjects[0] && typeof serverProjects[0].name === 'string') {
serverNames = new Set(serverProjects.map(function(p) { return p.name; }));
}
}
} catch (e) {
// Plain Caddy or proxy-stripped — trust the URL list as-is.
}
if (window.app.projectFilter && window.app.projectFilter.size > 0) {
// Listed names: intersect with server access, warn about misses.
if (serverNames) {
var accessible = new Set();
var missing = [];
window.app.projectFilter.forEach(function(p) {
if (serverNames.has(p)) accessible.add(p);
else missing.push(p);
});
window.app.projectFilter = accessible;
if (missing.length > 0) showProjectWarning(missing);
}
window.app.availableProjects = Array.from(window.app.projectFilter).sort();
} else if (serverNames) {
// Empty ?projects= — scan everything the user can access.
window.app.projectFilter = new Set(serverNames);
window.app.availableProjects = Array.from(serverNames).sort();
}
// else: ?projects= empty AND no server list — leave projectFilter
// empty; source.js will fall through to in-archive mode.
}
// visibleProjects: default to projectFilter (everything visible), or
// honor an explicit ?show= from the URL (intersected with projectFilter
// to drop names that aren't in scope). An empty ?show= means "hide
// everything" — distinct from "no ?show= at all".
var showInUrl = urlParams.has('show');
if (showInUrl) {
var inScope = new Set();
(window.app.visibleProjects || new Set()).forEach(function(n) {
if (window.app.projectFilter.has(n)) inScope.add(n);
});
window.app.visibleProjects = inScope;
} else {
window.app.visibleProjects = new Set(window.app.projectFilter);
}
await addHttpSource(baseUrl);
}
// Add an HTTP source root (analogous to addDirectory() for local mode)
async function addHttpSource(baseUrl) {
// Derive a display name from the URL path
var urlPath = baseUrl.replace(/\/$/, '');
var rootName = urlPath.substring(urlPath.lastIndexOf('/') + 1) || urlPath;
// Check if already added
var exists = window.app.directories.some(function(d) { return d.url === baseUrl; });
if (exists) return;
window.app.directories.push({
handle: null,
name: rootName,
path: rootName,
url: baseUrl
});
if (window.app.directories.length === 1) {
hideEmptyState();
}
await scanHttpSource(baseUrl, rootName);
updateUI();
}
// Scan an HTTP source root
async function scanHttpSource(baseUrl, rootName) {
window.app.isScanning = true;
window.app.scanProgress = 'Connecting to server...';
updateStatusBar();
var source = window.app.modules.source.createSource('http', { baseUrl: baseUrl });
var fileCount = 0;
var callbacks = {
onGroupingFolder: function(folder) {
window.app.groupingFolders.push(folder);
},
onTransmittalFolder: function(folder) {
window.app.transmittalFolders.push(folder);
},
onFile: function(file) {
window.app.files.push(file);
fileCount++;
// Throttled progress update — don't update DOM on every file
if (fileCount % 10 === 0) {
window.app.scanProgress = 'Scanning\u2026 ' + fileCount + ' files found';
updateStatusBar();
}
},
onProgress: function() { /* no-op: parallel scan — spinner is enough */ }
};
try {
await source.scan(baseUrl, callbacks);
// Auto-select top-level party folders (shallowest depth). Keyed by
// party NAME so duplicate third-party folders across projects merge.
var groupingDepths = window.app.groupingFolders.map(function(f) { return f.path.split('/').length; });
var minGroupingDepth = groupingDepths.length > 0 ? Math.min.apply(null, groupingDepths) : 1;
window.app.groupingFolders.forEach(function(folder) {
if (folder.path.split('/').length === minGroupingDepth) {
window.app.selectedGroupingFolders.add(folder.name);
}
});
window.app.transmittalFolders.forEach(function(folder) {
if (!isUnderHiddenFolderType(folder.path)) {
window.app.selectedTransmittalFolders.add(folder.path);
}
});
ensureOutstandingTransmittal();
// Auto-select Outstanding if selectAllTransmittals is active
if (window.app.selectAllTransmittals) {
window.app.selectedTransmittalFolders.add('__outstanding__');
}
collectModifiers();
updateUI();
window.app.modules.filtering.applyFilters();
if (window.app.modules.presets) {
window.app.modules.presets.init();
}
} catch (err) {
console.error('Error scanning HTTP source:', err);
showHttpErrorState(err.message);
} finally {
window.app.isScanning = false;
window.app.scanProgress = '';
updateStatusBar();
}
}
// Ensure the Outstanding virtual transmittal exists if there are any outstanding files.
// Called after each scan completes. Idempotent — safe to call multiple times.
function ensureOutstandingTransmittal() {
const hasOutstanding = window.app.files.some(f => f.folderPath === '__outstanding__');
const alreadyExists = window.app.transmittalFolders.some(f => f.path === '__outstanding__');
if (hasOutstanding && !alreadyExists) {
window.app.transmittalFolders.push({
name: 'Outstanding',
path: '__outstanding__',
displayPath: 'Outstanding',
handle: null,
url: null,
isVirtual: true
});
}
}
// Show error state when HTTP server is unreachable
function showHttpErrorState(message) {
var el = document.getElementById('noDirectoryMessage');
if (!el) return;
var content = el.querySelector('.empty-state-content');
if (content) {
content.innerHTML =
'
Could not connect to server
' +
'
The archive browser could not retrieve the directory listing from the server.
Ensure the server is running, CORS is not blocking the request, and Caddy\'s file browsing is enabled.
';
}
el.classList.remove('hidden');
}
// Show a warning banner listing projects in the URL filter that the user cannot access
function showProjectWarning(missingProjects) {
var el = document.getElementById('projectWarningBanner');
if (!el || missingProjects.length === 0) return;
var list = missingProjects.map(function(p) { return escapeHtml(p); }).join(', ');
el.querySelector('.project-warning-text').innerHTML =
'This link includes projects you don\'t have access to: ' + list + '';
el.classList.remove('hidden');
}
function dismissProjectWarning() {
var el = document.getElementById('projectWarningBanner');
if (el) el.classList.add('hidden');
}
// Show unsupported browser message
function showUnsupportedBrowserMessage() {
const app = document.getElementById('appContainer');
app.innerHTML = `
Browser Not Supported
This application requires a Chromium-based browser (Chrome, Edge, Brave) with File System Access API support.
Please use one of these browsers to access the Archive Browser.
`;
}
// Show empty state
function showEmptyState() {
document.getElementById('noDirectoryMessage').classList.remove('hidden');
document.querySelector('.main-container').style.display = 'none';
// Keep header visible
document.querySelector('.app-header').style.display = '';
var refreshBtn = document.getElementById('refreshHeaderBtn');
if (refreshBtn) { refreshBtn.classList.add('hidden'); }
}
// Hide empty state
function hideEmptyState() {
document.getElementById('noDirectoryMessage').classList.add('hidden');
document.querySelector('.main-container').style.display = '';
var refreshBtn = document.getElementById('refreshHeaderBtn');
if (refreshBtn) { refreshBtn.classList.remove('hidden'); }
}
// Update UI based on current state
function updateUI() {
renderFolderTypeBar();
renderFolderLists();
window.app.modules.table.updateFileTable();
updateStatusBar();
}
// Render folder lists (rebuilds DOM)
function renderFolderLists() {
renderGroupingFolders();
renderTransmittalFolders();
}
// Check if a folder path is under a hidden folder type
// Returns true if any path segment is a known folder type that is NOT currently enabled
function isUnderHiddenFolderType(path) {
const parts = path.toLowerCase().split('/');
return parts.some(part =>
window.app.FOLDER_TYPE_NAMES.includes(part) && !window.app.enabledFolderTypes.has(part)
);
}
// Get filtered grouping folders (single source of truth for filtering logic)
function getFilteredGroupingFolders() {
const filter = window.app.groupingFilter;
return window.app.groupingFolders.filter(folder => {
if (isUnderHiddenFolderType(folder.path)) {
return false;
}
if (!filter) return true;
const terms = parseSearchTerms(filter);
return matchesSearchTerms(folder.name.toLowerCase(), terms);
});
}
// Render grouping folders as a flat list of unique party names. Same-named
// third-party folders across multiple projects collapse to one row.
// selectedGroupingFolders is a Set of party NAMES (not paths) so toggling
// affects every project occurrence at once.
function renderGroupingFolders() {
const container = document.getElementById('groupingFoldersList');
// Get filtered grouping folders (uses shared filtering logic)
const filteredFolders = getFilteredGroupingFolders();
// Only show top-level party folders (the shallowest depth among all grouping folders)
const allDepths = window.app.groupingFolders.map(f => f.path.split('/').length);
const minDepth = allDepths.length > 0 ? Math.min(...allDepths) : 1;
const partyFolders = filteredFolders.filter(f => f.path.split('/').length === minDepth);
// Dedupe by name (keep first occurrence per name). In multi-project mode,
// skip parties whose every occurrence is under a hidden project — if at
// least one occurrence is in a visible project, the party stays.
const seen = new Set();
const uniqueParties = [];
for (const f of partyFolders) {
if (seen.has(f.name)) continue;
if (window.app.isMultiProject) {
const hasVisible = partyFolders.some(p =>
p.name === f.name && pathIsInVisibleProject(p.path)
);
if (!hasVisible) continue;
}
seen.add(f.name);
uniqueParties.push(f);
}
uniqueParties.sort((a, b) => a.name.localeCompare(b.name));
const partyNames = new Set(uniqueParties.map(f => f.name));
// If "Select All" mode is active, auto-select all visible party names.
if (window.app.selectAllGroupingFolders) {
window.app.selectedGroupingFolders.clear();
uniqueParties.forEach(f => window.app.selectedGroupingFolders.add(f.name));
} else {
// Remove selections for names that are no longer visible.
for (const selectedName of window.app.selectedGroupingFolders) {
if (!partyNames.has(selectedName)) {
window.app.selectedGroupingFolders.delete(selectedName);
}
}
}
// Sync checkbox state
const checkbox = document.getElementById('selectAllGroupingCheckbox');
if (checkbox) checkbox.checked = window.app.selectAllGroupingFolders;
if (uniqueParties.length === 0 && window.app.groupingFilter) {
container.innerHTML = '
`).join('');
updateFolderSelectionState('groupingFoldersList');
}
// Render the global folder type toggle bar
function renderFolderTypeBar() {
const bar = document.getElementById('folderTypeBar');
if (!bar) return;
const FOLDER_TYPE_LABELS = { mdl: 'MDL', incoming: 'Incoming', issued: 'Issued', received: 'Received' };
bar.innerHTML = window.app.FOLDER_TYPE_NAMES.map(type => {
const active = window.app.enabledFolderTypes.has(type);
const label = FOLDER_TYPE_LABELS[type] || (type.charAt(0).toUpperCase() + type.slice(1));
return ``;
}).join('');
}
// Toggle a folder type on/off globally.
// Off->on triggers a refresh because source.js skips listings for disabled folder types
// entirely (no listing fetched), so newly-enabled types need a rescan to surface their data.
function toggleFolderType(type) {
const wasEnabled = window.app.enabledFolderTypes.has(type);
if (wasEnabled) {
window.app.enabledFolderTypes.delete(type);
} else {
window.app.enabledFolderTypes.add(type);
}
renderFolderTypeBar();
renderGroupingFolders();
renderTransmittalFolders();
window.app.modules.filtering.applyFilters();
window.app.modules.urlState.push();
if (!wasEnabled && window.app.directories.length > 0) {
window.app.modules.directory.refreshDirectories();
}
}
// In multi-project mode, returns true if the path contains a segment matching
// a checked project in the picker. Single-project mode always returns true
// (no project segment to match against).
function pathIsInVisibleProject(path) {
if (!window.app.isMultiProject) return true;
if (!window.app.visibleProjects || window.app.visibleProjects.size === 0) return false;
return path.split('/').some(seg => window.app.visibleProjects.has(seg));
}
// Returns true if an outstanding file's actualPath has a path segment matching
// any selected party name and is not under a hidden folder type. Segment-equality
// (not prefix) so the same party name selected across projects matches all
// occurrences regardless of project ID prefix.
function outstandingFileIsVisible(file) {
const selectedGrouping = window.app.selectedGroupingFolders;
if (selectedGrouping.size === 0) return false;
if (isUnderHiddenFolderType(file.actualPath)) return false;
if (!pathIsInVisibleProject(file.actualPath)) return false;
return file.actualPath.split('/').some(seg => selectedGrouping.has(seg));
}
// Returns true if any outstanding (non-transmittal) files exist under the currently
// selected and visible grouping folders.
function hasVisibleOutstandingFiles() {
return window.app.files.some(function(f) {
if (f.folderPath !== '__outstanding__') return false;
return outstandingFileIsVisible(f);
});
}
// Returns true if the transmittal folder's path satisfies all three cascade
// layers: project visibility, party selection (any path segment matches a
// selected party name), and folder-type enablement (no segment is a
// folder-type marker that's currently disabled, regardless of where in the
// path it sits). Segment-equality matching means a party "BM" selected
// matches every "<...>/BM/<...>" path regardless of the prefix; and the
// folder-type check covers BOTH the canonical "/Issued/" layout
// AND nested layouts like "//Issued/" — a deeper folder-
// type marker still triggers the cascade.
function transmittalIsUnderVisibleParty(folder) {
if (!pathIsInVisibleProject(folder.path)) return false;
if (isUnderHiddenFolderType(folder.path)) return false;
return folder.path.split('/').some(seg =>
window.app.selectedGroupingFolders.has(seg)
);
}
// Render transmittal folders (rebuilds DOM)
function renderTransmittalFolders() {
const container = document.getElementById('transmittalFoldersList');
const filter = window.app.transmittalFilter;
// Filter transmittal folders based on grouping selection and name filter
const filteredFolders = window.app.transmittalFolders.filter(folder => {
// Outstanding virtual transmittal: include if there are visible outstanding files
if (folder.path === '__outstanding__') {
if (!hasVisibleOutstandingFiles()) return false;
// Apply name filter to "Outstanding" label too
if (filter && filter.trim()) {
const terms = parseSearchTerms(filter.trim());
if (!matchesSearchTerms('outstanding', terms)) return false;
}
return true;
}
// Check name filter
let matchesFilter = true;
if (filter && filter.trim()) {
const terms = parseSearchTerms(filter.trim());
const folderText = folder.name.toLowerCase();
matchesFilter = matchesSearchTerms(folderText, terms);
}
// If no grouping folders exist at all, show all transmittal folders (flat structure)
if (window.app.groupingFolders.length === 0) {
return matchesFilter;
}
// If grouping folders exist but none are selected, show nothing
if (window.app.selectedGroupingFolders.size === 0) {
return false;
}
// Check party + folder type visibility
return matchesFilter && transmittalIsUnderVisibleParty(folder);
});
// Sort regular transmittal folders by date (newest first); Outstanding handled separately
const regularFolders = filteredFolders.filter(f => f.path !== '__outstanding__');
regularFolders.sort((a, b) => b.name.localeCompare(a.name));
const showOutstanding = filteredFolders.some(f => f.path === '__outstanding__');
// Build set of visible folder paths (for Select All and deselection logic)
const filteredPaths = new Set(filteredFolders.map(f => f.path));
// If "Select All" mode is active, auto-select all visible transmittal folders
if (window.app.selectAllTransmittals) {
window.app.selectedTransmittalFolders.clear();
filteredFolders.forEach(f => window.app.selectedTransmittalFolders.add(f.path));
} else {
// Remove selections for folders that are now filtered out
for (const selectedPath of window.app.selectedTransmittalFolders) {
if (!filteredPaths.has(selectedPath)) {
window.app.selectedTransmittalFolders.delete(selectedPath);
}
}
}
// Sync checkbox state
const checkbox = document.getElementById('selectAllTransmittalsCheckbox');
if (checkbox) checkbox.checked = window.app.selectAllTransmittals;
// Group regular folders by date
const foldersByDate = new Map();
regularFolders.forEach(folder => {
const match = folder.name.match(/^(\d{4}-\d{2}-\d{2})/);
const date = match ? match[1] : 'Unknown';
if (!foldersByDate.has(date)) {
foldersByDate.set(date, []);
}
foldersByDate.get(date).push(folder);
});
// Build HTML
let html = '';
// Outstanding virtual transmittal — pinned at top
if (showOutstanding) {
const isSelected = window.app.selectedTransmittalFolders.has('__outstanding__');
html += `
⋯ Outstanding
`;
}
// Regular date-grouped folders
for (const [date, folders] of foldersByDate) {
const isCollapsed = window.app.collapsedDateGroups.has(date);
const folderCount = folders.length;
html += `