Skip to content

Commit 1fb570f

Browse files
committed
add INTnote writer mode
1 parent 037ec5c commit 1fb570f

File tree

6 files changed

+594
-14
lines changed

6 files changed

+594
-14
lines changed

Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ RUN source /home/atlas/release_setup.sh \
2727
RUN source /home/atlas/release_setup.sh \
2828
&& python3 -m pip install --quiet flask flask-cors pyyaml
2929

30+
# Install pdflatex for INTnote PDF generation
31+
# Tries dnf (RHEL 8/9) first, falls back to yum (RHEL 7); silently continues if unavailable
32+
RUN dnf install -y \
33+
texlive \
34+
texlive-latex \
35+
texlive-geometry \
36+
texlive-amsmath \
37+
texlive-xcolor \
38+
texlive-collection-fontsrecommended \
39+
2>/dev/null \
40+
|| yum install -y \
41+
texlive \
42+
texlive-latex \
43+
texlive-geometry \
44+
texlive-amsmath \
45+
texlive-color \
46+
texlive-collection-fontsrecommended \
47+
2>/dev/null \
48+
|| echo "WARNING: texlive not installed — PDF generation will be disabled"
49+
3050
# ── Clone and build TopCPToolkit ─────────────────────────────────────────────
3151
# Pass your CERN GitLab personal access token at build time:
3252
# docker build --secret id=cern_token,env=CERN_TOKEN ...
@@ -62,6 +82,7 @@ RUN --mount=type=secret,id=cern_token \
6282
&& cd /opt/TopCPToolkit/build \
6383
&& cmake /tmp/TopCPToolkit-src/source \
6484
&& make -j$(nproc) \
85+
&& cp -r /tmp/TopCPToolkit-src/source/ConfigDocumentation /opt/TopCPToolkit/ConfigDocumentation \
6586
&& rm -rf /tmp/TopCPToolkit-src ; \
6687
fi
6788

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.5.1
1+
0.6.0

backend/app.py

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
44
Endpoints
55
---------
6-
GET /api/schema Full block tree with introspected options
7-
GET /api/health Liveness check; reports Athena + version info
8-
POST /api/export-yaml Returns YAML content as a downloadable response
6+
GET /api/schema Full block tree with introspected options
7+
GET /api/health Liveness check; reports Athena + version info
8+
POST /api/export-yaml Returns YAML content as a downloadable response
9+
POST /api/generate-intnote Runs generateConfigInformation.py on a JSON file,
10+
compiles the resulting .tex to PDF, returns both
911
"""
1012

13+
import base64
1114
import copy
1215
import glob
1316
import logging
1417
import os
18+
import shutil
19+
import subprocess
20+
import tempfile
1521

1622
import yaml
1723
from flask import Flask, jsonify, request, send_from_directory, Response
@@ -32,6 +38,9 @@
3238

3339
_schema_cache = None
3440

41+
# Path to the ConfigDocumentation script kept from the TCT source tree
42+
_INTNOTE_SCRIPT = "/opt/TopCPToolkit/ConfigDocumentation/generateConfigInformation.py"
43+
3544

3645
def _build_schema():
3746
def enrich(block):
@@ -64,6 +73,16 @@ def _get_tct_version():
6473
return None
6574

6675

76+
def _pdflatex_available():
77+
"""Return True if pdflatex is on PATH."""
78+
return shutil.which("pdflatex") is not None
79+
80+
81+
def _intnote_available():
82+
"""Return True if both the TCT script and pdflatex are present."""
83+
return os.path.isfile(_INTNOTE_SCRIPT) and _pdflatex_available()
84+
85+
6786
@app.before_request
6887
def _warm_schema():
6988
global _schema_cache
@@ -86,6 +105,7 @@ def health():
86105
"app_version": APP_VERSION,
87106
"ab_version": _get_ab_version(),
88107
"tct_version": _get_tct_version(),
108+
"pdflatex": _pdflatex_available(),
89109
})
90110

91111

@@ -114,6 +134,126 @@ def export_yaml():
114134
)
115135

116136

137+
@app.route("/api/generate-intnote", methods=["POST"])
138+
def generate_intnote():
139+
"""
140+
Accept a TopCPToolkit JSON configuration file, run
141+
generateConfigInformation.py on it, compile the resulting .tex with
142+
pdflatex, and return the PDF (base64) + .tex source.
143+
144+
Form fields:
145+
json – the JSON file (required)
146+
sections – comma-separated list of sections, e.g. "muon,jet,met"
147+
(optional; omit to generate all sections)
148+
"""
149+
# ── Availability checks ──────────────────────────────────────────────────
150+
if not os.path.isfile(_INTNOTE_SCRIPT):
151+
return jsonify({
152+
"error": "TopCPToolkit not available — generateConfigInformation.py not found. "
153+
"Rebuild the image with TCT_VERSION set.",
154+
}), 400
155+
156+
if not _pdflatex_available():
157+
return jsonify({
158+
"error": "pdflatex not found. Rebuild the image with texlive installed.",
159+
}), 400
160+
161+
# ── Input validation ─────────────────────────────────────────────────────
162+
json_file = request.files.get("json")
163+
if not json_file:
164+
return jsonify({"error": "No JSON file provided"}), 400
165+
166+
sections = request.form.get("sections", "").strip()
167+
168+
# ── Run in a temp directory ──────────────────────────────────────────────
169+
with tempfile.TemporaryDirectory() as tmpdir:
170+
json_path = os.path.join(tmpdir, "config.json")
171+
tex_path = os.path.join(tmpdir, "output.tex")
172+
json_file.save(json_path)
173+
174+
# 1. Run the TopCPToolkit script
175+
cmd = ["python3", _INTNOTE_SCRIPT, json_path, "-o", tex_path]
176+
if sections:
177+
cmd += ["--sections", sections]
178+
179+
logger.info("Running: %s", " ".join(cmd))
180+
try:
181+
script_result = subprocess.run(
182+
cmd,
183+
capture_output=True,
184+
text=True,
185+
timeout=120,
186+
cwd=tmpdir,
187+
)
188+
except subprocess.TimeoutExpired:
189+
return jsonify({"error": "Script timed out after 120 s"}), 400
190+
191+
script_stdout = script_result.stdout
192+
script_stderr = script_result.stderr
193+
194+
if script_result.returncode != 0:
195+
return jsonify({
196+
"error": "generateConfigInformation.py exited with an error",
197+
"stdout": script_stdout,
198+
"stderr": script_stderr,
199+
}), 400
200+
201+
if not os.path.exists(tex_path):
202+
return jsonify({
203+
"error": "Script succeeded but produced no output file",
204+
"stdout": script_stdout,
205+
"stderr": script_stderr,
206+
}), 400
207+
208+
with open(tex_path, encoding="utf-8", errors="replace") as fh:
209+
tex_content = fh.read()
210+
211+
# 2. Compile to PDF with pdflatex
212+
logger.info("Compiling PDF with pdflatex…")
213+
try:
214+
pdf_result = subprocess.run(
215+
[
216+
"pdflatex",
217+
"-interaction=nonstopmode",
218+
"-output-directory", tmpdir,
219+
tex_path,
220+
],
221+
capture_output=True,
222+
text=True,
223+
timeout=120,
224+
cwd=tmpdir,
225+
)
226+
except subprocess.TimeoutExpired:
227+
return jsonify({
228+
"error": "pdflatex timed out after 120 s",
229+
"tex": tex_content,
230+
"stdout": script_stdout,
231+
"stderr": script_stderr,
232+
}), 400
233+
234+
# pdflatex names the PDF after the input filename (output.pdf)
235+
pdf_path = os.path.join(tmpdir, "output.pdf")
236+
237+
if not os.path.exists(pdf_path):
238+
return jsonify({
239+
"error": "pdflatex failed to produce a PDF",
240+
"tex": tex_content,
241+
"stdout": script_stdout,
242+
"stderr": script_stderr,
243+
"pdf_log": pdf_result.stdout + "\n" + pdf_result.stderr,
244+
}), 400
245+
246+
with open(pdf_path, "rb") as fh:
247+
pdf_bytes = fh.read()
248+
249+
return jsonify({
250+
"pdf": base64.b64encode(pdf_bytes).decode(),
251+
"tex": tex_content,
252+
"stdout": script_stdout,
253+
"stderr": script_stderr,
254+
})
255+
256+
117257
@app.route("/", defaults={"path": ""})
118258
@app.route("/<path:path>")
119259
def serve_frontend(path):

frontend/src/App.jsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import MobileLayout from './components/MobileLayout.jsx'
77
import SplashScreen from './components/SplashScreen.jsx'
88
import ModeSelector from './components/ModeSelector.jsx'
99
import ConfigReader from './components/ConfigReader.jsx'
10+
import IntNoteWriter from './components/IntNoteWriter.jsx'
1011
import SearchOverlay from './components/SearchOverlay.jsx'
1112
import { useConfig } from './hooks/useConfig.js'
1213
import { toYamlString } from './utils/yamlSerializer.js'
@@ -51,6 +52,7 @@ export default function App() {
5152
const [appVersion, setAppVersion] = useState(null)
5253
const [abVersion, setAbVersion] = useState(null)
5354
const [tctVersion, setTctVersion] = useState(undefined)
55+
const [pdflatex, setPdflatex] = useState(false)
5456
const [athena, setAthena] = useState(null)
5557
const [searchOpen, setSearchOpen] = useState(false)
5658
const isMobile = useIsMobile()
@@ -72,6 +74,7 @@ export default function App() {
7274
setAppVersion(health.app_version)
7375
setAbVersion(health.ab_version)
7476
setTctVersion(health.tct_version ?? null)
77+
setPdflatex(health.pdflatex ?? false)
7578
init(schemaData)
7679
setSelected(schemaData[0]?.name ?? null)
7780
setLoading(false)
@@ -305,7 +308,7 @@ export default function App() {
305308
{exportMsg && <span className="text-xs text-green-400 shrink-0">{exportMsg}</span>}
306309

307310
{/* Search button — shows Cmd+F / Ctrl+F shortcut */}
308-
{mode && (
311+
{mode && mode !== 'intnote' && (
309312
<button
310313
type="button"
311314
onClick={() => setSearchOpen(true)}
@@ -335,12 +338,19 @@ export default function App() {
335338
>
336339
◉ Reader
337340
</button>
341+
<button
342+
type="button"
343+
onClick={() => setMode('intnote')}
344+
className={`text-xs px-2 py-0.5 rounded transition-colors ${mode === 'intnote' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-700'}`}
345+
>
346+
✍ INTnote
347+
</button>
338348
</div>
339349
)}
340350
</header>
341351

342352
{!showSplash && mode === null && (
343-
<ModeSelector onSelect={setMode} appVersion={appVersion} />
353+
<ModeSelector onSelect={setMode} appVersion={appVersion} tctVersion={tctVersion} pdflatex={pdflatex} />
344354
)}
345355

346356
{mode === 'builder' && (
@@ -364,6 +374,12 @@ export default function App() {
364374
/>
365375
</div>
366376
)}
377+
378+
{mode === 'intnote' && (
379+
<div className="flex flex-1 overflow-hidden">
380+
<IntNoteWriter tctVersion={tctVersion} pdflatex={pdflatex} />
381+
</div>
382+
)}
367383
</div>
368384
</RegistryProvider>
369385
)

0 commit comments

Comments
 (0)