from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Preformatted
from reportlab.lib import colors
import html
import re

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "docs" / "vending-machine-setup-day-manual.md"
OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.pdf"

styles = getSampleStyleSheet()
base = ParagraphStyle("Base", parent=styles["BodyText"], fontName="Helvetica", fontSize=9, leading=12, spaceAfter=3)
code = ParagraphStyle("Code", parent=base, fontName="Courier", fontSize=8, leading=10, leftIndent=8, textColor=colors.HexColor("#222222"))
heading1 = ParagraphStyle("H1", parent=styles["Heading1"], fontName="Helvetica-Bold", fontSize=18, leading=22, spaceBefore=10, spaceAfter=8)
heading2 = ParagraphStyle("H2", parent=styles["Heading2"], fontName="Helvetica-Bold", fontSize=13, leading=16, spaceBefore=8, spaceAfter=5)
heading3 = ParagraphStyle("H3", parent=styles["Heading3"], fontName="Helvetica-Bold", fontSize=11, leading=14, spaceBefore=6, spaceAfter=4)
small = ParagraphStyle("Small", parent=base, fontSize=8, textColor=colors.HexColor("#555555"))

def esc(text: str) -> str:
    return html.escape(text).replace("\n", "<br/>")

story = []
story.append(Paragraph("Ballbox vending machine setup day manual", heading1))
story.append(Paragraph("Standalone arrival-day PDF.", small))
story.append(Spacer(1, 8))

in_code = False
code_buf = []
for raw in SRC.read_text().splitlines(True):
    line = raw.rstrip("\n")
    stripped = line.strip()
    if stripped.startswith("```"):
        if in_code:
            story.append(Preformatted("\n".join(code_buf), code))
            story.append(Spacer(1, 3))
            code_buf = []
            in_code = False
        else:
            in_code = True
        continue
    if in_code:
        code_buf.append(line)
        continue
    if not stripped:
        story.append(Spacer(1, 4))
        continue
    if stripped.startswith("### "):
        story.append(Paragraph(esc(stripped[4:]), heading3))
    elif stripped.startswith("## "):
        story.append(Paragraph(esc(stripped[3:]), heading2))
    elif stripped.startswith("# "):
        story.append(Paragraph(esc(stripped[2:]), heading1))
    elif stripped.startswith(("- ", "* ")):
        story.append(Paragraph("• " + esc(stripped[2:]), base))
    else:
        m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
        if m:
            story.append(Paragraph(f"{m.group(1)}. {esc(m.group(2))}", base))
        else:
            story.append(Paragraph(esc(line), base))
if code_buf:
    story.append(Preformatted("\n".join(code_buf), code))

def page_num(canvas, doc):
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(colors.HexColor("#666666"))
    canvas.drawRightString(200 * mm, 10 * mm, f"Page {doc.page}")

OUT.parent.mkdir(parents=True, exist_ok=True)
doc = SimpleDocTemplate(str(OUT), pagesize=A4, rightMargin=16*mm, leftMargin=16*mm, topMargin=16*mm, bottomMargin=16*mm)
doc.build(story, onFirstPage=page_num, onLaterPages=page_num)
print(OUT)


from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Preformatted
from reportlab.lib import colors
import html

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "docs" / "exports" / "vending-setup-day-pack.pdf"
FILES = [
    ROOT / "docs" / "vending-machine-setup-day-manual.md",
    ROOT / "docs" / "tcn-payments-integration-plan.md",
    ROOT / "docs" / "tcn-payments-open-questions.md",
    ROOT / "docs" / "tcn-payments-execution-plan.md",
]

styles = getSampleStyleSheet()
base = ParagraphStyle(
    "Base",
    parent=styles["BodyText"],
    fontName="Helvetica",
    fontSize=9,
    leading=12,
    spaceAfter=3,
)
code = ParagraphStyle(
    "Code",
    parent=base,
    fontName="Courier",
    fontSize=8,
    leading=10,
    leftIndent=8,
    textColor=colors.HexColor("#222222"),
)
heading1 = ParagraphStyle(
    "Heading1Custom",
    parent=styles["Heading1"],
    fontName="Helvetica-Bold",
    fontSize=18,
    leading=22,
    spaceBefore=10,
    spaceAfter=8,
)
heading2 = ParagraphStyle(
    "Heading2Custom",
    parent=styles["Heading2"],
    fontName="Helvetica-Bold",
    fontSize=13,
    leading=16,
    spaceBefore=8,
    spaceAfter=5,
)
heading3 = ParagraphStyle(
    "Heading3Custom",
    parent=styles["Heading3"],
    fontName="Helvetica-Bold",
    fontSize=11,
    leading=14,
    spaceBefore=6,
    spaceAfter=4,
)
small = ParagraphStyle(
    "Small",
    parent=base,
    fontSize=8,
    textColor=colors.HexColor("#555555"),
)

def esc(text: str) -> str:
    return html.escape(text).replace("\n", "<br/>")

def add_markdown(lines, story):
    in_code = False
    code_buf = []
    for raw in lines:
        line = raw.rstrip("\n")
        stripped = line.strip()
        if stripped.startswith("```"):
            if in_code:
                story.append(Preformatted("\n".join(code_buf), code))
                story.append(Spacer(1, 3))
                code_buf = []
                in_code = False
            else:
                in_code = True
            continue
        if in_code:
            code_buf.append(line)
            continue
        if not stripped:
            story.append(Spacer(1, 4))
            continue
        if stripped.startswith("### "):
            story.append(Paragraph(esc(stripped[4:]), heading3))
            continue
        if stripped.startswith("## "):
            story.append(Paragraph(esc(stripped[3:]), heading2))
            continue
        if stripped.startswith("# "):
            story.append(Paragraph(esc(stripped[2:]), heading1))
            continue
        if stripped.startswith(("- ", "* ")):
            story.append(Paragraph("• " + esc(stripped[2:]), base))
            continue
        import re
        m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
        if m:
            story.append(Paragraph(f"{m.group(1)}. {esc(m.group(2))}", base))
            continue
        story.append(Paragraph(esc(line), base))
    if code_buf:
        story.append(Preformatted("\n".join(code_buf), code))

def page_num(canvas, doc):
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(colors.HexColor("#666666"))
    canvas.drawRightString(200 * mm, 10 * mm, f"Page {doc.page}")

OUT.parent.mkdir(parents=True, exist_ok=True)
doc = SimpleDocTemplate(str(OUT), pagesize=A4, rightMargin=16*mm, leftMargin=16*mm, topMargin=16*mm, bottomMargin=16*mm)
story = []
story.append(Paragraph("Ballbox vending setup day pack", heading1))
story.append(Paragraph("Combined export for arrival day operations and TCN payment integration reference.", small))
story.append(Spacer(1, 8))
for i, path in enumerate(FILES):
    story.append(Paragraph(path.name, heading2))
    story.append(Paragraph(str(path.relative_to(ROOT)), small))
    story.append(Spacer(1, 4))
    add_markdown(path.read_text().splitlines(True), story)
    if i < len(FILES) - 1:
        story.append(PageBreak())
doc.build(story, onFirstPage=page_num, onLaterPages=page_num)
print(OUT)


**Improving document aesthetics**

I think I need to create a shared renderer for better formatting. A quick edit to both scripts might help, including adding heading levels and inline code or bold styles. The user wants the PDF to look nicer, so maybe I should incorporate handling with tables as well. It feels like there’s enough to work with, so let’s go ahead and make those adjustments to both documents!

write

from reportlab.lib import colors
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.platypus import Paragraph, Spacer, Preformatted
from xml.sax.saxutils import escape as xml_escape
import re

styles = getSampleStyleSheet()

BASE = ParagraphStyle(
    "Base",
    parent=styles["BodyText"],
    fontName="Helvetica",
    fontSize=9,
    leading=12,
    spaceAfter=3,
)
BULLET = ParagraphStyle(
    "Bullet",
    parent=BASE,
    leftIndent=10,
    firstLineIndent=-6,
)
CODE = ParagraphStyle(
    "Code",
    parent=BASE,
    fontName="Courier",
    fontSize=8,
    leading=10,
    leftIndent=8,
    textColor=colors.HexColor("#222222"),
    backColor=colors.HexColor("#f6f6f6"),
)
H1 = ParagraphStyle(
    "H1",
    parent=styles["Heading1"],
    fontName="Helvetica-Bold",
    fontSize=18,
    leading=22,
    spaceBefore=10,
    spaceAfter=8,
)
H2 = ParagraphStyle(
    "H2",
    parent=styles["Heading2"],
    fontName="Helvetica-Bold",
    fontSize=13,
    leading=16,
    spaceBefore=8,
    spaceAfter=5,
)
H3 = ParagraphStyle(
    "H3",
    parent=styles["Heading3"],
    fontName="Helvetica-Bold",
    fontSize=11,
    leading=14,
    spaceBefore=6,
    spaceAfter=4,
)
H4 = ParagraphStyle(
    "H4",
    parent=BASE,
    fontName="Helvetica-Bold",
    fontSize=9.5,
    leading=12,
    textColor=colors.HexColor("#333333"),
    spaceBefore=5,
    spaceAfter=3,
)
SMALL = ParagraphStyle(
    "Small",
    parent=BASE,
    fontSize=8,
    textColor=colors.HexColor("#555555"),
)

def inline_markup(text: str) -> str:
    text = xml_escape(text)
    text = re.sub(r"`([^`]+)`", r"\1", text)
    text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
    return text

def add_markdown(lines, story):
    in_code = False
    code_buf = []
    for raw in lines:
        line = raw.rstrip("\n")
        stripped = line.strip()
        if stripped.startswith("```"):
            if in_code:
                story.append(Preformatted("\n".join(code_buf), CODE))
                story.append(Spacer(1, 4))
                code_buf = []
                in_code = False
            else:
                in_code = True
            continue
        if in_code:
            code_buf.append(line)
            continue
        if not stripped:
            story.append(Spacer(1, 4))
            continue
        if stripped.startswith("#### "):
            story.append(Paragraph(inline_markup(stripped[5:]), H4))
            continue
        if stripped.startswith("### "):
            story.append(Paragraph(inline_markup(stripped[4:]), H3))
            continue
        if stripped.startswith("## "):
            story.append(Paragraph(inline_markup(stripped[3:]), H2))
            continue
        if stripped.startswith("# "):
            story.append(Paragraph(inline_markup(stripped[2:]), H1))
            continue
        if stripped.startswith(("- ", "* ")):
            story.append(Paragraph("• " + inline_markup(stripped[2:]), BULLET))
            continue
        m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
        if m:
            story.append(Paragraph(f"{m.group(1)}. {inline_markup(m.group(2))}", BASE))
            continue
        story.append(Paragraph(inline_markup(line), BASE))
    if code_buf:
        story.append(Preformatted("\n".join(code_buf), CODE))


Successfully wrote 3337 bytes to /mnt/rpi/worktrees/ballbox/docs-vending-manual/scripts/pdf_markdown.py


edit

from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Preformatted
from reportlab.lib import colors
import html
import re

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "docs" / "vending-machine-setup-day-manual.md"
OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.pdf"

styles = getSampleStyleSheet()
base = ParagraphStyle("Base", parent=styles["BodyText"], fontName="Helvetica", fontSize=9, leading=12, spaceAfter=3)
code = ParagraphStyle("Code", parent=base, fontName="Courier", fontSize=8, leading=10, leftIndent=8, textColor=colors.HexColor("#222222"))
heading1 = ParagraphStyle("H1", parent=styles["Heading1"], fontName="Helvetica-Bold", fontSize=18, leading=22, spaceBefore=10, spaceAfter=8)
heading2 = ParagraphStyle("H2", parent=styles["Heading2"], fontName="Helvetica-Bold", fontSize=13, leading=16, spaceBefore=8, spaceAfter=5)
heading3 = ParagraphStyle("H3", parent=styles["Heading3"], fontName="Helvetica-Bold", fontSize=11, leading=14, spaceBefore=6, spaceAfter=4)
small = ParagraphStyle("Small", parent=base, fontSize=8, textColor=colors.HexColor("#555555"))

def esc(text: str) -> str:
    return html.escape(text).replace("\n", "<br/>")

story = []
story.append(Paragraph("Ballbox vending machine setup day manual", heading1))
story.append(Paragraph("Standalone arrival-day PDF.", small))
story.append(Spacer(1, 8))

in_code = False
code_buf = []
for raw in SRC.read_text().splitlines(True):
    line = raw.rstrip("\n")
    stripped = line.strip()
    if stripped.startswith("```"):
        if in_code:
            story.append(Preformatted("\n".join(code_buf), code))
            story.append(Spacer(1, 3))
            code_buf = []
            in_code = False
        else:
            in_code = True
        continue
    if in_code:
        code_buf.append(line)
        continue
    if not stripped:
        story.append(Spacer(1, 4))
        continue
    if stripped.startswith("### "):
        story.append(Paragraph(esc(stripped[4:]), heading3))
    elif stripped.startswith("## "):
        story.append(Paragraph(esc(stripped[3:]), heading2))
    elif stripped.startswith("# "):
        story.append(Paragraph(esc(stripped[2:]), heading1))
    elif stripped.startswith(("- ", "* ")):
        story.append(Paragraph("• " + esc(stripped[2:]), base))
    else:
        m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
        if m:
            story.append(Paragraph(f"{m.group(1)}. {esc(m.group(2))}", base))
        else:
            story.append(Paragraph(esc(line), base))
if code_buf:
    story.append(Preformatted("\n".join(code_buf), code))

from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib import colors
from pdf_markdown import add_markdown, H1, SMALL

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "docs" / "vending-machine-setup-day-manual.md"
OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.pdf"

story = []
story.append(Paragraph("Ballbox vending machine setup day manual", H1))
story.append(Paragraph("Standalone arrival-day PDF.", SMALL))
story.append(Spacer(1, 8))
add_markdown(SRC.read_text().splitlines(True), story)


Successfully replaced 1 block(s) in /mnt/rpi/worktrees/ballbox/docs-vending-manual/scripts/export-setup-manual-standalone-pdf.py.

  1 from pathlib import Path
  2 from reportlab.lib.pagesizes import A4
- 3 from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
  4 from reportlab.lib.units import mm
- 5 from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Preformatted
+ 4 from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
  6 from reportlab.lib import colors
- 7 import html
- 8 import re
+ 6 from pdf_markdown import add_markdown, H1, SMALL
  9
 10 ROOT = Path(__file__).resolve().parents[1]
 11 SRC = ROOT / "docs" / "vending-machine-setup-day-manual.md"
 12 OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.pdf"
 13
-14 styles = getSampleStyleSheet()
-15 base = ParagraphStyle("Base", parent=styles["BodyText"], fontName="Helvetica", fontSize=9, leading=12, spaceAfter=3)
-16 code = ParagraphStyle("Code", parent=base, fontName="Courier", fontSize=8, leading=10, leftIndent=8, textColor=colors.HexColor("#222222"))
-17 heading1 = ParagraphStyle("H1", parent=styles["Heading1"], fontName="Helvetica-Bold", fontSize=18, leading=22, spaceBefore=10, spaceAfter=8)
-18 heading2 = ParagraphStyle("H2", parent=styles["Heading2"], fontName="Helvetica-Bold", fontSize=13, leading=16, spaceBefore=8, spaceAfter=5)
-19 heading3 = ParagraphStyle("H3", parent=styles["Heading3"], fontName="Helvetica-Bold", fontSize=11, leading=14, spaceBefore=6, spaceAfter=4)
-20 small = ParagraphStyle("Small", parent=base, fontSize=8, textColor=colors.HexColor("#555555"))
-21
-22 def esc(text: str) -> str:
-23     return html.escape(text).replace("\n", "<br/>")
-24
 25 story = []
-26 story.append(Paragraph("Ballbox vending machine setup day manual", heading1))
-27 story.append(Paragraph("Standalone arrival-day PDF.", small))
+13 story.append(Paragraph("Ballbox vending machine setup day manual", H1))
+14 story.append(Paragraph("Standalone arrival-day PDF.", SMALL))
 28 story.append(Spacer(1, 8))
+16 add_markdown(SRC.read_text().splitlines(True), story)
 29
-30 in_code = False
-31 code_buf = []
-32 for raw in SRC.read_text().splitlines(True):
-33     line = raw.rstrip("\n")
-34     stripped = line.strip()
-35     if stripped.startswith("```"):
-36         if in_code:
-37             story.append(Preformatted("\n".join(code_buf), code))
-38             story.append(Spacer(1, 3))
-39             code_buf = []
-40             in_code = False
-41         else:
-42             in_code = True
-43         continue
-44     if in_code:
-45         code_buf.append(line)
-46         continue
-47     if not stripped:
-48         story.append(Spacer(1, 4))
-49         continue
-50     if stripped.startswith("### "):
-51         story.append(Paragraph(esc(stripped[4:]), heading3))
-52     elif stripped.startswith("## "):
-53         story.append(Paragraph(esc(stripped[3:]), heading2))
-54     elif stripped.startswith("# "):
-55         story.append(Paragraph(esc(stripped[2:]), heading1))
-56     elif stripped.startswith(("- ", "* ")):
-57         story.append(Paragraph("• " + esc(stripped[2:]), base))
-58     else:
-59         m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
-60         if m:
-61             story.append(Paragraph(f"{m.group(1)}. {esc(m.group(2))}", base))
-62         else:
-63             story.append(Paragraph(esc(line), base))
-64 if code_buf:
-65     story.append(Preformatted("\n".join(code_buf), code))
-66
 67 def page_num(canvas, doc):
 68     canvas.setFont("Helvetica", 8)
 69     canvas.setFillColor(colors.HexColor("#666666"))
 70     canvas.drawRightString(200 * mm, 10 * mm, f"Page {doc.page}")
    ...


from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Preformatted
from reportlab.lib import colors
import html

from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.lib import colors
from pdf_markdown import add_markdown, H1, H2, SMALL

styles = getSampleStyleSheet()
base = ParagraphStyle(
    "Base",
    parent=styles["BodyText"],
    fontName="Helvetica",
    fontSize=9,
    leading=12,
    spaceAfter=3,
)
code = ParagraphStyle(
    "Code",
    parent=base,
    fontName="Courier",
    fontSize=8,
    leading=10,
    leftIndent=8,
    textColor=colors.HexColor("#222222"),
)
heading1 = ParagraphStyle(
    "Heading1Custom",
    parent=styles["Heading1"],
    fontName="Helvetica-Bold",
    fontSize=18,
    leading=22,
    spaceBefore=10,
    spaceAfter=8,
)
heading2 = ParagraphStyle(
    "Heading2Custom",
    parent=styles["Heading2"],
    fontName="Helvetica-Bold",
    fontSize=13,
    leading=16,
    spaceBefore=8,
    spaceAfter=5,
)
heading3 = ParagraphStyle(
    "Heading3Custom",
    parent=styles["Heading3"],
    fontName="Helvetica-Bold",
    fontSize=11,
    leading=14,
    spaceBefore=6,
    spaceAfter=4,
)
small = ParagraphStyle(
    "Small",
    parent=base,
    fontSize=8,
    textColor=colors.HexColor("#555555"),
)

def esc(text: str) -> str:
    return html.escape(text).replace("\n", "<br/>")

def add_markdown(lines, story):
    in_code = False
    code_buf = []
    for raw in lines:
        line = raw.rstrip("\n")
        stripped = line.strip()
        if stripped.startswith("```"):
            if in_code:
                story.append(Preformatted("\n".join(code_buf), code))
                story.append(Spacer(1, 3))
                code_buf = []
                in_code = False
            else:
                in_code = True
            continue
        if in_code:
            code_buf.append(line)
            continue
        if not stripped:
            story.append(Spacer(1, 4))
            continue
        if stripped.startswith("### "):
            story.append(Paragraph(esc(stripped[4:]), heading3))
            continue
        if stripped.startswith("## "):
            story.append(Paragraph(esc(stripped[3:]), heading2))
            continue
        if stripped.startswith("# "):
            story.append(Paragraph(esc(stripped[2:]), heading1))
            continue
        if stripped.startswith(("- ", "* ")):
            story.append(Paragraph("• " + esc(stripped[2:]), base))
            continue
        import re
        m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
        if m:
            story.append(Paragraph(f"{m.group(1)}. {esc(m.group(2))}", base))
            continue
        story.append(Paragraph(esc(line), base))
    if code_buf:
        story.append(Preformatted("\n".join(code_buf), code))

story.append(Paragraph("Ballbox vending setup day pack", heading1))
story.append(Paragraph("Combined export for arrival day operations and TCN payment integration reference.", small))

story.append(Paragraph("Ballbox vending setup day pack", H1))
story.append(Paragraph("Combined export for arrival day operations and TCN payment integration reference.", SMALL))

    story.append(Paragraph(path.name, heading2))
    story.append(Paragraph(str(path.relative_to(ROOT)), small))

    story.append(Paragraph(path.name, H2))
    story.append(Paragraph(str(path.relative_to(ROOT)), SMALL))


Successfully replaced 4 block(s) in /mnt/rpi/worktrees/ballbox/docs-vending-manual/scripts/export-setup-docs-pdf.py.

   1 from pathlib import Path
   2 from reportlab.lib.pagesizes import A4
-  3 from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
   4 from reportlab.lib.units import mm
-  5 from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Preformatted
+  4 from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
   6 from reportlab.lib import colors
-  7 import html
+  6 from pdf_markdown import add_markdown, H1, H2, SMALL
   8
   9 ROOT = Path(__file__).resolve().parents[1]
  10 OUT = ROOT / "docs" / "exports" / "vending-setup-day-pack.pdf"
  11 FILES = [
     ...
  14     ROOT / "docs" / "tcn-payments-open-questions.md",
  15     ROOT / "docs" / "tcn-payments-execution-plan.md",
  16 ]
  17
- 18 styles = getSampleStyleSheet()
- 19 base = ParagraphStyle(
- 20     "Base",
- 21     parent=styles["BodyText"],
- 22     fontName="Helvetica",
- 23     fontSize=9,
- 24     leading=12,
- 25     spaceAfter=3,
- 26 )
- 27 code = ParagraphStyle(
- 28     "Code",
- 29     parent=base,
- 30     fontName="Courier",
- 31     fontSize=8,
- 32     leading=10,
- 33     leftIndent=8,
- 34     textColor=colors.HexColor("#222222"),
- 35 )
- 36 heading1 = ParagraphStyle(
- 37     "Heading1Custom",
- 38     parent=styles["Heading1"],
- 39     fontName="Helvetica-Bold",
- 40     fontSize=18,
- 41     leading=22,
- 42     spaceBefore=10,
- 43     spaceAfter=8,
- 44 )
- 45 heading2 = ParagraphStyle(
- 46     "Heading2Custom",
- 47     parent=styles["Heading2"],
- 48     fontName="Helvetica-Bold",
- 49     fontSize=13,
- 50     leading=16,
- 51     spaceBefore=8,
- 52     spaceAfter=5,
- 53 )
- 54 heading3 = ParagraphStyle(
- 55     "Heading3Custom",
- 56     parent=styles["Heading3"],
- 57     fontName="Helvetica-Bold",
- 58     fontSize=11,
- 59     leading=14,
- 60     spaceBefore=6,
- 61     spaceAfter=4,
- 62 )
- 63 small = ParagraphStyle(
- 64     "Small",
- 65     parent=base,
- 66     fontSize=8,
- 67     textColor=colors.HexColor("#555555"),
- 68 )
  69
- 70 def esc(text: str) -> str:
- 71     return html.escape(text).replace("\n", "<br/>")
  72
- 73 def add_markdown(lines, story):
- 74     in_code = False
- 75     code_buf = []
- 76     for raw in lines:
- 77         line = raw.rstrip("\n")
- 78         stripped = line.strip()
- 79         if stripped.startswith("```"):
- 80             if in_code:
- 81                 story.append(Preformatted("\n".join(code_buf), code))
- 82                 story.append(Spacer(1, 3))
- 83                 code_buf = []
- 84                 in_code = False
- 85             else:
- 86                 in_code = True
- 87             continue
- 88         if in_code:
- 89             code_buf.append(line)
- 90             continue
- 91         if not stripped:
- 92             story.append(Spacer(1, 4))
- 93             continue
- 94         if stripped.startswith("### "):
- 95             story.append(Paragraph(esc(stripped[4:]), heading3))
- 96             continue
- 97         if stripped.startswith("## "):
- 98             story.append(Paragraph(esc(stripped[3:]), heading2))
- 99             continue
-100         if stripped.startswith("# "):
-101             story.append(Paragraph(esc(stripped[2:]), heading1))
-102             continue
-103         if stripped.startswith(("- ", "* ")):
-104             story.append(Paragraph("• " + esc(stripped[2:]), base))
-105             continue
-106         import re
-107         m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
-108         if m:
-109             story.append(Paragraph(f"{m.group(1)}. {esc(m.group(2))}", base))
-110             continue
-111         story.append(Paragraph(esc(line), base))
-112     if code_buf:
-113         story.append(Preformatted("\n".join(code_buf), code))
-114
-115
 116 def page_num(canvas, doc):
 117     canvas.setFont("Helvetica", 8)
 118     canvas.setFillColor(colors.HexColor("#666666"))
 119     canvas.drawRightString(200 * mm, 10 * mm, f"Page {doc.page}")
     ...
 121
 122 OUT.parent.mkdir(parents=True, exist_ok=True)
 123 doc = SimpleDocTemplate(str(OUT), pagesize=A4, rightMargin=16*mm, leftMargin=16*mm, topMargin=16*mm, bottomMargin=16*mm)
 124 story = []
-125 story.append(Paragraph("Ballbox vending setup day pack", heading1))
-126 story.append(Paragraph("Combined export for arrival day operations and TCN payment integration reference.", small))
+ 28 story.append(Paragraph("Ballbox vending setup day pack", H1))
+ 29 story.append(Paragraph("Combined export for arrival day operations and TCN payment integration reference.", SMALL))
 127 story.append(Spacer(1, 8))
 128 for i, path in enumerate(FILES):
-129     story.append(Paragraph(path.name, heading2))
-130     story.append(Paragraph(str(path.relative_to(ROOT)), small))
+ 32     story.append(Paragraph(path.name, H2))
+ 33     story.append(Paragraph(str(path.relative_to(ROOT)), SMALL))
 131     story.append(Spacer(1, 4))
 132     add_markdown(path.read_text().splitlines(True), story)
 133     if i < len(FILES) - 1:
 134         story.append(PageBreak())
     ...


bash

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && python scripts/export-setup-manual-standalone-pdf.py && python scripts/export-setup-docs-pdf.py && ls -lh docs/exports/*.pdf


/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.pdf
/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-setup-day-pack.pdf
-rw-r--r-- 1 sebas sebas 17K Apr 30 18:49 docs/exports/vending-machine-setup-day-manual-standalone.pdf
-rw-r--r-- 1 sebas sebas 27K Apr 30 18:49 docs/exports/vending-setup-day-pack.pdf


cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && git add scripts/pdf_markdown.py scripts/export-setup-manual-standalone-pdf.py scripts/export-setup-docs-pdf.py docs/exports/vending-machine-setup-day-manual-standalone.pdf docs/exports/vending-setup-day-pack.pdf && git commit -m "docs: improve setup pdf markdown rendering"


[docs-vending-manual 394a7cc] docs: improve setup pdf markdown rendering
 5 files changed, 305 insertions(+), 312 deletions(-)
 create mode 100644 scripts/pdf_markdown.py


Updated #5 (pending → completed)

Build vending machine setup manual from provided sources in isolated worktree

Create/update docs in a dedicated worktree using the provided links and local research file, then review whether the Payments Integration section still looks valid.

Audit existing setup-day docs and align vending manuals

Review repo documentation for existing setup-day manuals, especially remote vs on-site roles, then consolidate/index/update docs so all relevant manuals remain visible and non-duplicative.

Collapse setup-day docs into one manual with role sections

Remove redundant remote/on-site split docs if they duplicate the combined manual, and keep a single setup-day manual with clearly labeled role sections plus updated indexing.

Ingest laptop PDFs into setup-day manual

Read TECHNICAL_LEARNINGS.pdf and ARRIVAL_GUIDE.pdf from laptop, compare against current unified setup-day manual, and merge any missing high-signal content without reintroducing duplicate docs.

Improve PDF markdown rendering for setup manual

Update the PDF exporter so deeper markdown headings like #### render properly and the standalone setup PDF looks cleaner without changing the source manual structure.


Listo.

Arreglé el render del PDF:
- `#### Remote` y `#### On-site` ahora renderizan como subtítulos reales
- mejoré bullets
- mejoré bloques de código
- mejoré inline code como `` `CodeUrl` ``

PDF actualizado:
- https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.pdf

Commit:
- `394a7cc` — `docs: improve setup pdf markdown rendering`

Si ves otro detalle feo del PDF, decime cuál y lo ajusto.


bien. ahora hace una copia en espaniol argentino


**Creating Spanish copy**

I need to create a Spanish copy, which probably means I'll develop a standalone manual. Also, considering a PDF export seems like a good idea. I might need to ask for clarification or further details, but I'm starting to think through the steps. I'll ensure that the final product is clear and effective, so I'll keep that in mind as I move forward. Let's see how this unfolds!

todo

Create Argentine Spanish copy of setup-day manual

Translate the standalone vending-machine setup-day manual into Argentine Spanish, keeping the same operational meaning and standalone behavior, then export a matching PDF copy.


Created #6: Create Argentine Spanish copy of setup-day manual (pending)


write

# Manual del día de setup de vending machines

## Propósito
- Un manual único de Ballbox para el día de llegada.
- Enfoque: primer encendido, onboarding en cloud, carga de media, readiness de pagos y chequeos de salida a producción.
- Familia objetivo según la investigación actual: TCN CSC-8C(V49) o la variante 8C / 49 pulgadas más cercana que entregue el proveedor.

## Reparto de roles
### El operador remoto se ocupa de
- login en cloud y chequeos del tenant
- catálogo/productos y chequeos de sync remota
- carga y asignación de media
- chequeos de pagos de Ballbox
- log de evidencia y paquete de escalación
- coordinación final de go/no-go

### La persona en sitio se ocupa de
- recepción e inspección física
- energía, red y chequeos de pantalla táctil
- fotos/videos de la máquina
- reinicios locales o acciones de service
- carga de producto y chequeos de vend físico
- confirmar comportamiento de pantalla/media/pagos sobre hardware real

## Inputs de setup
- Cloud: `https://dnymer.ourvend.com/`
- Usuario de test: `cc`
- Contraseña de test: `ys123456`
- Video de referencia usado para la guía de media upload: `https://www.youtube.com/watch?v=Qwbp3muM7gA`
- Nota de producción: la cloud real y las credenciales reales llegan el día de entrega de las máquinas.

## Puntos no verificados o conflictivos
- La guía de red tiene conflicto en el camino principal:
  - sesgo de investigación previa: Ethernet first
  - otro insumo técnico: la máquina tiene módulo 4G y soporta Wi‑Fi y Ethernet como backup
  - otra guía de llegada: comprar SIMs locales y configurar APN en sitio
  - postura operativa actual: la SIM es opcional para el día de llegada; Wi‑Fi debería alcanzar si la red del lugar funciona; para instalaciones futuras en clubes, la SIM puede ser más segura porque no sabemos qué Wi‑Fi habrá ni cuán estable será
  - no asumir todavía un default productivo final; verificar por máquina qué camino existe y cuál es confiable
- La documentación de integración de pagos sigue incompleta:
  - la investigación y la doc del repo apuntan a QR dinámico controlado por Ballbox + handshake de vend del lado TCN
  - otro insumo técnico dice que Justin describió pagos de terceros como algo estándar y menciona `Payment > Payment`, `Remote Track Configuration`, `Remote Vend API`, `External Payment Protocol` y un flujo de callback/liberación
  - no asumir entrypoint de UI ni contrato exacto de API hasta que TCN entregue el protocolo o package real
- El sync remoto de precio/stock puede depender de features VIP según uno de los insumos técnicos. Eso entra en conflicto con asumir que toda capacidad de sync/admin está disponible por default. Verificar en vivo antes de depender de eso.
- El código admin `888888` aparece como un código usual para diagnóstico, pero no está confirmado para estas unidades. Verificar en vivo; no depender de eso ciegamente.

## No negociables
- No flashear firmware a ciegas desde otra familia de modelos.
- Pedir a TCN/proveedor el package exacto matcheado por serial, revisión de mainboard, familia de pantalla y BOM del módulo de pagos.
- Configurar fecha, hora y zona horaria correctas antes de cualquier prueba de cloud o pagos.
- No habilitar pagos live hasta que cloud, sync de producto y tests de vend estén limpios.
- Si el camino de conectividad no está claro, verificar Ethernet, Wi‑Fi y realidad de SIM local en la unidad antes de estandarizar el camino operativo.

## Evidencia compartida a capturar
- fotos de crate y etiquetas
- número de serie
- etiqueta de modelo
- familia de pantalla
- modelo del módulo de pagos
- versión de firmware/build
- url/nombre del tenant cloud
- screenshot de máquina online
- screenshot de sync de producto/precio
- screenshot de upload/asignación de media
- screenshot del QR de pago
- evidencia de vend exitoso
- timestamps con zona horaria

## Checklist del día anterior
- Tener Chrome listo.
- Confirmar tipo de enchufe. Si es EU, comprar 5 adaptadores EU-hembra a AR-macho. Si es SAA, puede que no haga falta.
- Tener Ethernet disponible en sitio.
- Las SIMs locales son opcionales para el día de llegada. Si se van a usar como fallback o para rollout futuro en clubes, preparar SIMs locales más datos de APN para Personal, Movistar o Claro.
- Tener verificados alargue / energía estable / puesta a tierra.
- Tener hotspot del teléfono solo como fallback.
- Tener lista la cuenta de test de pagos de Ballbox.
- Preparar una nota compartida con:
  - serial de máquina
  - etiqueta de modelo
  - familia de pantalla
  - modelo del módulo de pagos
  - cloud device id / qr / barcode si aparece
  - venue / club / nickname de la máquina
- Pedir al proveedor con anticipación:
  - invite al tenant o login cloud confirmado
  - package exacto de firmware/build
  - PIN/password de service
  - docs de open-device / payment API
  - mapeo de device ids / seriales
  - `External Payment Integration Protocol` o `Remote Vend API`

## Llegada y recepción
### En sitio
1. Fotografiar el crate antes de abrir.
2. Registrar daños, estado del pallet, etiquetas y stickers de serie.
3. Confirmar la etiqueta real de la máquina.
4. Sacar toda espuma de transporte, cintas, locks de bandejas y rellenos del pickup box.
5. Chequear:
   - vidrio de pantalla
   - alineación del bezel
   - lock / puerta
   - flap del pickup box
   - payment bay
   - patas niveladoras
6. Nivelar la máquina antes de los tests de vend.
7. Registrar todos los ids en la nota compartida.
8. Pasar fotos e ids al operador remoto.

### Remoto
- Chequear mismatch de modelo antes de seguir.
- Si hay daño visible de transporte, documentarlo antes del power-on y decidir si conviene pausar el setup.

## Primer encendido
### En sitio
1. Verificar tensión local y puesta a tierra.
2. Encender con el camino de red más confiable disponible, pero registrar si la unidad quedó por Ethernet, Wi‑Fi o SIM local / 4G.
3. Wi‑Fi debería alcanzar para el día de setup si la red del lugar funciona.
4. Si se usa SIM local / 4G, tratarlo como fallback opcional para el día de llegada o como opción de mayor confiabilidad para instalaciones futuras en clubes; insertar la SIM y configurar APN en Android settings según la operadora local.
5. Configurar:
   - fecha
   - hora
   - zona horaria
   - idioma / locale
6. Confirmar reachability a internet.
7. Probar touch en esquinas, botones de selección y botones de pago.
8. Registrar versión de firmware/build si aparece.
9. Capturar la pantalla de launcher/system info y pasar Machine Number más S/N al operador remoto.
10. Si el touch deriva:
   - limpiar vidrio
   - sacar protector temporal
   - reintentar
   - usar diagnósticos internos si existen
   - escalar a TCN por el package exacto de touch/display si sigue mal

### Remoto
- Registrar el camino exacto de red usado.
- Si la red falla, separar bien el problema:
  - problema del Ethernet del sitio
  - problema de Wi‑Fi
  - problema de SIM/APN local
  - problema del módem de la máquina

## Onboarding en OurVend cloud
Portal:
- URL: `https://dnymer.ourvend.com/`
- Navegador: preferir Chrome
- Credenciales de test:
  - usuario: `cc`
  - contraseña: `ys123456`

### Remoto
1. Hacer login.
2. Confirmar tenant, zona horaria, moneda y permisos/roles.
3. Crear o confirmar jerarquía de device groups:
   - Country > Region > Club > Machine
4. Crear/vincular el registro de máquina con:
   - serial number
   - familia/modelo
   - ubicación
   - zona horaria
   - idioma
   - perfil de temperatura
   - moneda
5. Confirmar que la máquina aparezca online.
6. Confirmar que heartbeat / last-seen se actualicen.
7. Cargar catálogo inicial.
8. Empujar un cambio remoto seguro:
   - nombre de producto o precio
9. Confirmar que la máquina reciba el sync.

### En sitio
- Estar listo para pasar serial, modelo, familia de pantalla, modelo del módulo de pagos, cloud device id, Machine Number y cualquier error visible en la máquina.
- Confirmar estado online/offline en la máquina si se ve.

## Catálogo de productos y setup de tracks
### Remoto
1. Cargar primero los productos en el catálogo global.
2. Subir fotos y nombres de producto antes de mapear tracks.
3. En `Products > Track Management`, mapear cada slot/track a un producto del catálogo.
4. Definir precios en ARS o en la unidad configurada.
5. Si existe, usar clone/copy para acelerar el mapeo entre máquinas del mismo modelo.
6. Disparar sync y confirmar que la máquina se actualice.

### En sitio
1. Cargar el producto físico según el planograma.
2. Confirmar que el mapping track-producto coincida con la carga física.
3. Confirmar que precios y fotos se actualicen en pantalla luego del sync.

### Notas
- El catálogo global debería ir primero y después el mapping por máquina.
- Los cambios en cloud pueden requerir `Sync` explícito o esperar el heartbeat de la máquina.
- El sync remoto de tracks/precios puede estar VIP-gated; verificar en vivo.

## Diagnóstico mecánico
### En sitio
1. Si el diagnóstico admin está accesible, correr motor test antes de cargar stock vendible.
2. Preferir pruebas con tracks vacíos primero para detectar daños de transporte.
3. Chequear estado de puerta, comportamiento del sensor de caída y alarmas visibles de hardware si aparecen.
4. Si pide código admin, `888888` es solo un código comúnmente mencionado, no uno confirmado para estas unidades.

### Remoto
- Registrar si se pudo entrar o no a diagnósticos.
- Si diagnósticos está bloqueado, anotar si el bloqueo fue por PIN desconocido, menú ausente o limitación de tenant/rol.

## Carga de media en la vending machine
Fuente: video público `How to upload videos to the vending machine`.
- Tratar esta sección como checklist operativo, no como click-path exacto ya verificado.

### Remoto
1. Encontrar la sección de device-management / media / advertising.
2. Subir primero un video corto de prueba.
3. Asignarlo a la máquina o grupo correcto.
4. Publicar/sincronizar.
5. Si sale bien, subir el pack final de producción.

Registrar:
- tipo de archivo
- tamaño del archivo
- target de asignación
- si hizo falta sync o reboot

### En sitio
1. Confirmar que la máquina reciba la media actualizada.
2. Reiniciar solo si hace falta o si se pide.
3. Confirmar reproducción en pantalla.
4. Confirmar que la reproducción sea full-screen y que el flujo touch siga funcionando.
5. Grabar video corto como prueba.

Puntos a confirmar en la UI del tenant:
- path exacto del menú de media upload
- tipos de archivo soportados / límites de tamaño
- si la media se asigna por máquina, por grupo o por playlist
- si hace falta sync/reboot manual después de publicar

## Estado de la integración de pagos
### Posición actual de Ballbox
- `/admin/payments` queda como harness de test/debug de Ballbox.
- El flujo productivo de vending debería ir por protocolo TCN.
- La máquina debería mostrar el QR buyer-facing en su propia pantalla.
- Ballbox debería ser dueño de la creación del pago, validación del webhook, persistencia de aprobaciones y reconciliación.

### Por qué esto todavía parece válido
- La investigación actual sigue apoyando OurVend/TCN como capa de cloud + control del dispositivo, no como dueña de la lógica de negocio de Ballbox.
- La evidencia pública sigue apuntando a device-open access con secret/signature del lado proveedor.
- La investigación sigue apoyando QR dinámico controlado por backend más autorización de vend del lado proveedor como el default más seguro hasta que lleguen docs exactas del tenant.
- El login cloud de test por sí solo no cambia esa dirección.

### Lo que todavía hay que confirmar en el setup day
- payload exacto de payment-init desde máquina/cloud
- path exacto de callback / notify de vend-success
- expectativa exacta de `CodeUrl`: payload QR vs link vs deep link específico del proveedor
- si la finalización del pago depende de callback, polling o ambos
- shape exacto de auth/signature para endpoints del tenant productivo

### Secuencia de test de pago del setup day
#### Remoto
1. Confirmar que la máquina aparezca online en cloud.
2. Confirmar que exista un producto vendible y esté bien mapeado.
3. Confirmar que el flujo de test de pagos de Ballbox siga pudiendo crear órdenes QR.
4. Confirmar que máquina/cloud puedan pedir payment init contra el adapter o stub de Ballbox.
5. Escanear y pagar el QR desde un teléfono.
6. Confirmar que Ballbox reciba el webhook del proveedor.
7. Confirmar que el pago aprobado se persista con correlación canónica de máquina.
8. Confirmar que el lado TCN reciba la señal de success/notify.
9. Reconciliar pago, resultado de vend y cambio de inventario.

#### En sitio
1. Confirmar que el producto vendible esté cargado físicamente.
2. Registrar la visualización del QR en la máquina.
3. Después de la aprobación, confirmar que cambie el estado de la máquina.
4. Confirmar que el vend físico ocurra con éxito.
5. Grabar video de prueba.
6. Reportar de inmediato cualquier mismatch entre pago exitoso y resultado de vend.

## Aceptación para salida a producción
- máquina online y estable
- zona horaria correcta
- touch OK en toda la pantalla
- al menos un sync de producto OK
- sync de precio OK
- sync de media OK
- test de pago OK
- test de vend OK
- observado al menos un path de retry/error
- todos los ids y credenciales archivados en la nota interna de ops

## Paquete de escalación para proveedor / TCN
Si algo bloquea el setup, mandar todo esto junto:
- serial de máquina
- etiqueta de modelo
- familia de pantalla
- modelo del módulo de pagos
- versión actual de firmware/build
- nombre/url del tenant cloud
- screenshots/video del problema
- timestamps con zona horaria
- si estaba por Ethernet o Wi‑Fi
- última acción exitosa
- si el problema parece de Ballbox, de cloud o de machine-side

## Regla standalone
- Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.
- Si algún detalle de este manual entra en conflicto con el comportamiento real de la máquina, frenar, capturar evidencia y verificar antes de seguir.


Successfully wrote 13949 bytes to /mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/vending-machine-setup-day-manual.es-ar.md


from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib import colors
from pdf_markdown import add_markdown, H1, SMALL

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "docs" / "vending-machine-setup-day-manual.es-ar.md"
OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.es-ar.pdf"

story = []
story.append(Paragraph("Manual del día de setup de vending machines", H1))
story.append(Paragraph("PDF standalone para el día de llegada.", SMALL))
story.append(Spacer(1, 8))
add_markdown(SRC.read_text().splitlines(True), story)

def page_num(canvas, doc):
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(colors.HexColor("#666666"))
    canvas.drawRightString(200 * mm, 10 * mm, f"Página {doc.page}")

OUT.parent.mkdir(parents=True, exist_ok=True)
doc = SimpleDocTemplate(str(OUT), pagesize=A4, rightMargin=16*mm, leftMargin=16*mm, topMargin=16*mm, bottomMargin=16*mm)
doc.build(story, onFirstPage=page_num, onLaterPages=page_num)
print(OUT)


Successfully wrote 1115 bytes to /mnt/rpi/worktrees/ballbox/docs-vending-manual/scripts/export-setup-manual-standalone-es-ar-pdf.py


bash

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && python scripts/export-setup-manual-standalone-es-ar-pdf.py && ls -lh docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf


/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf
-rw-r--r-- 1 sebas sebas 18K Apr 30 18:54 docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf


cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && git add docs/vending-machine-setup-day-manual.es-ar.md scripts/export-setup-manual-standalone-es-ar-pdf.py docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf && git commit -m "docs: add es-ar setup day manual"


[docs-vending-manual b9d788e] docs: add es-ar setup day manual
 3 files changed, 548 insertions(+)
 create mode 100644 docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf
 create mode 100644 docs/vending-machine-setup-day-manual.es-ar.md
 create mode 100644 scripts/export-setup-manual-standalone-es-ar-pdf.py


Updated #6 (pending → completed)


Hecho.

Archivo en español argentino:
- `docs/vending-machine-setup-day-manual.es-ar.md`

PDF:
- `/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf`
- https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf

Commit:
- `b9d788e` — `docs: add es-ar setup day manual`

Ojo:
- traduje a español argentino, pero mantuve algunos términos técnicos en inglés donde conviene (`tenant`, `sync`, `build`, `fallback`, `heartbeat`, `touch`, `Machine Number`).
- si querés, hago una segunda pasada para hacerlo:
  1. más argentino-operativo, o
  2. más neutro/formal.


Wi■Fi esta asi escrito raro aca


cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && rg -n "Wi|■|‑|–|—" docs/vending-machine-setup-day-manual.es-ar.md scripts/pdf_markdown.py docs/exports -g '!*.pdf' | sed -n '1,240p'


docs/vending-machine-setup-day-manual.es-ar.md:35:  - otro insumo técnico: la máquina tiene módulo 4G y soporta Wi‑Fi y Ethernet como backup
docs/vending-machine-setup-day-manual.es-ar.md:37:  - postura operativa actual: la SIM es opcional para el día de llegada; Wi‑Fi debería alcanzar si la red del lugar funciona; para instalaciones futuras en clubes, la SIM puede ser más segura porque no sabemos qué Wi‑Fi habrá ni cuán estable será
docs/vending-machine-setup-day-manual.es-ar.md:51:- Si el camino de conectividad no está claro, verificar Ethernet, Wi‑Fi y realidad de SIM local en la unidad antes de estandarizar el camino operativo.
docs/vending-machine-setup-day-manual.es-ar.md:115:2. Encender con el camino de red más confiable disponible, pero registrar si la unidad quedó por Ethernet, Wi‑Fi o SIM local / 4G.
docs/vending-machine-setup-day-manual.es-ar.md:116:3. Wi‑Fi debería alcanzar para el día de setup si la red del lugar funciona.
docs/vending-machine-setup-day-manual.es-ar.md:138:  - problema de Wi‑Fi
docs/vending-machine-setup-day-manual.es-ar.md:296:- si estaba por Ethernet o Wi‑Fi


**Fixing hyphen issues**

I need to replace a weird hyphen that might be a nonbreaking hyphen, as it's causing rendering problems. It’s interesting how something that seems so small can affect the overall output. I wonder if this will fix the layout issue that’s been bothering me. It's crucial to be thorough here, as I want everything to look perfect for the user. Let's make sure to address this correctly!


Found 8 occurrences of the text in /mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/vending-machine-setup-day-manual.es-ar.md. The text must be unique. Please provide more context to make it unique.


**Figuring out text replacement**

I need to execute a global text replacement using Perl in a bash environment. This means I should focus on finding the right command and syntax to ensure it works as intended across all relevant matches in the text. I might consider testing it on a smaller sample before applying it widely, just to make sure I don't accidentally alter something important. Let’s keep everything in mind as I start crafting the command!

python - <<'PY'
from pathlib import Path
p=Path('/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/vending-machine-setup-day-manual.es-ar.md')
text=p.read_text()
text=text.replace('Wi‑Fi','Wi-Fi')
p.write_text(text)
print('done')
PY
cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && python scripts/export-setup-manual-standalone-es-ar-pdf.py && git add docs/vending-machine-setup-day-manual.es-ar.md docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf && git commit -m "docs: normalize wifi spelling in es-ar manual"


done
/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf
[docs-vending-manual 58fd177] docs: normalize wifi spelling in es-ar manual
 2 files changed, 67 insertions(+), 73 deletions(-)


Sí. Era el guion raro.

Lo normalicé a:
- `Wi‑Fi` -> `Wi‑Fi` no, perdón: quedó `Wi-Fi` ASCII normal

Actualicé:
- `docs/vending-machine-setup-day-manual.es-ar.md`
- `docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf`

Commit:
- `58fd177` — `docs: normalize wifi spelling in es-ar manual`

PDF actualizado:
- https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/vending-machine-setup-day-manual-standalone.es-ar.pdf

Si ves más caracteres rotos, los limpio todos de una pasada.


hace uno ahora bien basico, lossy y happy-path para leer rapido (espaniol). que ese y el vending-machine-setup-day-manual-standalone.es-ar.pdf tengan numeracion para ir del basico al extendido en caso de blocker/necesitar mas info


**Organizing document edits**

I need to edit the documents, adding numbering and labels to help with organization. I’m thinking of creating a basic version of the lossy happy path in both markdown and PDF formats. It might also be helpful to include standalone sections for points 1 and 2, ensuring clarity in the presentation. Let's focus on getting this done systematically!

todo

Create basic happy-path Spanish setup manual with numbered progression

Add a short lossy happy-path Spanish manual for fast reading, and number both the basic and extended Spanish PDFs so operators can move from basic to extended if blocked or needing more detail.


Created #7: Create basic happy-path Spanish setup manual with numbered progression (pending)


write

# 01. Setup rápido de vending machine

## Objetivo
- Leer esto primero.
- Ir por happy path.
- Si algo falla o falta info, pasar al manual extendido `02`.

## Datos base
- Cloud: `https://dnymer.ourvend.com/`
- Usuario test: `cc`
- Password test: `ys123456`

## Reparto simple
### Remoto
- entra al cloud
- da de alta o verifica la máquina
- carga productos, precios y media
- valida pago

### En sitio
- enchufa
- conecta red
- saca fotos
- carga producto
- confirma que la máquina vende

## Happy path
1. Sacar foto de:
   - etiqueta/serial
   - pantalla de system info
   - máquina completa
2. Enchufar y encender.
3. Conectar red:
   - primero usar lo que funcione más rápido
   - Wi-Fi debería alcanzar para setup day si anda bien
   - SIM es opcional
4. Configurar:
   - fecha
   - hora
   - zona horaria
5. Confirmar que la máquina tenga internet.
6. En cloud, verificar que aparezca online.
7. Cargar o verificar catálogo.
8. Mapear tracks/productos/precios.
9. Hacer sync.
10. Confirmar en la pantalla de la máquina:
   - producto correcto
   - foto correcta
   - precio correcto
11. Subir un video corto de prueba.
12. Asignarlo a la máquina.
13. Publicar/sincronizar.
14. Confirmar que se vea bien en pantalla.
15. Cargar producto físico.
16. Hacer una compra real de prueba.
17. Confirmar:
   - aparece QR
   - entra el pago
   - la máquina vende
18. Guardar evidencia:
   - fotos
   - video corto del vend
   - screenshot de cloud

## Si algo falla
Pasar al manual extendido:
- `02-vending-machine-setup-day-manual.es-ar.md`

## Blockers típicos
- la máquina no aparece online
- sync no baja a la máquina
- media no aparece
- sale el pago pero no vende
- no coincide track/producto/precio
- hay duda con red, SIM, APN o menú admin

## Criterio de listo
- máquina online
- hora correcta
- catálogo OK
- precio OK
- media OK
- pago OK
- vend OK


Successfully wrote 1856 bytes to /mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/01-vending-machine-setup-day-basico.es-ar.md


bash

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && cp docs/vending-machine-setup-day-manual.es-ar.md docs/02-vending-machine-setup-day-manual.es-ar.md && python - <<'PY'
from pathlib import Path
p=Path('docs/02-vending-machine-setup-day-manual.es-ar.md')
text=p.read_text()
text=text.replace('# Manual del día de setup de vending machines','# 02. Manual extendido del día de setup de vending machines',1)
text=text.replace('- Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.','- Este es el manual extendido. Si alcanza con el happy path, leer primero `01-vending-machine-setup-day-basico.es-ar.md`.\n- Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.',1)
p.write_text(text)
print('done')
PY


done


write

from pathlib import Path
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib import colors
from pdf_markdown import add_markdown, H1, SMALL

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "docs" / "01-vending-machine-setup-day-basico.es-ar.md"
OUT = ROOT / "docs" / "exports" / "01-vending-machine-setup-day-basico.es-ar.pdf"

story = []
story.append(Paragraph("01. Setup rápido de vending machine", H1))
story.append(Paragraph("Versión básica, lossy y happy-path.", SMALL))
story.append(Spacer(1, 8))
add_markdown(SRC.read_text().splitlines(True), story)

def page_num(canvas, doc):
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(colors.HexColor("#666666"))
    canvas.drawRightString(200 * mm, 10 * mm, f"Página {doc.page}")

OUT.parent.mkdir(parents=True, exist_ok=True)
doc = SimpleDocTemplate(str(OUT), pagesize=A4, rightMargin=16*mm, leftMargin=16*mm, topMargin=16*mm, bottomMargin=16*mm)
doc.build(story, onFirstPage=page_num, onLaterPages=page_num)
print(OUT)


Successfully wrote 1099 bytes to /mnt/rpi/worktrees/ballbox/docs-vending-manual/scripts/export-setup-manual-standalone-es-ar-basic-pdf.py


edit

SRC = ROOT / "docs" / "vending-machine-setup-day-manual.es-ar.md"
OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.es-ar.pdf"

SRC = ROOT / "docs" / "02-vending-machine-setup-day-manual.es-ar.md"
OUT = ROOT / "docs" / "exports" / "02-vending-machine-setup-day-manual-extendido.es-ar.pdf"

story.append(Paragraph("Manual del día de setup de vending machines", H1))
story.append(Paragraph("PDF standalone para el día de llegada.", SMALL))

story.append(Paragraph("02. Manual extendido del día de setup de vending machines", H1))
story.append(Paragraph("PDF standalone extendido para el día de llegada.", SMALL))


Successfully replaced 2 block(s) in /mnt/rpi/worktrees/ballbox/docs-vending-manual/scripts/export-setup-manual-standalone-es-ar-pdf.py.

    ...
  5 from reportlab.lib import colors
  6 from pdf_markdown import add_markdown, H1, SMALL
  7
  8 ROOT = Path(__file__).resolve().parents[1]
- 9 SRC = ROOT / "docs" / "vending-machine-setup-day-manual.es-ar.md"
-10 OUT = ROOT / "docs" / "exports" / "vending-machine-setup-day-manual-standalone.es-ar.pdf"
+ 9 SRC = ROOT / "docs" / "02-vending-machine-setup-day-manual.es-ar.md"
+10 OUT = ROOT / "docs" / "exports" / "02-vending-machine-setup-day-manual-extendido.es-ar.pdf"
 11
 12 story = []
-13 story.append(Paragraph("Manual del día de setup de vending machines", H1))
-14 story.append(Paragraph("PDF standalone para el día de llegada.", SMALL))
+13 story.append(Paragraph("02. Manual extendido del día de setup de vending machines", H1))
+14 story.append(Paragraph("PDF standalone extendido para el día de llegada.", SMALL))
 15 story.append(Spacer(1, 8))
 16 add_markdown(SRC.read_text().splitlines(True), story)
 17
 18
    ...


bash

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && python scripts/export-setup-manual-standalone-es-ar-basic-pdf.py && python scripts/export-setup-manual-standalone-es-ar-pdf.py && ls -lh docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf


/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf
/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf
-rw-r--r-- 1 sebas sebas 4.8K Apr 30 19:13 docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf
-rw-r--r-- 1 sebas sebas  18K Apr 30 19:13 docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf


cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && git add docs/01-vending-machine-setup-day-basico.es-ar.md docs/02-vending-machine-setup-day-manual.es-ar.md scripts/export-setup-manual-standalone-es-ar-basic-pdf.py scripts/export-setup-manual-standalone-es-ar-pdf.py docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf && git commit -m "docs: add numbered spanish setup guides"


[docs-vending-manual 60eac5c] docs: add numbered spanish setup guides
 6 files changed, 749 insertions(+), 4 deletions(-)
 create mode 100644 docs/01-vending-machine-setup-day-basico.es-ar.md
 create mode 100644 docs/02-vending-machine-setup-day-manual.es-ar.md
 create mode 100644 docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf
 create mode 100644 docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf
 create mode 100644 scripts/export-setup-manual-standalone-es-ar-basic-pdf.py


Updated #7 (pending → completed)


Hecho.

Ahora tenés dos niveles en español:

Básico, rápido, happy-path:
- `docs/01-vending-machine-setup-day-basico.es-ar.md`
- PDF: `docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf`

Extendido:
- `docs/02-vending-machine-setup-day-manual.es-ar.md`
- PDF: `docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf`

Links:
- https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf
- https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf

Lógica:
- `01` = leer primero
- `02` = saltar ahí si hay blocker, mismatch o hace falta más detalle

Commit:
- `60eac5c` — `docs: add numbered spanish setup guides`

Si querés, siguiente paso: hago que el `01` y el `02` se referencien mejor entre sí adentro del PDF mismo, tipo “si falla X, ir a sección Y del 02”.


me referia numerar las secciones para que coincidan. no los archivos


**Updating document structure**

I need to modify the document by adjusting the headings and numbering, ensuring basic and extended sections are aligned. I wonder if I should ask for clarification, but it feels unnecessary. I'll just go ahead and renumber sections in both main documents. I'm also considering if I should keep the filenames since the user mentioned they aren't files. But, I guess, I’ll follow through and make the necessary changes!

todo

Align section numbering between basic and extended Spanish manuals

Number the sections inside the basic and extended Spanish manuals so operators can jump from the basic guide to matching numbered sections in the extended guide, without relying on numbered filenames.


Created #8: Align section numbering between basic and extended Spanish manuals (pending)


bash

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && sed -n '1,260p' docs/01-vending-machine-setup-day-basico.es-ar.md

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && sed -n '1,340p' docs/02-vending-machine-setup-day-manual.es-ar.md


# 01. Setup rápido de vending machine

## Objetivo
- Leer esto primero.
- Ir por happy path.
- Si algo falla o falta info, pasar al manual extendido `02`.

## Datos base
- Cloud: `https://dnymer.ourvend.com/`
- Usuario test: `cc`
- Password test: `ys123456`

## Reparto simple
### Remoto
- entra al cloud
- da de alta o verifica la máquina
- carga productos, precios y media
- valida pago

### En sitio
- enchufa
- conecta red
- saca fotos
- carga producto
- confirma que la máquina vende

## Happy path
1. Sacar foto de:
   - etiqueta/serial
   - pantalla de system info
   - máquina completa
2. Enchufar y encender.
3. Conectar red:
   - primero usar lo que funcione más rápido
   - Wi-Fi debería alcanzar para setup day si anda bien
   - SIM es opcional
4. Configurar:
   - fecha
   - hora
   - zona horaria
5. Confirmar que la máquina tenga internet.
6. En cloud, verificar que aparezca online.
7. Cargar o verificar catálogo.
8. Mapear tracks/productos/precios.
9. Hacer sync.
10. Confirmar en la pantalla de la máquina:
   - producto correcto
   - foto correcta
   - precio correcto
11. Subir un video corto de prueba.
12. Asignarlo a la máquina.
13. Publicar/sincronizar.
14. Confirmar que se vea bien en pantalla.
15. Cargar producto físico.
16. Hacer una compra real de prueba.
17. Confirmar:
   - aparece QR
   - entra el pago
   - la máquina vende
18. Guardar evidencia:
   - fotos
   - video corto del vend
   - screenshot de cloud

## Si algo falla
Pasar al manual extendido:
- `02-vending-machine-setup-day-manual.es-ar.md`

## Blockers típicos
- la máquina no aparece online
- sync no baja a la máquina
- media no aparece
- sale el pago pero no vende
- no coincide track/producto/precio
- hay duda con red, SIM, APN o menú admin

## Criterio de listo
- máquina online
- hora correcta
- catálogo OK
- precio OK
- media OK
- pago OK
- vend OK


# 02. Manual extendido del día de setup de vending machines

## Propósito
- Un manual único de Ballbox para el día de llegada.
- Enfoque: primer encendido, onboarding en cloud, carga de media, readiness de pagos y chequeos de salida a producción.
- Familia objetivo según la investigación actual: TCN CSC-8C(V49) o la variante 8C / 49 pulgadas más cercana que entregue el proveedor.

## Reparto de roles
### El operador remoto se ocupa de
- login en cloud y chequeos del tenant
- catálogo/productos y chequeos de sync remota
- carga y asignación de media
- chequeos de pagos de Ballbox
- log de evidencia y paquete de escalación
- coordinación final de go/no-go

### La persona en sitio se ocupa de
- recepción e inspección física
- energía, red y chequeos de pantalla táctil
- fotos/videos de la máquina
- reinicios locales o acciones de service
- carga de producto y chequeos de vend físico
- confirmar comportamiento de pantalla/media/pagos sobre hardware real

## Inputs de setup
- Cloud: `https://dnymer.ourvend.com/`
- Usuario de test: `cc`
- Contraseña de test: `ys123456`
- Video de referencia usado para la guía de media upload: `https://www.youtube.com/watch?v=Qwbp3muM7gA`
- Nota de producción: la cloud real y las credenciales reales llegan el día de entrega de las máquinas.

## Puntos no verificados o conflictivos
- La guía de red tiene conflicto en el camino principal:
  - sesgo de investigación previa: Ethernet first
  - otro insumo técnico: la máquina tiene módulo 4G y soporta Wi-Fi y Ethernet como backup
  - otra guía de llegada: comprar SIMs locales y configurar APN en sitio
  - postura operativa actual: la SIM es opcional para el día de llegada; Wi-Fi debería alcanzar si la red del lugar funciona; para instalaciones futuras en clubes, la SIM puede ser más segura porque no sabemos qué Wi-Fi habrá ni cuán estable será
  - no asumir todavía un default productivo final; verificar por máquina qué camino existe y cuál es confiable
- La documentación de integración de pagos sigue incompleta:
  - la investigación y la doc del repo apuntan a QR dinámico controlado por Ballbox + handshake de vend del lado TCN
  - otro insumo técnico dice que Justin describió pagos de terceros como algo estándar y menciona `Payment > Payment`, `Remote Track Configuration`, `Remote Vend API`, `External Payment Protocol` y un flujo de callback/liberación
  - no asumir entrypoint de UI ni contrato exacto de API hasta que TCN entregue el protocolo o package real
- El sync remoto de precio/stock puede depender de features VIP según uno de los insumos técnicos. Eso entra en conflicto con asumir que toda capacidad de sync/admin está disponible por default. Verificar en vivo antes de depender de eso.
- El código admin `888888` aparece como un código usual para diagnóstico, pero no está confirmado para estas unidades. Verificar en vivo; no depender de eso ciegamente.

## No negociables
- No flashear firmware a ciegas desde otra familia de modelos.
- Pedir a TCN/proveedor el package exacto matcheado por serial, revisión de mainboard, familia de pantalla y BOM del módulo de pagos.
- Configurar fecha, hora y zona horaria correctas antes de cualquier prueba de cloud o pagos.
- No habilitar pagos live hasta que cloud, sync de producto y tests de vend estén limpios.
- Si el camino de conectividad no está claro, verificar Ethernet, Wi-Fi y realidad de SIM local en la unidad antes de estandarizar el camino operativo.

## Evidencia compartida a capturar
- fotos de crate y etiquetas
- número de serie
- etiqueta de modelo
- familia de pantalla
- modelo del módulo de pagos
- versión de firmware/build
- url/nombre del tenant cloud
- screenshot de máquina online
- screenshot de sync de producto/precio
- screenshot de upload/asignación de media
- screenshot del QR de pago
- evidencia de vend exitoso
- timestamps con zona horaria

## Checklist del día anterior
- Tener Chrome listo.
- Confirmar tipo de enchufe. Si es EU, comprar 5 adaptadores EU-hembra a AR-macho. Si es SAA, puede que no haga falta.
- Tener Ethernet disponible en sitio.
- Las SIMs locales son opcionales para el día de llegada. Si se van a usar como fallback o para rollout futuro en clubes, preparar SIMs locales más datos de APN para Personal, Movistar o Claro.
- Tener verificados alargue / energía estable / puesta a tierra.
- Tener hotspot del teléfono solo como fallback.
- Tener lista la cuenta de test de pagos de Ballbox.
- Preparar una nota compartida con:
  - serial de máquina
  - etiqueta de modelo
  - familia de pantalla
  - modelo del módulo de pagos
  - cloud device id / qr / barcode si aparece
  - venue / club / nickname de la máquina
- Pedir al proveedor con anticipación:
  - invite al tenant o login cloud confirmado
  - package exacto de firmware/build
  - PIN/password de service
  - docs de open-device / payment API
  - mapeo de device ids / seriales
  - `External Payment Integration Protocol` o `Remote Vend API`

## Llegada y recepción
### En sitio
1. Fotografiar el crate antes de abrir.
2. Registrar daños, estado del pallet, etiquetas y stickers de serie.
3. Confirmar la etiqueta real de la máquina.
4. Sacar toda espuma de transporte, cintas, locks de bandejas y rellenos del pickup box.
5. Chequear:
   - vidrio de pantalla
   - alineación del bezel
   - lock / puerta
   - flap del pickup box
   - payment bay
   - patas niveladoras
6. Nivelar la máquina antes de los tests de vend.
7. Registrar todos los ids en la nota compartida.
8. Pasar fotos e ids al operador remoto.

### Remoto
- Chequear mismatch de modelo antes de seguir.
- Si hay daño visible de transporte, documentarlo antes del power-on y decidir si conviene pausar el setup.

## Primer encendido
### En sitio
1. Verificar tensión local y puesta a tierra.
2. Encender con el camino de red más confiable disponible, pero registrar si la unidad quedó por Ethernet, Wi-Fi o SIM local / 4G.
3. Wi-Fi debería alcanzar para el día de setup si la red del lugar funciona.
4. Si se usa SIM local / 4G, tratarlo como fallback opcional para el día de llegada o como opción de mayor confiabilidad para instalaciones futuras en clubes; insertar la SIM y configurar APN en Android settings según la operadora local.
5. Configurar:
   - fecha
   - hora
   - zona horaria
   - idioma / locale
6. Confirmar reachability a internet.
7. Probar touch en esquinas, botones de selección y botones de pago.
8. Registrar versión de firmware/build si aparece.
9. Capturar la pantalla de launcher/system info y pasar Machine Number más S/N al operador remoto.
10. Si el touch deriva:
   - limpiar vidrio
   - sacar protector temporal
   - reintentar
   - usar diagnósticos internos si existen
   - escalar a TCN por el package exacto de touch/display si sigue mal

### Remoto
- Registrar el camino exacto de red usado.
- Si la red falla, separar bien el problema:
  - problema del Ethernet del sitio
  - problema de Wi-Fi
  - problema de SIM/APN local
  - problema del módem de la máquina

## Onboarding en OurVend cloud
Portal:
- URL: `https://dnymer.ourvend.com/`
- Navegador: preferir Chrome
- Credenciales de test:
  - usuario: `cc`
  - contraseña: `ys123456`

### Remoto
1. Hacer login.
2. Confirmar tenant, zona horaria, moneda y permisos/roles.
3. Crear o confirmar jerarquía de device groups:
   - Country > Region > Club > Machine
4. Crear/vincular el registro de máquina con:
   - serial number
   - familia/modelo
   - ubicación
   - zona horaria
   - idioma
   - perfil de temperatura
   - moneda
5. Confirmar que la máquina aparezca online.
6. Confirmar que heartbeat / last-seen se actualicen.
7. Cargar catálogo inicial.
8. Empujar un cambio remoto seguro:
   - nombre de producto o precio
9. Confirmar que la máquina reciba el sync.

### En sitio
- Estar listo para pasar serial, modelo, familia de pantalla, modelo del módulo de pagos, cloud device id, Machine Number y cualquier error visible en la máquina.
- Confirmar estado online/offline en la máquina si se ve.

## Catálogo de productos y setup de tracks
### Remoto
1. Cargar primero los productos en el catálogo global.
2. Subir fotos y nombres de producto antes de mapear tracks.
3. En `Products > Track Management`, mapear cada slot/track a un producto del catálogo.
4. Definir precios en ARS o en la unidad configurada.
5. Si existe, usar clone/copy para acelerar el mapeo entre máquinas del mismo modelo.
6. Disparar sync y confirmar que la máquina se actualice.

### En sitio
1. Cargar el producto físico según el planograma.
2. Confirmar que el mapping track-producto coincida con la carga física.
3. Confirmar que precios y fotos se actualicen en pantalla luego del sync.

### Notas
- El catálogo global debería ir primero y después el mapping por máquina.
- Los cambios en cloud pueden requerir `Sync` explícito o esperar el heartbeat de la máquina.
- El sync remoto de tracks/precios puede estar VIP-gated; verificar en vivo.

## Diagnóstico mecánico
### En sitio
1. Si el diagnóstico admin está accesible, correr motor test antes de cargar stock vendible.
2. Preferir pruebas con tracks vacíos primero para detectar daños de transporte.
3. Chequear estado de puerta, comportamiento del sensor de caída y alarmas visibles de hardware si aparecen.
4. Si pide código admin, `888888` es solo un código comúnmente mencionado, no uno confirmado para estas unidades.

### Remoto
- Registrar si se pudo entrar o no a diagnósticos.
- Si diagnósticos está bloqueado, anotar si el bloqueo fue por PIN desconocido, menú ausente o limitación de tenant/rol.

## Carga de media en la vending machine
Fuente: video público `How to upload videos to the vending machine`.
- Tratar esta sección como checklist operativo, no como click-path exacto ya verificado.

### Remoto
1. Encontrar la sección de device-management / media / advertising.
2. Subir primero un video corto de prueba.
3. Asignarlo a la máquina o grupo correcto.
4. Publicar/sincronizar.
5. Si sale bien, subir el pack final de producción.

Registrar:
- tipo de archivo
- tamaño del archivo
- target de asignación
- si hizo falta sync o reboot

### En sitio
1. Confirmar que la máquina reciba la media actualizada.
2. Reiniciar solo si hace falta o si se pide.
3. Confirmar reproducción en pantalla.
4. Confirmar que la reproducción sea full-screen y que el flujo touch siga funcionando.
5. Grabar video corto como prueba.

Puntos a confirmar en la UI del tenant:
- path exacto del menú de media upload
- tipos de archivo soportados / límites de tamaño
- si la media se asigna por máquina, por grupo o por playlist
- si hace falta sync/reboot manual después de publicar

## Estado de la integración de pagos
### Posición actual de Ballbox
- `/admin/payments` queda como harness de test/debug de Ballbox.
- El flujo productivo de vending debería ir por protocolo TCN.
- La máquina debería mostrar el QR buyer-facing en su propia pantalla.
- Ballbox debería ser dueño de la creación del pago, validación del webhook, persistencia de aprobaciones y reconciliación.

### Por qué esto todavía parece válido
- La investigación actual sigue apoyando OurVend/TCN como capa de cloud + control del dispositivo, no como dueña de la lógica de negocio de Ballbox.
- La evidencia pública sigue apuntando a device-open access con secret/signature del lado proveedor.
- La investigación sigue apoyando QR dinámico controlado por backend más autorización de vend del lado proveedor como el default más seguro hasta que lleguen docs exactas del tenant.
- El login cloud de test por sí solo no cambia esa dirección.

### Lo que todavía hay que confirmar en el setup day
- payload exacto de payment-init desde máquina/cloud
- path exacto de callback / notify de vend-success
- expectativa exacta de `CodeUrl`: payload QR vs link vs deep link específico del proveedor
- si la finalización del pago depende de callback, polling o ambos
- shape exacto de auth/signature para endpoints del tenant productivo

### Secuencia de test de pago del setup day
#### Remoto
1. Confirmar que la máquina aparezca online en cloud.
2. Confirmar que exista un producto vendible y esté bien mapeado.
3. Confirmar que el flujo de test de pagos de Ballbox siga pudiendo crear órdenes QR.
4. Confirmar que máquina/cloud puedan pedir payment init contra el adapter o stub de Ballbox.
5. Escanear y pagar el QR desde un teléfono.
6. Confirmar que Ballbox reciba el webhook del proveedor.
7. Confirmar que el pago aprobado se persista con correlación canónica de máquina.
8. Confirmar que el lado TCN reciba la señal de success/notify.
9. Reconciliar pago, resultado de vend y cambio de inventario.

#### En sitio
1. Confirmar que el producto vendible esté cargado físicamente.
2. Registrar la visualización del QR en la máquina.
3. Después de la aprobación, confirmar que cambie el estado de la máquina.
4. Confirmar que el vend físico ocurra con éxito.
5. Grabar video de prueba.
6. Reportar de inmediato cualquier mismatch entre pago exitoso y resultado de vend.

## Aceptación para salida a producción
- máquina online y estable
- zona horaria correcta
- touch OK en toda la pantalla
- al menos un sync de producto OK
- sync de precio OK
- sync de media OK
- test de pago OK
- test de vend OK
- observado al menos un path de retry/error
- todos los ids y credenciales archivados en la nota interna de ops

## Paquete de escalación para proveedor / TCN
Si algo bloquea el setup, mandar todo esto junto:
- serial de máquina
- etiqueta de modelo
- familia de pantalla
- modelo del módulo de pagos
- versión actual de firmware/build
- nombre/url del tenant cloud
- screenshots/video del problema
- timestamps con zona horaria
- si estaba por Ethernet o Wi-Fi
- última acción exitosa
- si el problema parece de Ballbox, de cloud o de machine-side

## Regla standalone
- Este es el manual extendido. Si alcanza con el happy path, leer primero `01-vending-machine-setup-day-basico.es-ar.md`.
- Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.
- Si algún detalle de este manual entra en conflicto con el comportamiento real de la máquina, frenar, capturar evidencia y verificar antes de seguir.


**Renumbering section headings**

I need to make sure that the basic section headings match up with the extended headings. It makes sense to keep them consistent, so I’ll number them as follows: 1 for datos/setup, 2 for llegada, 3 for primer encendido, 4 for cloud, 5 for catalogo, 6 for media, 7 for pagos, and 8 for blockers. This way, the structure will be clear and easier to follow. Let's edit!

edit

## Objetivo
- Leer esto primero.
- Ir por happy path.
- Si algo falla o falta info, pasar al manual extendido `02`.

## Datos base
- Cloud: `https://dnymer.ourvend.com/`
- Usuario test: `cc`
- Password test: `ys123456`

## Reparto simple
### Remoto
- entra al cloud
- da de alta o verifica la máquina
- carga productos, precios y media
- valida pago

### En sitio
- enchufa
- conecta red
- saca fotos
- carga producto
- confirma que la máquina vende

## Happy path
1. Sacar foto de:

## Cómo usar este manual
- Leer esto primero.
- Ir por happy path.
- Si algo falla o falta info, pasar al manual extendido `02` y buscar la misma sección numerada.

## 1. Setup base
- Cloud: `https://dnymer.ourvend.com/`
- Usuario test: `cc`
- Password test: `ys123456`

### Remoto
- entra al cloud
- da de alta o verifica la máquina
- carga productos, precios y media
- valida pago

### En sitio
- enchufa
- conecta red
- saca fotos
- carga producto
- confirma que la máquina vende

## 2. Llegada y recepción
1. Sacar foto de:

## 3. Primer encendido y red
2. Enchufar y encender.
3. Conectar red:

6. En cloud, verificar que aparezca online.
7. Cargar o verificar catálogo.
8. Mapear tracks/productos/precios.
9. Hacer sync.

## 4. Cloud / OurVend
6. En cloud, verificar que aparezca online.

## 5. Catálogo, tracks y sync
7. Cargar o verificar catálogo.
8. Mapear tracks/productos/precios.
9. Hacer sync.

11. Subir un video corto de prueba.
12. Asignarlo a la máquina.
13. Publicar/sincronizar.
14. Confirmar que se vea bien en pantalla.
15. Cargar producto físico.
16. Hacer una compra real de prueba.
17. Confirmar:

## 6. Media
11. Subir un video corto de prueba.
12. Asignarlo a la máquina.
13. Publicar/sincronizar.
14. Confirmar que se vea bien en pantalla.

## 7. Pago y vend de prueba
15. Cargar producto físico.
16. Hacer una compra real de prueba.
17. Confirmar:

## Si algo falla
Pasar al manual extendido:
- `02-vending-machine-setup-day-manual.es-ar.md`

## Blockers típicos
- la máquina no aparece online
- sync no baja a la máquina
- media no aparece
- sale el pago pero no vende
- no coincide track/producto/precio
- hay duda con red, SIM, APN o menú admin

## Criterio de listo
- máquina online
- hora correcta
- catálogo OK
- precio OK
- media OK
- pago OK
- vend OK

## 8. Si algo falla
Pasar al manual extendido y buscar la misma sección numerada:
- `02-vending-machine-setup-day-manual.es-ar.md`

Blockers típicos:
- 4. la máquina no aparece online
- 5. sync no baja a la máquina
- 6. media no aparece
- 7. sale el pago pero no vende
- 5. no coincide track/producto/precio
- 3. hay duda con red, SIM, APN o menú admin

## 9. Criterio de listo
- máquina online
- hora correcta
- catálogo OK
- precio OK
- media OK
- pago OK
- vend OK


Successfully replaced 5 block(s) in /mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/01-vending-machine-setup-day-basico.es-ar.md.

  1 # 01. Setup rápido de vending machine
  2
- 3 ## Objetivo
+ 3 ## Cómo usar este manual
  4 - Leer esto primero.
  5 - Ir por happy path.
- 6 - Si algo falla o falta info, pasar al manual extendido `02`.
+ 6 - Si algo falla o falta info, pasar al manual extendido `02` y buscar la misma sección numerada.
  7
- 8 ## Datos base
+ 8 ## 1. Setup base
  9 - Cloud: `https://dnymer.ourvend.com/`
 10 - Usuario test: `cc`
 11 - Password test: `ys123456`
 12
-13 ## Reparto simple
 14 ### Remoto
 15 - entra al cloud
 16 - da de alta o verifica la máquina
 17 - carga productos, precios y media
    ...
 23 - saca fotos
 24 - carga producto
 25 - confirma que la máquina vende
 26
-27 ## Happy path
+26 ## 2. Llegada y recepción
 28 1. Sacar foto de:
 29    - etiqueta/serial
 30    - pantalla de system info
 31    - máquina completa
+31
+32 ## 3. Primer encendido y red
 32 2. Enchufar y encender.
 33 3. Conectar red:
 34    - primero usar lo que funcione más rápido
 35    - Wi-Fi debería alcanzar para setup day si anda bien
    ...
 38    - fecha
 39    - hora
 40    - zona horaria
 41 5. Confirmar que la máquina tenga internet.
+43
+44 ## 4. Cloud / OurVend
 42 6. En cloud, verificar que aparezca online.
+46
+47 ## 5. Catálogo, tracks y sync
 43 7. Cargar o verificar catálogo.
 44 8. Mapear tracks/productos/precios.
 45 9. Hacer sync.
 46 10. Confirmar en la pantalla de la máquina:
 47    - producto correcto
 48    - foto correcta
 49    - precio correcto
+55
+56 ## 6. Media
 50 11. Subir un video corto de prueba.
 51 12. Asignarlo a la máquina.
 52 13. Publicar/sincronizar.
 53 14. Confirmar que se vea bien en pantalla.
+61
+62 ## 7. Pago y vend de prueba
 54 15. Cargar producto físico.
 55 16. Hacer una compra real de prueba.
 56 17. Confirmar:
 57    - aparece QR
    ...
 61    - fotos
 62    - video corto del vend
 63    - screenshot de cloud
 64
-65 ## Si algo falla
-66 Pasar al manual extendido:
+74 ## 8. Si algo falla
+75 Pasar al manual extendido y buscar la misma sección numerada:
 67 - `02-vending-machine-setup-day-manual.es-ar.md`
 68
-69 ## Blockers típicos
-70 - la máquina no aparece online
-71 - sync no baja a la máquina
-72 - media no aparece
-73 - sale el pago pero no vende
-74 - no coincide track/producto/precio
-75 - hay duda con red, SIM, APN o menú admin
+78 Blockers típicos:
+79 - 4. la máquina no aparece online
+80 - 5. sync no baja a la máquina
+81 - 6. media no aparece
+82 - 7. sale el pago pero no vende
+83 - 5. no coincide track/producto/precio
+84 - 3. hay duda con red, SIM, APN o menú admin
 76
-77 ## Criterio de listo
+86 ## 9. Criterio de listo
 78 - máquina online
 79 - hora correcta
 80 - catálogo OK
 81 - precio OK
    ...


## Cómo usar este manual
- Este es el manual extendido.
- Si venís del manual básico, buscá la misma sección numerada.
- Si algún detalle de este manual entra en conflicto con el comportamiento real de la máquina, frenar, capturar evidencia y verificar antes de seguir.

## Propósito

## 1. Setup base
### Datos base
- Cloud: `https://dnymer.ourvend.com/`
- Usuario de test: `cc`
- Contraseña de test: `ys123456`
- Video de referencia usado para la guía de media upload: `https://www.youtube.com/watch?v=Qwbp3muM7gA`
- Nota de producción: la cloud real y las credenciales reales llegan el día de entrega de las máquinas.

### Reparto de roles

## Inputs de setup
- Cloud: `https://dnymer.ourvend.com/`
- Usuario de test: `cc`
- Contraseña de test: `ys123456`
- Video de referencia usado para la guía de media upload: `https://www.youtube.com/watch?v=Qwbp3muM7gA`
- Nota de producción: la cloud real y las credenciales reales llegan el día de entrega de las máquinas.

## Puntos no verificados o conflictivos

### Puntos no verificados o conflictivos

## Catálogo de productos y setup de tracks

## 7. Pago y vend de prueba
### Estado de la integración de pagos

## 8. Escalación y blockers
### Paquete de escalación para proveedor / TCN
Si algo bloquea el setup, mandar todo esto junto:
- serial de máquina
- etiqueta de modelo
- familia de pantalla
- modelo del módulo de pagos
- versión actual de firmware/build
- nombre/url del tenant cloud
- screenshots/video del problema
- timestamps con zona horaria
- si estaba por Ethernet o Wi-Fi
- última acción exitosa
- si el problema parece de Ballbox, de cloud o de machine-side

## 9. Criterio de listo

## Paquete de escalación para proveedor / TCN
Si algo bloquea el setup, mandar todo esto junto:
- serial de máquina
- etiqueta de modelo
- familia de pantalla
- modelo del módulo de pagos
- versión actual de firmware/build
- nombre/url del tenant cloud
- screenshots/video del problema
- timestamps con zona horaria
- si estaba por Ethernet o Wi-Fi
- última acción exitosa
- si el problema parece de Ballbox, de cloud o de machine-side

## Regla standalone
- Este es el manual extendido. Si alcanza con el happy path, leer primero `01-vending-machine-setup-day-basico.es-ar.md`.
- Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.
- Si algún detalle de este manual entra en conflicto con el comportamiento real de la máquina, frenar, capturar evidencia y verificar antes de seguir.

## Regla standalone
- Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.


Successfully replaced 15 block(s) in /mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/02-vending-machine-setup-day-manual.es-ar.md.

   1 # 02. Manual extendido del día de setup de vending machines
   2
+  3 ## Cómo usar este manual
+  4 - Este es el manual extendido.
+  5 - Si venís del manual básico, buscá la misma sección numerada.
+  6 - Si algún detalle de este manual entra en conflicto con el comportamiento real de la máquina, frenar, capturar evidencia y verificar antes de seguir.
+  7
   3 ## Propósito
   4 - Un manual único de Ballbox para el día de llegada.
   5 - Enfoque: primer encendido, onboarding en cloud, carga de media, readiness de pagos y chequeos de salida a producción.
   6 - Familia objetivo según la investigación actual: TCN CSC-8C(V49) o la variante 8C / 49 pulgadas más cercana que entregue el proveedor.
   7
-  8 ## Reparto de roles
+ 13 ## 1. Setup base
+ 14 ### Datos base
+ 15 - Cloud: `https://dnymer.ourvend.com/`
+ 16 - Usuario de test: `cc`
+ 17 - Contraseña de test: `ys123456`
+ 18 - Video de referencia usado para la guía de media upload: `https://www.youtube.com/watch?v=Qwbp3muM7gA`
+ 19 - Nota de producción: la cloud real y las credenciales reales llegan el día de entrega de las máquinas.
+ 20
+ 21 ### Reparto de roles
   9 ### El operador remoto se ocupa de
  10 - login en cloud y chequeos del tenant
  11 - catálogo/productos y chequeos de sync remota
  12 - carga y asignación de media
     ...
  21 - reinicios locales o acciones de service
  22 - carga de producto y chequeos de vend físico
  23 - confirmar comportamiento de pantalla/media/pagos sobre hardware real
  24
- 25 ## Inputs de setup
- 26 - Cloud: `https://dnymer.ourvend.com/`
- 27 - Usuario de test: `cc`
- 28 - Contraseña de test: `ys123456`
- 29 - Video de referencia usado para la guía de media upload: `https://www.youtube.com/watch?v=Qwbp3muM7gA`
- 30 - Nota de producción: la cloud real y las credenciales reales llegan el día de entrega de las máquinas.
- 31
- 32 ## Puntos no verificados o conflictivos
+ 38 ### Puntos no verificados o conflictivos
  33 - La guía de red tiene conflicto en el camino principal:
  34   - sesgo de investigación previa: Ethernet first
  35   - otro insumo técnico: la máquina tiene módulo 4G y soporta Wi-Fi y Ethernet como backup
  36   - otra guía de llegada: comprar SIMs locales y configurar APN en sitio
     ...
  42   - no asumir entrypoint de UI ni contrato exacto de API hasta que TCN entregue el protocolo o package real
  43 - El sync remoto de precio/stock puede depender de features VIP según uno de los insumos técnicos. Eso entra en conflicto con asumir que toda capacidad de sync/admin está disponible por default. Verificar en vivo antes de depender de eso.
  44 - El código admin `888888` aparece como un código usual para diagnóstico, pero no está confirmado para estas unidades. Verificar en vivo; no depender de eso ciegamente.
  45
- 46 ## No negociables
+ 52 ### No negociables
  47 - No flashear firmware a ciegas desde otra familia de modelos.
  48 - Pedir a TCN/proveedor el package exacto matcheado por serial, revisión de mainboard, familia de pantalla y BOM del módulo de pagos.
  49 - Configurar fecha, hora y zona horaria correctas antes de cualquier prueba de cloud o pagos.
  50 - No habilitar pagos live hasta que cloud, sync de producto y tests de vend estén limpios.
  51 - Si el camino de conectividad no está claro, verificar Ethernet, Wi-Fi y realidad de SIM local en la unidad antes de estandarizar el camino operativo.
  52
- 53 ## Evidencia compartida a capturar
+ 59 ### Evidencia compartida a capturar
  54 - fotos de crate y etiquetas
  55 - número de serie
  56 - etiqueta de modelo
  57 - familia de pantalla
     ...
  64 - screenshot del QR de pago
  65 - evidencia de vend exitoso
  66 - timestamps con zona horaria
  67
- 68 ## Checklist del día anterior
+ 74 ### Checklist del día anterior
  69 - Tener Chrome listo.
  70 - Confirmar tipo de enchufe. Si es EU, comprar 5 adaptadores EU-hembra a AR-macho. Si es SAA, puede que no haga falta.
  71 - Tener Ethernet disponible en sitio.
  72 - Las SIMs locales son opcionales para el día de llegada. Si se van a usar como fallback o para rollout futuro en clubes, preparar SIMs locales más datos de APN para Personal, Movistar o Claro.
     ...
  87   - docs de open-device / payment API
  88   - mapeo de device ids / seriales
  89   - `External Payment Integration Protocol` o `Remote Vend API`
  90
- 91 ## Llegada y recepción
+ 97 ## 2. Llegada y recepción
  92 ### En sitio
  93 1. Fotografiar el crate antes de abrir.
  94 2. Registrar daños, estado del pallet, etiquetas y stickers de serie.
  95 3. Confirmar la etiqueta real de la máquina.
     ...
 108 ### Remoto
 109 - Chequear mismatch de modelo antes de seguir.
 110 - Si hay daño visible de transporte, documentarlo antes del power-on y decidir si conviene pausar el setup.
 111
-112 ## Primer encendido
+118 ## 3. Primer encendido y red
 113 ### En sitio
 114 1. Verificar tensión local y puesta a tierra.
 115 2. Encender con el camino de red más confiable disponible, pero registrar si la unidad quedó por Ethernet, Wi-Fi o SIM local / 4G.
 116 3. Wi-Fi debería alcanzar para el día de setup si la red del lugar funciona.
     ...
 138   - problema de Wi-Fi
 139   - problema de SIM/APN local
 140   - problema del módem de la máquina
 141
-142 ## Onboarding en OurVend cloud
+148 ## 4. Cloud / OurVend
 143 Portal:
 144 - URL: `https://dnymer.ourvend.com/`
 145 - Navegador: preferir Chrome
 146 - Credenciales de test:
     ...
 170 ### En sitio
 171 - Estar listo para pasar serial, modelo, familia de pantalla, modelo del módulo de pagos, cloud device id, Machine Number y cualquier error visible en la máquina.
 172 - Confirmar estado online/offline en la máquina si se ve.
 173
-174 ## Catálogo de productos y setup de tracks
+180 ## 5. Catálogo, tracks y sync
 175 ### Remoto
 176 1. Cargar primero los productos en el catálogo global.
 177 2. Subir fotos y nombres de producto antes de mapear tracks.
 178 3. En `Products > Track Management`, mapear cada slot/track a un producto del catálogo.
     ...
 189 - El catálogo global debería ir primero y después el mapping por máquina.
 190 - Los cambios en cloud pueden requerir `Sync` explícito o esperar el heartbeat de la máquina.
 191 - El sync remoto de tracks/precios puede estar VIP-gated; verificar en vivo.
 192
-193 ## Diagnóstico mecánico
+199 ### 5A. Diagnóstico mecánico
 194 ### En sitio
 195 1. Si el diagnóstico admin está accesible, correr motor test antes de cargar stock vendible.
 196 2. Preferir pruebas con tracks vacíos primero para detectar daños de transporte.
 197 3. Chequear estado de puerta, comportamiento del sensor de caída y alarmas visibles de hardware si aparecen.
     ...
 200 ### Remoto
 201 - Registrar si se pudo entrar o no a diagnósticos.
 202 - Si diagnósticos está bloqueado, anotar si el bloqueo fue por PIN desconocido, menú ausente o limitación de tenant/rol.
 203
-204 ## Carga de media en la vending machine
+210 ## 6. Media
 205 Fuente: video público `How to upload videos to the vending machine`.
 206 - Tratar esta sección como checklist operativo, no como click-path exacto ya verificado.
 207
 208 ### Remoto
     ...
 230 - tipos de archivo soportados / límites de tamaño
 231 - si la media se asigna por máquina, por grupo o por playlist
 232 - si hace falta sync/reboot manual después de publicar
 233
-234 ## Estado de la integración de pagos
+240 ## 7. Pago y vend de prueba
+241 ### Estado de la integración de pagos
 235 ### Posición actual de Ballbox
 236 - `/admin/payments` queda como harness de test/debug de Ballbox.
 237 - El flujo productivo de vending debería ir por protocolo TCN.
 238 - La máquina debería mostrar el QR buyer-facing en su propia pantalla.
     ...
 270 4. Confirmar que el vend físico ocurra con éxito.
 271 5. Grabar video de prueba.
 272 6. Reportar de inmediato cualquier mismatch entre pago exitoso y resultado de vend.
 273
-274 ## Aceptación para salida a producción
-275 - máquina online y estable
-276 - zona horaria correcta
-277 - touch OK en toda la pantalla
-278 - al menos un sync de producto OK
-279 - sync de precio OK
-280 - sync de media OK
-281 - test de pago OK
-282 - test de vend OK
-283 - observado al menos un path de retry/error
-284 - todos los ids y credenciales archivados en la nota interna de ops
-285
-286 ## Paquete de escalación para proveedor / TCN
+281 ## 8. Escalación y blockers
+282 ### Paquete de escalación para proveedor / TCN
 287 Si algo bloquea el setup, mandar todo esto junto:
 288 - serial de máquina
 289 - etiqueta de modelo
 290 - familia de pantalla
     ...
 296 - si estaba por Ethernet o Wi-Fi
 297 - última acción exitosa
 298 - si el problema parece de Ballbox, de cloud o de machine-side
 299
+296 ## 9. Criterio de listo
+297 - máquina online y estable
+298 - zona horaria correcta
+299 - touch OK en toda la pantalla
+300 - al menos un sync de producto OK
+301 - sync de precio OK
+302 - sync de media OK
+303 - test de pago OK
+304 - test de vend OK
+305 - observado al menos un path de retry/error
+306 - todos los ids y credenciales archivados en la nota interna de ops
+307
 300 ## Regla standalone
-301 - Este es el manual extendido. Si alcanza con el happy path, leer primero `01-vending-machine-setup-day-basico.es-ar.md`.
 302 - Este manual debería alcanzar para operar el día de llegada sin depender de otros documentos.
-303 - Si algún detalle de este manual entra en conflicto con el comportamiento real de la máquina, frenar, capturar evidencia y verificar antes de seguir.


bash

cd /mnt/rpi/worktrees/ballbox/docs-vending-manual && python scripts/export-setup-manual-standalone-es-ar-basic-pdf.py && python scripts/export-setup-manual-standalone-es-ar-pdf.py && git add docs/01-vending-machine-setup-day-basico.es-ar.md docs/02-vending-machine-setup-day-manual.es-ar.md docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf && git commit -m "docs: align spanish setup section numbering"


/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf
/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf
[docs-vending-manual 32b8cd7] docs: align spanish setup section numbering
 4 files changed, 101 insertions(+), 86 deletions(-)


Updated #8 (pending → completed)


Sí. Ahora quedó como querías: numeración de secciones adentro.

Estructura alineada entre básico y extendido:
- `1. Setup base`
- `2. Llegada y recepción`
- `3. Primer encendido y red`
- `4. Cloud / OurVend`
- `5. Catálogo, tracks y sync`
- `6. Media`
- `7. Pago y vend de prueba`
- `8. Escalación y blockers` / `Si algo falla`
- `9. Criterio de listo`

Además:
- el básico ahora te dice “si falla, ir a la misma sección numerada del extendido”
- el extendido arranca aclarando eso mismo

PDFs actualizados:
- básico:
  - https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf
- extendido:
  - https://ballbox-first.emperor-ratio.ts.net/files/mnt/rpi/worktrees/ballbox/docs-vending-manual/docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf

Commit:
- `32b8cd7` — `docs: align spanish setup section numbering`

Si querés, siguiente mejora: hago que el extendido use subtítulos todavía más obvios tipo `7R Remoto` y `7S En sitio` para saltar más rápido.


que archivos quedaron de entregable pdf al final?


Quedaron estos PDFs entregables:

- `docs/exports/01-vending-machine-setup-day-basico.es-ar.pdf`
- `docs/exports/02-vending-machine-setup-day-manual-extendido.es-ar.pdf`
- `docs/exports/vending-machine-setup-day-manual-standalone.pdf`
- `docs/exports/vending-setup-day-pack.pdf`

Si querés los realmente finales/recomendados:
- `01-vending-machine-setup-day-basico.es-ar.pdf`
- `02-vending-machine-setup-day-manual-extendido.es-ar.pdf`

Los otros dos quedaron como auxiliares en inglés / pack combinado.


describilos