added fccs_report.py this generates a report of all the errors in a clean html format
This commit is contained in:
209
fccs_report.py
Normal file
209
fccs_report.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user