docs: handbook, diagrams and guides brought up to 2026-10-02
- technical-handbook: question packs instead of subscriptions, real hearts with the new wait screen (screenshot), math-only tutor and what is charged (new 9-7), admin cancel/refund, Bazaar planId, account deletion, 59 endpoints, 779 tests, 14 apps, status and open decisions. - Diagrams redrawn: use-case (buy a pack, refunds), data model (QuestionCredit, HeartsPass, Profile.birth_date), pack payment sequence. - The PDF is now built from the Markdown by docs/build_handbook.py (pandoc + Chrome, font in docs/assets); the hand-made HTML it used to come from was not in the repo and had been lost. - architecture, operations, security-before-production (item 13 closed), ai-cost-and-guardrails and README follow the same changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
95
docs/build_handbook.py
Normal file
95
docs/build_handbook.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
مستندِ فنی را از Markdown به PDF میسازد: docs/technical-handbook.md → docs/technical-handbook.pdf
|
||||
|
||||
python docs/build_handbook.py
|
||||
|
||||
چرا (۱۰ مهر ۱۴۰۵): PDF پیشتر از یک HTMLِ دستسازِ جدا ساخته میشد که در مخزن نبود. هر بهروزرسانی
|
||||
یعنی ویرایشِ دو نسخه، و بعد از ۹ مهر آن HTML گم شد. حالا Markdown تنها منبع است و PDF از روی آن
|
||||
ساخته میشود؛ نمودارها PNGهای `docs/images/technical-handbook/` هستند.
|
||||
|
||||
نیاز دارد: `pandoc` (brew install pandoc) و Google Chrome. فونت (وزیرمتن، مجوزِ OFL) کنارِ همین فایل در
|
||||
`docs/assets/` است، پس PDF روی هر ماشینی یکسان درمیآید و به اینترنت نیاز ندارد.
|
||||
"""
|
||||
import base64
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
DOCS = Path(__file__).resolve().parent
|
||||
SRC = DOCS / 'technical-handbook.md'
|
||||
OUT = DOCS / 'technical-handbook.pdf'
|
||||
CHROME_CANDIDATES = [
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'google-chrome', 'chromium', 'chromium-browser',
|
||||
]
|
||||
|
||||
CSS = '''
|
||||
@font-face{font-family:'Vazirmatn';font-weight:100 900;src:url(data:font/woff2;base64,%(font)s) format('woff2')}
|
||||
@page{size:A4;margin:16mm 15mm 16mm 15mm}
|
||||
:root{--ink:#1d2330;--muted:#5b6472;--line:#e4e0d6;--accent:#2f55c8;--accent-soft:#eaf0ff;--code-bg:#f6f4ee}
|
||||
*{box-sizing:border-box}
|
||||
html,body{background:#fff}
|
||||
body{margin:0;font-family:Vazirmatn,Tahoma,sans-serif;color:var(--ink);font-size:10.6pt;line-height:1.85;
|
||||
-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||||
h1{font-size:25pt;font-weight:900;margin:0 0 6px;color:var(--ink)}
|
||||
h1+table{margin-top:14px}
|
||||
h2{font-size:17pt;font-weight:800;margin:0 0 8px;padding-top:6px;border-top:3px solid var(--accent);break-before:page;break-after:avoid}
|
||||
h3{font-size:12.5pt;font-weight:700;margin:20px 0 6px;color:#26305a;break-after:avoid}
|
||||
h4{font-size:11pt;font-weight:700;margin:12px 0 4px;break-after:avoid}
|
||||
p{margin:6px 0}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
code{font-family:Menlo,Consolas,monospace;font-size:.84em;direction:ltr;unicode-bidi:isolate;background:var(--code-bg);
|
||||
padding:1px 4px;border-radius:4px;border:1px solid #ece8dd}
|
||||
pre{direction:ltr;text-align:left;background:#1f2430;color:#e8e6df;border-radius:8px;padding:10px 12px;
|
||||
font-size:8.6pt;line-height:1.55;white-space:pre-wrap;word-break:break-word;break-inside:avoid}
|
||||
pre code{background:none;border:0;padding:0;color:inherit;font-size:1em}
|
||||
table{width:100%%;border-collapse:collapse;margin:8px 0 12px;font-size:9.4pt;break-inside:auto}
|
||||
th,td{border:1px solid var(--line);padding:5px 8px;text-align:right;vertical-align:top}
|
||||
th{background:#f4f1e8;font-weight:700}
|
||||
tr{break-inside:avoid}
|
||||
blockquote{margin:10px 0;padding:8px 14px;border:1px solid #d9e2fb;border-right:4px solid var(--accent);
|
||||
background:var(--accent-soft);border-radius:8px;break-inside:avoid}
|
||||
blockquote h4{margin-top:2px;color:var(--accent)}
|
||||
img{max-width:100%%;display:block;margin:10px auto 4px;border-radius:6px}
|
||||
img[src*="screenshot-"]{max-width:62mm;border:1px solid var(--line);box-shadow:0 2px 8px rgba(30,40,80,.08)}
|
||||
p:has(> img){break-inside:avoid;margin:0}
|
||||
p > em:only-child{display:block;font-size:9pt;color:var(--muted);text-align:center;margin:2px 0 12px}
|
||||
ul,ol{padding-right:20px;margin:6px 0}
|
||||
li{margin:2px 0}
|
||||
hr{border:0;border-top:1px solid var(--line);margin:14px 0}
|
||||
'''
|
||||
|
||||
|
||||
def chrome() -> str:
|
||||
for c in CHROME_CANDIDATES:
|
||||
if Path(c).exists() or shutil.which(c):
|
||||
return c
|
||||
sys.exit('Chrome پیدا نشد؛ مسیرش را در CHROME_CANDIDATES بگذارید.')
|
||||
|
||||
|
||||
def main():
|
||||
if not shutil.which('pandoc'):
|
||||
sys.exit('pandoc نصب نیست: brew install pandoc')
|
||||
font = base64.b64encode((DOCS / 'assets' / 'Vazirmatn.woff2').read_bytes()).decode()
|
||||
body = subprocess.run(['pandoc', str(SRC), '-f', 'gfm', '-t', 'html5'],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
html = (f'<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">'
|
||||
f'<title>مستند فنی تدریسینو</title><style>{CSS % {"font": font}}</style></head>'
|
||||
f'<body>{body}</body></html>')
|
||||
# فایلِ موقت کنارِ docs تا مسیرِ نسبیِ عکسها (images/…) درست باز شود.
|
||||
with tempfile.NamedTemporaryFile('w', suffix='.html', dir=DOCS, delete=False, encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
subprocess.run([chrome(), '--headless=new', '--disable-gpu', '--no-pdf-header-footer',
|
||||
'--virtual-time-budget=10000', f'--print-to-pdf={OUT}', tmp.as_uri()],
|
||||
check=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
print(f'{OUT} ({OUT.stat().st_size // 1024} KB)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user