added fccs_report.py this generates a report of all the errors in a clean html format

This commit is contained in:
2026-07-19 21:47:31 -05:00
parent 3b2263b529
commit b270ca9186
6 changed files with 239 additions and 1 deletions

View File

@@ -21,6 +21,7 @@ Extracts and organizes documents from FileCabinet CS (Thomson Reuters) using GUI
| `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure | | `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure |
| `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files | | `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files |
| `fccs_check.py` | Utility: Interactively check if specific drawers exported completely | | `fccs_check.py` | Utility: Interactively check if specific drawers exported completely |
| `fccs_report.py` | Utility: Generate a clean client-facing HTML progress report (issues only) |
| `fccs_dump_controls.py` | Utility: Dump control identifiers of an on-screen dialog | | `fccs_dump_controls.py` | Utility: Dump control identifiers of an on-screen dialog |
## Setup Per Engagement ## Setup Per Engagement
@@ -115,10 +116,18 @@ Drawer ID(s): 08097 18430
> Note: because page-splitting means the number of files can't be mapped one-to-one to documents, completeness is judged by document *presence* (is each manifest document represented by at least one exported file), not by exact file counts. > Note: because page-splitting means the number of files can't be mapped one-to-one to documents, completeness is judged by document *presence* (is each manifest document represented by at least one exported file), not by exact file counts.
**Client-facing progress report:** For a clean summary to share with the client, generate an HTML report of outstanding work only:
```
python fccs_report.py
```
This runs the same document-level check as `fccs_verify.py` but writes a self-contained, print-friendly HTML file (`report_html`, default `C:\Migration\progress_report.html`) that lists **only** the drawers with missing documents, each with the client name and the specific documents still outstanding. Fully-migrated drawers are omitted (a headline shows how many are done). Drawers with no manifest or in `crashed.txt` are excluded since they reflect internal tooling state, not client-facing progress. Open it in any browser and print to PDF to send.
## Config Reference ## Config Reference
All scripts read from `config.ini` (or specify `--config path\to\config.ini`). All scripts read from `config.ini` (or specify `--config path\to\config.ini`).
- **`[paths]`** -- backup_dir, export_dir, output_dir, drawer_id_file, completed_file, ignore_file, crashed_file, log_file, verify_report, screenshot_dir, manifest_dir, folder_list - **`[paths]`** -- backup_dir, export_dir, output_dir, drawer_id_file, completed_file, ignore_file, crashed_file, log_file, verify_report, report_html, screenshot_dir, manifest_dir, folder_list
- **`[timeouts]`** -- nav_timeout, dialog_timeout, progress_appear, progress_finish, settle, confirm_timeout - **`[timeouts]`** -- nav_timeout, dialog_timeout, progress_appear, progress_finish, settle, confirm_timeout
- **`[controls]`** -- FCCS window class names and button titles (rarely need changing) - **`[controls]`** -- FCCS window class names and button titles (rarely need changing)

Binary file not shown.

View File

@@ -9,6 +9,7 @@ ignore_file = C:\Migration\ignore.txt
crashed_file = C:\Migration\crashed.txt crashed_file = C:\Migration\crashed.txt
log_file = C:\Migration\run_log.txt log_file = C:\Migration\run_log.txt
verify_report = C:\Migration\verify_report.txt verify_report = C:\Migration\verify_report.txt
report_html = C:\Migration\progress_report.html
screenshot_dir = C:\Migration\screenshots screenshot_dir = C:\Migration\screenshots
manifest_dir = C:\Migration\manifests manifest_dir = C:\Migration\manifests
folder_list = fccs_folders.txt folder_list = fccs_folders.txt

View File

@@ -229,6 +229,25 @@ def exported_doc_name(filename):
return stem[anchors[-1].end():] return stem[anchors[-1].end():]
def client_name_from_files(files):
"""Best-effort client name from a drawer's export filenames, or None.
Filenames are '{drawer}_{client}_{folder}_{MM-DD-YYYY}_{doc}.ext', so the
client name is the second underscore-delimited token. Client names carry
commas/spaces but not underscores; the most common value across the drawer's
files is returned to shrug off any oddball filename.
"""
counts = {}
for f in files:
parts = f.split("_")
if len(parts) >= 2 and parts[1].strip():
name = parts[1].strip()
counts[name] = counts.get(name, 0) + 1
if not counts:
return None
return max(counts, key=counts.get)
def index_files_by_drawer(export_dir): def index_files_by_drawer(export_dir):
"""Map each export file to its leading drawer-ID token (before first '_'). """Map each export file to its leading drawer-ID token (before first '_').

209
fccs_report.py Normal file
View 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} &middot; 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' &mdash; {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 &mdash; 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()