Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ PlanExe generates a **single HTML report** (a self-contained artifact you can op
Open the sample report and do this:

1. Read **Executive Summary** to see the top-level deliverables, budget, risks, and next steps.
2. Jump to **Gantt Interactive** to see how the goal gets broken down into many concrete tasks.
2. Jump to **Gantt** to see how the goal gets broken down into many concrete tasks.
3. Open **Premortem** to see what could go wrong and what to do about it.

---
Expand Down
42 changes: 42 additions & 0 deletions worker_plan/tests/test_report_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import tempfile
import unittest
from pathlib import Path

from worker_plan_internal.report.report_generator import ReportGenerator


class TestReportGeneratorAppendHtml(unittest.TestCase):
def test_subtitle_is_rendered_first_in_section_content(self):
with tempfile.TemporaryDirectory() as tmp:
html_path = Path(tmp) / "widget.html"
html_path.write_text(
"<!--HTML_HEAD_START--><style></style><!--HTML_HEAD_END-->"
"<!--HTML_BODY_CONTENT_START--><div id=\"widget\">chart</div><!--HTML_BODY_CONTENT_END-->"
"<!--HTML_BODY_SCRIPT_START--><script></script><!--HTML_BODY_SCRIPT_END-->"
)
rg = ReportGenerator()
rg.append_html("Gantt", html_path, subtitle="Unoptimized waterfall. Parallel work <not> modelled here.")
html = rg.generate_html_report(title="Sample")

button = html.index('<button class="collapsible">Gantt</button>')
subtitle = html.index("<p>Unoptimized waterfall. Parallel work &lt;not&gt; modelled here.</p>")
widget = html.index('id="widget"')
self.assertTrue(button < subtitle < widget)

def test_no_subtitle_by_default(self):
with tempfile.TemporaryDirectory() as tmp:
html_path = Path(tmp) / "widget.html"
html_path.write_text(
"<!--HTML_BODY_CONTENT_START--><div id=\"widget\">chart</div><!--HTML_BODY_CONTENT_END-->"
)
rg = ReportGenerator()
rg.append_html("Gantt", html_path)
html = rg.generate_html_report(title="Sample")

button = html.index('<button class="collapsible">Gantt</button>')
widget = html.index('id="widget"')
self.assertNotIn("<p>", html[button:widget])


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion worker_plan/worker_plan_internal/plan/nodes/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def run_inner(self):

rg = ReportGenerator()
rg.append_markdown('Executive Summary', self.input()['executive_summary']['markdown'].path)
rg.append_html('Gantt Interactive', self.input()['create_schedule']['dhtmlx_html'].path)
rg.append_html('Gantt', self.input()['create_schedule']['dhtmlx_html'].path, subtitle='Unoptimized waterfall. Parallel work not modelled here.')
rg.append_markdown('Pitch', self.input()['pitch_markdown']['markdown'].path)
rg.append_markdown('Project Plan', self.input()['project_plan']['markdown'].path)
rg.append_markdown('Strategic Decisions', self.input()['strategic_decisions_markdown']['markdown'].path)
Expand Down
10 changes: 7 additions & 3 deletions worker_plan/worker_plan_internal/report/report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,19 @@ def append_csv(self, document_title: str, file_path: Path, css_classes: list[str
markdown_content = f"```csv\n{csv_text}```"
self.report_markdown_item_list.append(ReportMarkdownItem(document_title, markdown_content))

def append_html(self, document_title: str, file_path: Path, css_classes: list[str] = []):
def append_html(self, document_title: str, file_path: Path, css_classes: list[str] = [], subtitle: Optional[str] = None):
"""Append an HTML document to the report.

HTML-only: not added to the markdown report (the markdown output is intended for LLM consumption,
and embedded JavaScript/HTML widgets are not useful there).

subtitle: optional one-line intro shown as the first paragraph of the section, above the embedded HTML.
"""
with open(file_path, 'r') as f:
html_raw = f.read()

subtitle_html = f"<p>{escape(subtitle)}</p>\n" if subtitle else ""

# Extract the html_head content between <!--HTML_HEAD_START--> and <!--HTML_HEAD_END-->
html_head_match = re.search(r'<!--HTML_HEAD_START-->(.*)<!--HTML_HEAD_END-->', html_raw, re.DOTALL)
if html_head_match:
Expand All @@ -175,11 +179,11 @@ def append_html(self, document_title: str, file_path: Path, css_classes: list[st
html_body_match = re.search(r'<!--HTML_BODY_CONTENT_START-->(.*)<!--HTML_BODY_CONTENT_END-->', html_raw, re.DOTALL)
if html_body_match:
html_body = html_body_match.group(1)
self.report_html_item_list.append(ReportDocumentItem(document_title, html_body))
self.report_html_item_list.append(ReportDocumentItem(document_title, subtitle_html + html_body, css_classes=css_classes))
else:
logging.warning(f"Document: '{document_title}'. Could not find HTML_BODY_CONTENT_START and HTML_BODY_CONTENT_END in {file_path}")
# If no markers found, use the entire content as the body
self.report_html_item_list.append(ReportDocumentItem(document_title, html_raw, css_classes=css_classes))
self.report_html_item_list.append(ReportDocumentItem(document_title, subtitle_html + html_raw, css_classes=css_classes))

# Extract the html_body_script content between <!--HTML_BODY_SCRIPT_START--> and <!--HTML_BODY_SCRIPT_END-->
html_body_script_match = re.search(r'<!--HTML_BODY_SCRIPT_START-->(.*)<!--HTML_BODY_SCRIPT_END-->', html_raw, re.DOTALL)
Expand Down