#!/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 (
);
}
function DatasetPanel({ onClose }) {
const [hlq, setHlq] = useState(“IBMUSER”);
const browse = async () => { await api(”/datasets/list”, { hlq }); onClose(); };
return (
);
}
function Toolbar({ onJCL, onDatasets, onSDSF }) {
const btn = (label, onClick) => (
);
return (
QUICK:
{btn(“3.4 DATASETS”, onDatasets)}
{btn(“SDSF”, onSDSF)}
{btn(“SUBMIT JCL”, onJCL)}
);
}
export default function App() {
const [screen, setScreen] = useState(””);
const [connected, setConnected] = useState(false);
const [message, setMessage] = useState(“Connecting…”);
const [showJCL, setShowJCL] = useState(false);
const [showDS, setShowDS] = useState(false);
const wsRef = useRef(null);
useEffect(() => {
const check = async () => {
try {
const r = await api(”/health”);
setConnected(r.connected);
setMessage(r.connected ? “” : “Backend disconnected from z/OS”);
} catch { setConnected(false); setMessage(“Cannot reach backend”); }
};
check();
const t = setInterval(check, 10000);
return () => clearInterval(t);
}, []);
useEffect(() => {
const connect = () => {
const ws = new WebSocket(WS);
ws.onmessage = e => { const d = JSON.parse(e.data); if (d.screen) setScreen(d.screen); };
ws.onopen = () => setMessage(””);
ws.onclose = () => setTimeout(connect, 3000);
wsRef.current = ws;
};
connect();
return () => wsRef.current?.close();
}, []);
const sendCommand = useCallback(async cmd => {
const r = await api(”/command”, { text: cmd });
if (r.screen) setScreen(r.screen);
}, []);
const sendPF = useCallback(async key => {
const r = await api(`/pf/${key}`);
if (r.screen) setScreen(r.screen);
}, []);
const openSDSF = async () => {
const r = await api(”/sdsf”);
if (r.screen) setScreen(r.screen);
};
return (
▋ z/OS WEB CONSOLE
TSO/ISPF • Hercules z/OS • 192.168.2.243
setShowJCL(true)} onDatasets={() => setShowDS(true)} onSDSF={openSDSF} />
{showJCL && setShowJCL(false)} />}
{showDS && setShowDS(false)} />}
);
}
JSEOF
cat > frontend/package.json << ‘EOF’
{
“name”: “zos-frontend”,
“version”: “1.0.0”,
“private”: true,
“dependencies”: {
“react”: “^18.3.1”,
“react-dom”: “^18.3.1”,
“react-scripts”: “5.0.1”
},
“scripts”: {
“start”: “react-scripts start”,
“build”: “react-scripts build”
},
“browserslist”: {
“production”: [”>0.2%”, “not dead”],
“development”: [“last 1 chrome version”]
}
}
EOF
cat > frontend/nginx.conf << ‘EOF’
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / { try_files $uri $uri/ /index.html; }
}
EOF
cat > frontend/Dockerfile << ‘EOF’
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY –from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [“nginx”, “-g”, “daemon off;”]
EOF
echo “✅ frontend files created”
# ============================================================
# KUBERNETES MANIFESTS
# ============================================================
cat > k8s/00-namespace.yaml << ‘EOF’
apiVersion: v1
kind: Namespace
metadata:
name: zos-web
EOF
cat > k8s/01-secret.yaml << ‘EOF’
apiVersion: v1
kind: Secret
metadata:
name: tso-credentials
namespace: zos-web
type: Opaque
stringData:
ZOS_HOST: “192.168.2.243”
ZOS_PORT: “23”
ZOS_USERID: “IBMUSER”
ZOS_PASSWORD: “allard”
ZOS_PROC: “DBSPROC9”
ZOS_ACCT: “ACCT#”
ZOS_SIZE: “2048000”
EOF
cat > k8s/02-backend-deploy.yaml << ‘EOF’
apiVersion: apps/v1
kind: Deployment
metadata:
name: zos-backend
namespace: zos-web
spec:
replicas: 1
selector:
matchLabels:
app: zos-backend
template:
metadata:
labels:
app: zos-backend
spec:
containers:
- name: zos-backend
image: allardkrings/zos-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
envFrom:
- secretRef:
name: tso-credentials
resources:
requests:
memory: “256Mi”
cpu: “250m”
limits:
memory: “512Mi”
cpu: “500m”
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 15
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
EOF
cat > k8s/03-backend-svc.yaml << ‘EOF’
apiVersion: v1
kind: Service
metadata:
name: zos-backend
namespace: zos-web
spec:
selector:
app: zos-backend
ports:
- protocol: TCP
port: 8000
targetPort: 8000
EOF
cat > k8s/04-frontend-deploy.yaml << ‘EOF’
apiVersion: apps/v1
kind: Deployment
metadata:
name: zos-frontend
namespace: zos-web
spec:
replicas: 2
selector:
matchLabels:
app: zos-frontend
template:
metadata:
labels:
app: zos-frontend
spec:
containers:
- name: zos-frontend
image: allardkrings/zos-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
resources:
requests:
memory: “64Mi”
cpu: “50m”
limits:
memory: “128Mi”
cpu: “100m”
EOF
cat > k8s/05-frontend-svc.yaml << ‘EOF’
apiVersion: v1
kind: Service
metadata:
name: zos-frontend
namespace: zos-web
spec:
selector:
app: zos-frontend
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
cat > k8s/06-ingress.yaml << ‘EOF’
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zos-ingress
namespace: zos-web
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-read-timeout: “3600”
nginx.ingress.kubernetes.io/proxy-send-timeout: “3600”
nginx.ingress.kubernetes.io/proxy-http-version: “1.1”
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
spec:
ingressClassName: public
rules:
- http:
paths:
- path: /api(/|$)(.*)
pathType: Prefix
backend:
service:
name: zos-backend
port:
number: 8000
- path: /()(.*)
pathType: Prefix
backend:
service:
name: zos-frontend
port:
number: 80
EOF
echo “✅ kubernetes manifests created”
# ============================================================
# DONE
# ============================================================
echo “”
echo “============================================”
echo “ ✅ Project created in: $(pwd)”
echo “============================================”
echo “”
echo “Next steps:”
echo “”
echo “ 1. Build & push backend:”
echo “ cd backend”
echo “ docker build -t allardkrings/zos-backend:latest .”
echo “ docker push allardkrings/zos-backend:latest”
echo “”
echo “ 2. Build & push frontend:”
echo “ cd ../frontend”
echo “ docker build -t allardkrings/zos-frontend:latest .”
echo “ docker push allardkrings/zos-frontend:latest”
echo “”
echo “ 3. Deploy to MicroK8s:”
echo “ cd ../k8s”
echo “ microk8s kubectl apply -f .”
echo “”
echo “ 4. Open browser:”
echo “ http://192.168.2.100”
echo “”