210 lines
6.7 KiB
Python
210 lines
6.7 KiB
Python
"""
|
|
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, that have no manifest, or that only appear in
|
|
crashed.txt are intentionally left out — the report shows outstanding work, not
|
|
internal tooling state. A headline shows how many drawers are fully migrated so
|
|
the overall progress is clear.
|
|
|
|
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,
|
|
evaluate_drawer, index_files_by_drawer, client_name_from_files,
|
|
)
|
|
|
|
|
|
PAGE_TEMPLATE = """<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Migration Progress Report</title>
|
|
<style>
|
|
* {{ box-sizing: border-box; }}
|
|
body {{
|
|
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
color: #1f2933; background: #f5f7fa; margin: 0; padding: 2.5rem 1rem;
|
|
line-height: 1.5;
|
|
}}
|
|
.sheet {{
|
|
max-width: 820px; margin: 0 auto; background: #fff; border-radius: 10px;
|
|
box-shadow: 0 1px 3px rgba(16,24,40,.1), 0 1px 2px rgba(16,24,40,.06);
|
|
overflow: hidden;
|
|
}}
|
|
header {{
|
|
padding: 2rem 2.25rem 1.5rem; border-bottom: 1px solid #e4e7eb;
|
|
}}
|
|
h1 {{ margin: 0 0 .25rem; font-size: 1.5rem; font-weight: 650; }}
|
|
.date {{ color: #7b8794; font-size: .9rem; }}
|
|
.summary {{
|
|
display: flex; gap: 2rem; flex-wrap: wrap;
|
|
padding: 1.25rem 2.25rem; background: #fafbfc; border-bottom: 1px solid #e4e7eb;
|
|
}}
|
|
.stat .num {{ font-size: 1.75rem; font-weight: 680; line-height: 1; }}
|
|
.stat .lbl {{ color: #7b8794; font-size: .8rem; text-transform: uppercase;
|
|
letter-spacing: .03em; margin-top: .35rem; }}
|
|
.stat.good .num {{ color: #2e7d32; }}
|
|
.stat.flag .num {{ color: #c0392b; }}
|
|
.body {{ padding: 1.5rem 2.25rem 2.25rem; }}
|
|
.all-clear {{
|
|
text-align: center; padding: 2.5rem 1rem; color: #2e7d32; font-size: 1.1rem;
|
|
}}
|
|
.drawer {{
|
|
border: 1px solid #e4e7eb; border-radius: 8px; padding: 1rem 1.25rem;
|
|
margin-bottom: 1rem;
|
|
}}
|
|
.drawer:last-child {{ margin-bottom: 0; }}
|
|
.drawer h2 {{
|
|
margin: 0 0 .1rem; font-size: 1.05rem; font-weight: 620;
|
|
}}
|
|
.drawer .id {{ color: #486581; font-variant-numeric: tabular-nums; }}
|
|
.drawer .meta {{ color: #7b8794; font-size: .82rem; margin-bottom: .6rem; }}
|
|
ul.docs {{ margin: 0; padding: 0; list-style: none; }}
|
|
ul.docs li {{
|
|
padding: .3rem 0 .3rem 1.5rem; position: relative; font-size: .93rem;
|
|
border-top: 1px solid #f0f2f5;
|
|
}}
|
|
ul.docs li:first-child {{ border-top: none; }}
|
|
ul.docs li::before {{
|
|
content: "\\2717"; color: #c0392b; position: absolute; left: 0; font-weight: 700;
|
|
}}
|
|
footer {{
|
|
padding: 1rem 2.25rem; border-top: 1px solid #e4e7eb; color: #9aa5b1;
|
|
font-size: .78rem;
|
|
}}
|
|
@media print {{
|
|
body {{ background: #fff; padding: 0; }}
|
|
.sheet {{ box-shadow: none; max-width: none; }}
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="sheet">
|
|
<header>
|
|
<h1>Migration Progress Report</h1>
|
|
<div class="date">{date}</div>
|
|
</header>
|
|
<div class="summary">
|
|
<div class="stat good"><div class="num">{complete}</div>
|
|
<div class="lbl">Drawers migrated</div></div>
|
|
<div class="stat flag"><div class="num">{incomplete}</div>
|
|
<div class="lbl">Drawers outstanding</div></div>
|
|
<div class="stat"><div class="num">{total}</div>
|
|
<div class="lbl">Total drawers</div></div>
|
|
</div>
|
|
<div class="body">
|
|
{content}
|
|
</div>
|
|
<footer>Generated {date} · Lists documents not yet present in the
|
|
export. Drawers not shown are fully migrated.</footer>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def build_drawer_block(drawer_id, client, missing):
|
|
"""Return the HTML for one outstanding drawer."""
|
|
head = f'<span class="id">{html.escape(drawer_id)}</span>'
|
|
if client:
|
|
head += f' — {html.escape(client)}'
|
|
n = len(missing)
|
|
docs = "\n".join(
|
|
f" <li>{html.escape(m)}</li>" for m in missing
|
|
)
|
|
return (
|
|
' <div class="drawer">\n'
|
|
f' <h2>{head}</h2>\n'
|
|
f' <div class="meta">{n} document{"s" if n != 1 else ""} '
|
|
'outstanding</div>\n'
|
|
f' <ul class="docs">\n{docs}\n </ul>\n'
|
|
' </div>'
|
|
)
|
|
|
|
|
|
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")
|
|
|
|
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)
|
|
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"]))
|
|
else:
|
|
complete += 1
|
|
|
|
total = len(drawer_ids)
|
|
date_str = f"{datetime.now():%B %d, %Y}"
|
|
|
|
if outstanding:
|
|
content = "\n".join(
|
|
build_drawer_block(d, c, m) for d, c, m in outstanding
|
|
)
|
|
else:
|
|
content = (' <div class="all-clear">All drawers are fully '
|
|
"migrated — no outstanding documents.</div>")
|
|
|
|
page = PAGE_TEMPLATE.format(
|
|
date=html.escape(date_str),
|
|
complete=complete,
|
|
incomplete=len(outstanding),
|
|
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 __name__ == "__main__":
|
|
main()
|