diff --git a/scripts/feedback_api.py b/scripts/feedback_api.py
index 6a96f8c..7462eb8 100644
--- a/scripts/feedback_api.py
+++ b/scripts/feedback_api.py
@@ -82,3 +82,46 @@ def collect_listings(db_path: Path | None = None, n: int = 5) -> list[dict]:
).fetchall()
conn.close()
return [{"title": r["title"], "company": r["company"], "url": r["url"]} for r in rows]
+
+
+def build_issue_body(form: dict, context: dict, attachments: dict) -> str:
+ """Assemble the Forgejo issue markdown body from form data, context, and attachments."""
+ _TYPE_LABELS = {"bug": "🐛 Bug", "feature": "✨ Feature Request", "other": "💬 Other"}
+ lines: list[str] = [
+ f"## {_TYPE_LABELS.get(form.get('type', 'other'), '💬 Other')}",
+ "",
+ form.get("description", ""),
+ "",
+ ]
+
+ if form.get("type") == "bug" and form.get("repro"):
+ lines += ["### Reproduction Steps", "", form["repro"], ""]
+
+ if context:
+ lines += ["### Context", ""]
+ for k, v in context.items():
+ lines.append(f"- **{k}:** {v}")
+ lines.append("")
+
+ if attachments.get("logs"):
+ lines += [
+ "",
+ "App Logs (last 100 lines)
",
+ "",
+ "```",
+ attachments["logs"],
+ "```",
+ " ",
+ "",
+ ]
+
+ if attachments.get("listings"):
+ lines += ["### Recent Listings", ""]
+ for j in attachments["listings"]:
+ lines.append(f"- [{j['title']} @ {j['company']}]({j['url']})")
+ lines.append("")
+
+ if attachments.get("submitter"):
+ lines += ["---", f"*Submitted by: {attachments['submitter']}*"]
+
+ return "\n".join(lines)
diff --git a/tests/test_feedback_api.py b/tests/test_feedback_api.py
index 263ba38..03de328 100644
--- a/tests/test_feedback_api.py
+++ b/tests/test_feedback_api.py
@@ -119,3 +119,62 @@ def test_collect_listings_respects_n(tmp_path):
"salary": "", "description": "", "date_found": "2026-03-01",
})
assert len(collect_listings(db_path=db, n=3)) == 3
+
+
+# ── build_issue_body ──────────────────────────────────────────────────────────
+
+def test_build_issue_body_contains_description():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "bug", "title": "Test", "description": "it broke", "repro": ""}
+ ctx = {"page": "Home", "version": "v1.0", "tier": "free",
+ "llm_backend": "ollama", "os": "Linux", "timestamp": "2026-03-03T00:00:00Z"}
+ body = build_issue_body(form, ctx, {})
+ assert "it broke" in body
+ assert "Home" in body
+ assert "v1.0" in body
+
+
+def test_build_issue_body_bug_includes_repro():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "bug", "title": "X", "description": "desc", "repro": "step 1\nstep 2"}
+ body = build_issue_body(form, {}, {})
+ assert "step 1" in body
+ assert "Reproduction" in body
+
+
+def test_build_issue_body_no_repro_for_feature():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "feature", "title": "X", "description": "add dark mode", "repro": "ignored"}
+ body = build_issue_body(form, {}, {})
+ assert "Reproduction" not in body
+
+
+def test_build_issue_body_logs_in_collapsible():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "other", "title": "X", "description": "Y", "repro": ""}
+ body = build_issue_body(form, {}, {"logs": "log line 1\nlog line 2"})
+ assert "" in body
+ assert "log line 1" in body
+
+
+def test_build_issue_body_omits_logs_when_not_provided():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "bug", "title": "X", "description": "Y", "repro": ""}
+ body = build_issue_body(form, {}, {})
+ assert "" not in body
+
+
+def test_build_issue_body_submitter_attribution():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "bug", "title": "X", "description": "Y", "repro": ""}
+ body = build_issue_body(form, {}, {"submitter": "Jane Doe "})
+ assert "Jane Doe" in body
+
+
+def test_build_issue_body_listings_shown():
+ from scripts.feedback_api import build_issue_body
+ form = {"type": "bug", "title": "X", "description": "Y", "repro": ""}
+ listings = [{"title": "CSM", "company": "Acme", "url": "https://example.com/1"}]
+ body = build_issue_body(form, {}, {"listings": listings})
+ assert "CSM" in body
+ assert "Acme" in body