Compare commits

...

2 Commits

Author SHA1 Message Date
Deeman
e537bfd9d3 fix(admin): protect cornerstone .md files from bulk delete + fix PDF 500
All checks were successful
CI / test (push) Successful in 1m0s
CI / tag (push) Successful in 3s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:13:23 +01:00
Deeman
a27da79705 fix(admin): protect cornerstone .md files from bulk delete + fix PDF 500
- Bulk delete (both explicit-IDs and apply_to_all paths) now only unlinks
  source .md files for generated articles (template_slug IS NOT NULL).
  Manual cornerstone articles keep their .md source on disk.

- _sync_static_articles() now also renders markdown → HTML and writes to
  BUILD_DIR/<lang>/<slug>.html after upserting the DB row, so cornerstones
  are immediately servable after a sync without a separate rebuild step.

- scenario_pdf(): replace d = json.loads(scenario["calc_json"]) with
  d = calc(state) so all current calc fields (moic, dscr, cashOnCash, …)
  are present and the PDF route no longer 500s on stale stored JSON.

- Restored data/content/articles/ cornerstone .md files via git checkout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:09:19 +01:00

View File

@@ -2142,7 +2142,7 @@ async def scenario_preview(scenario_id: int):
async def scenario_pdf(scenario_id: int):
"""Generate and immediately download a business plan PDF for a published scenario."""
from ..businessplan import get_plan_sections
from ..planner.calculator import validate_state
from ..planner.calculator import calc, validate_state
scenario = await fetch_one("SELECT * FROM published_scenarios WHERE id = ?", (scenario_id,))
if not scenario:
@@ -2153,7 +2153,7 @@ async def scenario_pdf(scenario_id: int):
lang = "en"
state = validate_state(json.loads(scenario["state_json"]))
d = json.loads(scenario["calc_json"])
d = calc(state)
sections = get_plan_sections(state, d, lang)
sections["scenario_name"] = scenario["title"]
sections["location"] = scenario.get("location", "")
@@ -2274,6 +2274,18 @@ async def _sync_static_articles() -> None:
template_slug, group_key, now_iso, now_iso),
)
# Build HTML so the article is immediately servable (cornerstones have no template)
if template_slug is None:
from ..content.routes import BUILD_DIR, bake_product_cards, bake_scenario_cards
body = raw[m.end():]
body_html = mistune.html(body)
body_html = await bake_scenario_cards(body_html, lang=language)
body_html = await bake_product_cards(body_html, lang=language)
build_dir = BUILD_DIR / language
build_dir.mkdir(parents=True, exist_ok=True)
(build_dir / f"{slug}.html").write_text(body_html)
def _build_article_where(
status: str = None,
@@ -2613,15 +2625,18 @@ async def articles_bulk():
from ..content.routes import BUILD_DIR
rows = await fetch_all(
f"SELECT id, slug FROM articles WHERE {where} LIMIT 5000", tuple(where_params)
f"SELECT id, slug, template_slug FROM articles WHERE {where} LIMIT 5000",
tuple(where_params),
)
for a in rows:
build_path = BUILD_DIR / f"{a['slug']}.html"
if build_path.exists():
build_path.unlink()
md_path = Path("data/content/articles") / f"{a['slug']}.md"
if md_path.exists():
md_path.unlink()
# Only remove source .md for generated articles; cornerstones have no template
if a["template_slug"] is not None:
md_path = Path("data/content/articles") / f"{a['slug']}.md"
if md_path.exists():
md_path.unlink()
await execute(f"DELETE FROM articles WHERE {where}", tuple(where_params))
from ..sitemap import invalidate_sitemap_cache
invalidate_sitemap_cache()
@@ -2667,16 +2682,18 @@ async def articles_bulk():
from ..content.routes import BUILD_DIR
articles_rows = await fetch_all(
f"SELECT id, slug FROM articles WHERE id IN ({placeholders})",
f"SELECT id, slug, template_slug FROM articles WHERE id IN ({placeholders})",
tuple(article_ids),
)
for a in articles_rows:
build_path = BUILD_DIR / f"{a['slug']}.html"
if build_path.exists():
build_path.unlink()
md_path = Path("data/content/articles") / f"{a['slug']}.md"
if md_path.exists():
md_path.unlink()
# Only remove source .md for generated articles; cornerstones have no template
if a["template_slug"] is not None:
md_path = Path("data/content/articles") / f"{a['slug']}.md"
if md_path.exists():
md_path.unlink()
await execute(
f"DELETE FROM articles WHERE id IN ({placeholders})",
tuple(article_ids),