#!/bin/bash # ============================================================ # z/OS Web Console — Project Setup Script # Run this on your Docker/build machine to create all files # ============================================================ set -e echo “🚀 Creating z/OS Web Console project…” # ── Directory structure ────────────────────────────────────── mkdir -p zos-web/backend mkdir -p zos-web/frontend/src/components mkdir -p zos-web/frontend/public mkdir -p zos-web/k8s cd zos-web # ============================================================ # BACKEND # ============================================================ cat > backend/tn3270_service.py << ‘PYEOF’ “”” TN3270 Session Service Manages a persistent s3270 connection to z/OS “”” import subprocess import time import os import threading import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(**name**) class TN3270Session: def **init**(self): self.host = os.environ.get(“ZOS_HOST”, “192.168.2.243”) self.port = os.environ.get(“ZOS_PORT”, “23”) self.userid = os.environ.get(“ZOS_USERID”, “IBMUSER”) self.password = os.environ.get(“ZOS_PASSWORD”, “allard”) self.proc = os.environ.get(“ZOS_PROC”, “DBSPROC9”) self.acct = os.environ.get(“ZOS_ACCT”, “ACCT#”) self.size = os.environ.get(“ZOS_SIZE”, “2048000”) self.s3270 = None self.lock = threading.Lock() self.connected = False ``` def _start_s3270(self): self.s3270 = subprocess.Popen( ["s3270", "-model", "3279-2", f"{self.host}:{self.port}"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, ) time.sleep(2) def _send(self, command: str): self.s3270.stdin.write(command + "\n") self.s3270.stdin.flush() return self._read_response() def _read_response(self): lines = [] while True: line = self.s3270.stdout.readline() if not line: return False, lines line = line.rstrip("\n") if line == "ok": return True, lines if line == "error": return False, lines lines.append(line) def _type(self, text: str): escaped = text.replace("\\", "\\\\").replace(")", "\\)").replace("(", "\\(") self._send(f"String({escaped})") def _tab(self): self._send("Tab()") def _enter(self): self._send("Enter()") def _pf(self, n): self._send(f"PF({n})") def connect(self): logger.info(f"Connecting to {self.host}:{self.port}") self._start_s3270() time.sleep(3) self._send("Home()") self._type(self.userid); self._tab() self._type(self.password); self._tab() self._tab() # New Password self._tab() # Procedure (pre-filled) self._tab() # Group Ident self._type(self.acct); self._tab() self._tab() # Size (pre-filled) self._tab() # Perform self._type("ispf") self._enter() time.sleep(5) self.connected = True logger.info("Connected to z/OS — ISPF loaded") def disconnect(self): try: self._type("=x"); self._enter(); time.sleep(1) finally: self.s3270.terminate() self.connected = False def get_screen(self) -> str: with self.lock: ok, lines = self._send("Ascii()") return "\n".join(lines) def send_command(self, command: str) -> str: with self.lock: self._type(command); self._enter() time.sleep(0.8) return self.get_screen() def send_pf(self, key: int) -> str: with self.lock: self._pf(key); time.sleep(0.5) return self.get_screen() def send_tab(self) -> str: with self.lock: self._tab() return self.get_screen() def navigate_ispf(self, option: str) -> str: return self.send_command(option) def submit_jcl(self, jcl_lines: list) -> str: with self.lock: self._type("=6"); self._enter(); time.sleep(1) return self.get_screen() def list_datasets(self, hlq: str) -> str: with self.lock: self._type("=3.4"); self._enter(); time.sleep(1) self._send("Home()") self._type(hlq); self._enter(); time.sleep(1) return self.get_screen() def open_sdsf(self) -> str: with self.lock: self._type("=m.sdsf"); self._enter(); time.sleep(2) return self.get_screen() def sdsf_st(self) -> str: with self.lock: self._type("ST"); self._enter(); time.sleep(1) return self.get_screen() ``` PYEOF cat > backend/main.py << ‘PYEOF’ “”” z/OS Web API — FastAPI Backend “”” from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from contextlib import asynccontextmanager from tn3270_service import TN3270Session import asyncio, logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(**name**) session: TN3270Session = None @asynccontextmanager async def lifespan(app: FastAPI): global session session = TN3270Session() try: session.connect() logger.info(“✅ z/OS session established”) except Exception as e: logger.error(f”❌ Failed to connect: {e}”) yield if session and session.connected: session.disconnect() app = FastAPI(title=“z/OS Web API”, version=“1.0.0”, lifespan=lifespan) app.add_middleware(CORSMiddleware, allow_origins=[”*”], allow_methods=[”*”], allow_headers=[”*”]) class CommandRequest(BaseModel): text: str class JCLRequest(BaseModel): jcl: list[str] class DatasetRequest(BaseModel): hlq: str def check(): if not session or not session.connected: raise HTTPException(503, “Not connected to z/OS”) @app.get(”/health”) def health(): return {“status”: “ok”, “connected”: session.connected if session else False} @app.get(”/screen”) def get_screen(): check(); return {“screen”: session.get_screen()} @app.post(”/command”) def send_command(req: CommandRequest): check(); return {“screen”: session.send_command(req.text)} @app.post(”/pf/{key}”) def send_pf(key: int): check() if not 1 <= key <= 24: raise HTTPException(400, “PF key must be 1-24”) return {“screen”: session.send_pf(key)} @app.post(”/tab”) def send_tab(): check(); return {“screen”: session.send_tab()} @app.post(”/ispf/navigate”) def navigate(req: CommandRequest): check(); return {“screen”: session.navigate_ispf(req.text)} @app.post(”/jcl/submit”) def submit_jcl(req: JCLRequest): check(); return {“screen”: session.submit_jcl(req.jcl)} @app.post(”/datasets/list”) def list_datasets(req: DatasetRequest): check(); return {“screen”: session.list_datasets(req.hlq)} @app.get(”/sdsf”) def open_sdsf(): check(); return {“screen”: session.open_sdsf()} @app.get(”/sdsf/st”) def sdsf_st(): check(); return {“screen”: session.sdsf_st()} @app.websocket(”/ws/screen”) async def ws_screen(websocket: WebSocket): await websocket.accept() last_screen = “” try: while True: if session and session.connected: screen = session.get_screen() if screen != last_screen: await websocket.send_json({“screen”: screen}) last_screen = screen await asyncio.sleep(0.5) except WebSocketDisconnect: pass PYEOF cat > backend/requirements.txt << ‘EOF’ fastapi==0.115.0 uvicorn==0.30.6 pydantic==2.9.2 websockets==13.1 EOF cat > backend/Dockerfile << ‘EOF’ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y python3 python3-pip x3270 && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip3 install –break-system-packages -r requirements.txt COPY . . EXPOSE 8000 CMD [“uvicorn”, “main:app”, “–host”, “0.0.0.0”, “–port”, “8000”, “–log-level”, “info”] EOF echo “✅ backend files created” # ============================================================ # FRONTEND # ============================================================ cat > frontend/public/index.html << ‘EOF’ z/OS Web Console
EOF cat > frontend/src/index.js << ‘EOF’ import React from ‘react’; import ReactDOM from ‘react-dom/client’; import App from ‘./App’; const root = ReactDOM.createRoot(document.getElementById(‘root’)); root.render(); EOF cat > frontend/src/App.jsx << ‘JSEOF’ import { useState, useEffect, useRef, useCallback } from “react”; const API = “http://192.168.2.100/api”; const WS = “ws://192.168.2.100/api/ws/screen”; const C = { bg: “#0a0f0a”, bgPanel: “#0d140d”, green: “#00ff41”, greenDim: “#00aa2a”, greenDark: “#004d10”, amber: “#ffb000”, red: “#ff3333”, border: “#1a3a1a”, }; async function api(path, body) { const opts = body ? { method: “POST”, headers: { “Content-Type”: “application/json” }, body: JSON.stringify(body) } : { method: “GET” }; const r = await fetch(API + path, opts); return r.json(); } function TerminalScreen({ screen }) { return (
{screen || “Connecting to z/OS…”}
); } function StatusBar({ connected, message }) { return (
{connected ? “● ONLINE” : “● OFFLINE”} z/OS 192.168.2.243 |IBMUSER |DBSPROC9 {message && <>|{message}}
); } function PFBar({ onPF }) { return (
{[1,2,3,4,5,6,7,8,9,10,11,12].map(k => ( ))}
); } function CommandInput({ onSend, disabled }) { const [cmd, setCmd] = useState(””); const [history, setHistory] = useState([]); const [hIdx, setHIdx] = useState(-1); const submit = () => { if (!cmd.trim()) return; setHistory(h => [cmd, …h].slice(0, 50)); setHIdx(-1); onSend(cmd); setCmd(””); }; const onKey = e => { if (e.key === “Enter”) submit(); if (e.key === “ArrowUp”) { const i = hIdx+1; if (i < history.length) { setHIdx(i); setCmd(history[i]); } } if (e.key === “ArrowDown”) { const i = hIdx-1; if (i >= 0) { setHIdx(i); setCmd(history[i]); } else { setHIdx(-1); setCmd(””); } } }; return (
COMMAND ===> setCmd(e.target.value)} onKeyDown={onKey} disabled={disabled} style={{ flex: 1, background: “transparent”, border: “none”, borderBottom: `1px solid ${C.greenDark}`, color: C.green, fontFamily: “monospace”, fontSize: “13px”, outline: “none”, padding: “2px 4px”, caretColor: C.green, }} placeholder=“Type command or ISPF option (e.g. =3.4)…” autoFocus />
); } function JCLPanel({ onClose }) { const [jcl, setJcl] = useState(`//MYJOB JOB (ACCT#),'MY JOB',CLASS=A,MSGCLASS=X\n//STEP1 EXEC PGM=IEFBR14\n//`); const [result, setResult] = useState(””); const submit = async () => { const r = await api(”/jcl/submit”, { jcl: jcl.split(”\n”) }); setResult(r.screen); }; return (
── JCL SUBMIT ──