"""
Utility: Generate a client-facing progress report (HTML).
Runs the same document-level completeness check as fccs_verify.py, but produces
a clean, self-contained HTML file listing ONLY the drawers with outstanding
items and, for each, the specific documents still missing from the export. It's
meant as a "check-in progress" document you can send to the client.
Drawers that are fully exported or that have no manifest are left out — the
report shows outstanding work. A headline shows how many drawers are fully
migrated so overall progress is clear.
Any outstanding drawer that also appears in crashed.txt (an FCCS converter crash
aborted its export) is badged CRASHED and sorted to the top: those are genuine
failures worth attention, as opposed to benign name-match false positives (e.g.
a collapsed "container" document whose children exported fine but whose name
lands in the filename's folder field rather than the document field).
Output goes to `report_html` (default C:\\Migration\\progress_report.html).
USAGE
-----
python fccs_report.py
"""
import os
import sys
import html
from datetime import datetime
from fccs_config import (
parse_args, load_config, load_lines,
evaluate_drawer, index_files_by_drawer, client_name_from_files,
)
PAGE_TEMPLATE = """
Migration Progress Report
Migration Progress Report
{date}
{complete}
Drawers migrated
{incomplete}
Drawers outstanding
{crashed}
Crashed in export
{total}
Total drawers
{content}
"""
def build_drawer_block(drawer_id, client, missing, crashed=False):
"""Return the HTML for one outstanding drawer."""
head = f'{html.escape(drawer_id)}'
if client:
head += f' — {html.escape(client)}'
if crashed:
head += ' Crashed'
n = len(missing)
docs = "\n".join(
f"
{html.escape(m)}
" for m in missing
)
cls = "drawer crashed" if crashed else "drawer"
return (
f'
\n'
f'
{head}
\n'
f'
{n} document{"s" if n != 1 else ""} '
'outstanding
\n'
f'
\n{docs}\n
\n'
'
'
)
def main():
args = parse_args()
cfg = load_config(args.config)
export_dir = cfg.get("paths", "export_dir")
manifest_dir = cfg.get("paths", "manifest_dir")
report_html = cfg.get("paths", "report_html")
crashed_set = set(load_lines(cfg.get("paths", "crashed_file")))
if not os.path.isdir(manifest_dir):
print(f"ERROR: manifest directory not found: {manifest_dir}")
print("Run fccs_export.py first to generate manifests.")
sys.exit(1)
if not os.path.isdir(export_dir):
print(f"ERROR: export directory not found: {export_dir}")
sys.exit(1)
drawer_ids = sorted(
os.path.splitext(f)[0]
for f in os.listdir(manifest_dir)
if f.endswith(".txt")
)
if not drawer_ids:
print("No manifest files found.")
sys.exit(1)
files_by_drawer = index_files_by_drawer(export_dir)
complete = 0
outstanding = [] # (drawer_id, client, missing_list, crashed)
for drawer_id in drawer_ids:
files = files_by_drawer.get(drawer_id, [])
r = evaluate_drawer(drawer_id, files, manifest_dir)
if r["missing"]:
client = client_name_from_files(files)
outstanding.append((drawer_id, client, r["missing"],
drawer_id in crashed_set))
else:
complete += 1
# Crashed drawers are the genuine failures — surface them first.
outstanding.sort(key=lambda o: (not o[3], o[0]))
crashed_ids = [d for d, _, _, cr in outstanding if cr]
total = len(drawer_ids)
date_str = f"{datetime.now():%B %d, %Y}"
if outstanding:
content = "\n".join(
build_drawer_block(d, c, m, cr) for d, c, m, cr in outstanding
)
else:
content = ('
All drawers are fully '
"migrated — no outstanding documents.
")
page = PAGE_TEMPLATE.format(
date=html.escape(date_str),
complete=complete,
incomplete=len(outstanding),
crashed=len(crashed_ids),
total=total,
content=content,
)
with open(report_html, "w", encoding="utf-8") as f:
f.write(page)
print(f"Progress report written to {report_html}")
print(f" {complete}/{total} drawers fully migrated, "
f"{len(outstanding)} outstanding.")
if outstanding:
print(" Outstanding: " + ", ".join(d for d, _, _, _ in outstanding))
if crashed_ids:
print(f" Outstanding AND crashed ({len(crashed_ids)}) "
"— spot-check these first: " + ", ".join(crashed_ids))
if __name__ == "__main__":
main()